diff --git a/Cargo.lock b/Cargo.lock index 5377fd3ae..8d742b235 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,188 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "actix-codec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" -dependencies = [ - "bitflags 1.3.2", - "bytes", - "futures-core", - "futures-sink", - "log", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-http" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash 0.8.3", - "base64 0.21.0", - "bitflags 1.3.2", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" -dependencies = [ - "quote", - "syn 1.0.104", -] - -[[package]] -name = "actix-router" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" -dependencies = [ - "bytestring", - "http", - "regex", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" -dependencies = [ - "actix-macros", - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "num_cpus", - "socket2 0.4.9", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash 0.7.6", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.4.9", - "time 0.3.20", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 1.0.104", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -210,18 +28,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "1.1.2" @@ -231,21 +37,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -255,55 +46,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" - -[[package]] -name = "anstyle-parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - [[package]] name = "anyhow" version = "1.0.66" @@ -418,27 +160,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "3.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.12.0" @@ -457,23 +178,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" -[[package]] -name = "bytestring" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" -dependencies = [ - "bytes", -] - [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" -dependencies = [ - "jobserver", -] [[package]] name = "cfg-if" @@ -497,63 +206,12 @@ dependencies = [ "winapi", ] -[[package]] -name = "clap" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" -dependencies = [ - "anstream", - "anstyle", - "bitflags 1.3.2", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_lex" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time 0.3.20", - "version_check", -] - [[package]] name = "core-foundation-sys" version = "0.8.3" @@ -599,19 +257,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.104", -] - [[package]] name = "diff" version = "0.1.12" @@ -643,19 +288,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "errno" version = "0.3.1" @@ -972,7 +604,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" dependencies = [ - "ahash 0.7.6", + "ahash", ] [[package]] @@ -1054,12 +686,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.20" @@ -1145,17 +771,12 @@ dependencies = [ name = "integration-tests" version = "0.4.1" dependencies = [ - "actix-rt", - "actix-web", "anyhow", "cc", - "env_logger", "lazy_static", - "log", "pretty_assertions", "taskchampion", "taskchampion-lib", - "taskchampion-sync-server", "tempfile", ] @@ -1176,18 +797,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "itertools" version = "0.10.5" @@ -1203,15 +812,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.59" @@ -1235,12 +835,6 @@ dependencies = [ "simple_asn1", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.4.0" @@ -1296,24 +890,6 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" -[[package]] -name = "local-channel" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" -dependencies = [ - "futures-core", - "futures-sink", - "futures-util", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" - [[package]] name = "lock_api" version = "0.4.7" @@ -1380,7 +956,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.48.0", ] @@ -1464,12 +1039,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - [[package]] name = "pem" version = "1.1.1" @@ -1928,17 +1497,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.6" @@ -1950,15 +1508,6 @@ dependencies = [ "digest", ] -[[package]] -name = "signal-hook-registry" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" -dependencies = [ - "libc", -] - [[package]] name = "simple_asn1" version = "0.6.2" @@ -2025,12 +1574,6 @@ dependencies = [ "der", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strum" version = "0.25.0" @@ -2110,27 +1653,6 @@ dependencies = [ "taskchampion", ] -[[package]] -name = "taskchampion-sync-server" -version = "0.4.1" -dependencies = [ - "actix-rt", - "actix-web", - "anyhow", - "chrono", - "clap", - "env_logger", - "futures", - "log", - "pretty_assertions", - "rusqlite", - "serde", - "serde_json", - "tempfile", - "thiserror", - "uuid", -] - [[package]] name = "tempfile" version = "3.6.0" @@ -2145,15 +1667,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.37" @@ -2239,7 +1752,6 @@ dependencies = [ "num_cpus", "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", @@ -2293,7 +1805,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if", - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2412,12 +1923,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "uuid" version = "1.8.0" @@ -2594,15 +2099,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2836,33 +2332,3 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" - -[[package]] -name = "zstd" -version = "0.12.3+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "6.0.5+zstd.1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" -dependencies = [ - "cc", - "libc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 09171118a..efe0c179d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "taskchampion/taskchampion", - "taskchampion/sync-server", "taskchampion/lib", "taskchampion/integration-tests", "taskchampion/xtask", @@ -16,17 +15,12 @@ exclude = [ "src/tc/rust" ] # All Rust dependencies are defined here, and then referenced by the # Cargo.toml's in the members with `foo.workspace = true`. [workspace.dependencies] -actix-rt = "2" -actix-web = "^4.3.1" anyhow = "1.0" byteorder = "1.5" cc = "1.0.73" chrono = { version = "^0.4.22", features = ["serde"] } -clap = { version = "^4.3.0", features = ["string"] } -env_logger = "^0.10.2" ffizz-header = "0.5" flate2 = "1" -futures = "^0.3.25" google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] } lazy_static = "1" libc = "0.2.136" diff --git a/doc/man/task-sync.5.in b/doc/man/task-sync.5.in index a8cf66d5f..70c45d216 100644 --- a/doc/man/task-sync.5.in +++ b/doc/man/task-sync.5.in @@ -144,16 +144,8 @@ Users are identified by a client ID, and users with different client IDs are entirely independent. Task data is encrypted by Taskwarrior, and the sync server never sees un-encrypted data. -To start the server, run it in your preferred HTTP hosting environment, using -`--port` to set the TCP port on which it should listen. It is recommended to -use TLS to protect communications with the server, but this is not required. - -The server stores its data in a database, the path to which is given by the -`--data-dir` argument, defaulting to "/var/lib/taskchampion-sync-server". - -For example: - - $ taskchampion-sync-server --port 8443 --data-dir /storage/taskdata +The server is developed in +https://github.com/GothenburgBitFactory/taskchampion-sync-server. .SS Adding a New User diff --git a/taskchampion/CONTRIBUTING.md b/taskchampion/CONTRIBUTING.md index 2adfeba3e..7b44aad75 100644 --- a/taskchampion/CONTRIBUTING.md +++ b/taskchampion/CONTRIBUTING.md @@ -24,11 +24,8 @@ Other ideas; TaskChampion is a typical Rust application. To work on TaskChampion, you'll need to [install the latest version of Rust](https://www.rust-lang.org/tools/install). -Once you've done that, run `cargo build` at the top level of this repository to build the binaries. -This will build `task` and `taskchampion-sync-server` executables in the `./target/debug` directory. -You can build optimized versions of these binaries with `cargo build --release`, but the performance difference in the resulting binaries is not noticeable, and the build process will take a long time, so this is not recommended. -## Running Test +## Running Tests It's always a good idea to make sure tests run before you start hacking on a project. Run `cargo test` from the top-level of this repository to run the tests. @@ -39,13 +36,13 @@ Aside from that, start reading the docs and the source to learn more! The book documentation explains lots of the concepts in the design of TaskChampion. It is linked from the README. -There are three crates in this repository. +There are three important crates in this repository. You may be able to limit the scope of what you need to understand to just one crate. * `taskchampion` is the core functionality of the application, implemented as a library - * `taskchampion-cli` implements the command-line interface (in `cli/`) - * `taskchampion-sync-server` implements the synchronization server (in `sync-server/`) + * `taskchampion-lib` implements a C API for `taskchampion`, used by Taskwarrior + * `integration-tests` contains some tests for integrations between multiple crates. - You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`. +You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`. ## Making a Pull Request diff --git a/taskchampion/README.md b/taskchampion/README.md index 665602361..ed7cfa77c 100644 --- a/taskchampion/README.md +++ b/taskchampion/README.md @@ -12,12 +12,11 @@ Until that is complete, the information here may be out-of-date. ## Structure -There are five crates here: +There are four crates here: * [taskchampion](./taskchampion) - the core of the tool - * [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates * [taskchampion-lib](./lib) - glue code to use _taskchampion_ from C - * [integration-tests](./integration-tests) (private) - integration tests covering _taskchampion-cli_, _taskchampion-sync-server_, and _taskchampion-lib_. + * [integration-tests](./integration-tests) (private) - integration tests covering _taskchampion_ and _taskchampion-lib_. * [xtask](./xtask) (private) - implementation of the `cargo xtask codegen` command ## Code Generation diff --git a/taskchampion/integration-tests/Cargo.toml b/taskchampion/integration-tests/Cargo.toml index 1a7176f0c..217961394 100644 --- a/taskchampion/integration-tests/Cargo.toml +++ b/taskchampion/integration-tests/Cargo.toml @@ -7,18 +7,13 @@ publish = false build = "build.rs" [dependencies] -taskchampion = { path = "../taskchampion", features = ["server-sync"] } +taskchampion = { path = "../taskchampion" } taskchampion-lib = { path = "../lib" } -taskchampion-sync-server = { path = "../sync-server" } [dev-dependencies] anyhow.workspace = true -actix-web.workspace = true -actix-rt.workspace = true tempfile.workspace = true pretty_assertions.workspace = true -log.workspace = true -env_logger.workspace = true lazy_static.workspace = true [build-dependencies] diff --git a/taskchampion/integration-tests/tests/cross-sync.rs b/taskchampion/integration-tests/tests/cross-sync.rs index 94b53558d..6cac3c293 100644 --- a/taskchampion/integration-tests/tests/cross-sync.rs +++ b/taskchampion/integration-tests/tests/cross-sync.rs @@ -1,97 +1,66 @@ -use actix_web::{App, HttpServer}; use pretty_assertions::assert_eq; -use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid}; -use taskchampion_sync_server::{storage::InMemoryStorage, Server}; +use taskchampion::{Replica, ServerConfig, Status, StorageConfig}; +use tempfile::TempDir; -#[actix_rt::test] -async fn cross_sync() -> anyhow::Result<()> { - async fn server() -> anyhow::Result { - let _ = env_logger::builder() - .is_test(true) - .filter_level(log::LevelFilter::Trace) - .try_init(); +#[test] +fn cross_sync() -> anyhow::Result<()> { + // set up two replicas, and demonstrate replication between them + let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?); + let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?); - let server = Server::new(Default::default(), Box::new(InMemoryStorage::new())); - let httpserver = HttpServer::new(move || App::new().configure(|sc| server.config(sc))) - .bind("0.0.0.0:0")?; + let tmp_dir = TempDir::new().expect("TempDir failed"); + let server_config = ServerConfig::Local { + server_dir: tmp_dir.path().to_path_buf(), + }; + let mut server = server_config.into_server()?; - // bind was to :0, so the kernel will have selected an unused port - let port = httpserver.addrs()[0].port(); - actix_rt::spawn(httpserver.run()); - Ok(port) - } + // add some tasks on rep1 + let t1 = rep1.new_task(Status::Pending, "test 1".into())?; + let t2 = rep1.new_task(Status::Pending, "test 2".into())?; - fn client(port: u16) -> anyhow::Result<()> { - // set up two replicas, and demonstrate replication between them - let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?); - let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?); + // modify t1 + let mut t1 = t1.into_mut(&mut rep1); + t1.start()?; + let t1 = t1.into_immut(); - let client_id = Uuid::new_v4(); - let encryption_secret = b"abc123".to_vec(); - let make_server = || { - ServerConfig::Remote { - origin: format!("http://127.0.0.1:{}", port), - client_id, - encryption_secret: encryption_secret.clone(), - } - .into_server() - }; + rep1.sync(&mut server, false)?; + rep2.sync(&mut server, false)?; - let mut serv1 = make_server()?; - let mut serv2 = make_server()?; + // those tasks should exist on rep2 now + let t12 = rep2 + .get_task(t1.get_uuid())? + .expect("expected task 1 on rep2"); + let t22 = rep2 + .get_task(t2.get_uuid())? + .expect("expected task 2 on rep2"); - // add some tasks on rep1 - let t1 = rep1.new_task(Status::Pending, "test 1".into())?; - let t2 = rep1.new_task(Status::Pending, "test 2".into())?; + assert_eq!(t12.get_description(), "test 1"); + assert_eq!(t12.is_active(), true); + assert_eq!(t22.get_description(), "test 2"); + assert_eq!(t22.is_active(), false); - // modify t1 - let mut t1 = t1.into_mut(&mut rep1); - t1.start()?; - let t1 = t1.into_immut(); + // make non-conflicting changes on the two replicas + let mut t2 = t2.into_mut(&mut rep1); + t2.set_status(Status::Completed)?; + let t2 = t2.into_immut(); - rep1.sync(&mut serv1, false)?; - rep2.sync(&mut serv2, false)?; + let mut t12 = t12.into_mut(&mut rep2); + t12.set_status(Status::Completed)?; - // those tasks should exist on rep2 now - let t12 = rep2 - .get_task(t1.get_uuid())? - .expect("expected task 1 on rep2"); - let t22 = rep2 - .get_task(t2.get_uuid())? - .expect("expected task 2 on rep2"); + // sync those changes back and forth + rep1.sync(&mut server, false)?; // rep1 -> server + rep2.sync(&mut server, false)?; // server -> rep2, rep2 -> server + rep1.sync(&mut server, false)?; // server -> rep1 - assert_eq!(t12.get_description(), "test 1"); - assert_eq!(t12.is_active(), true); - assert_eq!(t22.get_description(), "test 2"); - assert_eq!(t22.is_active(), false); + let t1 = rep1 + .get_task(t1.get_uuid())? + .expect("expected task 1 on rep1"); + assert_eq!(t1.get_status(), Status::Completed); - // make non-conflicting changes on the two replicas - let mut t2 = t2.into_mut(&mut rep1); - t2.set_status(Status::Completed)?; - let t2 = t2.into_immut(); + let t22 = rep2 + .get_task(t2.get_uuid())? + .expect("expected task 2 on rep2"); + assert_eq!(t22.get_status(), Status::Completed); - let mut t12 = t12.into_mut(&mut rep2); - t12.set_status(Status::Completed)?; - - // sync those changes back and forth - rep1.sync(&mut serv1, false)?; // rep1 -> server - rep2.sync(&mut serv2, false)?; // server -> rep2, rep2 -> server - rep1.sync(&mut serv1, false)?; // server -> rep1 - - let t1 = rep1 - .get_task(t1.get_uuid())? - .expect("expected task 1 on rep1"); - assert_eq!(t1.get_status(), Status::Completed); - - let t22 = rep2 - .get_task(t2.get_uuid())? - .expect("expected task 2 on rep2"); - assert_eq!(t22.get_status(), Status::Completed); - - Ok(()) - } - - let port = server().await?; - actix_rt::task::spawn_blocking(move || client(port)).await??; Ok(()) } diff --git a/taskchampion/integration-tests/tests/snapshots.rs b/taskchampion/integration-tests/tests/snapshots.rs deleted file mode 100644 index 004a57ecf..000000000 --- a/taskchampion/integration-tests/tests/snapshots.rs +++ /dev/null @@ -1,98 +0,0 @@ -use actix_web::{App, HttpServer}; -use pretty_assertions::assert_eq; -use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid}; -use taskchampion_sync_server::{ - storage::InMemoryStorage, Server, ServerConfig as SyncServerConfig, -}; - -const NUM_VERSIONS: u32 = 50; - -#[actix_rt::test] -async fn sync_with_snapshots() -> anyhow::Result<()> { - let _ = env_logger::builder() - .is_test(true) - .filter_level(log::LevelFilter::Trace) - .try_init(); - - async fn server() -> anyhow::Result { - let sync_server_config = SyncServerConfig { - snapshot_days: 100, - snapshot_versions: 3, - }; - let server = Server::new(sync_server_config, Box::new(InMemoryStorage::new())); - let httpserver = HttpServer::new(move || App::new().configure(|sc| server.config(sc))) - .bind("0.0.0.0:0")?; - - // bind was to :0, so the kernel will have selected an unused port - let port = httpserver.addrs()[0].port(); - actix_rt::spawn(httpserver.run()); - Ok(port) - } - - fn client(port: u16) -> anyhow::Result<()> { - let client_id = Uuid::new_v4(); - let encryption_secret = b"abc123".to_vec(); - let make_server = || { - ServerConfig::Remote { - origin: format!("http://127.0.0.1:{}", port), - client_id, - encryption_secret: encryption_secret.clone(), - } - .into_server() - }; - - // first we set up a single replica and sync it a lot of times, to establish a sync history. - let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?); - let mut serv1 = make_server()?; - - let mut t1 = rep1.new_task(Status::Pending, "test 1".into())?; - log::info!("Applying modifications on replica 1"); - for i in 0..=NUM_VERSIONS { - let mut t1m = t1.into_mut(&mut rep1); - t1m.start()?; - t1m.stop()?; - t1m.set_description(format!("revision {}", i))?; - t1 = t1m.into_immut(); - - rep1.sync(&mut serv1, false)?; - } - - // now set up a second replica and sync it; it should catch up on that history, using a - // snapshot. Note that we can't verify that it used a snapshot, because the server - // currently keeps all versions (so rep2 could sync from the beginning of the version - // history). You can manually verify that it is applying a snapshot by adding - // `assert!(false)` below and skimming the logs. - - let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?); - let mut serv2 = make_server()?; - - log::info!("Syncing replica 2"); - rep2.sync(&mut serv2, false)?; - - // those tasks should exist on rep2 now - let t12 = rep2 - .get_task(t1.get_uuid())? - .expect("expected task 1 on rep2"); - - assert_eq!(t12.get_description(), format!("revision {}", NUM_VERSIONS)); - assert_eq!(t12.is_active(), false); - - // sync that back to replica 1 - t12.into_mut(&mut rep2) - .set_description("sync-back".to_owned())?; - rep2.sync(&mut serv2, false)?; - rep1.sync(&mut serv1, false)?; - - let t11 = rep1 - .get_task(t1.get_uuid())? - .expect("expected task 1 on rep1"); - - assert_eq!(t11.get_description(), "sync-back"); - - Ok(()) - } - - let port = server().await?; - actix_rt::task::spawn_blocking(move || client(port)).await??; - Ok(()) -} diff --git a/taskchampion/sync-server/Cargo.toml b/taskchampion/sync-server/Cargo.toml deleted file mode 100644 index 1e2a8c225..000000000 --- a/taskchampion/sync-server/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "taskchampion-sync-server" -version = "0.4.1" -authors = ["Dustin J. Mitchell "] -edition = "2021" -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -uuid.workspace = true -actix-web.workspace = true -anyhow.workspace = true -thiserror.workspace = true -futures.workspace = true -serde.workspace = true -serde_json.workspace = true -clap.workspace = true -log.workspace = true -env_logger.workspace = true -rusqlite.workspace = true -chrono.workspace = true - -[dev-dependencies] -actix-rt.workspace = true -tempfile.workspace = true -pretty_assertions.workspace = true diff --git a/taskchampion/sync-server/src/api/add_snapshot.rs b/taskchampion/sync-server/src/api/add_snapshot.rs deleted file mode 100644 index d92174c7a..000000000 --- a/taskchampion/sync-server/src/api/add_snapshot.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::api::{client_id_header, failure_to_ise, ServerState, SNAPSHOT_CONTENT_TYPE}; -use crate::server::{add_snapshot, VersionId, NIL_VERSION_ID}; -use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result}; -use futures::StreamExt; -use std::sync::Arc; - -/// Max snapshot size: 100MB -const MAX_SIZE: usize = 100 * 1024 * 1024; - -/// Add a new snapshot, after checking prerequisites. The snapshot should be transmitted in the -/// request entity body and must have content-type `application/vnd.taskchampion.snapshot`. The -/// content can be encoded in any of the formats supported by actix-web. -/// -/// On success, the response is a 200 OK. Even in a 200 OK, the snapshot may not appear in a -/// subsequent `GetSnapshot` call. -/// -/// Returns other 4xx or 5xx responses on other errors. -#[post("/v1/client/add-snapshot/{version_id}")] -pub(crate) async fn service( - req: HttpRequest, - server_state: web::Data>, - path: web::Path, - mut payload: web::Payload, -) -> Result { - let version_id = path.into_inner(); - - // check content-type - if req.content_type() != SNAPSHOT_CONTENT_TYPE { - return Err(error::ErrorBadRequest("Bad content-type")); - } - - let client_id = client_id_header(&req)?; - - // read the body in its entirety - let mut body = web::BytesMut::new(); - while let Some(chunk) = payload.next().await { - let chunk = chunk?; - // limit max size of in-memory payload - if (body.len() + chunk.len()) > MAX_SIZE { - return Err(error::ErrorBadRequest("Snapshot over maximum allowed size")); - } - body.extend_from_slice(&chunk); - } - - if body.is_empty() { - return Err(error::ErrorBadRequest("No snapshot supplied")); - } - - // note that we do not open the transaction until the body has been read - // completely, to avoid blocking other storage access while that data is - // in transit. - let mut txn = server_state.storage.txn().map_err(failure_to_ise)?; - - // get, or create, the client - let client = match txn.get_client(client_id).map_err(failure_to_ise)? { - Some(client) => client, - None => { - txn.new_client(client_id, NIL_VERSION_ID) - .map_err(failure_to_ise)?; - txn.get_client(client_id).map_err(failure_to_ise)?.unwrap() - } - }; - - add_snapshot( - txn, - &server_state.config, - client_id, - client, - version_id, - body.to_vec(), - ) - .map_err(failure_to_ise)?; - Ok(HttpResponse::Ok().body("")) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::api::CLIENT_ID_HEADER; - use crate::storage::{InMemoryStorage, Storage}; - use crate::Server; - use actix_web::{http::StatusCode, test, App}; - use pretty_assertions::assert_eq; - use uuid::Uuid; - - #[actix_rt::test] - async fn test_success() -> anyhow::Result<()> { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - - // set up the storage contents.. - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, version_id).unwrap(); - txn.add_version(client_id, version_id, NIL_VERSION_ID, vec![])?; - } - - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/add-snapshot/{}", version_id); - let req = test::TestRequest::post() - .uri(&uri) - .insert_header(("Content-Type", "application/vnd.taskchampion.snapshot")) - .insert_header((CLIENT_ID_HEADER, client_id.to_string())) - .set_payload(b"abcd".to_vec()) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::OK); - - // read back that snapshot - let uri = "/v1/client/snapshot"; - let req = test::TestRequest::get() - .uri(uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::OK); - - use actix_web::body::MessageBody; - let bytes = resp.into_body().try_into_bytes().unwrap(); - assert_eq!(bytes.as_ref(), b"abcd"); - - Ok(()) - } - - #[actix_rt::test] - async fn test_not_added_200() { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - - // set up the storage contents.. - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, NIL_VERSION_ID).unwrap(); - } - - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - // add a snapshot for a nonexistent version - let uri = format!("/v1/client/add-snapshot/{}", version_id); - let req = test::TestRequest::post() - .uri(&uri) - .append_header(("Content-Type", "application/vnd.taskchampion.snapshot")) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .set_payload(b"abcd".to_vec()) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::OK); - - // read back, seeing no snapshot - let uri = "/v1/client/snapshot"; - let req = test::TestRequest::get() - .uri(uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[actix_rt::test] - async fn test_bad_content_type() { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/add-snapshot/{}", version_id); - let req = test::TestRequest::post() - .uri(&uri) - .append_header(("Content-Type", "not/correct")) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .set_payload(b"abcd".to_vec()) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - - #[actix_rt::test] - async fn test_empty_body() { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/add-snapshot/{}", version_id); - let req = test::TestRequest::post() - .uri(&uri) - .append_header(( - "Content-Type", - "application/vnd.taskchampion.history-segment", - )) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } -} diff --git a/taskchampion/sync-server/src/api/add_version.rs b/taskchampion/sync-server/src/api/add_version.rs deleted file mode 100644 index 7428d94e0..000000000 --- a/taskchampion/sync-server/src/api/add_version.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::api::{ - client_id_header, failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, - PARENT_VERSION_ID_HEADER, SNAPSHOT_REQUEST_HEADER, VERSION_ID_HEADER, -}; -use crate::server::{add_version, AddVersionResult, SnapshotUrgency, VersionId, NIL_VERSION_ID}; -use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result}; -use futures::StreamExt; -use std::sync::Arc; - -/// Max history segment size: 100MB -const MAX_SIZE: usize = 100 * 1024 * 1024; - -/// Add a new version, after checking prerequisites. The history segment should be transmitted in -/// the request entity body and must have content-type -/// `application/vnd.taskchampion.history-segment`. The content can be encoded in any of the -/// formats supported by actix-web. -/// -/// On success, the response is a 200 OK with the new version ID in the `X-Version-Id` header. If -/// the version cannot be added due to a conflict, the response is a 409 CONFLICT with the expected -/// parent version ID in the `X-Parent-Version-Id` header. -/// -/// If included, a snapshot request appears in the `X-Snapshot-Request` header with value -/// `urgency=low` or `urgency=high`. -/// -/// Returns other 4xx or 5xx responses on other errors. -#[post("/v1/client/add-version/{parent_version_id}")] -pub(crate) async fn service( - req: HttpRequest, - server_state: web::Data>, - path: web::Path, - mut payload: web::Payload, -) -> Result { - let parent_version_id = path.into_inner(); - - // check content-type - if req.content_type() != HISTORY_SEGMENT_CONTENT_TYPE { - return Err(error::ErrorBadRequest("Bad content-type")); - } - - let client_id = client_id_header(&req)?; - - // read the body in its entirety - let mut body = web::BytesMut::new(); - while let Some(chunk) = payload.next().await { - let chunk = chunk?; - // limit max size of in-memory payload - if (body.len() + chunk.len()) > MAX_SIZE { - return Err(error::ErrorBadRequest("overflow")); - } - body.extend_from_slice(&chunk); - } - - if body.is_empty() { - return Err(error::ErrorBadRequest("Empty body")); - } - - // note that we do not open the transaction until the body has been read - // completely, to avoid blocking other storage access while that data is - // in transit. - let mut txn = server_state.storage.txn().map_err(failure_to_ise)?; - - // get, or create, the client - let client = match txn.get_client(client_id).map_err(failure_to_ise)? { - Some(client) => client, - None => { - txn.new_client(client_id, NIL_VERSION_ID) - .map_err(failure_to_ise)?; - txn.get_client(client_id).map_err(failure_to_ise)?.unwrap() - } - }; - - let (result, snap_urgency) = add_version( - txn, - &server_state.config, - client_id, - client, - parent_version_id, - body.to_vec(), - ) - .map_err(failure_to_ise)?; - - Ok(match result { - AddVersionResult::Ok(version_id) => { - let mut rb = HttpResponse::Ok(); - rb.append_header((VERSION_ID_HEADER, version_id.to_string())); - match snap_urgency { - SnapshotUrgency::None => {} - SnapshotUrgency::Low => { - rb.append_header((SNAPSHOT_REQUEST_HEADER, "urgency=low")); - } - SnapshotUrgency::High => { - rb.append_header((SNAPSHOT_REQUEST_HEADER, "urgency=high")); - } - }; - rb.finish() - } - AddVersionResult::ExpectedParentVersion(parent_version_id) => { - let mut rb = HttpResponse::Conflict(); - rb.append_header((PARENT_VERSION_ID_HEADER, parent_version_id.to_string())); - rb.finish() - } - }) -} - -#[cfg(test)] -mod test { - use crate::api::CLIENT_ID_HEADER; - use crate::storage::{InMemoryStorage, Storage}; - use crate::Server; - use actix_web::{http::StatusCode, test, App}; - use pretty_assertions::assert_eq; - use uuid::Uuid; - - #[actix_rt::test] - async fn test_success() { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - - // set up the storage contents.. - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, Uuid::nil()).unwrap(); - } - - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/add-version/{}", parent_version_id); - let req = test::TestRequest::post() - .uri(&uri) - .append_header(( - "Content-Type", - "application/vnd.taskchampion.history-segment", - )) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .set_payload(b"abcd".to_vec()) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::OK); - - // the returned version ID is random, but let's check that it's not - // the passed parent version ID, at least - let new_version_id = resp.headers().get("X-Version-Id").unwrap(); - assert!(new_version_id != &version_id.to_string()); - - // Shapshot should be requested, since there is no existing snapshot - let snapshot_request = resp.headers().get("X-Snapshot-Request").unwrap(); - assert_eq!(snapshot_request, "urgency=high"); - - assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); - } - - #[actix_rt::test] - async fn test_conflict() { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - - // set up the storage contents.. - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, version_id).unwrap(); - } - - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/add-version/{}", parent_version_id); - let req = test::TestRequest::post() - .uri(&uri) - .append_header(( - "Content-Type", - "application/vnd.taskchampion.history-segment", - )) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .set_payload(b"abcd".to_vec()) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::CONFLICT); - assert_eq!(resp.headers().get("X-Version-Id"), None); - assert_eq!( - resp.headers().get("X-Parent-Version-Id").unwrap(), - &version_id.to_string() - ); - } - - #[actix_rt::test] - async fn test_bad_content_type() { - let client_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/add-version/{}", parent_version_id); - let req = test::TestRequest::post() - .uri(&uri) - .append_header(("Content-Type", "not/correct")) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .set_payload(b"abcd".to_vec()) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - - #[actix_rt::test] - async fn test_empty_body() { - let client_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/add-version/{}", parent_version_id); - let req = test::TestRequest::post() - .uri(&uri) - .append_header(( - "Content-Type", - "application/vnd.taskchampion.history-segment", - )) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } -} diff --git a/taskchampion/sync-server/src/api/get_child_version.rs b/taskchampion/sync-server/src/api/get_child_version.rs deleted file mode 100644 index cb0e74fd7..000000000 --- a/taskchampion/sync-server/src/api/get_child_version.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::api::{ - client_id_header, failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE, - PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER, -}; -use crate::server::{get_child_version, GetVersionResult, VersionId}; -use actix_web::{error, get, web, HttpRequest, HttpResponse, Result}; -use std::sync::Arc; - -/// Get a child version. -/// -/// On succcess, the response is the same sequence of bytes originally sent to the server, -/// with content-type `application/vnd.taskchampion.history-segment`. The `X-Version-Id` and -/// `X-Parent-Version-Id` headers contain the corresponding values. -/// -/// If no such child exists, returns a 404 with no content. -/// Returns other 4xx or 5xx responses on other errors. -#[get("/v1/client/get-child-version/{parent_version_id}")] -pub(crate) async fn service( - req: HttpRequest, - server_state: web::Data>, - path: web::Path, -) -> Result { - let parent_version_id = path.into_inner(); - - let mut txn = server_state.storage.txn().map_err(failure_to_ise)?; - - let client_id = client_id_header(&req)?; - - let client = txn - .get_client(client_id) - .map_err(failure_to_ise)? - .ok_or_else(|| error::ErrorNotFound("no such client"))?; - - return match get_child_version( - txn, - &server_state.config, - client_id, - client, - parent_version_id, - ) - .map_err(failure_to_ise)? - { - GetVersionResult::Success { - version_id, - parent_version_id, - history_segment, - } => Ok(HttpResponse::Ok() - .content_type(HISTORY_SEGMENT_CONTENT_TYPE) - .append_header((VERSION_ID_HEADER, version_id.to_string())) - .append_header((PARENT_VERSION_ID_HEADER, parent_version_id.to_string())) - .body(history_segment)), - GetVersionResult::NotFound => Err(error::ErrorNotFound("no such version")), - GetVersionResult::Gone => Err(error::ErrorGone("version has been deleted")), - }; -} - -#[cfg(test)] -mod test { - use crate::api::CLIENT_ID_HEADER; - use crate::server::NIL_VERSION_ID; - use crate::storage::{InMemoryStorage, Storage}; - use crate::Server; - use actix_web::{http::StatusCode, test, App}; - use pretty_assertions::assert_eq; - use uuid::Uuid; - - #[actix_rt::test] - async fn test_success() { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - - // set up the storage contents.. - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, Uuid::new_v4()).unwrap(); - txn.add_version(client_id, version_id, parent_version_id, b"abcd".to_vec()) - .unwrap(); - } - - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/get-child-version/{}", parent_version_id); - let req = test::TestRequest::get() - .uri(&uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get("X-Version-Id").unwrap(), - &version_id.to_string() - ); - assert_eq!( - resp.headers().get("X-Parent-Version-Id").unwrap(), - &parent_version_id.to_string() - ); - assert_eq!( - resp.headers().get("Content-Type").unwrap(), - &"application/vnd.taskchampion.history-segment".to_string() - ); - - use actix_web::body::MessageBody; - let bytes = resp.into_body().try_into_bytes().unwrap(); - assert_eq!(bytes.as_ref(), b"abcd"); - } - - #[actix_rt::test] - async fn test_client_not_found() { - let client_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = format!("/v1/client/get-child-version/{}", parent_version_id); - let req = test::TestRequest::get() - .uri(&uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - assert_eq!(resp.headers().get("X-Version-Id"), None); - assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); - } - - #[actix_rt::test] - async fn test_version_not_found_and_gone() { - let client_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - - // create the client, but not the version - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, Uuid::new_v4()).unwrap(); - } - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - // the child of an unknown parent_version_id is GONE - let uri = format!("/v1/client/get-child-version/{}", parent_version_id); - let req = test::TestRequest::get() - .uri(&uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::GONE); - assert_eq!(resp.headers().get("X-Version-Id"), None); - assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); - - // but the child of the nil parent_version_id is NOT FOUND, since - // there is no snapshot. The tests in crate::server test more - // corner cases. - let uri = format!("/v1/client/get-child-version/{}", NIL_VERSION_ID); - let req = test::TestRequest::get() - .uri(&uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - assert_eq!(resp.headers().get("X-Version-Id"), None); - assert_eq!(resp.headers().get("X-Parent-Version-Id"), None); - } -} diff --git a/taskchampion/sync-server/src/api/get_snapshot.rs b/taskchampion/sync-server/src/api/get_snapshot.rs deleted file mode 100644 index 4135257f4..000000000 --- a/taskchampion/sync-server/src/api/get_snapshot.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::api::{ - client_id_header, failure_to_ise, ServerState, SNAPSHOT_CONTENT_TYPE, VERSION_ID_HEADER, -}; -use crate::server::get_snapshot; -use actix_web::{error, get, web, HttpRequest, HttpResponse, Result}; -use std::sync::Arc; - -/// Get a snapshot. -/// -/// If a snapshot for this client exists, it is returned with content-type -/// `application/vnd.taskchampion.snapshot`. The `X-Version-Id` header contains the version of the -/// snapshot. -/// -/// If no snapshot exists, returns a 404 with no content. Returns other 4xx or 5xx responses on -/// other errors. -#[get("/v1/client/snapshot")] -pub(crate) async fn service( - req: HttpRequest, - server_state: web::Data>, -) -> Result { - let mut txn = server_state.storage.txn().map_err(failure_to_ise)?; - - let client_id = client_id_header(&req)?; - - let client = txn - .get_client(client_id) - .map_err(failure_to_ise)? - .ok_or_else(|| error::ErrorNotFound("no such client"))?; - - if let Some((version_id, data)) = - get_snapshot(txn, &server_state.config, client_id, client).map_err(failure_to_ise)? - { - Ok(HttpResponse::Ok() - .content_type(SNAPSHOT_CONTENT_TYPE) - .append_header((VERSION_ID_HEADER, version_id.to_string())) - .body(data)) - } else { - Err(error::ErrorNotFound("no snapshot")) - } -} - -#[cfg(test)] -mod test { - use crate::api::CLIENT_ID_HEADER; - use crate::storage::{InMemoryStorage, Snapshot, Storage}; - use crate::Server; - use actix_web::{http::StatusCode, test, App}; - use chrono::{TimeZone, Utc}; - use pretty_assertions::assert_eq; - use uuid::Uuid; - - #[actix_rt::test] - async fn test_not_found() { - let client_id = Uuid::new_v4(); - let storage: Box = Box::new(InMemoryStorage::new()); - - // set up the storage contents.. - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, Uuid::new_v4()).unwrap(); - } - - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = "/v1/client/snapshot"; - let req = test::TestRequest::get() - .uri(uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[actix_rt::test] - async fn test_success() { - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let snapshot_data = vec![1, 2, 3, 4]; - let storage: Box = Box::new(InMemoryStorage::new()); - - // set up the storage contents.. - { - let mut txn = storage.txn().unwrap(); - txn.new_client(client_id, Uuid::new_v4()).unwrap(); - txn.set_snapshot( - client_id, - Snapshot { - version_id, - versions_since: 3, - timestamp: Utc.ymd(2001, 9, 9).and_hms(1, 46, 40), - }, - snapshot_data.clone(), - ) - .unwrap(); - } - - let server = Server::new(Default::default(), storage); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let uri = "/v1/client/snapshot"; - let req = test::TestRequest::get() - .uri(uri) - .append_header((CLIENT_ID_HEADER, client_id.to_string())) - .to_request(); - let resp = test::call_service(&mut app, req).await; - assert_eq!(resp.status(), StatusCode::OK); - - use actix_web::body::MessageBody; - let bytes = resp.into_body().try_into_bytes().unwrap(); - assert_eq!(bytes.as_ref(), snapshot_data); - } -} diff --git a/taskchampion/sync-server/src/api/mod.rs b/taskchampion/sync-server/src/api/mod.rs deleted file mode 100644 index bb5001f3b..000000000 --- a/taskchampion/sync-server/src/api/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::server::ClientId; -use crate::storage::Storage; -use crate::ServerConfig; -use actix_web::{error, http::StatusCode, web, HttpRequest, Result, Scope}; - -mod add_snapshot; -mod add_version; -mod get_child_version; -mod get_snapshot; - -/// The content-type for history segments (opaque blobs of bytes) -pub(crate) const HISTORY_SEGMENT_CONTENT_TYPE: &str = - "application/vnd.taskchampion.history-segment"; - -/// The content-type for snapshots (opaque blobs of bytes) -pub(crate) const SNAPSHOT_CONTENT_TYPE: &str = "application/vnd.taskchampion.snapshot"; - -/// The header name for version ID -pub(crate) const VERSION_ID_HEADER: &str = "X-Version-Id"; - -/// The header name for client id -pub(crate) const CLIENT_ID_HEADER: &str = "X-Client-Id"; - -/// The header name for parent version ID -pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id"; - -/// The header name for parent version ID -pub(crate) const SNAPSHOT_REQUEST_HEADER: &str = "X-Snapshot-Request"; - -/// The type containing a reference to the persistent state for the server -pub(crate) struct ServerState { - pub(crate) storage: Box, - pub(crate) config: ServerConfig, -} - -pub(crate) fn api_scope() -> Scope { - web::scope("") - .service(get_child_version::service) - .service(add_version::service) - .service(get_snapshot::service) - .service(add_snapshot::service) -} - -/// Convert a failure::Error to an Actix ISE -fn failure_to_ise(err: anyhow::Error) -> impl actix_web::ResponseError { - error::InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR) -} - -/// Get the client id -fn client_id_header(req: &HttpRequest) -> Result { - fn badrequest() -> error::Error { - error::ErrorBadRequest("bad x-client-id") - } - if let Some(client_id_hdr) = req.headers().get(CLIENT_ID_HEADER) { - let client_id = client_id_hdr.to_str().map_err(|_| badrequest())?; - let client_id = ClientId::parse_str(client_id).map_err(|_| badrequest())?; - Ok(client_id) - } else { - Err(badrequest()) - } -} diff --git a/taskchampion/sync-server/src/bin/taskchampion-sync-server.rs b/taskchampion/sync-server/src/bin/taskchampion-sync-server.rs deleted file mode 100644 index 64210e413..000000000 --- a/taskchampion/sync-server/src/bin/taskchampion-sync-server.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![deny(clippy::all)] - -use actix_web::{middleware::Logger, App, HttpServer}; -use clap::{arg, builder::ValueParser, value_parser, Command}; -use std::ffi::OsString; -use taskchampion_sync_server::storage::SqliteStorage; -use taskchampion_sync_server::{Server, ServerConfig}; - -#[actix_web::main] -async fn main() -> anyhow::Result<()> { - env_logger::init(); - let defaults = ServerConfig::default(); - let default_snapshot_versions = defaults.snapshot_versions.to_string(); - let default_snapshot_days = defaults.snapshot_days.to_string(); - let matches = Command::new("taskchampion-sync-server") - .version(env!("CARGO_PKG_VERSION")) - .about("Server for TaskChampion") - .arg( - arg!(-p --port "Port on which to serve") - .help("Port on which to serve") - .value_parser(value_parser!(usize)) - .default_value("8080"), - ) - .arg( - arg!(-d --"data-dir" "Directory in which to store data") - .value_parser(ValueParser::os_string()) - .default_value("/var/lib/taskchampion-sync-server"), - ) - .arg( - arg!(--"snapshot-versions" "Target number of versions between snapshots") - .value_parser(value_parser!(u32)) - .default_value(default_snapshot_versions), - ) - .arg( - arg!(--"snapshot-days" "Target number of days between snapshots") - .value_parser(value_parser!(i64)) - .default_value(default_snapshot_days), - ) - .get_matches(); - - let data_dir: &OsString = matches.get_one("data-dir").unwrap(); - let port: usize = *matches.get_one("port").unwrap(); - let snapshot_versions: u32 = *matches.get_one("snapshot-versions").unwrap(); - let snapshot_days: i64 = *matches.get_one("snapshot-days").unwrap(); - - let config = ServerConfig::from_args(snapshot_days, snapshot_versions)?; - let server = Server::new(config, Box::new(SqliteStorage::new(data_dir)?)); - - log::warn!("Serving on port {}", port); - HttpServer::new(move || { - App::new() - .wrap(Logger::default()) - .configure(|cfg| server.config(cfg)) - }) - .bind(format!("0.0.0.0:{}", port))? - .run() - .await?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use actix_web::{test, App}; - use taskchampion_sync_server::storage::InMemoryStorage; - - #[actix_rt::test] - async fn test_index_get() { - let server = Server::new(Default::default(), Box::new(InMemoryStorage::new())); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let req = test::TestRequest::get().uri("/").to_request(); - let resp = test::call_service(&mut app, req).await; - assert!(resp.status().is_success()); - } -} diff --git a/taskchampion/sync-server/src/lib.rs b/taskchampion/sync-server/src/lib.rs deleted file mode 100644 index 5ba4658b1..000000000 --- a/taskchampion/sync-server/src/lib.rs +++ /dev/null @@ -1,72 +0,0 @@ -#![deny(clippy::all)] - -mod api; -mod server; -pub mod storage; - -use crate::storage::Storage; -use actix_web::{get, middleware, web, Responder}; -use api::{api_scope, ServerState}; -use std::sync::Arc; - -pub use server::ServerConfig; - -#[get("/")] -async fn index() -> impl Responder { - format!("TaskChampion sync server v{}", env!("CARGO_PKG_VERSION")) -} - -/// A Server represents a sync server. -#[derive(Clone)] -pub struct Server { - server_state: Arc, -} - -impl Server { - /// Create a new sync server with the given storage implementation. - pub fn new(config: ServerConfig, storage: Box) -> Self { - Self { - server_state: Arc::new(ServerState { config, storage }), - } - } - - /// Get an Actix-web service for this server. - pub fn config(&self, cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("") - .app_data(web::Data::new(self.server_state.clone())) - .wrap( - middleware::DefaultHeaders::new().add(("Cache-Control", "no-store, max-age=0")), - ) - .service(index) - .service(api_scope()), - ); - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::storage::InMemoryStorage; - use actix_web::{test, App}; - use pretty_assertions::assert_eq; - - pub(crate) fn init_logging() { - let _ = env_logger::builder().is_test(true).try_init(); - } - - #[actix_rt::test] - async fn test_cache_control() { - let server = Server::new(Default::default(), Box::new(InMemoryStorage::new())); - let app = App::new().configure(|sc| server.config(sc)); - let mut app = test::init_service(app).await; - - let req = test::TestRequest::get().uri("/").to_request(); - let resp = test::call_service(&mut app, req).await; - assert!(resp.status().is_success()); - assert_eq!( - resp.headers().get("Cache-Control").unwrap(), - &"no-store, max-age=0".to_string() - ) - } -} diff --git a/taskchampion/sync-server/src/server.rs b/taskchampion/sync-server/src/server.rs deleted file mode 100644 index 61e349e2c..000000000 --- a/taskchampion/sync-server/src/server.rs +++ /dev/null @@ -1,1048 +0,0 @@ -//! This module implements the core logic of the server: handling transactions, upholding -//! invariants, and so on. This does not implement the HTTP-specific portions; those -//! are in [`crate::api`]. See the protocol documentation for details. -use crate::storage::{Client, Snapshot, StorageTxn}; -use chrono::Utc; -use uuid::Uuid; - -/// The distinguished value for "no version" -pub const NIL_VERSION_ID: VersionId = Uuid::nil(); - -/// Number of versions to search back from the latest to find the -/// version for a newly-added snapshot. Snapshots for versions older -/// than this will be rejected. -const SNAPSHOT_SEARCH_LEN: i32 = 5; - -pub(crate) type HistorySegment = Vec; -pub(crate) type ClientId = Uuid; -pub(crate) type VersionId = Uuid; - -/// ServerConfig contains configuration parameters for the server. -pub struct ServerConfig { - /// Target number of days between snapshots. - pub snapshot_days: i64, - - /// Target number of versions between snapshots. - pub snapshot_versions: u32, -} - -impl Default for ServerConfig { - fn default() -> Self { - ServerConfig { - snapshot_days: 14, - snapshot_versions: 100, - } - } -} - -impl ServerConfig { - pub fn from_args(snapshot_days: i64, snapshot_versions: u32) -> anyhow::Result { - Ok(ServerConfig { - snapshot_days, - snapshot_versions, - }) - } -} - -/// Response to get_child_version. See the protocol documentation. -#[derive(Clone, PartialEq, Debug)] -pub(crate) enum GetVersionResult { - NotFound, - Gone, - Success { - version_id: Uuid, - parent_version_id: Uuid, - history_segment: HistorySegment, - }, -} - -/// Implementation of the GetChildVersion protocol transaction -pub(crate) fn get_child_version<'a>( - mut txn: Box, - _config: &ServerConfig, - client_id: ClientId, - client: Client, - parent_version_id: VersionId, -) -> anyhow::Result { - // If a version with parentVersionId equal to the requested parentVersionId exists, it is returned. - if let Some(version) = txn.get_version_by_parent(client_id, parent_version_id)? { - return Ok(GetVersionResult::Success { - version_id: version.version_id, - parent_version_id: version.parent_version_id, - history_segment: version.history_segment, - }); - } - - // If the requested parentVersionId is the nil UUID .. - if parent_version_id == NIL_VERSION_ID { - return Ok(match client.snapshot { - // ..and snapshotVersionId is nil, the response is _not-found_ (the client has no - // versions). - None => GetVersionResult::NotFound, - // ..and snapshotVersionId is not nil, the response is _gone_ (the first version has - // been deleted). - Some(_) => GetVersionResult::Gone, - }); - } - - // If a version with versionId equal to the requested parentVersionId exists, the response is _not-found_ (the client is up-to-date) - if txn.get_version(client_id, parent_version_id)?.is_some() { - return Ok(GetVersionResult::NotFound); - } - - // Otherwise, the response is _gone_ (the requested version has been deleted). - Ok(GetVersionResult::Gone) -} - -/// Response to add_version -#[derive(Clone, PartialEq, Debug)] -pub(crate) enum AddVersionResult { - /// OK, version added with the given ID - Ok(VersionId), - /// Rejected; expected a version with the given parent version - ExpectedParentVersion(VersionId), -} - -/// Urgency of a snapshot for a client; used to create the `X-Snapshot-Request` header. -#[derive(PartialEq, Debug, Clone, Copy, Eq, PartialOrd, Ord)] -pub(crate) enum SnapshotUrgency { - /// Don't need a snapshot right now. - None, - - /// A snapshot would be good, but can wait for other replicas to provide it. - Low, - - /// A snapshot is needed right now. - High, -} - -impl SnapshotUrgency { - /// Calculate the urgency for a snapshot based on its age in days - fn for_days(config: &ServerConfig, days: i64) -> Self { - if days >= config.snapshot_days * 3 / 2 { - SnapshotUrgency::High - } else if days >= config.snapshot_days { - SnapshotUrgency::Low - } else { - SnapshotUrgency::None - } - } - - /// Calculate the urgency for a snapshot based on its age in versions - fn for_versions_since(config: &ServerConfig, versions_since: u32) -> Self { - if versions_since >= config.snapshot_versions * 3 / 2 { - SnapshotUrgency::High - } else if versions_since >= config.snapshot_versions { - SnapshotUrgency::Low - } else { - SnapshotUrgency::None - } - } -} - -/// Implementation of the AddVersion protocol transaction -pub(crate) fn add_version<'a>( - mut txn: Box, - config: &ServerConfig, - client_id: ClientId, - client: Client, - parent_version_id: VersionId, - history_segment: HistorySegment, -) -> anyhow::Result<(AddVersionResult, SnapshotUrgency)> { - log::debug!( - "add_version(client_id: {}, parent_version_id: {})", - client_id, - parent_version_id, - ); - - // check if this version is acceptable, under the protection of the transaction - if client.latest_version_id != NIL_VERSION_ID && parent_version_id != client.latest_version_id { - log::debug!("add_version request rejected: mismatched latest_version_id"); - return Ok(( - AddVersionResult::ExpectedParentVersion(client.latest_version_id), - SnapshotUrgency::None, - )); - } - - // invent a version ID - let version_id = Uuid::new_v4(); - log::debug!( - "add_version request accepted: new version_id: {}", - version_id - ); - - // update the DB - txn.add_version(client_id, version_id, parent_version_id, history_segment)?; - txn.commit()?; - - // calculate the urgency - let time_urgency = match client.snapshot { - None => SnapshotUrgency::High, - Some(Snapshot { timestamp, .. }) => { - SnapshotUrgency::for_days(config, (Utc::now() - timestamp).num_days()) - } - }; - - let version_urgency = match client.snapshot { - None => SnapshotUrgency::High, - Some(Snapshot { versions_since, .. }) => { - SnapshotUrgency::for_versions_since(config, versions_since) - } - }; - - Ok(( - AddVersionResult::Ok(version_id), - std::cmp::max(time_urgency, version_urgency), - )) -} - -/// Implementation of the AddSnapshot protocol transaction -pub(crate) fn add_snapshot<'a>( - mut txn: Box, - _config: &ServerConfig, - client_id: ClientId, - client: Client, - version_id: VersionId, - data: Vec, -) -> anyhow::Result<()> { - log::debug!( - "add_snapshot(client_id: {}, version_id: {})", - client_id, - version_id, - ); - - // NOTE: if the snapshot is rejected, this function logs about it and returns - // Ok(()), as there's no reason to report an errot to the client / user. - - let last_snapshot = client.snapshot.map(|snap| snap.version_id); - if Some(version_id) == last_snapshot { - log::debug!( - "rejecting snapshot for version {}: already exists", - version_id - ); - return Ok(()); - } - - // look for this version in the history of this client, starting at the latest version, and - // only iterating for a limited number of versions. - let mut search_len = SNAPSHOT_SEARCH_LEN; - let mut vid = client.latest_version_id; - - loop { - if vid == version_id && version_id != NIL_VERSION_ID { - // the new snapshot is for a recent version, so proceed - break; - } - - if Some(vid) == last_snapshot { - // the new snapshot is older than the last snapshot, so ignore it - log::debug!( - "rejecting snapshot for version {}: newer snapshot already exists or no such version", - version_id - ); - return Ok(()); - } - - search_len -= 1; - if search_len <= 0 || vid == NIL_VERSION_ID { - // this should not happen in normal operation, so warn about it - log::warn!( - "rejecting snapshot for version {}: version is too old or no such version", - version_id - ); - return Ok(()); - } - - // get the parent version ID - if let Some(parent) = txn.get_version(client_id, vid)? { - vid = parent.parent_version_id; - } else { - // this version does not exist; "this should not happen" but if it does, - // we don't need a snapshot earlier than the missing version. - log::warn!( - "rejecting snapshot for version {}: newer versions have already been deleted", - version_id - ); - return Ok(()); - } - } - - log::warn!("accepting snapshot for version {}", version_id); - txn.set_snapshot( - client_id, - Snapshot { - version_id, - timestamp: Utc::now(), - versions_since: 0, - }, - data, - )?; - txn.commit()?; - Ok(()) -} - -/// Implementation of the GetSnapshot protocol transaction -pub(crate) fn get_snapshot<'a>( - mut txn: Box, - _config: &ServerConfig, - client_id: ClientId, - client: Client, -) -> anyhow::Result)>> { - Ok(if let Some(snap) = client.snapshot { - txn.get_snapshot_data(client_id, snap.version_id)? - .map(|data| (snap.version_id, data)) - } else { - None - }) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::storage::{InMemoryStorage, Snapshot, Storage}; - use crate::test::init_logging; - use chrono::{Duration, TimeZone, Utc}; - use pretty_assertions::assert_eq; - - #[test] - fn snapshot_urgency_max() { - use SnapshotUrgency::*; - assert_eq!(std::cmp::max(None, None), None); - assert_eq!(std::cmp::max(None, Low), Low); - assert_eq!(std::cmp::max(None, High), High); - assert_eq!(std::cmp::max(Low, None), Low); - assert_eq!(std::cmp::max(Low, Low), Low); - assert_eq!(std::cmp::max(Low, High), High); - assert_eq!(std::cmp::max(High, None), High); - assert_eq!(std::cmp::max(High, Low), High); - assert_eq!(std::cmp::max(High, High), High); - } - - #[test] - fn snapshot_urgency_for_days() { - use SnapshotUrgency::*; - let config = ServerConfig::default(); - assert_eq!(SnapshotUrgency::for_days(&config, 0), None); - assert_eq!( - SnapshotUrgency::for_days(&config, config.snapshot_days), - Low - ); - assert_eq!( - SnapshotUrgency::for_days(&config, config.snapshot_days * 2), - High - ); - } - - #[test] - fn snapshot_urgency_for_versions_since() { - use SnapshotUrgency::*; - let config = ServerConfig::default(); - assert_eq!(SnapshotUrgency::for_versions_since(&config, 0), None); - assert_eq!( - SnapshotUrgency::for_versions_since(&config, config.snapshot_versions), - Low - ); - assert_eq!( - SnapshotUrgency::for_versions_since(&config, config.snapshot_versions * 2), - High - ); - } - - #[test] - fn get_child_version_not_found_initial() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - txn.new_client(client_id, NIL_VERSION_ID)?; - - // when no snapshot exists, the first version is NotFound - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!( - get_child_version( - txn, - &ServerConfig::default(), - client_id, - client, - NIL_VERSION_ID - )?, - GetVersionResult::NotFound - ); - Ok(()) - } - - #[test] - fn get_child_version_gone_initial() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - - txn.new_client(client_id, Uuid::new_v4())?; - txn.set_snapshot( - client_id, - Snapshot { - version_id: Uuid::new_v4(), - versions_since: 0, - timestamp: Utc.ymd(2001, 9, 9).and_hms(1, 46, 40), - }, - vec![1, 2, 3], - )?; - - // when a snapshot exists, the first version is GONE - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!( - get_child_version( - txn, - &ServerConfig::default(), - client_id, - client, - NIL_VERSION_ID - )?, - GetVersionResult::Gone - ); - Ok(()) - } - - #[test] - fn get_child_version_not_found_up_to_date() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - - // add a parent version, but not the requested child version - let parent_version_id = Uuid::new_v4(); - txn.new_client(client_id, parent_version_id)?; - txn.add_version(client_id, parent_version_id, NIL_VERSION_ID, vec![])?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!( - get_child_version( - txn, - &ServerConfig::default(), - client_id, - client, - parent_version_id - )?, - GetVersionResult::NotFound - ); - Ok(()) - } - - #[test] - fn get_child_version_gone() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - - // make up a parent version id, but neither that version - // nor its child exists (presumed to have been deleted) - let parent_version_id = Uuid::new_v4(); - txn.new_client(client_id, Uuid::new_v4())?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!( - get_child_version( - txn, - &ServerConfig::default(), - client_id, - client, - parent_version_id - )?, - GetVersionResult::Gone - ); - Ok(()) - } - - #[test] - fn get_child_version_found() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let history_segment = b"abcd".to_vec(); - - txn.new_client(client_id, version_id)?; - txn.add_version( - client_id, - version_id, - parent_version_id, - history_segment.clone(), - )?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!( - get_child_version( - txn, - &ServerConfig::default(), - client_id, - client, - parent_version_id - )?, - GetVersionResult::Success { - version_id, - parent_version_id, - history_segment, - } - ); - Ok(()) - } - - /// Utility setup function for add_version tests - fn av_setup( - storage: &InMemoryStorage, - num_versions: u32, - snapshot_version: Option, - snapshot_days_ago: Option, - ) -> anyhow::Result<(Uuid, Vec)> { - init_logging(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let mut versions = vec![]; - - let mut version_id = Uuid::nil(); - txn.new_client(client_id, Uuid::nil())?; - for vnum in 0..num_versions { - let parent_version_id = version_id; - version_id = Uuid::new_v4(); - versions.push(version_id); - txn.add_version( - client_id, - version_id, - parent_version_id, - vec![0, 0, vnum as u8], - )?; - if Some(vnum) == snapshot_version { - txn.set_snapshot( - client_id, - Snapshot { - version_id, - versions_since: 0, - timestamp: Utc::now() - Duration::days(snapshot_days_ago.unwrap_or(0)), - }, - vec![vnum as u8], - )?; - } - } - - Ok((client_id, versions)) - } - - /// Utility function to check the results of an add_version call - fn av_success_check( - storage: &InMemoryStorage, - client_id: Uuid, - existing_versions: &[Uuid], - result: (AddVersionResult, SnapshotUrgency), - expected_history: Vec, - expected_urgency: SnapshotUrgency, - ) -> anyhow::Result<()> { - if let AddVersionResult::Ok(new_version_id) = result.0 { - // check that it invented a new version ID - for v in existing_versions { - assert_ne!(&new_version_id, v); - } - - // verify that the storage was updated - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!(client.latest_version_id, new_version_id); - - let parent_version_id = existing_versions.last().cloned().unwrap_or_else(Uuid::nil); - let version = txn.get_version(client_id, new_version_id)?.unwrap(); - assert_eq!(version.version_id, new_version_id); - assert_eq!(version.parent_version_id, parent_version_id); - assert_eq!(version.history_segment, expected_history); - } else { - panic!("did not get Ok from add_version: {:?}", result); - } - - assert_eq!(result.1, expected_urgency); - - Ok(()) - } - - #[test] - fn add_version_conflict() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let (client_id, versions) = av_setup(&storage, 3, None, None)?; - - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - - // try to add a child of a version other than the latest - assert_eq!( - add_version( - txn, - &ServerConfig::default(), - client_id, - client, - versions[1], - vec![3, 6, 9] - )? - .0, - AddVersionResult::ExpectedParentVersion(versions[2]) - ); - - // verify that the storage wasn't updated - txn = storage.txn()?; - assert_eq!( - txn.get_client(client_id)?.unwrap().latest_version_id, - versions[2] - ); - assert_eq!(txn.get_version_by_parent(client_id, versions[2])?, None); - - Ok(()) - } - - #[test] - fn add_version_with_existing_history() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let (client_id, versions) = av_setup(&storage, 1, None, None)?; - - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - - let result = add_version( - txn, - &ServerConfig::default(), - client_id, - client, - versions[0], - vec![3, 6, 9], - )?; - - av_success_check( - &storage, - client_id, - &versions, - result, - vec![3, 6, 9], - // urgency=high because there are no snapshots yet - SnapshotUrgency::High, - )?; - - Ok(()) - } - - #[test] - fn add_version_with_no_history() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let (client_id, versions) = av_setup(&storage, 0, None, None)?; - - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - - let parent_version_id = Uuid::nil(); - let result = add_version( - txn, - &ServerConfig::default(), - client_id, - client, - parent_version_id, - vec![3, 6, 9], - )?; - - av_success_check( - &storage, - client_id, - &versions, - result, - vec![3, 6, 9], - // urgency=high because there are no snapshots yet - SnapshotUrgency::High, - )?; - - Ok(()) - } - - #[test] - fn add_version_success_recent_snapshot() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let (client_id, versions) = av_setup(&storage, 1, Some(0), None)?; - - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - - let result = add_version( - txn, - &ServerConfig::default(), - client_id, - client, - versions[0], - vec![1, 2, 3], - )?; - - av_success_check( - &storage, - client_id, - &versions, - result, - vec![1, 2, 3], - // no snapshot request since the previous version has a snapshot - SnapshotUrgency::None, - )?; - - Ok(()) - } - - #[test] - fn add_version_success_aged_snapshot() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - // one snapshot, but it was 50 days ago - let (client_id, versions) = av_setup(&storage, 1, Some(0), Some(50))?; - - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - - let result = add_version( - txn, - &ServerConfig::default(), - client_id, - client, - versions[0], - vec![1, 2, 3], - )?; - - av_success_check( - &storage, - client_id, - &versions, - result, - vec![1, 2, 3], - // urgency=high due to days since the snapshot - SnapshotUrgency::High, - )?; - - Ok(()) - } - - #[test] - fn add_version_success_snapshot_many_versions_ago() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - // one snapshot, but it was 50 versions ago - let (client_id, versions) = av_setup(&storage, 50, Some(0), None)?; - - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - - let result = add_version( - txn, - &ServerConfig { - snapshot_versions: 30, - ..ServerConfig::default() - }, - client_id, - client, - versions[49], - vec![1, 2, 3], - )?; - - av_success_check( - &storage, - client_id, - &versions, - result, - vec![1, 2, 3], - // urgency=high due to number of versions since the snapshot - SnapshotUrgency::High, - )?; - - Ok(()) - } - - #[test] - fn add_snapshot_success_latest() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - - // set up a task DB with one version in it - txn.new_client(client_id, version_id)?; - txn.add_version(client_id, version_id, NIL_VERSION_ID, vec![])?; - - // add a snapshot for that version - let client = txn.get_client(client_id)?.unwrap(); - add_snapshot( - txn, - &ServerConfig::default(), - client_id, - client, - version_id, - vec![1, 2, 3], - )?; - - // verify the snapshot - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - let snapshot = client.snapshot.unwrap(); - assert_eq!(snapshot.version_id, version_id); - assert_eq!(snapshot.versions_since, 0); - assert_eq!( - txn.get_snapshot_data(client_id, version_id).unwrap(), - Some(vec![1, 2, 3]) - ); - - Ok(()) - } - - #[test] - fn add_snapshot_success_older() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let version_id_1 = Uuid::new_v4(); - let version_id_2 = Uuid::new_v4(); - - // set up a task DB with two versions in it - txn.new_client(client_id, version_id_2)?; - txn.add_version(client_id, version_id_1, NIL_VERSION_ID, vec![])?; - txn.add_version(client_id, version_id_2, version_id_1, vec![])?; - - // add a snapshot for version 1 - let client = txn.get_client(client_id)?.unwrap(); - add_snapshot( - txn, - &ServerConfig::default(), - client_id, - client, - version_id_1, - vec![1, 2, 3], - )?; - - // verify the snapshot - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - let snapshot = client.snapshot.unwrap(); - assert_eq!(snapshot.version_id, version_id_1); - assert_eq!(snapshot.versions_since, 0); - assert_eq!( - txn.get_snapshot_data(client_id, version_id_1).unwrap(), - Some(vec![1, 2, 3]) - ); - - Ok(()) - } - - #[test] - fn add_snapshot_fails_no_such() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let version_id_1 = Uuid::new_v4(); - let version_id_2 = Uuid::new_v4(); - - // set up a task DB with two versions in it - txn.new_client(client_id, version_id_2)?; - txn.add_version(client_id, version_id_1, NIL_VERSION_ID, vec![])?; - txn.add_version(client_id, version_id_2, version_id_1, vec![])?; - - // add a snapshot for unknown version - let client = txn.get_client(client_id)?.unwrap(); - let version_id_unk = Uuid::new_v4(); - add_snapshot( - txn, - &ServerConfig::default(), - client_id, - client, - version_id_unk, - vec![1, 2, 3], - )?; - - // verify the snapshot does not exist - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - assert!(client.snapshot.is_none()); - - Ok(()) - } - - #[test] - fn add_snapshot_fails_too_old() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let mut version_id = Uuid::new_v4(); - let mut parent_version_id = Uuid::nil(); - let mut version_ids = vec![]; - - // set up a task DB with 10 versions in it (oldest to newest) - txn.new_client(client_id, Uuid::nil())?; - for _ in 0..10 { - txn.add_version(client_id, version_id, parent_version_id, vec![])?; - version_ids.push(version_id); - parent_version_id = version_id; - version_id = Uuid::new_v4(); - } - - // add a snapshot for the earliest of those - let client = txn.get_client(client_id)?.unwrap(); - add_snapshot( - txn, - &ServerConfig::default(), - client_id, - client, - version_ids[0], - vec![1, 2, 3], - )?; - - // verify the snapshot does not exist - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - assert!(client.snapshot.is_none()); - - Ok(()) - } - - #[test] - fn add_snapshot_fails_newer_exists() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let mut version_id = Uuid::new_v4(); - let mut parent_version_id = Uuid::nil(); - let mut version_ids = vec![]; - - // set up a task DB with 5 versions in it (oldest to newest) and a snapshot of the middle - // one - txn.new_client(client_id, Uuid::nil())?; - for _ in 0..5 { - txn.add_version(client_id, version_id, parent_version_id, vec![])?; - version_ids.push(version_id); - parent_version_id = version_id; - version_id = Uuid::new_v4(); - } - txn.set_snapshot( - client_id, - Snapshot { - version_id: version_ids[2], - versions_since: 2, - timestamp: Utc.ymd(2001, 9, 9).and_hms(1, 46, 40), - }, - vec![1, 2, 3], - )?; - - // add a snapshot for the earliest of those - let client = txn.get_client(client_id)?.unwrap(); - add_snapshot( - txn, - &ServerConfig::default(), - client_id, - client, - version_ids[0], - vec![9, 9, 9], - )?; - - // verify the snapshot was not replaced - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - let snapshot = client.snapshot.unwrap(); - assert_eq!(snapshot.version_id, version_ids[2]); - assert_eq!(snapshot.versions_since, 2); - assert_eq!( - txn.get_snapshot_data(client_id, version_ids[2]).unwrap(), - Some(vec![1, 2, 3]) - ); - - Ok(()) - } - - #[test] - fn add_snapshot_fails_nil_version() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - - // just set up the client - txn.new_client(client_id, NIL_VERSION_ID)?; - - // add a snapshot for the nil version - let client = txn.get_client(client_id)?.unwrap(); - add_snapshot( - txn, - &ServerConfig::default(), - client_id, - client, - NIL_VERSION_ID, - vec![9, 9, 9], - )?; - - // verify the snapshot does not exist - let mut txn = storage.txn()?; - let client = txn.get_client(client_id)?.unwrap(); - assert!(client.snapshot.is_none()); - - Ok(()) - } - - #[test] - fn get_snapshot_found() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - let data = vec![1, 2, 3]; - let snapshot_version_id = Uuid::new_v4(); - - txn.new_client(client_id, snapshot_version_id)?; - txn.set_snapshot( - client_id, - Snapshot { - version_id: snapshot_version_id, - versions_since: 3, - timestamp: Utc.ymd(2001, 9, 9).and_hms(1, 46, 40), - }, - data.clone(), - )?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!( - get_snapshot(txn, &ServerConfig::default(), client_id, client)?, - Some((snapshot_version_id, data)) - ); - - Ok(()) - } - - #[test] - fn get_snapshot_not_found() -> anyhow::Result<()> { - init_logging(); - - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let client_id = Uuid::new_v4(); - - txn.new_client(client_id, NIL_VERSION_ID)?; - let client = txn.get_client(client_id)?.unwrap(); - - assert_eq!( - get_snapshot(txn, &ServerConfig::default(), client_id, client)?, - None - ); - - Ok(()) - } -} diff --git a/taskchampion/sync-server/src/storage/inmemory.rs b/taskchampion/sync-server/src/storage/inmemory.rs deleted file mode 100644 index 3b17720eb..000000000 --- a/taskchampion/sync-server/src/storage/inmemory.rs +++ /dev/null @@ -1,286 +0,0 @@ -use super::{Client, Snapshot, Storage, StorageTxn, Uuid, Version}; -use std::collections::HashMap; -use std::sync::{Mutex, MutexGuard}; - -struct Inner { - /// Clients, indexed by client_id - clients: HashMap, - - /// Snapshot data, indexed by client id - snapshots: HashMap>, - - /// Versions, indexed by (client_id, version_id) - versions: HashMap<(Uuid, Uuid), Version>, - - /// Child versions, indexed by (client_id, parent_version_id) - children: HashMap<(Uuid, Uuid), Uuid>, -} - -pub struct InMemoryStorage(Mutex); - -impl InMemoryStorage { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self(Mutex::new(Inner { - clients: HashMap::new(), - snapshots: HashMap::new(), - versions: HashMap::new(), - children: HashMap::new(), - })) - } -} - -struct InnerTxn<'a>(MutexGuard<'a, Inner>); - -/// In-memory storage for testing and experimentation. -/// -/// NOTE: this does not implement transaction rollback. -impl Storage for InMemoryStorage { - fn txn<'a>(&'a self) -> anyhow::Result> { - Ok(Box::new(InnerTxn(self.0.lock().expect("poisoned lock")))) - } -} - -impl<'a> StorageTxn for InnerTxn<'a> { - fn get_client(&mut self, client_id: Uuid) -> anyhow::Result> { - Ok(self.0.clients.get(&client_id).cloned()) - } - - fn new_client(&mut self, client_id: Uuid, latest_version_id: Uuid) -> anyhow::Result<()> { - if self.0.clients.get(&client_id).is_some() { - return Err(anyhow::anyhow!("Client {} already exists", client_id)); - } - self.0.clients.insert( - client_id, - Client { - latest_version_id, - snapshot: None, - }, - ); - Ok(()) - } - - fn set_snapshot( - &mut self, - client_id: Uuid, - snapshot: Snapshot, - data: Vec, - ) -> anyhow::Result<()> { - let client = self - .0 - .clients - .get_mut(&client_id) - .ok_or_else(|| anyhow::anyhow!("no such client"))?; - client.snapshot = Some(snapshot); - self.0.snapshots.insert(client_id, data); - Ok(()) - } - - fn get_snapshot_data( - &mut self, - client_id: Uuid, - version_id: Uuid, - ) -> anyhow::Result>> { - // sanity check - let client = self.0.clients.get(&client_id); - let client = client.ok_or_else(|| anyhow::anyhow!("no such client"))?; - if Some(&version_id) != client.snapshot.as_ref().map(|snap| &snap.version_id) { - return Err(anyhow::anyhow!("unexpected snapshot_version_id")); - } - Ok(self.0.snapshots.get(&client_id).cloned()) - } - - fn get_version_by_parent( - &mut self, - client_id: Uuid, - parent_version_id: Uuid, - ) -> anyhow::Result> { - if let Some(parent_version_id) = self.0.children.get(&(client_id, parent_version_id)) { - Ok(self - .0 - .versions - .get(&(client_id, *parent_version_id)) - .cloned()) - } else { - Ok(None) - } - } - - fn get_version( - &mut self, - client_id: Uuid, - version_id: Uuid, - ) -> anyhow::Result> { - Ok(self.0.versions.get(&(client_id, version_id)).cloned()) - } - - fn add_version( - &mut self, - client_id: Uuid, - version_id: Uuid, - parent_version_id: Uuid, - history_segment: Vec, - ) -> anyhow::Result<()> { - // TODO: verify it doesn't exist (`.entry`?) - let version = Version { - version_id, - parent_version_id, - history_segment, - }; - - if let Some(client) = self.0.clients.get_mut(&client_id) { - client.latest_version_id = version_id; - if let Some(ref mut snap) = client.snapshot { - snap.versions_since += 1; - } - } else { - return Err(anyhow::anyhow!("Client {} does not exist", client_id)); - } - - self.0 - .children - .insert((client_id, parent_version_id), version_id); - self.0.versions.insert((client_id, version_id), version); - - Ok(()) - } - - fn commit(&mut self) -> anyhow::Result<()> { - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use chrono::Utc; - - #[test] - fn test_get_client_empty() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let maybe_client = txn.get_client(Uuid::new_v4())?; - assert!(maybe_client.is_none()); - Ok(()) - } - - #[test] - fn test_client_storage() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - - let client_id = Uuid::new_v4(); - let latest_version_id = Uuid::new_v4(); - txn.new_client(client_id, latest_version_id)?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!(client.latest_version_id, latest_version_id); - assert!(client.snapshot.is_none()); - - let latest_version_id = Uuid::new_v4(); - txn.add_version(client_id, latest_version_id, Uuid::new_v4(), vec![1, 1])?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!(client.latest_version_id, latest_version_id); - assert!(client.snapshot.is_none()); - - let snap = Snapshot { - version_id: Uuid::new_v4(), - timestamp: Utc::now(), - versions_since: 4, - }; - txn.set_snapshot(client_id, snap.clone(), vec![1, 2, 3])?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!(client.latest_version_id, latest_version_id); - assert_eq!(client.snapshot.unwrap(), snap); - - Ok(()) - } - - #[test] - fn test_gvbp_empty() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - let maybe_version = txn.get_version_by_parent(Uuid::new_v4(), Uuid::new_v4())?; - assert!(maybe_version.is_none()); - Ok(()) - } - - #[test] - fn test_add_version_and_get_version() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let history_segment = b"abc".to_vec(); - - txn.new_client(client_id, parent_version_id)?; - txn.add_version( - client_id, - version_id, - parent_version_id, - history_segment.clone(), - )?; - - let expected = Version { - version_id, - parent_version_id, - history_segment, - }; - - let version = txn - .get_version_by_parent(client_id, parent_version_id)? - .unwrap(); - assert_eq!(version, expected); - - let version = txn.get_version(client_id, version_id)?.unwrap(); - assert_eq!(version, expected); - - Ok(()) - } - - #[test] - fn test_snapshots() -> anyhow::Result<()> { - let storage = InMemoryStorage::new(); - let mut txn = storage.txn()?; - - let client_id = Uuid::new_v4(); - - txn.new_client(client_id, Uuid::new_v4())?; - assert!(txn.get_client(client_id)?.unwrap().snapshot.is_none()); - - let snap = Snapshot { - version_id: Uuid::new_v4(), - timestamp: Utc::now(), - versions_since: 3, - }; - txn.set_snapshot(client_id, snap.clone(), vec![9, 8, 9])?; - - assert_eq!( - txn.get_snapshot_data(client_id, snap.version_id)?.unwrap(), - vec![9, 8, 9] - ); - assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap)); - - let snap2 = Snapshot { - version_id: Uuid::new_v4(), - timestamp: Utc::now(), - versions_since: 10, - }; - txn.set_snapshot(client_id, snap2.clone(), vec![0, 2, 4, 6])?; - - assert_eq!( - txn.get_snapshot_data(client_id, snap2.version_id)?.unwrap(), - vec![0, 2, 4, 6] - ); - assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap2)); - - // check that mismatched version is detected - assert!(txn.get_snapshot_data(client_id, Uuid::new_v4()).is_err()); - - Ok(()) - } -} diff --git a/taskchampion/sync-server/src/storage/mod.rs b/taskchampion/sync-server/src/storage/mod.rs deleted file mode 100644 index a2e468fbb..000000000 --- a/taskchampion/sync-server/src/storage/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -use chrono::{DateTime, Utc}; -use uuid::Uuid; - -#[cfg(debug_assertions)] -mod inmemory; - -#[cfg(debug_assertions)] -pub use inmemory::InMemoryStorage; - -mod sqlite; -pub use self::sqlite::SqliteStorage; - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Client { - /// The latest version for this client (may be the nil version) - pub latest_version_id: Uuid, - /// Data about the latest snapshot for this client - pub snapshot: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Snapshot { - /// ID of the version at which this snapshot was made - pub version_id: Uuid, - - /// Timestamp at which this snapshot was set - pub timestamp: DateTime, - - /// Number of versions since this snapshot was made - pub versions_since: u32, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Version { - pub version_id: Uuid, - pub parent_version_id: Uuid, - pub history_segment: Vec, -} - -pub trait StorageTxn { - /// Get information about the given client - fn get_client(&mut self, client_id: Uuid) -> anyhow::Result>; - - /// Create a new client with the given latest_version_id - fn new_client(&mut self, client_id: Uuid, latest_version_id: Uuid) -> anyhow::Result<()>; - - /// Set the client's most recent snapshot. - fn set_snapshot( - &mut self, - client_id: Uuid, - snapshot: Snapshot, - data: Vec, - ) -> anyhow::Result<()>; - - /// Get the data for the most recent snapshot. The version_id - /// is used to verify that the snapshot is for the correct version. - fn get_snapshot_data( - &mut self, - client_id: Uuid, - version_id: Uuid, - ) -> anyhow::Result>>; - - /// Get a version, indexed by parent version id - fn get_version_by_parent( - &mut self, - client_id: Uuid, - parent_version_id: Uuid, - ) -> anyhow::Result>; - - /// Get a version, indexed by its own version id - fn get_version(&mut self, client_id: Uuid, version_id: Uuid) - -> anyhow::Result>; - - /// Add a version (that must not already exist), and - /// - update latest_version_id - /// - increment snapshot.versions_since - fn add_version( - &mut self, - client_id: Uuid, - version_id: Uuid, - parent_version_id: Uuid, - history_segment: Vec, - ) -> anyhow::Result<()>; - - /// Commit any changes made in the transaction. It is an error to call this more than - /// once. It is safe to skip this call for read-only operations. - fn commit(&mut self) -> anyhow::Result<()>; -} - -/// A trait for objects able to act as storage. Most of the interesting behavior is in the -/// [`crate::storage::StorageTxn`] trait. -pub trait Storage: Send + Sync { - /// Begin a transaction - fn txn<'a>(&'a self) -> anyhow::Result>; -} diff --git a/taskchampion/sync-server/src/storage/sqlite.rs b/taskchampion/sync-server/src/storage/sqlite.rs deleted file mode 100644 index a8571bb36..000000000 --- a/taskchampion/sync-server/src/storage/sqlite.rs +++ /dev/null @@ -1,451 +0,0 @@ -use super::{Client, Snapshot, Storage, StorageTxn, Uuid, Version}; -use anyhow::Context; -use chrono::{TimeZone, Utc}; -use rusqlite::types::{FromSql, ToSql}; -use rusqlite::{params, Connection, OptionalExtension}; -use std::path::Path; - -#[derive(Debug, thiserror::Error)] -enum SqliteError { - #[error("Failed to create SQLite transaction")] - CreateTransactionFailed, -} - -/// Newtype to allow implementing `FromSql` for foreign `uuid::Uuid` -struct StoredUuid(Uuid); - -/// Conversion from Uuid stored as a string (rusqlite's uuid feature stores as binary blob) -impl FromSql for StoredUuid { - fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { - let u = Uuid::parse_str(value.as_str()?) - .map_err(|_| rusqlite::types::FromSqlError::InvalidType)?; - Ok(StoredUuid(u)) - } -} - -/// Store Uuid as string in database -impl ToSql for StoredUuid { - fn to_sql(&self) -> rusqlite::Result> { - let s = self.0.to_string(); - Ok(s.into()) - } -} - -/// An on-disk storage backend which uses SQLite -pub struct SqliteStorage { - db_file: std::path::PathBuf, -} - -impl SqliteStorage { - fn new_connection(&self) -> anyhow::Result { - Ok(Connection::open(&self.db_file)?) - } - - pub fn new>(directory: P) -> anyhow::Result { - std::fs::create_dir_all(&directory)?; - let db_file = directory.as_ref().join("taskchampion-sync-server.sqlite3"); - - let o = SqliteStorage { db_file }; - - { - let mut con = o.new_connection()?; - let txn = con.transaction()?; - - let queries = vec![ - "CREATE TABLE IF NOT EXISTS clients ( - client_id STRING PRIMARY KEY, - latest_version_id STRING, - snapshot_version_id STRING, - versions_since_snapshot INTEGER, - snapshot_timestamp INTEGER, - snapshot BLOB);", - "CREATE TABLE IF NOT EXISTS versions (version_id STRING PRIMARY KEY, client_id STRING, parent_version_id STRING, history_segment BLOB);", - "CREATE INDEX IF NOT EXISTS versions_by_parent ON versions (parent_version_id);", - ]; - for q in queries { - txn.execute(q, []) - .context("Error while creating SQLite tables")?; - } - txn.commit()?; - } - - Ok(o) - } -} - -impl Storage for SqliteStorage { - fn txn<'a>(&'a self) -> anyhow::Result> { - let con = self.new_connection()?; - let t = Txn { con }; - Ok(Box::new(t)) - } -} - -struct Txn { - con: Connection, -} - -impl Txn { - fn get_txn(&mut self) -> Result { - self.con - .transaction() - .map_err(|_e| SqliteError::CreateTransactionFailed) - } - - /// Implementation for queries from the versions table - fn get_version_impl( - &mut self, - query: &'static str, - client_id: Uuid, - version_id_arg: Uuid, - ) -> anyhow::Result> { - let t = self.get_txn()?; - let r = t - .query_row( - query, - params![&StoredUuid(version_id_arg), &StoredUuid(client_id)], - |r| { - let version_id: StoredUuid = r.get("version_id")?; - let parent_version_id: StoredUuid = r.get("parent_version_id")?; - - Ok(Version { - version_id: version_id.0, - parent_version_id: parent_version_id.0, - history_segment: r.get("history_segment")?, - }) - }, - ) - .optional() - .context("Error getting version")?; - Ok(r) - } -} - -impl StorageTxn for Txn { - fn get_client(&mut self, client_id: Uuid) -> anyhow::Result> { - let t = self.get_txn()?; - let result: Option = t - .query_row( - "SELECT - latest_version_id, - snapshot_timestamp, - versions_since_snapshot, - snapshot_version_id - FROM clients - WHERE client_id = ? - LIMIT 1", - [&StoredUuid(client_id)], - |r| { - let latest_version_id: StoredUuid = r.get(0)?; - let snapshot_timestamp: Option = r.get(1)?; - let versions_since_snapshot: Option = r.get(2)?; - let snapshot_version_id: Option = r.get(3)?; - - // if all of the relevant fields are non-NULL, return a snapshot - let snapshot = match ( - snapshot_timestamp, - versions_since_snapshot, - snapshot_version_id, - ) { - (Some(ts), Some(vs), Some(v)) => Some(Snapshot { - version_id: v.0, - timestamp: Utc.timestamp(ts, 0), - versions_since: vs, - }), - _ => None, - }; - Ok(Client { - latest_version_id: latest_version_id.0, - snapshot, - }) - }, - ) - .optional() - .context("Error getting client")?; - - Ok(result) - } - - fn new_client(&mut self, client_id: Uuid, latest_version_id: Uuid) -> anyhow::Result<()> { - let t = self.get_txn()?; - - t.execute( - "INSERT OR REPLACE INTO clients (client_id, latest_version_id) VALUES (?, ?)", - params![&StoredUuid(client_id), &StoredUuid(latest_version_id)], - ) - .context("Error creating/updating client")?; - t.commit()?; - Ok(()) - } - - fn set_snapshot( - &mut self, - client_id: Uuid, - snapshot: Snapshot, - data: Vec, - ) -> anyhow::Result<()> { - let t = self.get_txn()?; - - t.execute( - "UPDATE clients - SET - snapshot_version_id = ?, - snapshot_timestamp = ?, - versions_since_snapshot = ?, - snapshot = ? - WHERE client_id = ?", - params![ - &StoredUuid(snapshot.version_id), - snapshot.timestamp.timestamp(), - snapshot.versions_since, - data, - &StoredUuid(client_id), - ], - ) - .context("Error creating/updating snapshot")?; - t.commit()?; - Ok(()) - } - - fn get_snapshot_data( - &mut self, - client_id: Uuid, - version_id: Uuid, - ) -> anyhow::Result>> { - let t = self.get_txn()?; - let r = t - .query_row( - "SELECT snapshot, snapshot_version_id FROM clients WHERE client_id = ?", - params![&StoredUuid(client_id)], - |r| { - let v: StoredUuid = r.get("snapshot_version_id")?; - let d: Vec = r.get("snapshot")?; - Ok((v.0, d)) - }, - ) - .optional() - .context("Error getting snapshot")?; - r.map(|(v, d)| { - if v != version_id { - return Err(anyhow::anyhow!("unexpected snapshot_version_id")); - } - - Ok(d) - }) - .transpose() - } - - fn get_version_by_parent( - &mut self, - client_id: Uuid, - parent_version_id: Uuid, - ) -> anyhow::Result> { - self.get_version_impl( - "SELECT version_id, parent_version_id, history_segment FROM versions WHERE parent_version_id = ? AND client_id = ?", - client_id, - parent_version_id) - } - - fn get_version( - &mut self, - client_id: Uuid, - version_id: Uuid, - ) -> anyhow::Result> { - self.get_version_impl( - "SELECT version_id, parent_version_id, history_segment FROM versions WHERE version_id = ? AND client_id = ?", - client_id, - version_id) - } - - fn add_version( - &mut self, - client_id: Uuid, - version_id: Uuid, - parent_version_id: Uuid, - history_segment: Vec, - ) -> anyhow::Result<()> { - let t = self.get_txn()?; - - t.execute( - "INSERT INTO versions (version_id, client_id, parent_version_id, history_segment) VALUES(?, ?, ?, ?)", - params![ - StoredUuid(version_id), - StoredUuid(client_id), - StoredUuid(parent_version_id), - history_segment - ] - ) - .context("Error adding version")?; - t.execute( - "UPDATE clients - SET - latest_version_id = ?, - versions_since_snapshot = versions_since_snapshot + 1 - WHERE client_id = ?", - params![StoredUuid(version_id), StoredUuid(client_id),], - ) - .context("Error updating client for new version")?; - - t.commit()?; - Ok(()) - } - - fn commit(&mut self) -> anyhow::Result<()> { - // FIXME: Note the queries aren't currently run in a - // transaction, as storing the transaction object and a pooled - // connection in the `Txn` object is complex. - // https://github.com/taskchampion/taskchampion/pull/206#issuecomment-860336073 - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use chrono::DateTime; - use pretty_assertions::assert_eq; - use tempfile::TempDir; - - #[test] - fn test_emtpy_dir() -> anyhow::Result<()> { - let tmp_dir = TempDir::new()?; - let non_existant = tmp_dir.path().join("subdir"); - let storage = SqliteStorage::new(non_existant)?; - let mut txn = storage.txn()?; - let maybe_client = txn.get_client(Uuid::new_v4())?; - assert!(maybe_client.is_none()); - Ok(()) - } - - #[test] - fn test_get_client_empty() -> anyhow::Result<()> { - let tmp_dir = TempDir::new()?; - let storage = SqliteStorage::new(tmp_dir.path())?; - let mut txn = storage.txn()?; - let maybe_client = txn.get_client(Uuid::new_v4())?; - assert!(maybe_client.is_none()); - Ok(()) - } - - #[test] - fn test_client_storage() -> anyhow::Result<()> { - let tmp_dir = TempDir::new()?; - let storage = SqliteStorage::new(tmp_dir.path())?; - let mut txn = storage.txn()?; - - let client_id = Uuid::new_v4(); - let latest_version_id = Uuid::new_v4(); - txn.new_client(client_id, latest_version_id)?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!(client.latest_version_id, latest_version_id); - assert!(client.snapshot.is_none()); - - let latest_version_id = Uuid::new_v4(); - txn.add_version(client_id, latest_version_id, Uuid::new_v4(), vec![1, 1])?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!(client.latest_version_id, latest_version_id); - assert!(client.snapshot.is_none()); - - let snap = Snapshot { - version_id: Uuid::new_v4(), - timestamp: "2014-11-28T12:00:09Z".parse::>().unwrap(), - versions_since: 4, - }; - txn.set_snapshot(client_id, snap.clone(), vec![1, 2, 3])?; - - let client = txn.get_client(client_id)?.unwrap(); - assert_eq!(client.latest_version_id, latest_version_id); - assert_eq!(client.snapshot.unwrap(), snap); - - Ok(()) - } - - #[test] - fn test_gvbp_empty() -> anyhow::Result<()> { - let tmp_dir = TempDir::new()?; - let storage = SqliteStorage::new(tmp_dir.path())?; - let mut txn = storage.txn()?; - let maybe_version = txn.get_version_by_parent(Uuid::new_v4(), Uuid::new_v4())?; - assert!(maybe_version.is_none()); - Ok(()) - } - - #[test] - fn test_add_version_and_get_version() -> anyhow::Result<()> { - let tmp_dir = TempDir::new()?; - let storage = SqliteStorage::new(tmp_dir.path())?; - let mut txn = storage.txn()?; - - let client_id = Uuid::new_v4(); - let version_id = Uuid::new_v4(); - let parent_version_id = Uuid::new_v4(); - let history_segment = b"abc".to_vec(); - txn.add_version( - client_id, - version_id, - parent_version_id, - history_segment.clone(), - )?; - - let expected = Version { - version_id, - parent_version_id, - history_segment, - }; - - let version = txn - .get_version_by_parent(client_id, parent_version_id)? - .unwrap(); - assert_eq!(version, expected); - - let version = txn.get_version(client_id, version_id)?.unwrap(); - assert_eq!(version, expected); - - Ok(()) - } - - #[test] - fn test_snapshots() -> anyhow::Result<()> { - let tmp_dir = TempDir::new()?; - let storage = SqliteStorage::new(tmp_dir.path())?; - let mut txn = storage.txn()?; - - let client_id = Uuid::new_v4(); - - txn.new_client(client_id, Uuid::new_v4())?; - assert!(txn.get_client(client_id)?.unwrap().snapshot.is_none()); - - let snap = Snapshot { - version_id: Uuid::new_v4(), - timestamp: "2013-10-08T12:00:09Z".parse::>().unwrap(), - versions_since: 3, - }; - txn.set_snapshot(client_id, snap.clone(), vec![9, 8, 9])?; - - assert_eq!( - txn.get_snapshot_data(client_id, snap.version_id)?.unwrap(), - vec![9, 8, 9] - ); - assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap)); - - let snap2 = Snapshot { - version_id: Uuid::new_v4(), - timestamp: "2014-11-28T12:00:09Z".parse::>().unwrap(), - versions_since: 10, - }; - txn.set_snapshot(client_id, snap2.clone(), vec![0, 2, 4, 6])?; - - assert_eq!( - txn.get_snapshot_data(client_id, snap2.version_id)?.unwrap(), - vec![0, 2, 4, 6] - ); - assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap2)); - - // check that mismatched version is detected - assert!(txn.get_snapshot_data(client_id, Uuid::new_v4()).is_err()); - - Ok(()) - } -}