Merge branch '7-access-token-expired' into 'master'

automatically refresh invalid grants

Closes #7

See merge request !8
This commit is contained in:
Sebastian Hugentobler 2017-05-05 14:11:57 +00:00
commit db1c5af255
5 changed files with 128 additions and 75 deletions

View File

@ -1,5 +1,8 @@
## 0.3.1 (2017-05-05)
- automatically check if grant is still valid and refresh if necessary (fixes #7)
## 0.3.0 (2017-05-04)
- move hashes and content info into seperate files as to not clutter up the main config file.
- move hashes and content info into seperate files as to not clutter up the main config file
## 0.2.4 (2017-03-23)
- fix a bug in serial key parsing

8
Cargo.lock generated
View File

@ -1,9 +1,9 @@
[root]
name = "gog-sync"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.24.0 (registry+https://github.com/rust-lang/crates.io-index)",
"curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
@ -53,7 +53,7 @@ dependencies = [
[[package]]
name = "clap"
version = "2.23.3"
version = "2.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -429,7 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159"
"checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4"
"checksum chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056"
"checksum clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f57e9b63057a545ad2ecd773ea61e49422ed1b1d63d74d5da5ecaee55b3396cd"
"checksum clap 2.24.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f31c42d0cecb245c1a0bee00ef433eb1bf253897fe472b6a3f4202e9dbbc4b25"
"checksum curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c90e1240ef340dd4027ade439e5c7c2064dd9dc652682117bd50d1486a3add7b"
"checksum curl-sys 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "23e7e544dc5e1ba42c4a4a678bd47985e84b9c3f4d3404c29700622a029db9c3"
"checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90"

View File

@ -1,6 +1,6 @@
[package]
name = "gog-sync"
version = "0.3.0"
version = "0.3.1"
authors = ["Sebastian Hugentobler <sebastian@vanwa.ch>"]
description = "Synchronizes a GOG library with a local folder."
documentation = "https://docs.rs/crate/gog-sync"

View File

@ -24,6 +24,7 @@ use std::fs::File;
use std::io;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
/// Wraps `ConfigError`, `HttpError`, `serde_json::Error`, and `io::Error`.
#[derive(Debug)]
@ -76,8 +77,6 @@ impl From<io::Error> for GogError {
pub struct Gog<'a> {
client_id: String,
client_secret: String,
redirect_uri: String,
games_uri: String,
http_client: &'a mut Http,
}
@ -87,8 +86,6 @@ impl<'a> Gog<'a> {
Gog {
client_id: String::from("46899977096215655"),
client_secret: String::from("9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"),
redirect_uri: String::from("https://embed.gog.com/on_login_success?origin=client"),
games_uri: String::from("https://embed.gog.com/user/data/games"),
http_client: http_client,
}
}
@ -98,6 +95,15 @@ impl<'a> Gog<'a> {
/// will get an url which you have to open in a browser. There you have to
/// login and copy the code parameter from the next page into the prompt.
pub fn login(&mut self) -> Result<(), GogError> {
let token = self.refresh_token_from_file()?;
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
self.http_client.add_header(auth_header.as_str())?;
Ok(())
}
fn refresh_token_from_file(&mut self) -> Result<Token, GogError> {
let config = ConfigFiles::new();
let mut token: Token = match config.load("token.json") {
Ok(value) => value,
@ -113,10 +119,7 @@ impl<'a> Gog<'a> {
config.save("token.json", &token)?;
let auth_header = format!("Authorization: Bearer {token}", token = token.access_token);
self.http_client.add_header(auth_header.as_str())?;
Ok(())
Ok(token)
}
/// Syncs the contents of a gog account with a local folder.
@ -214,7 +217,7 @@ impl<'a> Gog<'a> {
let data_uri = format!("https://embed.gog.com{}", data.manual_url);
info!("downloading {} for {}...", &data.manual_url, &content.title);
let filename = self.http_client.download(data_uri.as_str(), &data_root)?;
let filename = self.api_request_download(data_uri.as_str(), &data_root)?;
let data_info = DataInfo {
hash: data_hash,
@ -222,16 +225,18 @@ impl<'a> Gog<'a> {
language: data.language,
};
content_info.data.insert(data.manual_url.clone(), data_info);
content_info
.data
.insert(data.manual_url.clone(), data_info);
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
}
for extra in content.extras {
let extra_hash_saved = match content_info_saved.extras
.get(&content_id.to_string()) {
Some(value) => value.hash,
None => u64::min_value(),
};
let extra_hash_saved =
match content_info_saved.extras.get(&content_id.to_string()) {
Some(value) => value.hash,
None => u64::min_value(),
};
let extra_hash = models::get_hash(&extra);
@ -243,7 +248,7 @@ impl<'a> Gog<'a> {
let extra_uri = format!("https://embed.gog.com{}", extra.manual_url);
info!("downloading {} for {}...", &extra.name, &content.title);
let filename = self.http_client.download(extra_uri.as_str(), &content_root)?;
let filename = self.api_request_download(extra_uri.as_str(), &content_root)?;
let extra_info = ExtraInfo {
hash: extra_hash,
@ -251,7 +256,9 @@ impl<'a> Gog<'a> {
name: extra.name,
};
content_info.extras.insert(extra.manual_url.clone(), extra_info);
content_info
.extras
.insert(extra.manual_url.clone(), extra_info);
ConfigFiles::save_to_path(&content_info_path, &content_info)?;
}
}
@ -259,8 +266,8 @@ impl<'a> Gog<'a> {
Ok(())
}
fn get_code(&self) -> Result<String, GogError> {
let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str());
fn get_code(&mut self) -> Result<String, GogError> {
let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri().as_str());
println!("Open the following url in a browser, login to your account and paste the \
resulting code parameter.");
@ -282,12 +289,9 @@ impl<'a> Gog<'a> {
let token_uri = self.token_uri(self.client_id.as_str(),
self.client_secret.as_str(),
code,
self.redirect_uri.as_str());
self.redirect_uri().as_str());
let token_response = match self.http_client.get(token_uri.as_str()) {
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
let token_response = self.api_request_get(token_uri.as_str())?;
match serde_json::from_str(&token_response) {
Ok(value) => Ok(value),
@ -300,10 +304,7 @@ impl<'a> Gog<'a> {
self.client_secret.as_str(),
refresh_token);
let token_response = match self.http_client.get(token_refresh_uri.as_str()) {
Ok(value) => value,
Err(error) => return Err(GogError::HttpError(error)),
};
let token_response = self.api_request_get(token_refresh_uri.as_str())?;
match serde_json::from_str(&token_response) {
Ok(value) => Ok(value),
@ -312,7 +313,8 @@ impl<'a> Gog<'a> {
}
fn get_content_ids(&mut self) -> Result<Vec<u64>, GogError> {
let response = self.http_client.get(self.games_uri.as_str())?;
let games_uri = self.games_uri();
let response = self.api_request_get(games_uri.as_str())?;
let content_ids_raw: Value = serde_json::from_str(response.as_str())?;
let content_ids_serde = &content_ids_raw["owned"];
@ -347,7 +349,8 @@ impl<'a> Gog<'a> {
let mut raw_cd_keys_fix = raw_cd_keys.to_owned();
if raw_cd_keys_fix.contains("<span>") {
raw_cd_keys_fix = raw_cd_keys_fix.replace("</span><span>", ":")
raw_cd_keys_fix = raw_cd_keys_fix
.replace("</span><span>", ":")
.replace("<span>", "")
.replace("</span>", "");
}
@ -379,6 +382,41 @@ impl<'a> Gog<'a> {
return cd_keys;
}
fn api_request_ensure_token(&mut self, response: &str) -> Result<bool, GogError> {
let response_json: Value = match serde_json::from_str(response) {
Ok(value) => value,
Err(_) => return Ok(false),
};
if response_json.is_object() && response_json.as_object().unwrap().contains_key("error") &&
response_json["error"] == "invalid_grant" {
debug!("invalid grant, refreshing token...");
self.refresh_token_from_file()?;
Ok(true)
} else {
Ok(false)
}
}
fn api_request_get(&mut self, uri: &str) -> Result<String, GogError> {
let response = self.http_client.get(uri)?;
if self.api_request_ensure_token(&response)? {
Ok(self.http_client.get(uri)?)
} else {
Ok(response)
}
}
fn api_request_download(&mut self, uri: &str, path: &PathBuf) -> Result<String, GogError> {
let response = self.http_client.download(uri, path)?;
if self.api_request_ensure_token(&response)? {
Ok(self.http_client.download(uri, path)?)
} else {
Ok(response)
}
}
fn get_content(&mut self,
content_id: u64,
os_filters: &Vec<String>,
@ -398,7 +436,11 @@ impl<'a> Gog<'a> {
let downloads = &content_raw["downloads"];
if content_raw.is_object() && !content_raw.as_object().unwrap().contains_key("forumLink") {
if content_raw.is_object() &&
!content_raw
.as_object()
.unwrap()
.contains_key("forumLink") {
return Err(GogError::Error("No forumLink property"));
}
@ -479,11 +521,11 @@ impl<'a> Gog<'a> {
let data = Data {
manual_url: String::from(download["manualUrl"]
.as_str()
.unwrap()),
.as_str()
.unwrap()),
version: String::from(download["version"]
.as_str()
.unwrap_or("")),
.as_str()
.unwrap_or("")),
os: system.clone(),
language: data_language.clone(),
};
@ -500,6 +542,14 @@ impl<'a> Gog<'a> {
Ok(content)
}
fn games_uri(&self) -> String {
String::from("https://embed.gog.com/user/data/games")
}
fn redirect_uri(&self) -> String {
String::from("https://embed.gog.com/on_login_success?origin=client")
}
fn auth_uri(&self, client_id: &str, redirect_uri: &str) -> String {
format!("https://auth.gog.\
com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2",

View File

@ -28,50 +28,50 @@ fn main() {
env_logger::init().unwrap();
let matches = App::new("Gog Synchronizer")
.version("0.3.0")
.version("0.3.1")
.author("Sebastian Hugentobler <sebastian@vanwa.ch>")
.about("Synchronizes your gog library to a local folder.")
.arg(Arg::with_name("game-storage")
.short("s")
.long("game-storage")
.value_name("FOLDER")
.help("Sets the download folder (defaults to the working directory).")
.takes_value(true))
.short("s")
.long("game-storage")
.value_name("FOLDER")
.help("Sets the download folder (defaults to the working directory).")
.takes_value(true))
.arg(Arg::with_name("movie-storage")
.short("m")
.long("movie-storage")
.value_name("FOLDER")
.help("Sets the download folder for movies (defaults to the working directory).")
.takes_value(true))
.short("m")
.long("movie-storage")
.value_name("FOLDER")
.help("Sets the download folder for movies (defaults to the working directory).")
.takes_value(true))
.arg(Arg::with_name("os")
.short("o")
.long("os")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of operating systems.\n\
.short("o")
.long("os")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of operating systems.\n\
Valid values are 'linux', 'mac' and 'windows'.")
.takes_value(true))
.takes_value(true))
.arg(Arg::with_name("language")
.short("l")
.long("language")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of languages.")
.takes_value(true))
.short("l")
.long("language")
.value_name("FILTER")
.help("Only sync files for this comma seperated list of languages.")
.takes_value(true))
.arg(Arg::with_name("resolution")
.short("r")
.long("resolution")
.value_name("FILTER")
.help("Only sync movies for this comma seperated list of resolutions.")
.takes_value(true))
.short("r")
.long("resolution")
.value_name("FILTER")
.help("Only sync movies for this comma seperated list of resolutions.")
.takes_value(true))
.arg(Arg::with_name("skip-movies")
.short("f")
.long("skip-movies")
.help("Skip movie content.")
.takes_value(false))
.short("f")
.long("skip-movies")
.help("Skip movie content.")
.takes_value(false))
.arg(Arg::with_name("skip-games")
.short("g")
.long("skip-games")
.help("Skip game content.")
.takes_value(false))
.short("g")
.long("skip-games")
.help("Skip game content.")
.takes_value(false))
.get_matches();
let configfiles = ConfigFiles::new();