Repository navigation
fix: reconcile restored upload target during recovery drill #95
Changes from all commits
99e6e97
9695afd
bdaff49
7811d82
185664a
4073162
120874a
29dbed4
72baa3c
6ade8fc
4cb300c
80f2ef1
e43f5a0
44cc27f
b4edb4a
5957950
3368f99
8cb82ab
d361e2b
66e4bc8
bc3af03
a0001c0
6e2f4c5
abac61f
7c35342
58a0fcb
48ba99f
729094c
5543396
1758a9f
57ece7f
4c377b1
d6dabd8
c3bfc1c
a813f33
215c8b3
f8de3ee
74932ba
59a3004
431d7da
f448f36
f4ce253
f4dd3c9
1f08777
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,36 @@ | ||
| import { mkdir, readdir, rm } from "node:fs/promises"; | ||
| import { homedir } from "node:os"; | ||
| import path from "node:path"; | ||
| import { prisma } from "../src/lib/prisma"; | ||
|
|
||
| const uploadDir = path.join(process.cwd(), "uploads"); | ||
| const trashDir = path.join(uploadDir, ".trash"); | ||
| const RESTORE_UPLOAD_OVERRIDE_CONFIRM = "restore-drill-disposable-upload-target"; | ||
|
|
||
| function resolveUploadDir() { | ||
| const configured = process.argv[2]; | ||
| if (!configured) return path.join(process.cwd(), "uploads"); | ||
|
|
||
| if (process.env.SYNAPSE_UPLOAD_DIR_CONFIRM !== RESTORE_UPLOAD_OVERRIDE_CONFIRM) { | ||
| throw new Error( | ||
| `explicit upload target requires SYNAPSE_UPLOAD_DIR_CONFIRM=${RESTORE_UPLOAD_OVERRIDE_CONFIRM}` | ||
| ); | ||
| } | ||
| if (!path.isAbsolute(configured)) { | ||
| throw new Error("explicit upload target must be an absolute path"); | ||
| } | ||
|
|
||
| const resolved = path.resolve(configured); | ||
| const root = path.parse(resolved).root; | ||
| if (resolved === root || resolved === homedir() || resolved === process.cwd()) { | ||
| throw new Error("Refusing unsafe explicit upload target"); | ||
| } | ||
|
|
||
| return resolved; | ||
| } | ||
|
|
||
| async function main() { | ||
| const explicitRestoreTarget = Boolean(process.argv[2]); | ||
| const uploadDir = resolveUploadDir(); | ||
| const trashDir = path.join(uploadDir, ".trash"); | ||
| await mkdir(uploadDir, { recursive: true }); | ||
|
|
||
| // Query the authoritative database before removing anything. If the database | ||
|
|
@@ -19,8 +44,28 @@ async function main() { | |
| .filter((filename): filename is string => Boolean(filename)) | ||
| ); | ||
|
|
||
| const uploadEntries = await readdir(uploadDir, { withFileTypes: true }); | ||
| const regularFiles = new Set( | ||
| uploadEntries.filter((entry) => entry.isFile()).map((entry) => entry.name) | ||
| ); | ||
|
|
||
| // The explicit target is available only to the disposable restore drill. In | ||
| // that mode, reconciliation must prove completeness as well as remove stale | ||
| // files: every attachment row restored from PostgreSQL must have its matching | ||
| // regular file before any orphan cleanup can make the result look healthy. | ||
| if (explicitRestoreTarget) { | ||
| const missingReferenced = [...referenced].filter( | ||
| (filename) => !regularFiles.has(filename) | ||
| ); | ||
|
Comment on lines
+57
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an attachment deletion overlaps Useful? React with 👍 / 👎. |
||
| if (missingReferenced.length > 0) { | ||
| throw new Error( | ||
| `restore upload target is missing ${missingReferenced.length} referenced attachment file(s)` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| let removedOrphans = 0; | ||
| for (const entry of await readdir(uploadDir, { withFileTypes: true })) { | ||
| for (const entry of uploadEntries) { | ||
| if (!entry.isFile() || referenced.has(entry.name)) continue; | ||
| await rm(path.join(uploadDir, entry.name), { force: true }); | ||
| removedOrphans++; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.