mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-27 09:05:01 +00:00
134 lines
5.8 KiB
JavaScript
134 lines
5.8 KiB
JavaScript
'use strict';
|
|
|
|
// Auto-Cleanup's uploads limit: the oldest messages with files go until the
|
|
// uploads folder is back under the limit, and nothing else is touched.
|
|
|
|
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 Database = require('better-sqlite3');
|
|
|
|
const { uploadsSizeBytes, trimUploadsToLimit } = require('../src/uploadsTrim');
|
|
|
|
const KB = 1024;
|
|
const RE = /\/uploads\/([A-Za-z0-9_.-]+)/g;
|
|
const extractPaths = (content) => {
|
|
const out = [];
|
|
if (typeof content !== 'string') return out;
|
|
RE.lastIndex = 0;
|
|
let m;
|
|
while ((m = RE.exec(content)) !== null) out.push(m[1]);
|
|
return out;
|
|
};
|
|
|
|
function setup() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'haven-uploads-trim-'));
|
|
const moved = [];
|
|
const file = (name, kb) => fs.writeFileSync(path.join(root, name), Buffer.alloc(kb * KB));
|
|
const move = (rel) => {
|
|
const dst = path.join(root, 'deleted-attachments', rel);
|
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
fs.renameSync(path.join(root, rel), dst);
|
|
moved.push(rel);
|
|
};
|
|
const db = new Database(':memory:');
|
|
db.exec(`
|
|
CREATE TABLE channels (id INTEGER PRIMARY KEY, cleanup_exempt INTEGER DEFAULT 0);
|
|
CREATE TABLE messages (id INTEGER PRIMARY KEY, channel_id INTEGER, content TEXT, e2e_files TEXT,
|
|
is_archived INTEGER DEFAULT 0, thread_id INTEGER, created_at TEXT);
|
|
CREATE TABLE pinned_messages (message_id INTEGER);
|
|
CREATE TABLE reactions (message_id INTEGER, emoji TEXT);
|
|
INSERT INTO channels (id, cleanup_exempt) VALUES (1, 0), (2, 1);
|
|
`);
|
|
let n = 0;
|
|
const msg = (content, opts = {}) => {
|
|
n++;
|
|
const at = `2026-01-${String(n).padStart(2, '0')} 12:00:00`;
|
|
return db.prepare('INSERT INTO messages (channel_id, content, e2e_files, is_archived, created_at) VALUES (?, ?, ?, ?, ?)')
|
|
.run(opts.channel || 1, content, opts.e2e || null, opts.archived ? 1 : 0, at).lastInsertRowid;
|
|
};
|
|
const ids = () => db.prepare('SELECT id FROM messages ORDER BY id').all().map(r => r.id);
|
|
return { root, db, file, msg, move, moved, ids };
|
|
}
|
|
|
|
test('the folder size leaves out deleted-attachments', async () => {
|
|
const t = setup();
|
|
t.file('a.png', 10);
|
|
fs.mkdirSync(path.join(t.root, 'deleted-attachments'));
|
|
fs.writeFileSync(path.join(t.root, 'deleted-attachments', 'old.png'), Buffer.alloc(50 * KB));
|
|
assert.equal(await uploadsSizeBytes(t.root), 10 * KB);
|
|
});
|
|
|
|
test('the oldest messages with files go, just enough to get under the limit', async () => {
|
|
const t = setup();
|
|
t.file('avatar.png', 30); // no message points at it
|
|
for (const f of ['one', 'two', 'three', 'four']) t.file(`${f}.png`, 20);
|
|
const text = t.msg('just text, no file');
|
|
const one = t.msg('/uploads/one.png');
|
|
const two = t.msg('look /uploads/two.png');
|
|
const three = t.msg('/uploads/three.png');
|
|
const four = t.msg('/uploads/four.png');
|
|
t.db.prepare('INSERT INTO reactions (message_id, emoji) VALUES (?, ?)').run(one, 'x');
|
|
|
|
// 110 KB in the folder, limit 75: two 20 KB files have to go.
|
|
const r = await trimUploadsToLimit({ db: t.db, uploadsDir: t.root, maxBytes: 75 * KB, extractPaths: extractPaths, move: t.move });
|
|
assert.equal(r.deleted, 2);
|
|
assert.deepEqual(t.ids(), [text, three, four]);
|
|
assert.deepEqual(t.moved.sort(), ['one.png', 'two.png']);
|
|
assert.ok(fs.existsSync(path.join(t.root, 'avatar.png')), 'files no message points at stay');
|
|
assert.equal(t.db.prepare('SELECT COUNT(*) c FROM reactions').get().c, 0);
|
|
assert.ok(await uploadsSizeBytes(t.root) <= 75 * KB);
|
|
void two;
|
|
});
|
|
|
|
test('pinned, archived and exempt messages stay, and so do messages whose files are gone', async () => {
|
|
const t = setup();
|
|
for (const f of ['p', 'a', 'e', 'x']) t.file(`${f}.png`, 20);
|
|
const pinned = t.msg('/uploads/p.png');
|
|
t.db.prepare('INSERT INTO pinned_messages (message_id) VALUES (?)').run(pinned);
|
|
const archived = t.msg('/uploads/a.png', { archived: true });
|
|
const exempt = t.msg('/uploads/e.png', { channel: 2 });
|
|
const missing = t.msg('/uploads/nothere.png');
|
|
const plain = t.msg('/uploads/x.png');
|
|
|
|
const r = await trimUploadsToLimit({ db: t.db, uploadsDir: t.root, maxBytes: 10 * KB, extractPaths, move: t.move });
|
|
assert.equal(r.deleted, 1);
|
|
assert.deepEqual(t.ids(), [pinned, archived, exempt, missing]);
|
|
assert.deepEqual(t.moved, ['x.png']);
|
|
void plain;
|
|
});
|
|
|
|
test('a file another message still links to is not moved', async () => {
|
|
const t = setup();
|
|
t.file('shared.png', 40);
|
|
t.file('late.png', 5);
|
|
t.msg('/uploads/shared.png');
|
|
const later = t.msg('same file again /uploads/shared.png');
|
|
t.msg('/uploads/late.png');
|
|
await trimUploadsToLimit({ db: t.db, uploadsDir: t.root, maxBytes: 20 * KB, extractPaths, move: t.move });
|
|
assert.ok(t.ids().includes(later));
|
|
assert.deepEqual(t.moved, []);
|
|
assert.ok(fs.existsSync(path.join(t.root, 'shared.png')));
|
|
});
|
|
|
|
test('encrypted DM files count, and their message goes (the release trigger moves the file)', async () => {
|
|
const t = setup();
|
|
t.file('dm.bin', 30);
|
|
const dm = t.msg('ciphertext', { e2e: JSON.stringify(['dm.bin']) });
|
|
const r = await trimUploadsToLimit({ db: t.db, uploadsDir: t.root, maxBytes: 10 * KB, extractPaths, move: t.move });
|
|
assert.equal(r.deleted, 1);
|
|
assert.ok(!t.ids().includes(dm));
|
|
assert.deepEqual(t.moved, [], 'the move is left to the e2e release sweep');
|
|
});
|
|
|
|
test('under the limit, or with the limit off, nothing happens', async () => {
|
|
const t = setup();
|
|
t.file('a.png', 10);
|
|
const id = t.msg('/uploads/a.png');
|
|
assert.equal((await trimUploadsToLimit({ db: t.db, uploadsDir: t.root, maxBytes: 20 * KB, extractPaths, move: t.move })).deleted, 0);
|
|
assert.equal((await trimUploadsToLimit({ db: t.db, uploadsDir: t.root, maxBytes: 0, extractPaths, move: t.move })).deleted, 0);
|
|
assert.deepEqual(t.ids(), [id]);
|
|
});
|