feat: 使用持久选择解决 Sync 冲突并支持重启恢复

This commit is contained in:
2026-09-08 15:50:31 +08:00
parent 251d3bac7d
commit 3a4d6e5586
8 changed files with 572 additions and 11 deletions
+15 -4
View File
@@ -104,7 +104,6 @@ impl Workspace {
{
return Err(HostError::new("SYNC_HASH_INVALID"));
}
self.resolve(&format!("attachments/{digest}"))?; // Enforce the platform's general path rules.
let root = self.root.join(".ainote/sync-spool");
if root.exists() {
let meta = fs::symlink_metadata(&root)?;
@@ -163,17 +162,29 @@ impl Workspace {
}
pub fn sync_next(&self, binding: &str) -> Result<Option<Job>> {
self.check_binding(binding)?;
Ok(self.db.query_row("SELECT binding,operation_id,file_id,path,hash,size,operation,state,base_revision,upload_id FROM sync_jobs WHERE binding=?1 AND state NOT IN ('acked','archived') ORDER BY rowid LIMIT 1", [binding], |r| {
Ok(self.db.query_row("SELECT binding,operation_id,file_id,path,hash,size,operation,state,base_revision,upload_id FROM sync_jobs WHERE binding=?1 AND state NOT IN ('acked','archived','conflict') AND NOT EXISTS (SELECT 1 FROM sync_conflicts c WHERE c.binding=sync_jobs.binding AND c.file_id=sync_jobs.file_id AND c.state='open') ORDER BY rowid LIMIT 1", [binding], |r| {
Ok(Job { binding:r.get(0)?,operation_id:r.get(1)?,file_id:r.get(2)?,path:r.get(3)?,hash:r.get(4)?,size:r.get(5)?,operation:r.get(6)?,state:r.get(7)?,base_revision:r.get(8)?,upload_id:r.get(9)? })
}).optional()?)
}
pub fn sync_upload(&self, job: &Job, upload: Option<&str>) -> Result<()> {
fn check_job(&self, job: &Job) -> Result<()> {
self.check_binding(&job.binding)?;
let state: String = self.db.query_row(
"SELECT state FROM sync_jobs WHERE binding=?1 AND operation_id=?2",
params![job.binding, job.operation_id],
|r| r.get(0),
)?;
if matches!(state.as_str(), "archived" | "conflict") {
return Err(HostError::new("SYNC_OPERATION_SUPERSEDED"));
}
Ok(())
}
pub fn sync_upload(&self, job: &Job, upload: Option<&str>) -> Result<()> {
self.check_job(job)?;
self.db.execute("UPDATE sync_jobs SET state='uploading',upload_id=?3 WHERE binding=?1 AND operation_id=?2 AND base_revision IS NULL", params![job.binding,job.operation_id,upload])?;
Ok(())
}
pub fn sync_commit_payload(&self, job: &Job) -> Result<serde_json::Value> {
self.check_binding(&job.binding)?;
self.check_job(job)?;
// The base is frozen exactly once. A response loss reuses the byte-equivalent payload.
self.db.execute("UPDATE sync_jobs SET state='committing',base_revision=COALESCE((SELECT revision FROM sync_heads WHERE binding=?1 AND file_id=?3),0) WHERE binding=?1 AND operation_id=?2 AND base_revision IS NULL",
params![job.binding,job.operation_id,job.file_id])?;