Initial commit

This commit is contained in:
Maurus Decimus
2026-05-29 18:02:15 +02:00
commit 576073f8c9
263 changed files with 78638 additions and 0 deletions
+321
View File
@@ -0,0 +1,321 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use std::collections::{HashMap, HashSet};
use rusqlite::params;
use serde_json::Value;
use super::common::{create_batch, jid, target_query_get};
use super::{Net, Plan, Uploader};
use crate::error::Error;
use crate::jmap::wire::JmapId;
use crate::logging::Logger;
use crate::sync::import_jmap::mapping::{
FILE_NODE_SELECT, MAILBOX_SELECT, TargetResolver, row_to_file_node, row_to_mailbox,
};
use crate::sync::keys::fold_name;
use crate::sync::{Context, TypeCounts};
use crate::types::ObjectType;
use super::Maps;
struct LocalNode {
local: i64,
parent: Option<i64>,
name: String,
role: Option<String>,
}
struct TargetNode {
id: String,
parent: Option<String>,
name: String,
role: Option<String>,
may_delete: bool,
}
fn load_local(ctx: &Context, ty: ObjectType) -> Result<Vec<LocalNode>, Error> {
let table = crate::sync::table_name(ty);
let mut stmt = ctx
.conn
.prepare(&format!("SELECT id, parent_id, name, role FROM {table}"))
.map_err(|e| Error::Partial(e.to_string()))?;
let rows = stmt
.query_map([], |r| {
Ok(LocalNode {
local: r.get(0)?,
parent: r.get(1)?,
name: r.get(2)?,
role: r.get(3)?,
})
})
.and_then(|m| m.collect::<Result<Vec<_>, _>>())
.map_err(|e| Error::Partial(e.to_string()))?;
Ok(rows)
}
fn load_target(net: &Net, ty: ObjectType) -> Result<Vec<TargetNode>, Error> {
let props: &[&str] = &["role", "name", "parentId", "myRights"];
let list = target_query_get(net, ty, Some(props)).map_err(Error::from)?;
Ok(list
.iter()
.filter_map(|v| {
Some(TargetNode {
id: jid(v)?,
parent: v.get("parentId").and_then(Value::as_str).map(str::to_owned),
name: v.get("name").and_then(Value::as_str)?.to_owned(),
role: v.get("role").and_then(Value::as_str).map(str::to_owned),
may_delete: v
.get("myRights")
.and_then(|r| r.get("mayDelete"))
.and_then(Value::as_bool)
.unwrap_or(true),
})
})
.collect())
}
fn depth(local: i64, by: &HashMap<i64, Option<i64>>) -> usize {
let mut d = 0;
let mut cur = by.get(&local).copied().flatten();
let mut seen = HashSet::new();
while let Some(p) = cur {
if !seen.insert(p) {
break;
}
d += 1;
cur = by.get(&p).copied().flatten();
}
d
}
pub fn reconcile(
ctx: &Context,
net: &Net,
ty: ObjectType,
maps: &mut Maps,
counts: &mut TypeCounts,
logger: &Logger,
) -> Result<Plan, Error> {
let locals = load_local(ctx, ty)?;
let targets = load_target(net, ty)?;
let mut matched: HashMap<i64, String> = HashMap::new();
let mut tmatched: HashSet<String> = HashSet::new();
if ty == ObjectType::Mailbox {
for t in &targets {
if let Some(r) = &t.role
&& let Some(l) = locals.iter().find(|l| {
l.role.as_deref() == Some(r.as_str()) && !matched.contains_key(&l.local)
})
{
matched.insert(l.local, t.id.clone());
tmatched.insert(t.id.clone());
}
}
}
let mut stack: Vec<(Option<i64>, Option<String>)> = vec![(None, None)];
for (l, tid) in matched.clone() {
stack.push((Some(l), Some(tid)));
}
while let Some((lp, tp)) = stack.pop() {
let lchildren: Vec<&LocalNode> = locals
.iter()
.filter(|n| n.parent == lp && !matched.contains_key(&n.local))
.collect();
for ln in lchildren {
if let Some(tn) = targets.iter().find(|t| {
t.parent.as_deref() == tp.as_deref()
&& !tmatched.contains(&t.id)
&& fold_name(&t.name) == fold_name(&ln.name)
}) {
matched.insert(ln.local, tn.id.clone());
tmatched.insert(tn.id.clone());
stack.push((Some(ln.local), Some(tn.id.clone())));
}
}
}
for (l, tid) in &matched {
maps.insert(ty, *l, JmapId(tid.clone()));
}
counts.skipped += matched.len() as u64;
let by: HashMap<i64, Option<i64>> = locals.iter().map(|n| (n.local, n.parent)).collect();
let mut to_create: Vec<&LocalNode> = locals
.iter()
.filter(|n| !matched.contains_key(&n.local))
.collect();
to_create.sort_by_key(|n| depth(n.local, &by));
let max_depth = to_create
.iter()
.map(|n| depth(n.local, &by))
.max()
.unwrap_or(0);
let mut uploader = Uploader::new(net, &ctx.conn);
let interleave = ty == ObjectType::FileNode;
for d in 0..=max_depth {
let level: Vec<&LocalNode> = to_create
.iter()
.copied()
.filter(|n| depth(n.local, &by) == d)
.collect();
if interleave {
for n in &level {
if let Some(p) = n.parent
&& maps.target(ty, p).is_none()
{
logger.warn(&format!(
"{} local {} skipped: parent {} not created",
ty.jmap_name(),
n.local,
p
));
counts.failed += 1;
continue;
}
let obj = match build_create(ctx, ty, n.local, maps, &mut uploader) {
Ok(o) => o,
Err(e) => {
logger.warn(&format!(
"{} local {} skipped: {e}",
ty.jmap_name(),
n.local
));
counts.failed += 1;
continue;
}
};
let outcome = create_batch(net, ty, vec![(format!("c{}", n.local), obj)])
.map_err(Error::from)?;
for (cid, v) in &outcome.created {
if let Some(local) = cid.strip_prefix('c').and_then(|s| s.parse::<i64>().ok())
&& let Some(id) = jid(v)
{
maps.insert(ty, local, JmapId(id));
counts.created += 1;
}
}
for (cid, err) in &outcome.not_created {
logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name()));
counts.failed += 1;
}
}
continue;
}
let mut batch: Vec<(String, Value)> = Vec::new();
for n in &level {
if let Some(p) = n.parent
&& maps.target(ty, p).is_none()
{
logger.warn(&format!(
"{} local {} skipped: parent {} not created",
ty.jmap_name(),
n.local,
p
));
counts.failed += 1;
continue;
}
match build_create(ctx, ty, n.local, maps, &mut uploader) {
Ok(obj) => {
batch.push((format!("c{}", n.local), obj));
}
Err(e) => {
logger.warn(&format!(
"{} local {} skipped: {e}",
ty.jmap_name(),
n.local
));
counts.failed += 1;
}
}
}
if batch.is_empty() {
continue;
}
let outcome = create_batch(net, ty, batch).map_err(Error::from)?;
for (cid, v) in &outcome.created {
if let Some(local) = cid.strip_prefix('c').and_then(|s| s.parse::<i64>().ok())
&& let Some(id) = jid(v)
{
maps.insert(ty, local, JmapId(id));
counts.created += 1;
}
}
for (cid, err) in &outcome.not_created {
logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name()));
counts.failed += 1;
}
}
let objs: Vec<crate::sync::prune::TargetObj> = targets
.iter()
.map(|t| crate::sync::prune::TargetObj {
id: t.id.clone(),
matched: tmatched.contains(&t.id),
protected: t.role.is_some(),
may_delete: t.may_delete,
parent: t.parent.clone(),
})
.collect();
Ok(Plan {
prune_candidates: crate::sync::prune::candidates(&objs, true),
active_sieve_target: None,
})
}
fn build_create(
ctx: &Context,
ty: ObjectType,
local: i64,
maps: &Maps,
uploader: &mut Uploader<'_>,
) -> Result<Value, Error> {
if ty == ObjectType::Mailbox {
let wire = ctx
.conn
.query_row(
&format!("{MAILBOX_SELECT} WHERE id = ?1"),
params![local],
|row| Ok(row_to_mailbox(row, maps)),
)
.map_err(|e| Error::Partial(e.to_string()))?
.map_err(Error::from)?;
return serde_json::to_value(&wire).map_err(|e| Error::Partial(e.to_string()));
}
let fnrow = ctx
.conn
.query_row(
&format!("{FILE_NODE_SELECT} WHERE id = ?1"),
params![local],
|row| Ok(row_to_file_node(row, maps)),
)
.map_err(|e| Error::Partial(e.to_string()))?
.map_err(Error::from)?;
let mut wire = fnrow.wire;
if let (crate::jmap::wire::file_node::NodeType::File, Some(blob_local)) =
(&wire.node_type, fnrow.blob_local_id)
{
let ct = wire
.media_type
.clone()
.unwrap_or_else(|| "application/octet-stream".to_owned());
let id = uploader.upload_with(blob_local, &ct).map_err(Error::from)?;
wire.blob_id = Some(id);
}
let mut obj = serde_json::to_value(&wire).map_err(|e| Error::Partial(e.to_string()))?;
if let Value::Object(m) = &mut obj {
m.remove("created");
m.remove("modified");
m.remove("isSubscribed");
}
Ok(obj)
}