Files
Haven/test/deletedAttachments.test.js
T
ancsemi 9b4560cf08 Deleted attachments expire after a week by default, whether or not auto-cleanup is on
Files from deleted messages and channels were parked in deleted-attachments and only ever removed when auto-cleanup was enabled with a max age set, and even then only from the folder's top level, so on most servers they sat there forever. They now expire on their own clock, a week after deletion by default, with a Keep deleted files for (days) setting under Auto-Cleanup (1 to 3650, validated on the server). The move stamps the file's time so the window counts from the deletion rather than the upload, and the sweep walks sub-folders and prunes the empty ones.
2026-09-10 21:52:56 -04:00

89 lines
3.3 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
DEFAULT_DELETED_RETENTION_DAYS,
resolveDeletedRetentionDays,
purgeDeletedAttachments,
} = require('../src/deletedAttachments');
const DAY = 24 * 60 * 60 * 1000;
function makeTree() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'haven-deleted-'));
const now = Date.now();
const write = (rel, ageDays) => {
const full = path.join(root, rel);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, 'x');
const when = new Date(now - ageDays * DAY);
fs.utimesSync(full, when, when);
return full;
};
return { root, now, write };
}
test('resolveDeletedRetentionDays falls back to the default for anything that is not a valid day count', () => {
assert.equal(DEFAULT_DELETED_RETENTION_DAYS, 7);
assert.equal(resolveDeletedRetentionDays(undefined), 7);
assert.equal(resolveDeletedRetentionDays(null), 7);
assert.equal(resolveDeletedRetentionDays(''), 7);
assert.equal(resolveDeletedRetentionDays('0'), 7);
assert.equal(resolveDeletedRetentionDays('-3'), 7);
assert.equal(resolveDeletedRetentionDays('abc'), 7);
assert.equal(resolveDeletedRetentionDays('99999'), 7);
assert.equal(resolveDeletedRetentionDays('1'), 1);
assert.equal(resolveDeletedRetentionDays(' 30 '), 30);
assert.equal(resolveDeletedRetentionDays(3650), 3650);
});
test('purgeDeletedAttachments removes only files older than the window, in sub-folders too, and prunes empty folders', () => {
const { root, now, write } = makeTree();
try {
const oldTop = write('old.png', 8);
const freshTop = write('fresh.png', 2);
const oldNested = write('2026-01/old-nested.png', 30);
const freshNested = write('2026-09/fresh-nested.png', 1);
const edge = write('edge.png', 7.01);
const removed = purgeDeletedAttachments(root, 7, now);
assert.equal(removed, 3);
assert.equal(fs.existsSync(oldTop), false);
assert.equal(fs.existsSync(oldNested), false);
assert.equal(fs.existsSync(edge), false);
assert.equal(fs.existsSync(freshTop), true);
assert.equal(fs.existsSync(freshNested), true);
// The emptied sub-folder goes, the one still holding a file stays, and
// the root itself is always left in place.
assert.equal(fs.existsSync(path.join(root, '2026-01')), false);
assert.equal(fs.existsSync(path.join(root, '2026-09')), true);
assert.equal(fs.existsSync(root), true);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('purgeDeletedAttachments uses the default window when the setting is unusable', () => {
const { root, now, write } = makeTree();
try {
const sixDays = write('six.png', 6);
const eightDays = write('eight.png', 8);
assert.equal(purgeDeletedAttachments(root, '', now), 1);
assert.equal(fs.existsSync(sixDays), true);
assert.equal(fs.existsSync(eightDays), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('purgeDeletedAttachments is a no-op for a missing folder', () => {
assert.equal(purgeDeletedAttachments(path.join(os.tmpdir(), 'haven-does-not-exist-' + Date.now()), 7), 0);
assert.equal(purgeDeletedAttachments('', 7), 0);
});