fix(backup): include storage/business-docs/ in the in-app backup walker
backupService.getFilesToBackupInternal() enumerated a fixed list of
storage subdirectories (events/active, events/archived, thumbnails,
previews, heroes, uploads) and silently omitted the entire
business-docs/ tree. Every CRM PDF artefact and signature image fell
outside the in-app scheduled backup — restoring the DB without the
PDFs would have left every *_path column on quotes/contracts/invoices
as a broken FK and lost forensic evidence (the customer signature
PNG/JPG drawn on the public signing page is referenced by
contracts.signed_customer_signature_path; the rendered contract PDF
is referenced by signed_pdf_path with a stored signed_pdf_sha256
that would have nothing to verify against; wet-uploaded contracts
and admin-imported historical invoices are irrecoverable by design
since no renderer can reproduce them).
Single new scanDirectory call after the existing uploads scan,
covering:
- business-docs/quote/<year>/*.pdf
- business-docs/contract/<year>/*.pdf
- business-docs/contract/signatures/<contract_id>/*.{png,jpg}
- business-docs/invoice/<year>/*.pdf
- business-docs/invoice-imports/<year>/*.pdf
- and incidentally business-docs/dev-test/ (managed by adminDev.js,
bounded to 7 newest files, harmless to back up)
Verified that no migration is needed: hasFileChanged returns
!existing || checksum mismatch, so the first backup after this lands
flags every business-docs/** file as new and copies it. Restore path
in restoreService.performFilesRestore uses fs.mkdir({ recursive:
true }) on path.dirname(targetPath), so business-docs subdirectories
are recreated automatically from manifest entries — no restore-side
code change required.
Integration test pins the contract so a future refactor cannot
silently drop business-docs again.
The shell-script backup at scripts/backup.sh already covered all of
this via blanket `tar -czf storage`; only the in-app service was
affected.
This commit is contained in:
@@ -372,6 +372,21 @@ async function getFilesToBackupInternal(includeArchived = true) {
|
||||
// by the original backup walk before this addition.
|
||||
await scanDirectory(path.join(storagePath, 'heroes'), files, storagePath);
|
||||
await scanDirectory(path.join(storagePath, 'uploads'), files, storagePath);
|
||||
// CRM document estate — every PDF and signature artefact the
|
||||
// service persists for legal-evidence purposes:
|
||||
// - business-docs/quote/<year>/*.pdf
|
||||
// - business-docs/contract/<year>/*.pdf (system-rendered + wet uploads)
|
||||
// - business-docs/contract/signatures/<contract_id>/*.{png,jpg}
|
||||
// (drawn signatures, forensic-preserved per Date.now() filename)
|
||||
// - business-docs/invoice/<year>/*.pdf (issued invoices + Storno)
|
||||
// - business-docs/invoice-imports/<year>/*.pdf (admin-imported
|
||||
// historical invoices — irrecoverable if not backed up)
|
||||
// Without this scan, the audit trail (signed_pdf_sha256, signed_*
|
||||
// _ip, accepted_at, etc.) survives the restore but the documents
|
||||
// those values refer to do not, leaving every CRM *_path column a
|
||||
// broken FK. scanDirectory short-circuits on ENOENT so installs
|
||||
// that never used CRM features won't error.
|
||||
await scanDirectory(path.join(storagePath, 'business-docs'), files, storagePath);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user