mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
✨(yhub) add a restore endpoint
We will use the delete endpoint available in the yhub server with the soft delete feature in the backend application, but we also need a restore endpoint and this endpoint is not available in the yhub server. This commit adds a new custom endpoint implementing the restore action.
This commit is contained in:
@@ -42,6 +42,10 @@ It is not a fork of yhub — it is a thin wrapper:
|
||||
- exposes `POST /collaboration/migrate/v1/{org}/{docid}`, which replays a
|
||||
document's **full** legacy version history out of the S3 media bucket (see
|
||||
"Full migration" below) — admin JWT only, like `reset-connections`,
|
||||
- exposes `POST /collaboration/restore-ydoc/v1/{org}/{docid}`, which undoes the
|
||||
deletion of a document — admin JWT only, like `reset-connections`. Deleting
|
||||
one needs nothing custom, the built-in `DELETE .../ydoc/` does it (see
|
||||
"Deletion" below); restoring has no built-in route,
|
||||
- notifies the Django backend on
|
||||
`POST /api/v1.0/documents/{id}/content-updated/` whenever the worker
|
||||
persists new content for a document, so that lists ordered by `updated_at`
|
||||
@@ -61,8 +65,9 @@ the websocket and the built-in document APIs (`ydoc`, `rollback`, `prune`,
|
||||
`changeset`, `activity`) are all guarded by the same cookie-based document
|
||||
authorization and are meant to be reachable by browsers, as is
|
||||
`/collaboration/jwks/v1`, which carries public keys and nothing else. The one
|
||||
exception is `/collaboration/reset-connections/` and `/collaboration/migrate/`, which are
|
||||
backend-internal and should not be routed through the public ingress.
|
||||
exception is `/collaboration/reset-connections/`, `/collaboration/migrate/` and
|
||||
`/collaboration/restore-ydoc/`, which are backend-internal and should not be
|
||||
routed through the public ingress.
|
||||
|
||||
## Container image
|
||||
|
||||
@@ -113,6 +118,34 @@ the counterpart of `make migrate` for the Django database. `make bootstrap`
|
||||
already includes it, so a fresh checkout needs nothing extra; an upgrade is
|
||||
`make migrate-yhub` and restart the service.
|
||||
|
||||
## Deletion
|
||||
|
||||
The content of a document lives here, so deleting one in Docs has to be said
|
||||
here too — otherwise the clients already connected keep editing it and the
|
||||
content outlives the document. The backend does that from
|
||||
`sync_service_deletions_in_cascade`, which walks the deleted subtree and tells
|
||||
this server what became of each of its documents.
|
||||
|
||||
Deleting is `DELETE /collaboration/ydoc/v1/{org}/{docid}`, built into yhub
|
||||
0.6.0. It is a **soft** deletion: the deletion is recorded, the clients editing
|
||||
the document are disconnected (websocket close code 4404) and every route
|
||||
answers 404 for it (`{"code": "doc-deleted"}`, which a document that was never
|
||||
written does not — that one answers an empty document), but its content is left
|
||||
untouched. Deleting twice keeps the date of the first deletion.
|
||||
|
||||
Restoring is the custom `POST /collaboration/restore-ydoc/v1/{org}/{docid}`
|
||||
above: yhub 0.6.0 has no built-in route for it. The content was never touched,
|
||||
so the document comes back with its whole history. Restoring one that is not
|
||||
deleted answers 200 and changes nothing, which is what lets the backend restore
|
||||
a subtree without asking what became of each document in it.
|
||||
|
||||
Erasing the content for good is a third operation (`YHub.deleteDoc(room, {
|
||||
hard: true })`), reachable from inside this process only — yhub deliberately
|
||||
keeps it off the REST API. Nothing here calls it: Docs never erases a document
|
||||
either, a soft-deleted one simply stops being restorable after
|
||||
`TRASHBIN_CUTOFF_DAYS`. Note that a hard deletion is final for that room — the
|
||||
docid can never be written again, and `restore-ydoc` answers 409 for it.
|
||||
|
||||
## Soft migration (`SOFT_MIGRATION=true`)
|
||||
|
||||
Documents were historically stored by the Django backend in the S3 media
|
||||
|
||||
@@ -556,6 +556,53 @@ const api = [
|
||||
},
|
||||
},
|
||||
}),
|
||||
// POST /collaboration/restore-ydoc/v1/{org}/{docid} — undo the deletion of a
|
||||
// document, putting back what `DELETE .../ydoc/` took away.
|
||||
//
|
||||
// Deleting has a built-in route, restoring does not: yhub 0.6.0 exposes
|
||||
// `restoreDoc` to the process embedding it and nothing else. Backend-internal
|
||||
// like reset-connections and migrate, gated to the admin token by the
|
||||
// 'restore' purpose — a document leaves the trashbin because the backend
|
||||
// says so, never because an editor asked.
|
||||
createApiEndpoint('restore-ydoc', {
|
||||
accessPurpose: 'restore',
|
||||
post: {
|
||||
handler: async (req) => {
|
||||
if (req.org !== ORG) {
|
||||
return jsonResponse(400, { error: 'Unknown org' });
|
||||
}
|
||||
if (!UUID4.test(req.docid)) {
|
||||
return jsonResponse(400, { error: 'Room name is invalid' });
|
||||
}
|
||||
if (req.branch !== 'main') {
|
||||
// as in create-ydoc: the admin token is not fenced to main by
|
||||
// getAccessType, and a deletion is recorded per branch
|
||||
return jsonResponse(400, { error: 'Unknown branch' });
|
||||
}
|
||||
// read the deletion before undoing it: `restoreDoc` throws a plain
|
||||
// Error for a document whose content was erased, and that is a
|
||||
// conflict to report as one — catching around the call would turn
|
||||
// every failure alike, a database outage included, into the same answer
|
||||
const tombstone = await req.yhub.persistence.retrieveTombstone(req.room);
|
||||
if (tombstone == null) {
|
||||
// not an error: the backend restores a whole subtree, of which only
|
||||
// the part that was deleted with it has anything to put back
|
||||
return jsonResponse(200, {
|
||||
message: 'Document is not deleted',
|
||||
restored: false,
|
||||
});
|
||||
}
|
||||
if (tombstone.hard || tombstone.purgedAt != null) {
|
||||
return jsonResponse(409, { error: 'Document content was erased' });
|
||||
}
|
||||
await req.yhub.restoreDoc(req.room);
|
||||
return jsonResponse(200, {
|
||||
message: 'Document restored',
|
||||
restored: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
// Django orders the document lists by `updated_at` and no edit goes through it
|
||||
|
||||
Reference in New Issue
Block a user