From 5ce0b953cbcae781d601abd5388f94c126b7a073 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Mon, 20 Mar 2017 13:14:49 +0100 Subject: [PATCH] add error handling --- .vscode/launch.json | 12 ++ Cargo.lock | 241 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 12 +- src/config.rs | 71 +++++++++++ src/gog.rs | 293 ++++++++++++++++++++++++++++++++++++++++++++ src/http.rs | 137 +++++++++++++++++++++ src/main.rs | 202 +++++------------------------- src/models.rs | 89 ++++++++++++++ 8 files changed, 886 insertions(+), 171 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/config.rs create mode 100644 src/gog.rs create mode 100644 src/http.rs create mode 100644 src/models.rs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2463afa --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug", + "type": "lldb-mi", + "request": "launch", + "target": "./target/debug/gog-backup", + "cwd": "${workspaceRoot}" + } + ] +} diff --git a/Cargo.lock b/Cargo.lock index fa640b9..9a69ca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,69 @@ name = "gog-backup" version = "0.1.0" dependencies = [ + "chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.21.1 (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)", "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aho-corasick" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "atty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.21.1" +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)", + "atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term_size 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -39,6 +97,15 @@ name = "dtoa" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "env_logger" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gcc" version = "0.3.43" @@ -68,6 +135,15 @@ name = "itoa" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "libc" version = "0.2.21" @@ -83,11 +159,51 @@ dependencies = [ "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "log" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "matches" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "memchr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-iter" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num-traits" version = "0.1.37" @@ -120,6 +236,28 @@ name = "quote" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "redox_syscall" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "regex" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "serde" version = "0.9.11" @@ -154,6 +292,11 @@ dependencies = [ "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "strsim" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "syn" version = "0.11.9" @@ -172,6 +315,52 @@ dependencies = [ "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "term_size" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread-id" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "unicode-bidi" version = "0.2.5" @@ -185,6 +374,16 @@ name = "unicode-normalization" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-segmentation" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-xid" version = "0.0.4" @@ -208,6 +407,16 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "winapi" version = "0.2.8" @@ -218,32 +427,64 @@ name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "xdg" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [metadata] +"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" +"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" +"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" +"checksum bitflags 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e1ab483fc81a8143faa7203c4a3c02888ebd1a782e37e41fa34753ba9a162" +"checksum chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "158b0bd7d75cbb6bf9c25967a48a2e9f77da95876b858eadfabaa99cd069de6e" +"checksum clap 2.21.1 (registry+https://github.com/rust-lang/crates.io-index)" = "74a80f603221c9cd9aa27a28f52af452850051598537bb6b359c38a7d61e5cda" "checksum curl 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c90e1240ef340dd4027ade439e5c7c2064dd9dc652682117bd50d1486a3add7b" "checksum curl-sys 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d909dc402ae80b6f7b0118c039203436061b9d9a3ca5d2c2546d93e0a61aaa" "checksum dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "80c8b71fd71146990a9742fc06dcbbde19161a267e0ad4e572c35162f4578c90" +"checksum env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f" "checksum gcc 0.3.43 (registry+https://github.com/rust-lang/crates.io-index)" = "c07c758b972368e703a562686adb39125707cc1ef3399da8c019fc6c2498a75d" "checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518" "checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11" "checksum itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2f404fbc66fd9aac13e998248505e7ecb2ad8e44ab6388684c5fb11c6c251c" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" "checksum libz-sys 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e5ee912a45d686d393d5ac87fac15ba0ba18daae14e8e7543c63ebf7fb7e970c" +"checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" "checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1" +"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" +"checksum num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "98b15ba84e910ea7a1973bccd3df7b31ae282bf9d8bd2897779950c9b8303d40" +"checksum num-integer 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "21e4df1098d1d797d27ef0c69c178c3fab64941559b290fcae198e0825c9c8b5" +"checksum num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d1891bd7b936f12349b7d1403761c8a0b85a18b148e9da4429d5d102c1a41e" "checksum num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "e1cbfa3781f3fe73dc05321bed52a06d2d491eaa764c52335cf4399f046ece99" "checksum openssl-probe 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "756d49c8424483a3df3b5d735112b4da22109ced9a8294f1f5cdf80fb3810919" "checksum openssl-sys 0.9.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ddd1154228cf62c05114a82e26b1a0093f90815367699ee3f3dafb62b9602111" "checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum redox_syscall 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "8dd35cc9a8bdec562c757e3d43c1526b5c6d2653e23e2315065bc25556550753" +"checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" +"checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" "checksum serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)" = "a702319c807c016e51f672e5c77d6f0b46afddd744b5e437d6b8436b888b458f" "checksum serde_codegen_internals 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d52006899f910528a10631e5b727973fe668f3228109d1707ccf5bad5490b6e" "checksum serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)" = "f15ea24bd037b2d64646b4d934fa99c649be66e3f7b29fb595a5543b212b1452" "checksum serde_json 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc45439552eb8fb86907a2c41c1fd0ef97458efb87ff7f878db466eb581824e" +"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" "checksum syn 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)" = "480c834701caba3548aa991e54677281be3a5414a9d09ddbdf4ed74a569a9d19" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum term_size 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "07b6c1ac5b3fffd75073276bca1ceed01f67a28537097a2a9539e116e50fb21a" +"checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" +"checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" +"checksum time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "211b63c112206356ef1ff9b19355f43740fc3f85960c598a93d3a3d3ba7beade" +"checksum toml 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3474f3c6eaf32eedb4f4a66a26214f020f828a6d96c37e38a35e3a379bbcfd11" "checksum unicode-bidi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a078ebdd62c0e71a709c3d53d2af693fe09fe93fbff8344aebe289b78f9032" "checksum unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e28fa37426fceeb5cf8f41ee273faa7c82c47dc8fba5853402841e665fcd86ff" +"checksum unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18127285758f0e2c6cf325bb3f3d138a12fee27de4f23e146cd6a179f26c2cf3" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" "checksum url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5ba8a749fb4479b043733416c244fa9d1d3af3d7c23804944651c8a448cb87e" "checksum user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47" +"checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" +"checksum vec_map 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8cdc8b93bd0198ed872357fb2e667f7125646b1762f16d60b2c96350d361897" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a66b7c2281ebde13cf4391d70d4c7e5946c3c25e72a7b859ca8f677dcd0b0c61" diff --git a/Cargo.toml b/Cargo.toml index fe8b11b..b8fbb0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,17 @@ [package] name = "gog-backup" version = "0.1.0" -authors = ["Sebastian Hugentobler "] +authors = ["Sebastian Hugentobler "] [dependencies] -url = "1.4.0" +chrono = "0.3.0" +clap = "2.21.1" +curl = "0.4.6" +env_logger = "0.3" +log = "0.3" serde = "0.9.11" serde_derive = "0.9.11" serde_json = "0.9" -curl = "0.4.6" +toml = "0.3.1" +url = "1.4.0" +xdg = "2.1.0" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..b6c71d2 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std; +use std::io::{Read, Write}; +use toml; +use xdg; +use xdg::BaseDirectories; + +#[derive(Debug)] +pub enum ConfigError { + IOError(std::io::Error), + TomlDeError(toml::de::Error), + TomlSeError(toml::ser::Error), +} + +impl From for ConfigError { + fn from(e: std::io::Error) -> Self { + ConfigError::IOError(e) + } +} + +impl From for ConfigError { + fn from(e: toml::de::Error) -> Self { + ConfigError::TomlDeError(e) + } +} + +impl From for ConfigError { + fn from(e: toml::ser::Error) -> Self { + ConfigError::TomlSeError(e) + } +} + +pub struct Config { + pub xdg_dirs: BaseDirectories, +} + +impl Config { + pub fn new() -> Config { + let xdg_dirs = xdg::BaseDirectories::with_prefix("gog-sync").unwrap(); + Config { xdg_dirs: xdg_dirs } + } + + pub fn load(&self, name: &str) -> Result + where T: Deserialize + { + let config_path = self.xdg_dirs.place_config_file(name)?; + let mut config_file = File::open(config_path)?; + + let mut config_contents = String::new(); + config_file.read_to_string(&mut config_contents)?; + + match toml::from_str(config_contents.as_str()) { + Ok(value) => Ok(value), + Err(error) => Err(ConfigError::TomlDeError(error)), + } + } + + pub fn save(&self, name: &str, content: &T) -> Result<(), ConfigError> + where T: Serialize + { + let config_path = self.xdg_dirs.place_config_file(name)?; + let content_toml = toml::to_string(content)?; + + let mut config_file = File::create(&config_path)?; + match config_file.write_all(content_toml.as_bytes()) { + Ok(_) => Ok(()), + Err(error) => Err(ConfigError::IOError(error)), + } + } +} diff --git a/src/gog.rs b/src/gog.rs new file mode 100644 index 0000000..fefe81a --- /dev/null +++ b/src/gog.rs @@ -0,0 +1,293 @@ +use config::{Config, ConfigError}; +use http::{Http, HttpError}; +use models; +use models::{Token, Game, Installer}; +use serde_json; +use serde_json::Value; +use std::fs; +use std::io; +use std::io::Write; +use std::path::Path; + +#[derive(Debug)] +pub enum GogError { + Error(&'static str), + ConfigError(ConfigError), + HttpError(HttpError), + SerdeError(serde_json::Error), + IOError(io::Error), +} + +impl From for GogError { + fn from(e: ConfigError) -> Self { + GogError::ConfigError(e) + } +} + +impl From for GogError { + fn from(e: HttpError) -> Self { + GogError::HttpError(e) + } +} + +impl From for GogError { + fn from(e: serde_json::Error) -> Self { + GogError::SerdeError(e) + } +} + +impl From for GogError { + fn from(e: io::Error) -> Self { + GogError::IOError(e) + } +} + +pub struct Gog<'a> { + client_id: String, + client_secret: String, + redirect_uri: String, + games_uri: String, + http_client: &'a mut Http, +} + +impl<'a> Gog<'a> { + pub fn new(http_client: &'a mut Http) -> 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, + } + } + + pub fn login(&mut self) -> Result<(), GogError> { + let config = Config::new(); + let mut token: Token = match config.load("token.toml") { + Ok(value) => value, + Err(_) => { + let code = self.get_code()?; + self.get_token(code.as_str())? + } + }; + + if token.is_expired() { + token = self.refresh_token(token.refresh_token.as_str())?; + } + + config.save("token.toml", &token)?; + + let auth_header = format!("Authorization: Bearer {token}", token = token.access_token); + self.http_client.add_header(auth_header.as_str())?; + + Ok(()) + } + + pub fn sync(&mut self, storage_path: &str) -> Result<(), GogError> { + let game_ids = self.get_game_ids()?; + + for game_id in game_ids { + let game = self.get_game(game_id)?; + let game_hash = models::get_hash(&game); + + let game_root = Path::new(storage_path).join(&game.title); + fs::create_dir_all(&game_root)?; + + for installer in game.installers { + let installer_hash = models::get_hash(&installer); + + let installer_uri = format!("https://embed.gog.com{}", installer.manual_url); + + info!("downloading {} for {}...", + &installer.manual_url, + &game.title); + self.http_client.download(installer_uri.as_str(), &game_root)?; + } + + for extra in game.extras { + let extra_hash = models::get_hash(&extra); + + let extra_uri = format!("https://embed.gog.com{}", extra.manual_url); + + info!("downloading {} for {}...", &extra.name, &game.title); + self.http_client.download(extra_uri.as_str(), &game_root)?; + } + } + + Ok(()) + } + + fn get_code(&self) -> Result { + let auth_uri = self.auth_uri(self.client_id.as_str(), self.redirect_uri.as_str()); + + println!("{}", auth_uri); + + let mut code = String::new(); + + print!("Code: "); + io::stdout().flush()?; + + io::stdin() + .read_line(&mut code) + .expect("Failed to read line"); + + Ok(code) + } + + fn get_token(&mut self, code: &str) -> Result { + let token_uri = self.token_uri(self.client_id.as_str(), + self.client_secret.as_str(), + code, + 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)), + }; + + match serde_json::from_str(&token_response) { + Ok(value) => Ok(value), + Err(error) => Err(GogError::SerdeError(error)), + } + } + + fn refresh_token(&mut self, refresh_token: &str) -> Result { + let token_refresh_uri = self.token_refresh_uri(self.client_id.as_str(), + 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)), + }; + + match serde_json::from_str(&token_response) { + Ok(value) => Ok(value), + Err(error) => Err(GogError::SerdeError(error)), + } + } + + fn get_game_ids(&mut self) -> Result, GogError> { + let response = self.http_client.get(self.games_uri.as_str())?; + let game_ids_raw: Value = serde_json::from_str(response.as_str())?; + let game_ids_serde = &game_ids_raw["owned"]; + + let mut game_ids: Vec = Vec::new(); + + if !game_ids_serde.is_array() { + return Err(GogError::Error("Error parsing game ids.")); + } + + for game_id in game_ids_serde.as_array().unwrap() { + let game_id_parsed = game_id.as_u64().unwrap_or(0); + + if game_id_parsed == 0 { + error!("Cant parse game id {}", game_id); + continue; + } + + game_ids.push(game_id_parsed); + } + + return Ok(game_ids); + } + + fn get_game(&mut self, game_id: u64) -> Result { + let game_uri = self.game_uri(game_id); + + let response = self.http_client.get(game_uri.as_str())?; + + let mut game: Game = serde_json::from_str(&response)?; + + let game_raw: Value = serde_json::from_str(response.as_str())?; + let downloads = &game_raw["downloads"]; + + for languages in downloads.as_array() { + for language in languages { + if !language.is_array() || language.as_array().unwrap().len() < 2 { + error!("Skipping a language for {}", game.title); + continue; + } + + let installer_language = match language[0].as_str() { + Some(value) => value, + None => "", + }; + + if installer_language == "" { + error!("Skipping a language for {}", game.title); + continue; + } + + for systems in language[1].as_object() { + for system in systems.keys() { + for real_downloads in systems.get(system) { + for real_download in real_downloads.as_array() { + let download = &real_download[0]; + + if !download.is_object() || + !download.as_object().unwrap().contains_key("manualUrl") { + error!("Skipping an installer for {}", game.title); + continue; + } + + let installer = Installer { + manual_url: String::from(download["manualUrl"] + .as_str() + .unwrap()), + version: String::from(download["version"] + .as_str() + .unwrap_or("")), + os: system.clone(), + language: String::from(installer_language), + }; + + game.installers.push(installer); + } + } + } + } + } + } + + Ok(game) + } + + 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", + client_id = client_id, + redirect_uri = redirect_uri) + } + + fn token_uri(&self, + client_id: &str, + client_secret: &str, + code: &str, + redirect_uri: &str) + -> String { + format!("https://auth.gog.\ + com/token?client_id={client_id}&client_secret={client_secret}&grant_type=authorization_code&code={code}&redirect_uri={redirect_uri}", + client_id = client_id, + client_secret = client_secret, + code = code.trim(), + redirect_uri = redirect_uri) + } + + fn token_refresh_uri(&self, + client_id: &str, + client_secret: &str, + refresh_token: &str) + -> String { + format!("https://auth.gog.\ + com/token?client_id={client_id}&client_secret={client_secret}&grant_type=refresh_token&refresh_token={refresh_token}", + client_id = client_id, + client_secret = client_secret, + refresh_token = refresh_token) + } + + fn game_uri(&self, game_id: u64) -> String { + format!("https://embed.gog.com/account/gameDetails/{game_id}.json", + game_id = game_id) + } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..1ee3ed4 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,137 @@ +use curl; +use curl::easy::{Easy, List, WriteError}; +use std::fs; +use std::fs::File; +use std::io; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::str; +use std::str::Utf8Error; +use url; +use url::Url; + +#[derive(Debug)] +pub enum HttpError { + Error(&'static str), + CurlError(curl::Error), + Utf8Error(Utf8Error), + IOError(io::Error), + UrlParseError(url::ParseError), +} + +impl From for HttpError { + fn from(e: curl::Error) -> Self { + HttpError::CurlError(e) + } +} + +impl From for HttpError { + fn from(e: Utf8Error) -> Self { + HttpError::Utf8Error(e) + } +} + +impl From for HttpError { + fn from(e: io::Error) -> Self { + HttpError::IOError(e) + } +} + +impl From for HttpError { + fn from(e: url::ParseError) -> Self { + HttpError::UrlParseError(e) + } +} + +impl From for WriteError { + fn from(e: HttpError) -> Self { + WriteError::__Nonexhaustive + } +} + +pub struct Http { + curl: Easy, +} + +impl Http { + pub fn new() -> Http { + let mut curl = Easy::new(); + curl.follow_location(true).unwrap(); + + Http { curl: curl } + } + + pub fn get(&mut self, uri: &str) -> Result { + let mut response_body = String::new(); + + self.curl.url(uri)?; + { + let mut transfer = self.curl.transfer(); + transfer.write_function(|data| { + response_body = String::from(str::from_utf8(data).unwrap()); + Ok(data.len()) + })?; + + transfer.perform()?; + } + + Ok(response_body) + } + + pub fn add_header(&mut self, header: &str) -> Result<(), HttpError> { + let mut list = List::new(); + list.append(header)?; + + match self.curl.http_headers(list) { + Ok(_) => Ok(()), + Err(error) => Err(HttpError::CurlError(error)), + } + } + + pub fn download(&mut self, + download_uri: &str, + download_dir: &PathBuf) + -> Result<(), HttpError> { + let download_path_tmp = Path::new(download_dir.as_os_str()).join(".progress"); + let mut file_download = File::create(&download_path_tmp)?; + + self.curl.url(download_uri)?; + { + let mut transfer = self.curl.transfer(); + transfer.write_function(|data| { + match file_download.write(data) { + Ok(_) => Ok(()), + Err(error) => Err(HttpError::IOError(error)), + }?; + Ok(data.len()) + })?; + transfer.perform()?; + } + + let download_url_string = match self.curl.effective_url()? { + Some(value) => value, + None => return Err(HttpError::Error("Can't get effective download url.")), + }; + + let download_url = Url::parse(download_url_string)?; + + let download_url_segments = match download_url.path_segments() { + Some(value) => value, + None => return Err(HttpError::Error("Can't parse download segments.")), + }; + + let file_name = match download_url_segments.last() { + Some(value) => value, + None => return Err(HttpError::Error("No segments in download url.")), + }; + + let download_path = Path::new(download_dir.as_os_str()).join(file_name); + match fs::rename(download_path_tmp, download_path) { + Ok(_) => Ok(()), + Err(error) => Err(HttpError::IOError(error)), + }?; + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index a0d3172..e7126da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,182 +1,48 @@ -extern crate url; +extern crate chrono; +extern crate clap; extern crate curl; +extern crate env_logger; +#[macro_use] +extern crate log; extern crate serde; extern crate serde_json; #[macro_use] extern crate serde_derive; +extern crate toml; +extern crate url; +extern crate xdg; -use std::io; -use std::fs::File; -use std::io::Write; -use curl::easy::{Easy, List}; -use std::str; -use serde_json::Value; -use url::{Url, Host}; -use std::path::Path; +mod config; +mod gog; +mod http; +mod models; -#[derive(Serialize, Deserialize)] -struct Token { - access_token: String, - expires_in: u16, - token_type: String, - scope: String, - session_id: String, - refresh_token: String, - user_id: String, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Game { - title: String, - cd_key: String, - #[serde(skip_deserializing)] - installers: Vec, - extras: Vec, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Installer { - manual_url: String, - version: String, - os: String, - language: String, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct Extra { - manual_url: String, - name: String, -} +use config::Config; +use clap::{Arg, App}; +use gog::Gog; +use http::Http; fn main() { - const CLIENT_ID: &'static str = "46899977096215655"; - const CLIENT_SECRET: &'static str = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"; - // const redirect_uri: &'static str = "https://embed.gog.com/on_login_success?origin=client"; - const REDIRECT_URI_QUOTED: &'static str = "https%3A%2F%2Fembed.gog.\ - com%2Fon_login_success%3Forigin%3Dclient"; + env_logger::init().unwrap(); - let auth_url = format!("https://auth.gog.\ - com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2", - client_id = CLIENT_ID, - redirect_uri = REDIRECT_URI_QUOTED); + let matches = App::new("Gog Synchronizer") + .version("0.1.0") + .author("Sebastian Hugentobler ") + .about("Synchronizes your gog library to a local folder.") + .arg(Arg::with_name("storage") + .short("s") + .long("storage") + .value_name("FOLDER") + .help("Sets the download folder (defaults to the working directory).") + .takes_value(true)) + .get_matches(); - println!("{}", auth_url); + let download_folder = matches.value_of("storage").unwrap_or("."); - let mut code = String::new(); + let config = Config::new(); - print!("Code: "); - io::stdout().flush().unwrap(); - - io::stdin() - .read_line(&mut code) - .expect("Failed to read line"); - - let token_query = format!("https://auth.gog.\ - com/token?client_id={client_id}&client_secret={client_secret}&grant_type=authorization_code&code={code}&redirect_uri={redirect_uri_quoted}", - client_id = CLIENT_ID, - client_secret = CLIENT_SECRET, - code = code.trim(), - redirect_uri_quoted = REDIRECT_URI_QUOTED); - - let mut token_response_body = String::new(); - - let mut curl = Easy::new(); - curl.url(token_query.as_str()).unwrap(); - { - let mut transfer = curl.transfer(); - transfer.write_function(|data| { - token_response_body = String::from(str::from_utf8(data).unwrap()); - Ok(data.len()) - }) - .unwrap(); - transfer.perform().unwrap(); - } - - let token: Token = serde_json::from_str(&token_response_body).unwrap(); - - let mut list = List::new(); - list.append(format!("Authorization: Bearer {token}", token = token.access_token).as_str()) - .unwrap(); - curl.http_headers(list).unwrap(); - curl.follow_location(true); - - let mut games_response_body = String::new(); - curl.url("https://embed.gog.com/user/data/games").unwrap(); - { - let mut transfer = curl.transfer(); - transfer.write_function(|data| { - games_response_body = String::from(str::from_utf8(data).unwrap()); - Ok(data.len()) - }) - .unwrap(); - transfer.perform().unwrap(); - } - - let game_ids_raw: Value = serde_json::from_str(games_response_body.as_str()).unwrap(); - let game_ids = &game_ids_raw["owned"]; - - let mut game_response_body = String::new(); - let game_query = format!("https://embed.gog.com/account/gameDetails/{game_id}.json", - game_id = game_ids[10]); - curl.url(game_query.as_str()).unwrap(); - { - let mut transfer = curl.transfer(); - transfer.write_function(|data| { - game_response_body = String::from(str::from_utf8(data).unwrap()); - Ok(data.len()) - }) - .unwrap(); - transfer.perform().unwrap(); - } - - let mut game: Game = serde_json::from_str(&game_response_body).unwrap(); - - let game_raw: Value = serde_json::from_str(game_response_body.as_str()).unwrap(); - let downloads = &game_raw["downloads"]; - - for languages in downloads.as_array() { - for language in languages { - for systems in language[1].as_object() { - for system in systems.keys() { - for real_downloads in systems.get(system) { - for real_download in real_downloads.as_array() { - let download = &real_download[0]; - - let installer = Installer { - manual_url: String::from(download["manualUrl"].as_str().unwrap()), - version: String::from(download["manualUrl"].as_str().unwrap()), - os: system.clone(), - language: String::from(language[0].as_str().unwrap()), - }; - - game.installers.push(installer); - } - } - } - } - } - } - - let download_query = format!("https://embed.gog.com{}", game.installers[0].manual_url); - curl.url(download_query.as_str()).unwrap(); - curl.perform().unwrap(); - - let download_url = Url::parse(curl.effective_url().unwrap().unwrap()).unwrap(); - let file_name = download_url.path_segments().unwrap().last().unwrap(); - - let download_path = Path::new("/Users/sebastianhugentobler/Downloads/").join(file_name); - let mut file_download = File::create(download_path).unwrap(); - curl.url(download_query.as_str()).unwrap(); - { - let mut transfer = curl.transfer(); - transfer.write_function(|data| { - file_download.write(data); - Ok(data.len()) - }) - .unwrap(); - transfer.perform().unwrap(); - } + let mut http_client = Http::new(); + let mut gog = Gog::new(&mut http_client); + gog.login().unwrap(); + gog.sync(download_folder).unwrap(); } diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..30c4e19 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,89 @@ +use chrono::UTC; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::collections::hash_map::DefaultHasher; + +#[derive(Serialize, Deserialize)] +pub struct Token { + pub access_token: String, + pub expires_in: u16, + pub token_type: String, + pub scope: String, + pub session_id: String, + pub refresh_token: String, + pub user_id: String, + #[serde(default = "timestamp")] + pub timestamp: i64, +} + +impl Token { + pub fn is_expired(&self) -> bool { + let now = UTC::now().timestamp(); + + now > self.timestamp + self.expires_in as i64 + } +} + +fn timestamp() -> i64 { + UTC::now().timestamp() +} + +#[derive(Hash)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Game { + pub title: String, + pub cd_key: String, + #[serde(skip_deserializing)] + pub installers: Vec, + pub extras: Vec, +} + +impl fmt::Display for Game { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({})", self.title) + } +} + +#[derive(Hash)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Installer { + pub manual_url: String, + pub version: String, + pub os: String, + pub language: String, +} + +impl fmt::Display for Installer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "({}, {}, {}, {})", + self.manual_url, + self.version, + self.os, + self.language) + } +} + +#[derive(Hash)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Extra { + pub manual_url: String, + pub name: String, +} + +impl fmt::Display for Extra { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({}, {})", self.manual_url, self.name) + } +} + +pub fn get_hash(obj: &T) -> u64 + where T: Hash +{ + let mut s = DefaultHasher::new(); + obj.hash(&mut s); + s.finish() +}