From f8f21ab42a49fa2cbe240e3be3961bfeeef6df3e Mon Sep 17 00:00:00 2001 From: maashkinoozhe Date: Thu, 22 Oct 2026 18:12:41 +0200 Subject: [PATCH] Backup: probe the target with a real write instead of access(W_OK) On the server the backup folder is a Synology NFS share with "map all users to admin": access(W_OK) returns EACCES for uid 1000 although writes succeed, so the first scheduled run failed with "not usable". Create and remove a probe file instead; test that it leaves nothing. Co-Authored-By: Claude Opus 5.5 --- backup.js | 6 +++++- docs/backup.md | 4 +++- test/backup.test.js | 10 ++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backup.js b/backup.js index 58ed5a1..ad91b6e 100644 --- a/backup.js +++ b/backup.js @@ -169,7 +169,11 @@ class BackupService { try { const st = await fsp.stat(this.backupRoot); if (!st.isDirectory()) throw new Error('not a directory'); - await fsp.access(this.backupRoot, fs.constants.W_OK); + // Probe with a real write: access(W_OK) is unreliable on NFS with squashed ids (Synology + // "map all users to admin" denies it although writes succeed). + const probe = path.join(this.backupRoot, `.circus-backup-probe-${process.pid}`); + await fsp.writeFile(probe, ''); + await fsp.rm(probe, { force: true }); const names = (await fsp.readdir(this.backupRoot)).filter(name => ARCHIVE.test(name)).sort(); this.archives = await Promise.all(names.map(async name => ({ name, size: (await fsp.stat(path.join(this.backupRoot, name))).size, at: archiveTime(name) }))); this.targetError = null; diff --git a/docs/backup.md b/docs/backup.md index 130a1f1..6c5a957 100644 --- a/docs/backup.md +++ b/docs/backup.md @@ -14,7 +14,9 @@ holds it). Implementation: [`backup.js`](../backup.js); tests: `test/backup.test The folder must already exist and be writable by uid 1000 (`create_host_path: false`: a missing folder makes `docker compose up` fail instead of silently creating an empty -one on the wrong disk). Use another disk or a NAS share, never a folder inside `DATA`. +one on the wrong disk). Use another disk or a NAS share, never a folder inside `DATA`. Writability is checked by +creating and removing a probe file (`.circus-backup-probe-`), not with `access()`, which +NFS shares with squashed ids (e.g. Synology "map all users to admin") answer wrongly. Without `BACKUP_PATH`, Compose still mounts the unused default `./backup`, so that folder must exist too (`node tools/prepare-compose.js` creates it). Upgrading an existing deployment therefore needs either `BACKUP_PATH` in `.env` or an empty diff --git a/test/backup.test.js b/test/backup.test.js index 4fad568..a49f9b5 100644 --- a/test/backup.test.js +++ b/test/backup.test.js @@ -168,6 +168,16 @@ test('backups are off without BACKUP, and a missing folder is reported, never cr } finally { f.cleanup(); } }); +test('the writability probe leaves nothing behind in the backup folder', async () => { + const f = fixture(); + try { + const backup = service(f); + await backup.refreshTarget(); + assert.equal(backup.publicState().targetError, null); + assert.deepEqual(fs.readdirSync(f.target).filter(name => name.startsWith('.circus-backup-probe')), []); + } finally { f.cleanup(); } +}); + test('only one run at a time', async () => { const f = fixture(); try {