Mapping existing special mailbox fails after alreadyExists response (fixes #17)

This commit is contained in:
Maurus Decimus
2026-07-07 11:36:47 +02:00
parent ceb8e1815c
commit 24dadff463
4 changed files with 521 additions and 89 deletions
Generated
+89 -70
View File
@@ -91,9 +91,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]] [[package]]
name = "arrayref" name = "arrayref"
@@ -103,9 +103,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.7" version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]] [[package]]
name = "assert-json-diff" name = "assert-json-diff"
@@ -119,15 +119,15 @@ dependencies = [
[[package]] [[package]]
name = "astral-tokio-tar" name = "astral-tokio-tar"
version = "0.6.2" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd"
dependencies = [ dependencies = [
"filetime",
"futures-core", "futures-core",
"libc", "libc",
"portable-atomic", "portable-atomic",
"rustc-hash", "rustc-hash",
"rustix 0.38.44",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"xattr", "xattr",
@@ -180,9 +180,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "aws-lc-rs" name = "aws-lc-rs"
version = "1.17.0" version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad"
dependencies = [ dependencies = [
"aws-lc-sys", "aws-lc-sys",
"zeroize", "zeroize",
@@ -190,14 +190,15 @@ dependencies = [
[[package]] [[package]]
name = "aws-lc-sys" name = "aws-lc-sys"
version = "0.41.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444"
dependencies = [ dependencies = [
"cc", "cc",
"cmake", "cmake",
"dunce", "dunce",
"fs_extra", "fs_extra",
"pkg-config",
] ]
[[package]] [[package]]
@@ -384,9 +385,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.65" version = "1.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -408,9 +409,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "chacha20" name = "chacha20"
version = "0.10.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
@@ -586,18 +587,18 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.15" version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
[[package]] [[package]]
name = "darling" name = "darling"
@@ -742,20 +743,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4"
dependencies = [ dependencies = [
"portable-atomic", "portable-atomic",
"rand 0.10.1", "rand 0.10.2",
"web-time", "web-time",
] ]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -979,9 +970,9 @@ dependencies = [
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.12.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b" checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248"
dependencies = [ dependencies = [
"hashbrown 0.17.1", "hashbrown 0.17.1",
] ]
@@ -1422,19 +1413,19 @@ dependencies = [
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.34" version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [ dependencies = [
"getrandom 0.3.4", "getrandom 0.4.3",
"libc", "libc",
] ]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.102" version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -1458,6 +1449,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.12.1" version = "0.12.1"
@@ -1586,9 +1583,9 @@ dependencies = [
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
@@ -1854,9 +1851,9 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.40.1" version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -1894,9 +1891,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.10.1" version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [ dependencies = [
"chacha20", "chacha20",
"getrandom 0.4.3", "getrandom 0.4.3",
@@ -2027,9 +2024,9 @@ dependencies = [
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" 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 = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
@@ -2040,6 +2037,19 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.4" version = "1.1.4"
@@ -2049,7 +2059,7 @@ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys 0.12.1",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -2083,9 +2093,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.1" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
dependencies = [ dependencies = [
"zeroize", "zeroize",
] ]
@@ -2152,9 +2162,9 @@ dependencies = [
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "ryu" name = "ryu"
@@ -2505,7 +2515,7 @@ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.3", "getrandom 0.4.3",
"once_cell", "once_cell",
"rustix", "rustix 1.1.4",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -2582,9 +2592,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.51" version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
dependencies = [ dependencies = [
"deranged", "deranged",
"num-conv", "num-conv",
@@ -2602,9 +2612,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.30" version = "0.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@@ -2883,9 +2893,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.23.3" version = "1.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
dependencies = [ dependencies = [
"sha1_smol", "sha1_smol",
] ]
@@ -2969,9 +2979,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.125" version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -2982,9 +2992,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.125" version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -2992,9 +3002,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.125" version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -3005,9 +3015,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.125" version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -3148,6 +3158,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -3297,7 +3316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [ dependencies = [
"libc", "libc",
"rustix", "rustix 1.1.4",
] ]
[[package]] [[package]]
@@ -3325,18 +3344,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.52" version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.52" version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+1 -1
View File
@@ -39,7 +39,7 @@ rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs
rustls-platform-verifier = "0.7" rustls-platform-verifier = "0.7"
regex = "1.12" regex = "1.12"
flate2 = "1.1.9" flate2 = "1.1.9"
quick-xml = "0.40" quick-xml = "0.41"
chrono = { version = "0.4", default-features = false, features = ["std"] } chrono = { version = "0.4", default-features = false, features = ["std"] }
chrono-tz = { version = "0.10", default-features = false } chrono-tz = { version = "0.10", default-features = false }
+106 -6
View File
@@ -93,6 +93,44 @@ fn depth(local: i64, by: &HashMap<i64, Option<i64>>) -> usize {
d d
} }
fn already_exists_id(err: &Value) -> Option<String> {
if err.get("type").and_then(Value::as_str) == Some("alreadyExists") {
err.get("existingId")
.and_then(Value::as_str)
.map(str::to_owned)
} else {
None
}
}
fn find_name_collision(
targets: &[TargetNode],
parent_target: Option<&str>,
name: &str,
) -> Option<String> {
targets
.iter()
.find(|t| t.parent.as_deref() == parent_target && fold_name(&t.name) == fold_name(name))
.map(|t| t.id.clone())
}
fn record_merge(
ty: ObjectType,
local: i64,
target_id: String,
reason: &str,
maps: &mut Maps,
tmatched: &mut HashSet<String>,
logger: &Logger,
) {
logger.warn(&format!(
"{} local {local} merged into existing target {target_id}: {reason}",
ty.jmap_name()
));
maps.insert(ty, local, JmapId(target_id.clone()));
tmatched.insert(target_id);
}
pub fn reconcile( pub fn reconcile(
ctx: &Context, ctx: &Context,
net: &Net, net: &Net,
@@ -170,9 +208,11 @@ pub fn reconcile(
.collect(); .collect();
if interleave { if interleave {
for n in &level { for n in &level {
if let Some(p) = n.parent let parent_target = match n.parent {
&& maps.target(ty, p).is_none() None => None,
{ Some(p) => match maps.target(ty, p) {
Some(t) => Some(t.0),
None => {
logger.warn(&format!( logger.warn(&format!(
"{} local {} skipped: parent {} not created", "{} local {} skipped: parent {} not created",
ty.jmap_name(), ty.jmap_name(),
@@ -182,6 +222,21 @@ pub fn reconcile(
counts.failed += 1; counts.failed += 1;
continue; continue;
} }
},
};
if let Some(tid) = find_name_collision(&targets, parent_target.as_deref(), &n.name) {
record_merge(
ty,
n.local,
tid,
"name already present on target",
maps,
&mut tmatched,
logger,
);
counts.skipped += 1;
continue;
}
let cid = format!("c{}", n.local); let cid = format!("c{}", n.local);
let _ = uploader.take_touched(); let _ = uploader.take_touched();
let obj = match build_create(ctx, ty, n.local, maps, &mut uploader) { let obj = match build_create(ctx, ty, n.local, maps, &mut uploader) {
@@ -228,6 +283,20 @@ pub fn reconcile(
} }
} }
for (cid, err) in &outcome.not_created { for (cid, err) in &outcome.not_created {
let local = cid.strip_prefix('c').and_then(|s| s.parse::<i64>().ok());
if let (Some(local), Some(existing)) = (local, already_exists_id(err)) {
record_merge(
ty,
local,
existing,
"alreadyExists: reusing existing target",
maps,
&mut tmatched,
logger,
);
counts.skipped += 1;
continue;
}
logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name())); logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name()));
counts.failed += 1; counts.failed += 1;
} }
@@ -236,9 +305,11 @@ pub fn reconcile(
} }
let mut batch: Vec<(String, Value)> = Vec::new(); let mut batch: Vec<(String, Value)> = Vec::new();
for n in &level { for n in &level {
if let Some(p) = n.parent let parent_target = match n.parent {
&& maps.target(ty, p).is_none() None => None,
{ Some(p) => match maps.target(ty, p) {
Some(t) => Some(t.0),
None => {
logger.warn(&format!( logger.warn(&format!(
"{} local {} skipped: parent {} not created", "{} local {} skipped: parent {} not created",
ty.jmap_name(), ty.jmap_name(),
@@ -248,6 +319,21 @@ pub fn reconcile(
counts.failed += 1; counts.failed += 1;
continue; continue;
} }
},
};
if let Some(tid) = find_name_collision(&targets, parent_target.as_deref(), &n.name) {
record_merge(
ty,
n.local,
tid,
"name already present on target",
maps,
&mut tmatched,
logger,
);
counts.skipped += 1;
continue;
}
match build_create(ctx, ty, n.local, maps, &mut uploader) { match build_create(ctx, ty, n.local, maps, &mut uploader) {
Ok(mut obj) => { Ok(mut obj) => {
if ty == ObjectType::Mailbox if ty == ObjectType::Mailbox
@@ -290,6 +376,20 @@ pub fn reconcile(
} }
} }
for (cid, err) in &outcome.not_created { for (cid, err) in &outcome.not_created {
let local = cid.strip_prefix('c').and_then(|s| s.parse::<i64>().ok());
if let (Some(local), Some(existing)) = (local, already_exists_id(err)) {
record_merge(
ty,
local,
existing,
"alreadyExists: reusing existing target",
maps,
&mut tmatched,
logger,
);
counts.skipped += 1;
continue;
}
logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name())); logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name()));
counts.failed += 1; counts.failed += 1;
} }
+313
View File
@@ -173,6 +173,319 @@ fn export_email_already_exists_is_matched_not_failed() {
let _ = std::fs::remove_file(&archive); let _ = std::fs::remove_file(&archive);
} }
#[test]
fn export_mailbox_name_collision_merges_without_create() {
let mut server = mockito::Server::new();
let base = server.url();
let api = "/jmap/api";
let archive = tmp();
{
let conn = db::init::open(&archive).unwrap();
conn.execute(
"INSERT INTO mailboxes (id,name,parent_id,role) VALUES
(1,'Junk Email',NULL,'junk'),
(2,'Junk Mail',NULL,NULL)",
[],
)
.unwrap();
let blob = db::blobs::intern_blob(
&conn,
b"From: a@x\r\nSubject: spam\r\nMessage-ID: <m-2@h>\r\n\r\nbody",
)
.unwrap();
conn.execute(
"INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords)
VALUES (?1,'2020-01-01T00:00:00Z','[2]','[\"$seen\"]')",
rusqlite::params![blob],
)
.unwrap();
}
let _root = server.mock("GET", "/").with_status(404).create();
let _wk = server
.mock("GET", "/.well-known/jmap")
.with_body(session_body(&base))
.create();
let _mq1 = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/query".into()))
.with_body(
json!({"methodResponses":[["Mailbox/query",
{"accountId":"w","ids":["c"]},"q"]]})
.to_string(),
)
.expect(1)
.create();
let _mq2 = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/query".into()))
.with_body(
json!({"methodResponses":[["Mailbox/query",
{"accountId":"w","ids":[]},"q"]]})
.to_string(),
)
.expect(1)
.create();
let _mg = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/get".into()))
.with_body(
json!({"methodResponses":[["Mailbox/get",{"accountId":"w","list":[
{"id":"c","name":"Junk Mail","role":"junk","parentId":null,
"myRights":{"mayDelete":true}}],"notFound":[]},"g"]]})
.to_string(),
)
.expect(1)
.create();
let no_set = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/set".into()))
.expect(0)
.create();
let _eq = server
.mock("POST", api)
.match_body(Matcher::Regex("Email/query".into()))
.with_body(
json!({"methodResponses":[["Email/query",
{"accountId":"w","ids":[]},"q"]]})
.to_string(),
)
.expect(1)
.create();
let _up = server
.mock("POST", Matcher::Regex("/jmap/upload/".into()))
.with_body(json!({"blobId":"UPLOADED"}).to_string())
.expect(1)
.create();
let import_into_c = server
.mock("POST", api)
.match_body(Matcher::AllOf(vec![
Matcher::Regex("Email/import".into()),
Matcher::Regex(r#""c":true"#.into()),
]))
.with_body(
json!({"methodResponses":[["Email/import",{"accountId":"w",
"created":{"e1":{"id":"E1","blobId":"b","threadId":"t","size":10}}},"i"]]})
.to_string(),
)
.expect(1)
.create();
let summary = sync::export::run(
CommonConfig {
archive: archive.clone(),
threads: 1,
dry_run: false,
max_retries: 1,
allow_invalid_certs: false,
logger: Logger::from_flags(true, 0),
},
ExportConfig {
connect: ConnectConfig {
url: base.clone(),
auth: Auth::Basic {
user: "u".into(),
password: "p".into(),
},
account: AccountSelector::Id("w".into()),
},
objects: None,
prune: false,
yes: true,
},
)
.expect("export run");
let mailbox = summary
.per_type
.iter()
.find(|(t, _)| *t == "Mailbox")
.map(|(_, c)| c.clone())
.expect("mailbox counts");
assert_eq!(mailbox.created, 0, "no mailbox is created");
assert_eq!(mailbox.failed, 0, "the name collision is not a failure");
assert_eq!(
mailbox.skipped, 2,
"role-matched Junk Email + merged Junk Mail"
);
let email = summary
.per_type
.iter()
.find(|(t, _)| *t == "Email")
.map(|(_, c)| c.clone())
.expect("email counts");
assert_eq!(email.created, 1, "the Junk Mail email lands on the target");
assert_eq!(email.failed, 0);
assert!(!summary.any_failed());
no_set.assert();
import_into_c.assert();
let _ = std::fs::remove_file(&archive);
}
#[test]
fn export_mailbox_already_exists_maps_existing_id() {
let mut server = mockito::Server::new();
let base = server.url();
let api = "/jmap/api";
let archive = tmp();
{
let conn = db::init::open(&archive).unwrap();
conn.execute(
"INSERT INTO mailboxes (id,name,parent_id,role) VALUES (1,'Junk Mail',NULL,NULL)",
[],
)
.unwrap();
let blob = db::blobs::intern_blob(
&conn,
b"From: a@x\r\nSubject: spam\r\nMessage-ID: <m-3@h>\r\n\r\nbody",
)
.unwrap();
conn.execute(
"INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords)
VALUES (?1,'2020-01-01T00:00:00Z','[1]','[\"$seen\"]')",
rusqlite::params![blob],
)
.unwrap();
}
let _root = server.mock("GET", "/").with_status(404).create();
let _wk = server
.mock("GET", "/.well-known/jmap")
.with_body(session_body(&base))
.create();
let _mq1 = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/query".into()))
.with_body(
json!({"methodResponses":[["Mailbox/query",
{"accountId":"w","ids":["t1"]},"q"]]})
.to_string(),
)
.expect(1)
.create();
let _mq2 = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/query".into()))
.with_body(
json!({"methodResponses":[["Mailbox/query",
{"accountId":"w","ids":[]},"q"]]})
.to_string(),
)
.expect(1)
.create();
let _mg = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/get".into()))
.with_body(
json!({"methodResponses":[["Mailbox/get",{"accountId":"w","list":[
{"id":"t1","name":"Inbox","role":"inbox","parentId":null,
"myRights":{"mayDelete":true}}],"notFound":[]},"g"]]})
.to_string(),
)
.expect(1)
.create();
let set_collides = server
.mock("POST", api)
.match_body(Matcher::Regex("Mailbox/set".into()))
.with_body(
json!({"methodResponses":[["Mailbox/set",{"accountId":"w",
"notCreated":{"c1":{"type":"alreadyExists","existingId":"c"}}},"s"]]})
.to_string(),
)
.expect(1)
.create();
let _eq = server
.mock("POST", api)
.match_body(Matcher::Regex("Email/query".into()))
.with_body(
json!({"methodResponses":[["Email/query",
{"accountId":"w","ids":[]},"q"]]})
.to_string(),
)
.expect(1)
.create();
let _up = server
.mock("POST", Matcher::Regex("/jmap/upload/".into()))
.with_body(json!({"blobId":"UPLOADED"}).to_string())
.expect(1)
.create();
let import_into_c = server
.mock("POST", api)
.match_body(Matcher::AllOf(vec![
Matcher::Regex("Email/import".into()),
Matcher::Regex(r#""c":true"#.into()),
]))
.with_body(
json!({"methodResponses":[["Email/import",{"accountId":"w",
"created":{"e1":{"id":"E1","blobId":"b","threadId":"t","size":10}}},"i"]]})
.to_string(),
)
.expect(1)
.create();
let summary = sync::export::run(
CommonConfig {
archive: archive.clone(),
threads: 1,
dry_run: false,
max_retries: 1,
allow_invalid_certs: false,
logger: Logger::from_flags(true, 0),
},
ExportConfig {
connect: ConnectConfig {
url: base.clone(),
auth: Auth::Basic {
user: "u".into(),
password: "p".into(),
},
account: AccountSelector::Id("w".into()),
},
objects: None,
prune: false,
yes: true,
},
)
.expect("export run");
let mailbox = summary
.per_type
.iter()
.find(|(t, _)| *t == "Mailbox")
.map(|(_, c)| c.clone())
.expect("mailbox counts");
assert_eq!(mailbox.created, 0, "the create collided");
assert_eq!(
mailbox.failed, 0,
"alreadyExists on Mailbox/set is not a failure"
);
assert_eq!(mailbox.skipped, 1, "existingId folds into matched");
let email = summary
.per_type
.iter()
.find(|(t, _)| *t == "Email")
.map(|(_, c)| c.clone())
.expect("email counts");
assert_eq!(email.created, 1, "the email lands in the existing folder");
assert_eq!(email.failed, 0);
assert!(!summary.any_failed());
set_collides.assert();
import_into_c.assert();
let _ = std::fs::remove_file(&archive);
}
#[test] #[test]
fn email_export_sends_one_email_per_import_call() { fn email_export_sends_one_email_per_import_call() {
let mut server = mockito::Server::new(); let mut server = mockito::Server::new();