diff --git a/.gitignore b/.gitignore index ea8c4bf..0b745e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..71a0365 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,667 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "backup-rs" +version = "0.1.0" +dependencies = [ + "chrono", + "dotenv", + "hex", + "main_error", + "md5", + "serde", + "smol_str", + "thiserror", + "ureq", +] + +[[package]] +name = "base-x" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "cc" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" +dependencies = [ + "num-integer", + "num-traits", + "serde", + "time 0.1.43", +] + +[[package]] +name = "chunked_transfer" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d29eb15132782371f71da8f947dba48b3717bdb6fa771b9b434d645e40a7193" + +[[package]] +name = "cookie" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c60ef6d0bbf56ad2674249b6bb74f2c6aeb98b98dd57b5d3e37cace33011d69" +dependencies = [ + "percent-encoding", + "time 0.2.16", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "js-sys" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4b9172132a62451e56142bff9afc91c8e4a4500aa5b847da36815b63bfda916" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "main_error" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3516df0fb44d98fe6d6e859d224adfb7b6686447937e5b96308d6061595eed04" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "proc-macro-hack" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" + +[[package]] +name = "proc-macro2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.16.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +dependencies = [ + "base64 0.11.0", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "smol_str" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7909a1d8bc166a862124d84fdc11bda0ea4ed3157ccca662296919c2972db1" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "standback" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0437cfb83762844799a60e1e3b489d5ceb6a650fbacb86437badc1b6d87b246" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "syn" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cae2873c940d92e697597c5eee105fb570cd5689c695806f672883653349b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "time" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a51cadc5b1eec673a685ff7c33192ff7b7603d0b75446fb354939ee615acb15" +dependencies = [ + "cfg-if", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi", +] + +[[package]] +name = "time-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9b6e9f095bc105e183e3cd493d72579be3181ad4004fceb01adbe9eecab2d" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677df6896edc382f1a2abcbb3e4058edfe973cdc4e1ed764b11891a7a289bfc0" +dependencies = [ + "base64 0.12.3", + "chunked_transfer", + "cookie", + "lazy_static", + "qstring", + "rustls", + "serde", + "serde_json", + "url", + "webpki", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +dependencies = [ + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wasm-bindgen" +version = "0.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae" + +[[package]] +name = "web-sys" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863539788676619aac1a23e2df3655e96b32b0e05eb72ca34ba045ad573c625d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 4e67cfe..d6b690e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,13 @@ version = "0.1.0" authors = ["Robin Appelman "] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +serde = { version = "1.0", features = ["derive"] } +ureq = { version = "1.3", features = ["json"] } +thiserror = "1.0" +chrono = { version = "0.4", features = ["serde"] } +smol_str = { version = "0.1", features = ["serde"] } +md5 = "0.7" +hex = "0.4" +dotenv = "0.15" +main_error = "0.1" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..02a2922 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# backup-rs + +Backup program for demos.tf demos. + +A simple program that incrementally backs up every demo file from demos.tf to a local directory. + +## Usage + +The following environment variables are required for the program + + STORAGE_ROOT: The directory to store the demos in + STATE_FILE: The textfile to store the backup progress in between runs + +The program will look in a .env file if the variables aren't set in the environment \ No newline at end of file diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..27d197e --- /dev/null +++ b/src/api.rs @@ -0,0 +1,103 @@ +use crate::Error; +use chrono::{DateTime, Utc}; +use md5::Digest; +use serde::{Deserialize, Deserializer}; +use smol_str::SmolStr; +use std::fmt; + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Demo { + pub id: u32, + pub url: String, + pub name: String, + pub server: SmolStr, + pub duration: u16, + pub nick: SmolStr, + pub map: SmolStr, + #[serde(with = "chrono::serde::ts_seconds")] + pub time: DateTime, + pub red: SmolStr, + pub blue: SmolStr, + pub red_score: u8, + pub blue_score: u8, + pub player_count: u8, + pub uploader: u32, + #[serde(deserialize_with = "hex_to_digest")] + pub hash: Digest, + pub backend: SmolStr, + pub path: String, +} + +/// Deserializes a lowercase hex string to a `Vec`. +pub fn hex_to_digest<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + use hex::FromHex; + use serde::de::Error; + + let string = String::deserialize(deserializer)?; + + if string.len() == 0 { + return Ok(Digest([0; 16])); + } + + <[u8; 16]>::from_hex(&string) + .map_err(|err| Error::custom(err.to_string())) + .map(Digest) +} + +#[derive(Debug)] +pub enum ListOrder { + Ascending, + Descending, +} + +impl Default for ListOrder { + fn default() -> Self { + ListOrder::Descending + } +} + +impl fmt::Display for ListOrder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ListOrder::Ascending => "ASC".fmt(f), + ListOrder::Descending => "DESC".fmt(f), + } + } +} + +#[derive(Debug, Default)] +pub struct ListParams { + order: ListOrder, + backend: Option, +} + +impl ListParams { + pub fn with_backend(self, backend: impl ToString) -> Self { + ListParams { + backend: Some(backend.to_string()), + ..self + } + } + + pub fn with_order(self, order: ListOrder) -> Self { + ListParams { order, ..self } + } +} + +pub fn list_demos(params: ListParams, page: u32) -> Result, Error> { + let mut req = ureq::get("https://api.demos.tf/demos"); + req.query("page", &format!("{}", page)) + .query("order", &format!("{}", params.order)); + + if let Some(backend) = params.backend.as_ref() { + req.query("backend", backend); + } + + let resp = req.call(); + + Ok(resp.into_json_deserialize()?) +} diff --git a/src/backup.rs b/src/backup.rs new file mode 100644 index 0000000..dff793f --- /dev/null +++ b/src/backup.rs @@ -0,0 +1,54 @@ +use crate::api::{list_demos, ListOrder, ListParams}; +use crate::store::Store; +use crate::Error; +use md5::Digest; + +pub struct Backup { + store: Store, +} + +impl Backup { + pub fn new(store: Store) -> Self { + Backup { store } + } + + fn backup_demo(&self, name: &str, url: &str, hash: Digest) -> Result<(), Error> { + let resp = ureq::get(url).call(); + + let digest = self.store.store(name, &mut resp.into_reader())?; + + if digest == hash || digest == Digest([0; 16]) { + Ok(()) + } else { + let _ = self.store.remove(name); + Err(Error::DigestMismatch { + expected: hash, + got: digest, + }) + } + } + + fn backup_page(&self, page: u32) -> Result { + let demos = list_demos(ListParams::default().with_order(ListOrder::Ascending), page)?; + + for demo in demos.iter() { + if demo.url != "" { + let name = demo.url.rsplit('/').next().unwrap(); + println!("{} {}", demo.id, name); + if !self.store.exists(name) { + self.backup_demo(name, &demo.url, demo.hash)?; + } + } + } + + Ok(demos.len()) + } + + pub fn backup_from(&self, mut page: u32) -> Result { + while self.backup_page(page)? > 0 { + page += 1; + } + + Ok(page) + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..2733468 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,46 @@ -fn main() { - println!("Hello, world!"); +mod backup; +mod store; + +use crate::backup::Backup; +use crate::store::Store; +use main_error::MainError; +use md5::Digest; +use std::cmp::max; +use std::collections::HashMap; +use std::path::PathBuf; +use thiserror::Error; + +mod api; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Request failed: {0}")] + Request(#[from] std::io::Error), + #[error("MD5 digest mismatch for downloaded demo, expected {expected:?}, received {got:?}")] + DigestMismatch { expected: Digest, got: Digest }, +} + +fn main() -> Result<(), MainError> { + let mut args: HashMap<_, _> = dotenv::vars().collect(); + let store = Store::new(args.get("STORAGE_ROOT").expect("no STORAGE_ROOT set")); + let state_path = PathBuf::from(args.remove("STATE_FILE").expect("no STATE_FILE set")); + let backup = Backup::new(store); + + let last_page = if state_path.is_file() { + max( + std::fs::read_to_string(&state_path)? + .trim() + .parse::()? + - 1, + 1, + ) + } else { + 1u32 + }; + + let current_page = backup.backup_from(last_page)?; + + std::fs::write(&state_path, format!("{}", current_page))?; + + Ok(()) } diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..a1e70f4 --- /dev/null +++ b/src/store.rs @@ -0,0 +1,58 @@ +use md5::{Context, Digest}; +use std::fs; +use std::fs::File; +use std::io::{ErrorKind, Read, Write}; +use std::path::{Path, PathBuf}; + +pub struct Store { + basedir: PathBuf, +} + +impl Store { + pub fn new(basedir: impl AsRef) -> Self { + Store { + basedir: basedir.as_ref().to_path_buf(), + } + } + + pub fn store(&self, name: &str, data: &mut impl Read) -> std::io::Result { + let path = self.generate_path(name); + fs::create_dir_all(path.parent().unwrap())?; + + let mut file = File::create(&path)?; + + let mut context = Context::new(); + let mut buf = [0u8; 8 * 1024]; + + // copy the file and compute the digest was we go + loop { + let len = match data.read(&mut buf) { + Ok(0) => return Ok(context.compute()), + Ok(len) => len, + Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + }; + + let data = &buf[..len]; + context.consume(data); + + file.write_all(data)?; + } + } + + pub fn exists(&self, name: &str) -> bool { + self.generate_path(name).is_file() + } + + pub fn remove(&self, name: &str) -> std::io::Result<()> { + fs::remove_file(self.generate_path(name)) + } + + fn generate_path(&self, name: &str) -> PathBuf { + let mut path = self.basedir.clone(); + path.push(&name[0..2]); + path.push(&name[2..4]); + path.push(name); + path + } +}