mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Remove taskchampion-sync-server (#3380)
This crate has been moved to https://github.com/GothenburgBitFactory/taskchampion-sync-server. The integration-tests repo used the sync server to test integration between taskchampion and the sync-server. We should do that again, but after taskchampion moves to its own repo (#3209). In the interim, the cross-sync integration test can simply test syncing between local servers, but the snapshot test is no longer useful as the local server does not support snapshots.
This commit is contained in:
parent
304b84e4da
commit
f054a4061e
20 changed files with 61 additions and 3589 deletions
536
Cargo.lock
generated
536
Cargo.lock
generated
|
@ -2,188 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
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]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
|
@ -210,18 +28,6 @@ dependencies = [
|
||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
@ -231,21 +37,6 @@ dependencies = [
|
||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -255,55 +46,6 @@ dependencies = [
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
|
@ -418,27 +160,6 @@ dependencies = [
|
||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.12.0"
|
version = "3.12.0"
|
||||||
|
@ -457,23 +178,11 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytestring"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.73"
|
version = "1.0.73"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||||
dependencies = [
|
|
||||||
"jobserver",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
|
@ -497,63 +206,12 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -599,19 +257,6 @@ dependencies = [
|
||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
@ -643,19 +288,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -972,7 +604,7 @@ version = "0.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.7.6",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1054,12 +686,6 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "humantime"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.20"
|
version = "0.14.20"
|
||||||
|
@ -1145,17 +771,12 @@ dependencies = [
|
||||||
name = "integration-tests"
|
name = "integration-tests"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-rt",
|
|
||||||
"actix-web",
|
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cc",
|
"cc",
|
||||||
"env_logger",
|
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"taskchampion",
|
"taskchampion",
|
||||||
"taskchampion-lib",
|
"taskchampion-lib",
|
||||||
"taskchampion-sync-server",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1176,18 +797,6 @@ version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
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]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
|
@ -1203,15 +812,6 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jobserver"
|
|
||||||
version = "0.1.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.59"
|
version = "0.3.59"
|
||||||
|
@ -1235,12 +835,6 @@ dependencies = [
|
||||||
"simple_asn1",
|
"simple_asn1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "language-tags"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -1296,24 +890,6 @@ version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
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]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
@ -1380,7 +956,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
@ -1464,12 +1039,6 @@ dependencies = [
|
||||||
"windows-sys 0.45.0",
|
"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]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -1928,17 +1497,6 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
@ -1950,15 +1508,6 @@ dependencies = [
|
||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
name = "simple_asn1"
|
name = "simple_asn1"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
@ -2025,12 +1574,6 @@ dependencies = [
|
||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
|
@ -2110,27 +1653,6 @@ dependencies = [
|
||||||
"taskchampion",
|
"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]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.6.0"
|
version = "3.6.0"
|
||||||
|
@ -2145,15 +1667,6 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.37"
|
version = "1.0.37"
|
||||||
|
@ -2239,7 +1752,6 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2 0.5.5",
|
"socket2 0.5.5",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
|
@ -2293,7 +1805,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
|
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"log",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
|
@ -2412,12 +1923,6 @@ version = "2.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8parse"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -2594,15 +2099,6 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
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]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -2836,33 +2332,3 @@ name = "zeroize"
|
||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
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",
|
|
||||||
]
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"taskchampion/taskchampion",
|
"taskchampion/taskchampion",
|
||||||
"taskchampion/sync-server",
|
|
||||||
"taskchampion/lib",
|
"taskchampion/lib",
|
||||||
"taskchampion/integration-tests",
|
"taskchampion/integration-tests",
|
||||||
"taskchampion/xtask",
|
"taskchampion/xtask",
|
||||||
|
@ -16,17 +15,12 @@ exclude = [ "src/tc/rust" ]
|
||||||
# All Rust dependencies are defined here, and then referenced by the
|
# All Rust dependencies are defined here, and then referenced by the
|
||||||
# Cargo.toml's in the members with `foo.workspace = true`.
|
# Cargo.toml's in the members with `foo.workspace = true`.
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-rt = "2"
|
|
||||||
actix-web = "^4.3.1"
|
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
cc = "1.0.73"
|
cc = "1.0.73"
|
||||||
chrono = { version = "^0.4.22", features = ["serde"] }
|
chrono = { version = "^0.4.22", features = ["serde"] }
|
||||||
clap = { version = "^4.3.0", features = ["string"] }
|
|
||||||
env_logger = "^0.10.2"
|
|
||||||
ffizz-header = "0.5"
|
ffizz-header = "0.5"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
futures = "^0.3.25"
|
|
||||||
google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] }
|
google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] }
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
libc = "0.2.136"
|
libc = "0.2.136"
|
||||||
|
|
|
@ -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
|
entirely independent. Task data is encrypted by Taskwarrior, and the sync
|
||||||
server never sees un-encrypted data.
|
server never sees un-encrypted data.
|
||||||
|
|
||||||
To start the server, run it in your preferred HTTP hosting environment, using
|
The server is developed in
|
||||||
`--port` to set the TCP port on which it should listen. It is recommended to
|
https://github.com/GothenburgBitFactory/taskchampion-sync-server.
|
||||||
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
|
|
||||||
|
|
||||||
.SS Adding a New User
|
.SS Adding a New User
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,8 @@ Other ideas;
|
||||||
|
|
||||||
TaskChampion is a typical Rust application.
|
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).
|
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.
|
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.
|
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.
|
The book documentation explains lots of the concepts in the design of TaskChampion.
|
||||||
It is linked from the README.
|
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.
|
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` is the core functionality of the application, implemented as a library
|
||||||
* `taskchampion-cli` implements the command-line interface (in `cli/`)
|
* `taskchampion-lib` implements a C API for `taskchampion`, used by Taskwarrior
|
||||||
* `taskchampion-sync-server` implements the synchronization server (in `sync-server/`)
|
* `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
|
## Making a Pull Request
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,11 @@ Until that is complete, the information here may be out-of-date.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
There are five crates here:
|
There are four crates here:
|
||||||
|
|
||||||
* [taskchampion](./taskchampion) - the core of the tool
|
* [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
|
* [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
|
* [xtask](./xtask) (private) - implementation of the `cargo xtask codegen` command
|
||||||
|
|
||||||
## Code Generation
|
## Code Generation
|
||||||
|
|
|
@ -7,18 +7,13 @@ publish = false
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
taskchampion = { path = "../taskchampion", features = ["server-sync"] }
|
taskchampion = { path = "../taskchampion" }
|
||||||
taskchampion-lib = { path = "../lib" }
|
taskchampion-lib = { path = "../lib" }
|
||||||
taskchampion-sync-server = { path = "../sync-server" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
actix-web.workspace = true
|
|
||||||
actix-rt.workspace = true
|
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
log.workspace = true
|
|
||||||
env_logger.workspace = true
|
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|
|
@ -1,44 +1,18 @@
|
||||||
use actix_web::{App, HttpServer};
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
|
use taskchampion::{Replica, ServerConfig, Status, StorageConfig};
|
||||||
use taskchampion_sync_server::{storage::InMemoryStorage, Server};
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[test]
|
||||||
async fn cross_sync() -> anyhow::Result<()> {
|
fn cross_sync() -> anyhow::Result<()> {
|
||||||
async fn server() -> anyhow::Result<u16> {
|
|
||||||
let _ = env_logger::builder()
|
|
||||||
.is_test(true)
|
|
||||||
.filter_level(log::LevelFilter::Trace)
|
|
||||||
.try_init();
|
|
||||||
|
|
||||||
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")?;
|
|
||||||
|
|
||||||
// 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<()> {
|
|
||||||
// set up two replicas, and demonstrate replication between them
|
// set up two replicas, and demonstrate replication between them
|
||||||
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||||
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||||
|
|
||||||
let client_id = Uuid::new_v4();
|
let tmp_dir = TempDir::new().expect("TempDir failed");
|
||||||
let encryption_secret = b"abc123".to_vec();
|
let server_config = ServerConfig::Local {
|
||||||
let make_server = || {
|
server_dir: tmp_dir.path().to_path_buf(),
|
||||||
ServerConfig::Remote {
|
|
||||||
origin: format!("http://127.0.0.1:{}", port),
|
|
||||||
client_id,
|
|
||||||
encryption_secret: encryption_secret.clone(),
|
|
||||||
}
|
|
||||||
.into_server()
|
|
||||||
};
|
};
|
||||||
|
let mut server = server_config.into_server()?;
|
||||||
let mut serv1 = make_server()?;
|
|
||||||
let mut serv2 = make_server()?;
|
|
||||||
|
|
||||||
// add some tasks on rep1
|
// add some tasks on rep1
|
||||||
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
||||||
|
@ -49,8 +23,8 @@ async fn cross_sync() -> anyhow::Result<()> {
|
||||||
t1.start()?;
|
t1.start()?;
|
||||||
let t1 = t1.into_immut();
|
let t1 = t1.into_immut();
|
||||||
|
|
||||||
rep1.sync(&mut serv1, false)?;
|
rep1.sync(&mut server, false)?;
|
||||||
rep2.sync(&mut serv2, false)?;
|
rep2.sync(&mut server, false)?;
|
||||||
|
|
||||||
// those tasks should exist on rep2 now
|
// those tasks should exist on rep2 now
|
||||||
let t12 = rep2
|
let t12 = rep2
|
||||||
|
@ -74,9 +48,9 @@ async fn cross_sync() -> anyhow::Result<()> {
|
||||||
t12.set_status(Status::Completed)?;
|
t12.set_status(Status::Completed)?;
|
||||||
|
|
||||||
// sync those changes back and forth
|
// sync those changes back and forth
|
||||||
rep1.sync(&mut serv1, false)?; // rep1 -> server
|
rep1.sync(&mut server, false)?; // rep1 -> server
|
||||||
rep2.sync(&mut serv2, false)?; // server -> rep2, rep2 -> server
|
rep2.sync(&mut server, false)?; // server -> rep2, rep2 -> server
|
||||||
rep1.sync(&mut serv1, false)?; // server -> rep1
|
rep1.sync(&mut server, false)?; // server -> rep1
|
||||||
|
|
||||||
let t1 = rep1
|
let t1 = rep1
|
||||||
.get_task(t1.get_uuid())?
|
.get_task(t1.get_uuid())?
|
||||||
|
@ -88,10 +62,5 @@ async fn cross_sync() -> anyhow::Result<()> {
|
||||||
.expect("expected task 2 on rep2");
|
.expect("expected task 2 on rep2");
|
||||||
assert_eq!(t22.get_status(), Status::Completed);
|
assert_eq!(t22.get_status(), Status::Completed);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let port = server().await?;
|
|
||||||
actix_rt::task::spawn_blocking(move || client(port)).await??;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<u16> {
|
|
||||||
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(())
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "taskchampion-sync-server"
|
|
||||||
version = "0.4.1"
|
|
||||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
|
||||||
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
|
|
|
@ -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<Arc<ServerState>>,
|
|
||||||
path: web::Path<VersionId>,
|
|
||||||
mut payload: web::Payload,
|
|
||||||
) -> Result<HttpResponse> {
|
|
||||||
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<dyn Storage> = 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<dyn Storage> = 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<dyn Storage> = 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<dyn Storage> = 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Arc<ServerState>>,
|
|
||||||
path: web::Path<VersionId>,
|
|
||||||
mut payload: web::Payload,
|
|
||||||
) -> Result<HttpResponse> {
|
|
||||||
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<dyn Storage> = 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<dyn Storage> = 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<dyn Storage> = 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<dyn Storage> = 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Arc<ServerState>>,
|
|
||||||
path: web::Path<VersionId>,
|
|
||||||
) -> Result<HttpResponse> {
|
|
||||||
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<dyn Storage> = 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<dyn Storage> = 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<dyn Storage> = 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Arc<ServerState>>,
|
|
||||||
) -> Result<HttpResponse> {
|
|
||||||
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<dyn Storage> = 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<dyn Storage> = 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<dyn Storage>,
|
|
||||||
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<ClientId> {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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> "Port on which to serve")
|
|
||||||
.help("Port on which to serve")
|
|
||||||
.value_parser(value_parser!(usize))
|
|
||||||
.default_value("8080"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
arg!(-d --"data-dir" <DIR> "Directory in which to store data")
|
|
||||||
.value_parser(ValueParser::os_string())
|
|
||||||
.default_value("/var/lib/taskchampion-sync-server"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
arg!(--"snapshot-versions" <NUM> "Target number of versions between snapshots")
|
|
||||||
.value_parser(value_parser!(u32))
|
|
||||||
.default_value(default_snapshot_versions),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
arg!(--"snapshot-days" <NUM> "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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<ServerState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
/// Create a new sync server with the given storage implementation.
|
|
||||||
pub fn new(config: ServerConfig, storage: Box<dyn Storage>) -> 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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -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<Uuid, Client>,
|
|
||||||
|
|
||||||
/// Snapshot data, indexed by client id
|
|
||||||
snapshots: HashMap<Uuid, Vec<u8>>,
|
|
||||||
|
|
||||||
/// 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<Inner>);
|
|
||||||
|
|
||||||
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<Box<dyn StorageTxn + 'a>> {
|
|
||||||
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<Option<Client>> {
|
|
||||||
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<u8>,
|
|
||||||
) -> 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<Option<Vec<u8>>> {
|
|
||||||
// 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<Option<Version>> {
|
|
||||||
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<Option<Version>> {
|
|
||||||
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<u8>,
|
|
||||||
) -> 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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Snapshot>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Utc>,
|
|
||||||
|
|
||||||
/// 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<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait StorageTxn {
|
|
||||||
/// Get information about the given client
|
|
||||||
fn get_client(&mut self, client_id: Uuid) -> anyhow::Result<Option<Client>>;
|
|
||||||
|
|
||||||
/// 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<u8>,
|
|
||||||
) -> 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<Option<Vec<u8>>>;
|
|
||||||
|
|
||||||
/// Get a version, indexed by parent version id
|
|
||||||
fn get_version_by_parent(
|
|
||||||
&mut self,
|
|
||||||
client_id: Uuid,
|
|
||||||
parent_version_id: Uuid,
|
|
||||||
) -> anyhow::Result<Option<Version>>;
|
|
||||||
|
|
||||||
/// Get a version, indexed by its own version id
|
|
||||||
fn get_version(&mut self, client_id: Uuid, version_id: Uuid)
|
|
||||||
-> anyhow::Result<Option<Version>>;
|
|
||||||
|
|
||||||
/// 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<u8>,
|
|
||||||
) -> 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<Box<dyn StorageTxn + 'a>>;
|
|
||||||
}
|
|
|
@ -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<Self> {
|
|
||||||
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<rusqlite::types::ToSqlOutput<'_>> {
|
|
||||||
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<Connection> {
|
|
||||||
Ok(Connection::open(&self.db_file)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new<P: AsRef<Path>>(directory: P) -> anyhow::Result<SqliteStorage> {
|
|
||||||
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<Box<dyn StorageTxn + 'a>> {
|
|
||||||
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<rusqlite::Transaction, SqliteError> {
|
|
||||||
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<Option<Version>> {
|
|
||||||
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<Option<Client>> {
|
|
||||||
let t = self.get_txn()?;
|
|
||||||
let result: Option<Client> = 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<i64> = r.get(1)?;
|
|
||||||
let versions_since_snapshot: Option<u32> = r.get(2)?;
|
|
||||||
let snapshot_version_id: Option<StoredUuid> = 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<u8>,
|
|
||||||
) -> 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<Option<Vec<u8>>> {
|
|
||||||
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<u8> = 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<Option<Version>> {
|
|
||||||
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<Option<Version>> {
|
|
||||||
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<u8>,
|
|
||||||
) -> 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::<DateTime<Utc>>().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::<DateTime<Utc>>().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::<DateTime<Utc>>().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(())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue