export: bring matched items up to date on every run
Export matched each item against the target and then skipped it, so a second run -- the usual final pass of a cutover -- never carried anything that had changed at the source since the first: read and flagged state, moves between folders, edited contacts, events and Sieve scripts. It reported them as skipped and exited 0, while the usage guide said matched items were updated. Matched items are now updated, with one batched /set per type: - Email: keywords are set to the archive's, added and removed, compared case-insensitively. Memberships of folders this run migrated are added and removed to match; folders that exist only on the target are left alone, and a message is never left in no folder. Properties the server did not report are not touched. - Contacts and events: when both copies carry `updated`, the archive's is written only if it is newer, compared as instants so an offset or a fraction of a second is not taken for a change; otherwise each property the archive writes is compared, and those that differ are sent whole. - Sieve scripts: the target's copy is downloaded and compared byte for byte with what export would write -- after renaming Stalwart's vendor names for an inbuxa target -- and replaced when it differs, so a renamed script is not re-uploaded on every run. Updated items are counted as `updated`; unchanged ones stay `skipped`. The usage guide now describes this.
This commit is contained in:
+59
-1
@@ -5,7 +5,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::{IsTerminal, Write};
|
||||
|
||||
use rusqlite::Connection;
|
||||
@@ -49,6 +49,14 @@ impl Maps {
|
||||
fn insert(&mut self, ty: ObjectType, local: i64, target: JmapId) {
|
||||
self.m.entry(ty).or_default().insert(local, target);
|
||||
}
|
||||
|
||||
/// Every target id this run mapped for `ty`: the objects it migrated.
|
||||
fn targets_of(&self, ty: ObjectType) -> HashSet<String> {
|
||||
self.m
|
||||
.get(&ty)
|
||||
.map(|m| m.values().map(|id| id.0.clone()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl TargetResolver for Maps {
|
||||
@@ -533,6 +541,56 @@ mod common {
|
||||
)
|
||||
}
|
||||
|
||||
/// Sends `updates` (target id, patch) as batched `/set` calls and counts
|
||||
/// the result into `counts`. A dry run counts them as updated and sends
|
||||
/// nothing.
|
||||
pub fn update_batch(
|
||||
net: &Net,
|
||||
ty: ObjectType,
|
||||
updates: Vec<(String, Value)>,
|
||||
counts: &mut TypeCounts,
|
||||
logger: &Logger,
|
||||
) {
|
||||
if updates.is_empty() {
|
||||
return;
|
||||
}
|
||||
if net.dry_run {
|
||||
counts.updated += updates.len() as u64;
|
||||
return;
|
||||
}
|
||||
let total = updates.len() as u64;
|
||||
let mut map = Map::new();
|
||||
for (id, patch) in updates {
|
||||
map.insert(id, patch);
|
||||
}
|
||||
match set_call(
|
||||
&net.client,
|
||||
&net.api,
|
||||
&net.account,
|
||||
ty.jmap_name(),
|
||||
SetRequest {
|
||||
update: Some(Value::Object(map)),
|
||||
..Default::default()
|
||||
},
|
||||
&net.limits,
|
||||
) {
|
||||
Ok(outcome) => {
|
||||
counts.updated += outcome.updated.len() as u64;
|
||||
for (id, err) in &outcome.not_updated {
|
||||
logger.warn(&format!("{}/set {id} not updated: {err}", ty.jmap_name()));
|
||||
counts.failed += 1;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logger.warn(&format!(
|
||||
"{}/set: updating {total} object(s) failed: {e}",
|
||||
ty.jmap_name()
|
||||
));
|
||||
counts.failed += total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blob_not_found(outcome: &crate::jmap::request::SetOutcome, cid: &str) -> bool {
|
||||
outcome.not_created.iter().any(|(c, err)| {
|
||||
c == cid && err.get("type").and_then(Value::as_str) == Some("blobNotFound")
|
||||
|
||||
Reference in New Issue
Block a user