add error handling
This commit is contained in:
parent
65692ec481
commit
5ce0b953cb
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug",
|
||||||
|
"type": "lldb-mi",
|
||||||
|
"request": "launch",
|
||||||
|
"target": "./target/debug/gog-backup",
|
||||||
|
"cwd": "${workspaceRoot}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
241
Cargo.lock
generated
241
Cargo.lock
generated
@ -2,11 +2,69 @@
|
|||||||
name = "gog-backup"
|
name = "gog-backup"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
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)",
|
"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 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_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)",
|
"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)",
|
"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]]
|
[[package]]
|
||||||
@ -39,6 +97,15 @@ name = "dtoa"
|
|||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "gcc"
|
name = "gcc"
|
||||||
version = "0.3.43"
|
version = "0.3.43"
|
||||||
@ -68,6 +135,15 @@ name = "itoa"
|
|||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@ -83,11 +159,51 @@ dependencies = [
|
|||||||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.1.37"
|
version = "0.1.37"
|
||||||
@ -120,6 +236,28 @@ name = "quote"
|
|||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "0.9.11"
|
version = "0.9.11"
|
||||||
@ -154,6 +292,11 @@ dependencies = [
|
|||||||
"serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@ -172,6 +315,52 @@ dependencies = [
|
|||||||
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@ -185,6 +374,16 @@ name = "unicode-normalization"
|
|||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
@ -208,6 +407,16 @@ dependencies = [
|
|||||||
"winapi-build 0.1.1 (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 = "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]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
@ -218,32 +427,64 @@ name = "winapi-build"
|
|||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]
|
[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 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 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 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 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 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 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 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 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 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 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 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-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 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 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 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 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_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_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 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 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 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-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-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 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 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 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 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 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"
|
||||||
|
12
Cargo.toml
12
Cargo.toml
@ -1,11 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gog-backup"
|
name = "gog-backup"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Sebastian Hugentobler <sebastian.hugentobler@idparc.ch>"]
|
authors = ["Sebastian Hugentobler <sebastian@vanwa.ch>"]
|
||||||
|
|
||||||
[dependencies]
|
[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 = "0.9.11"
|
||||||
serde_derive = "0.9.11"
|
serde_derive = "0.9.11"
|
||||||
serde_json = "0.9"
|
serde_json = "0.9"
|
||||||
curl = "0.4.6"
|
toml = "0.3.1"
|
||||||
|
url = "1.4.0"
|
||||||
|
xdg = "2.1.0"
|
||||||
|
71
src/config.rs
Normal file
71
src/config.rs
Normal file
@ -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<std::io::Error> for ConfigError {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
ConfigError::IOError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for ConfigError {
|
||||||
|
fn from(e: toml::de::Error) -> Self {
|
||||||
|
ConfigError::TomlDeError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::ser::Error> 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<T>(&self, name: &str) -> Result<T, ConfigError>
|
||||||
|
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<T>(&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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
293
src/gog.rs
Normal file
293
src/gog.rs
Normal file
@ -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<ConfigError> for GogError {
|
||||||
|
fn from(e: ConfigError) -> Self {
|
||||||
|
GogError::ConfigError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HttpError> for GogError {
|
||||||
|
fn from(e: HttpError) -> Self {
|
||||||
|
GogError::HttpError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for GogError {
|
||||||
|
fn from(e: serde_json::Error) -> Self {
|
||||||
|
GogError::SerdeError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> 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<String, GogError> {
|
||||||
|
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<Token, GogError> {
|
||||||
|
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<Token, GogError> {
|
||||||
|
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<Vec<u64>, 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<u64> = 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<Game, GogError> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
137
src/http.rs
Normal file
137
src/http.rs
Normal file
@ -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<curl::Error> for HttpError {
|
||||||
|
fn from(e: curl::Error) -> Self {
|
||||||
|
HttpError::CurlError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Utf8Error> for HttpError {
|
||||||
|
fn from(e: Utf8Error) -> Self {
|
||||||
|
HttpError::Utf8Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for HttpError {
|
||||||
|
fn from(e: io::Error) -> Self {
|
||||||
|
HttpError::IOError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<url::ParseError> for HttpError {
|
||||||
|
fn from(e: url::ParseError) -> Self {
|
||||||
|
HttpError::UrlParseError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HttpError> 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<String, HttpError> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
202
src/main.rs
202
src/main.rs
@ -1,182 +1,48 @@
|
|||||||
extern crate url;
|
extern crate chrono;
|
||||||
|
extern crate clap;
|
||||||
extern crate curl;
|
extern crate curl;
|
||||||
|
extern crate env_logger;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
extern crate toml;
|
||||||
|
extern crate url;
|
||||||
|
extern crate xdg;
|
||||||
|
|
||||||
use std::io;
|
mod config;
|
||||||
use std::fs::File;
|
mod gog;
|
||||||
use std::io::Write;
|
mod http;
|
||||||
use curl::easy::{Easy, List};
|
mod models;
|
||||||
use std::str;
|
|
||||||
use serde_json::Value;
|
|
||||||
use url::{Url, Host};
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
use config::Config;
|
||||||
struct Token {
|
use clap::{Arg, App};
|
||||||
access_token: String,
|
use gog::Gog;
|
||||||
expires_in: u16,
|
use http::Http;
|
||||||
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<Installer>,
|
|
||||||
extras: Vec<Extra>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
const CLIENT_ID: &'static str = "46899977096215655";
|
env_logger::init().unwrap();
|
||||||
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";
|
|
||||||
|
|
||||||
let auth_url = format!("https://auth.gog.\
|
let matches = App::new("Gog Synchronizer")
|
||||||
com/auth?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&layout=client2",
|
.version("0.1.0")
|
||||||
client_id = CLIENT_ID,
|
.author("Sebastian Hugentobler <sebastian@vanwa.ch>")
|
||||||
redirect_uri = REDIRECT_URI_QUOTED);
|
.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: ");
|
let mut http_client = Http::new();
|
||||||
io::stdout().flush().unwrap();
|
let mut gog = Gog::new(&mut http_client);
|
||||||
|
gog.login().unwrap();
|
||||||
io::stdin()
|
gog.sync(download_folder).unwrap();
|
||||||
.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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
89
src/models.rs
Normal file
89
src/models.rs
Normal file
@ -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<Installer>,
|
||||||
|
pub extras: Vec<Extra>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<T>(obj: &T) -> u64
|
||||||
|
where T: Hash
|
||||||
|
{
|
||||||
|
let mut s = DefaultHasher::new();
|
||||||
|
obj.hash(&mut s);
|
||||||
|
s.finish()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user