mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(docsync): act on a deletion once, and know what to do when it is undone
Both planner branches planned local_deleted for as long as a file sat in TREK's trash. Under unlink nothing showed, until the binding was switched to trash: the next run then binned every document ever deleted in TREK, months back included. A binding on trash from the start asked the provider to bin the same copy on every run. A local_deleted row is now left alone, so the policy is applied once, under the policy in force at that moment. That guard also meant a file taken back out of TREK's trash stayed on local_deleted for good, which it already did before: no transfer, no upload, no flag, and a second deletion of the same file would never have propagated. The row now leaves that state by what became of the provider copy: - still there: the pairing counts again and the row is judged like any synced one, so an upstream edit made meanwhile comes down; - binned by TREK: the pairing is dropped and the file goes up again as a new document through the ordinary push, keeping its trek_doc_uid; - deleted by somebody else: flagged remote_missing, never uploaded over their deletion. Telling the last two apart needs a record of what TREK did, so the new remote_trashed_at column (migration 239) is written when TREK bins a copy and cleared when the pairing is back, or when the binned copy is listed again because somebody restored it at the provider. A file purged from the trash came straight back as well: file_id is ON DELETE SET NULL, and the row read as a download that never landed. A tombstone now stays a tombstone, and a purge no run had seen yet is treated as the deletion it is. Also drops snapshotHash, which nothing read.
This commit is contained in:
@@ -277,12 +277,54 @@ export class DocSyncService {
|
||||
return 'ok';
|
||||
}
|
||||
case 'local_deleted': {
|
||||
if (ctx.link.delete_policy === 'trash') {
|
||||
const binned = ctx.link.delete_policy === 'trash';
|
||||
if (binned) {
|
||||
const res = await ctx.provider.trash(ctx.ref, ctx.scope, action.remoteId);
|
||||
if (docFailed(res)) return res.error.code;
|
||||
}
|
||||
// The policy is applied here and never again: the planner leaves a
|
||||
// `local_deleted` row alone, so a policy changed later cannot reach
|
||||
// back. Whether TREK binned the copy is written down with it, because a
|
||||
// restore in TREK later has to know whether the gap upstream is TREK's.
|
||||
this.db.connection
|
||||
.prepare("UPDATE document_sync_items SET state = 'local_deleted', last_seen_at = CURRENT_TIMESTAMP WHERE id = ?")
|
||||
.prepare(
|
||||
`UPDATE document_sync_items
|
||||
SET state = 'local_deleted', remote_missing_at = NULL,
|
||||
remote_trashed_at = CASE WHEN ? = 1 THEN CURRENT_TIMESTAMP ELSE NULL END,
|
||||
last_seen_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
)
|
||||
.run(binned ? 1 : 0, action.itemId);
|
||||
return 'ok';
|
||||
}
|
||||
case 'local_restored': {
|
||||
this.db.connection
|
||||
.prepare(
|
||||
`UPDATE document_sync_items
|
||||
SET state = 'synced', remote_missing_at = NULL, remote_trashed_at = NULL, last_seen_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
)
|
||||
.run(action.itemId);
|
||||
return 'ok';
|
||||
}
|
||||
case 'detach': {
|
||||
// The trek_doc_uid stays: it is the same document to TREK, only the
|
||||
// provider copy is new. The push the planner queued after this fills
|
||||
// the pairing in again, and if that push fails the row is an ordinary
|
||||
// unpaired file that the next run retries.
|
||||
this.db.connection
|
||||
.prepare(
|
||||
`UPDATE document_sync_items
|
||||
SET state = 'pending', remote_id = NULL, remote_version = NULL, remote_trashed_at = NULL,
|
||||
error_code = NULL, last_seen_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
)
|
||||
.run(action.itemId);
|
||||
return 'ok';
|
||||
}
|
||||
case 'remote_restored': {
|
||||
this.db.connection
|
||||
.prepare('UPDATE document_sync_items SET remote_trashed_at = NULL, last_seen_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.run(action.itemId);
|
||||
return 'ok';
|
||||
}
|
||||
@@ -546,7 +588,7 @@ export class DocSyncService {
|
||||
const rows = this.db.connection
|
||||
.prepare(
|
||||
`SELECT id, file_id, trek_doc_uid, remote_id, remote_version, remote_name, content_sha256,
|
||||
pushed_sha256, state, attempts, next_attempt_at, remote_missing_at
|
||||
pushed_sha256, state, attempts, next_attempt_at, remote_missing_at, remote_trashed_at
|
||||
FROM document_sync_items WHERE link_id = ?`,
|
||||
)
|
||||
.all(linkId) as Array<Record<string, unknown>>;
|
||||
@@ -563,6 +605,7 @@ export class DocSyncService {
|
||||
attempts: Number(r.attempts ?? 0),
|
||||
nextAttemptAt: r.next_attempt_at === null || r.next_attempt_at === undefined ? null : String(r.next_attempt_at),
|
||||
remoteMissingAt: r.remote_missing_at === null ? null : String(r.remote_missing_at),
|
||||
remoteTrashedAt: r.remote_trashed_at === null ? null : String(r.remote_trashed_at),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -913,7 +956,8 @@ export class DocSyncService {
|
||||
.prepare(
|
||||
`SELECT link_id,
|
||||
SUM(CASE WHEN file_id IS NOT NULL AND remote_id IS NOT NULL AND state = 'synced' THEN 1 ELSE 0 END) AS paired,
|
||||
SUM(CASE WHEN remote_id IS NOT NULL AND state != 'remote_missing' THEN 1 ELSE 0 END) AS atProvider,
|
||||
SUM(CASE WHEN remote_id IS NOT NULL AND state != 'remote_missing' AND remote_trashed_at IS NULL
|
||||
THEN 1 ELSE 0 END) AS atProvider,
|
||||
SUM(CASE WHEN state = 'remote_missing' THEN 1 ELSE 0 END) AS missing
|
||||
FROM document_sync_items
|
||||
WHERE trip_id = ?
|
||||
|
||||
Reference in New Issue
Block a user