mirror of
https://github.com/ancsemi/Haven.git
synced 2026-11-08 23:14:08 +00:00
111 lines
4.7 KiB
JavaScript
111 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
// The storage layer on the local disk driver: files move where they should
|
|
// and the media_objects index follows every move.
|
|
//
|
|
// node --test test/storageLocal.test.js
|
|
|
|
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
|
|
const DATA = fs.mkdtempSync(path.join(os.tmpdir(), 'haven-storage-local-'));
|
|
process.env.HAVEN_DATA_DIR = DATA;
|
|
|
|
const { initDatabase } = require('../src/database');
|
|
const { UPLOADS_DIR, UNPROCESSED_DIR } = require('../src/paths');
|
|
const storage = require('../src/storage');
|
|
|
|
const db = initDatabase();
|
|
const row = (rel) => db.prepare('SELECT bytes, state, deleted_at FROM media_objects WHERE rel_path = ?').get(rel);
|
|
const staged = (name, body) => { const p = path.join(UNPROCESSED_DIR, name); fs.writeFileSync(p, body); return p; };
|
|
const live = (rel) => path.join(UPLOADS_DIR, rel);
|
|
const held = (rel) => path.join(UPLOADS_DIR, 'deleted-attachments', rel);
|
|
|
|
test.after(() => {
|
|
db.close();
|
|
fs.rmSync(DATA, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
});
|
|
|
|
test('admit moves the file into uploads and indexes it live', async () => {
|
|
const src = staged('a.tmp', 'hello');
|
|
const { size } = await storage.admit('stickers/a.png', src, { contentType: 'image/png' });
|
|
assert.equal(size, 5);
|
|
assert.equal(fs.existsSync(src), false, 'the staged copy is gone');
|
|
assert.equal(fs.readFileSync(live('stickers/a.png'), 'utf8'), 'hello');
|
|
assert.deepEqual({ ...row('stickers/a.png'), deleted_at: null }, { bytes: 5, state: 'live', deleted_at: null });
|
|
|
|
const head = await storage.head('stickers/a.png');
|
|
assert.equal(head.size, 5);
|
|
assert.match(head.etag, /^W\/"5-[0-9a-f]+"$/, 'the same weak tag express.static sent');
|
|
const got = await storage.getStream('stickers/a.png', { range: { start: 1, end: 3 } });
|
|
const chunks = []; for await (const c of got.stream) chunks.push(c);
|
|
assert.equal(Buffer.concat(chunks).toString(), 'ell');
|
|
assert.equal(await storage.getStream('nope.png'), null);
|
|
});
|
|
|
|
test('softDelete stamps the deletion time; restore and purge follow the index', async () => {
|
|
await storage.admit('old.png', staged('old.tmp', 'x'));
|
|
const monthAgo = new Date(Date.now() - 30 * 24 * 3600 * 1000);
|
|
fs.utimesSync(live('old.png'), monthAgo, monthAgo);
|
|
|
|
assert.equal(await storage.softDelete('old.png'), true);
|
|
assert.equal(fs.existsSync(live('old.png')), false);
|
|
assert.ok(Date.now() - fs.statSync(held('old.png')).mtimeMs < 60 * 1000, 'stamped now, not the upload time');
|
|
assert.equal(row('old.png').state, 'deleted');
|
|
assert.ok(row('old.png').deleted_at);
|
|
assert.equal(await storage.softDelete('old.png'), false, 'a second delete is a no-op');
|
|
|
|
assert.equal(await storage.restore('old.png'), true);
|
|
assert.ok(fs.existsSync(live('old.png')));
|
|
assert.equal(row('old.png').state, 'live');
|
|
|
|
await storage.softDelete('old.png');
|
|
await storage.purge('old.png');
|
|
assert.equal(fs.existsSync(held('old.png')), false);
|
|
assert.equal(row('old.png'), undefined);
|
|
});
|
|
|
|
test('remove deletes for good, and a missing file is not an error', async () => {
|
|
await storage.admit('gone.mp3', staged('gone.tmp', 'abc'));
|
|
await storage.remove('gone.mp3');
|
|
assert.equal(fs.existsSync(live('gone.mp3')), false);
|
|
assert.equal(row('gone.mp3'), undefined);
|
|
await storage.remove('gone.mp3');
|
|
});
|
|
|
|
test('names with spaces and accents are accepted; paths leaving uploads are not', async () => {
|
|
await storage.admit('1-ab.my fïle', staged('odd.tmp', 'o'));
|
|
assert.ok(fs.existsSync(live('1-ab.my fïle')));
|
|
for (const bad of ['../escape.png', '/abs.png', 'a/../../b', 'c:\\x', 'a\\b', '']) {
|
|
await assert.rejects(storage.admit(bad, staged('bad.tmp', 'b')), /unsafe upload path/, bad);
|
|
assert.equal(await storage.softDelete(bad), false, bad);
|
|
}
|
|
});
|
|
|
|
test('list walks every file with its size', async () => {
|
|
const all = [];
|
|
for await (const f of storage.list('')) all.push(f.rel);
|
|
assert.ok(all.includes('stickers/a.png'));
|
|
assert.ok(all.includes('1-ab.my fïle'));
|
|
const stickers = [];
|
|
for await (const f of storage.list('stickers/')) stickers.push(f.rel);
|
|
assert.deepEqual(stickers, ['stickers/a.png']);
|
|
});
|
|
|
|
test('list lets the server answer requests during a long walk', async () => {
|
|
const dir = path.join(UPLOADS_DIR, 'walk-test');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
for (let i = 0; i < 450; i++) fs.writeFileSync(path.join(dir, `f${i}.bin`), 'x');
|
|
let ticks = 0;
|
|
const timer = setInterval(() => { ticks++; }, 0);
|
|
let n = 0;
|
|
for await (const f of storage.list('walk-test/')) { if (f) n++; }
|
|
clearInterval(timer);
|
|
assert.equal(n, 450);
|
|
assert.ok(ticks > 0, 'other work ran while the folder was walked');
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|