Add configurable history and storage retention policies
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { initializeStandaloneSettings } from '../helpers/settings.js';
|
||||
import Database from 'better-sqlite3';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises';
|
||||
@@ -28,7 +29,7 @@ const begin = (game: 't5' | 't6' = 't6', files = [file()]) => createLibraryUploa
|
||||
|
||||
beforeEach(async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), 'ec-browser-library-'));
|
||||
state.handle = new Database(join(directory, 'controller.sqlite'));
|
||||
state.handle = new Database(join(directory, 'controller.sqlite')); initializeStandaloneSettings(state.handle);
|
||||
state.handle.pragma('foreign_keys=ON');
|
||||
vi.stubEnv('GAME_FILES_ROOT', join(directory, 'libraries'));
|
||||
});
|
||||
@@ -127,7 +128,7 @@ describe('browser folder uploads', () => {
|
||||
await upload(initial.id, 'main/archive.iwd', first);
|
||||
expect((await browserUploadStatus(initial.id)).files[0]).toMatchObject({ receivedBytes: chunkSize, complete: false });
|
||||
state.handle!.close();
|
||||
state.handle = new Database(join(directory, 'controller.sqlite'));
|
||||
state.handle = new Database(join(directory, 'controller.sqlite')); initializeStandaloneSettings(state.handle);
|
||||
state.handle.pragma('foreign_keys=ON');
|
||||
await expect(upload(initial.id, 'main/archive.iwd', first)).rejects.toMatchObject({ receivedBytes: chunkSize });
|
||||
expect((await browserUploadStatus(initial.id)).files[0].receivedBytes).toBe(chunkSize);
|
||||
@@ -206,7 +207,7 @@ describe('browser folder uploads', () => {
|
||||
})();
|
||||
await mkdir(dirname(initial.storageLocation), { recursive: true });
|
||||
await rename(join(directory, 'libraries', '.imports', initial.id), initial.storageLocation);
|
||||
state.handle!.close(); state.handle = new Database(join(directory, 'controller.sqlite'));
|
||||
state.handle!.close(); state.handle = new Database(join(directory, 'controller.sqlite')); initializeStandaloneSettings(state.handle);
|
||||
state.handle.pragma('foreign_keys=ON');
|
||||
expect(await browserUploadStatus(initial.id)).toMatchObject({ status: 'pending', files: [{ complete: true }] });
|
||||
const ready = await publishBrowserUpload(initial.id);
|
||||
@@ -262,3 +263,52 @@ describe('browser folder uploads', () => {
|
||||
expect(requireReadyLibrary(target.id, ready.id).manifestSha256).toBe(ready.manifestSha256);
|
||||
});
|
||||
});
|
||||
|
||||
describe('game-file retention policies', () => {
|
||||
beforeEach(() => state.handle!.exec('CREATE TABLE native_instances (id TEXT, host_id TEXT, library_id TEXT, retired_at TEXT)'));
|
||||
it('expires inactive uploads while protecting resumed and explicitly held uploads', async () => {
|
||||
const { fleetSettings,saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
const { cleanupControllerLibraries } = await import('../../src/lib/server/game-files/index.js');
|
||||
saveFleetSettings({...fleetSettings(),uploadKeepDays:1});
|
||||
const expired = begin(); const resumed = begin(); const held = begin();
|
||||
for (const v of [expired,resumed,held]) await upload(v.id,file().path,bytes);
|
||||
state.handle!.prepare("UPDATE game_file_versions SET last_activity_at='2020-01-01T00:00:00.000Z'").run();
|
||||
state.handle!.prepare("INSERT INTO retention_controls(category,record_id,protected) VALUES('upload',?,1)").run(held.id);
|
||||
await browserUploadStatus(resumed.id);
|
||||
await cleanupControllerLibraries();
|
||||
expect(getLibraryVersion(expired.id)?.filesRemoved).toBe(true);
|
||||
await expect(upload(expired.id,file().path,bytes)).rejects.toThrow('expired');
|
||||
expect((await browserUploadStatus(resumed.id)).files[0].complete).toBe(true);
|
||||
expect((await browserUploadStatus(held.id)).files[0].complete).toBe(true);
|
||||
});
|
||||
it('keeps extra published versions and applies a supersession grace period', async () => {
|
||||
const { fleetSettings,saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
const { cleanupControllerLibraries } = await import('../../src/lib/server/game-files/index.js');
|
||||
saveFleetSettings({...fleetSettings(),gameFileKeepCount:2});
|
||||
const first = begin(); await upload(first.id,file().path,bytes); await publishBrowserUpload(first.id);
|
||||
const second = begin(); await upload(second.id,file().path,bytes); await publishBrowserUpload(second.id);
|
||||
expect(getLibraryVersion(first.id)?.filesRemoved).toBe(false);
|
||||
saveFleetSettings({...fleetSettings(),gameFileKeepCount:1,gameFileGraceDays:1});
|
||||
await cleanupControllerLibraries();
|
||||
expect(getLibraryVersion(first.id)?.filesRemoved).toBe(false);
|
||||
saveFleetSettings({...fleetSettings(),gameFileGraceDays:0});
|
||||
await cleanupControllerLibraries();
|
||||
expect(getLibraryVersion(first.id)?.filesRemoved).toBe(true);
|
||||
expect(getLibraryVersion(second.id)?.filesRemoved).toBe(false);
|
||||
});
|
||||
});
|
||||
it('keeps an upload while its chunk is actively streaming, even if its activity timestamp is old', async () => {
|
||||
const { fleetSettings,saveFleetSettings } = await import('../../src/lib/server/settings.js');
|
||||
const { cleanupControllerLibraries } = await import('../../src/lib/server/game-files/index.js');
|
||||
saveFleetSettings({...fleetSettings(),uploadKeepDays:1});
|
||||
const version = begin();
|
||||
let finish!: () => void;
|
||||
const body = new ReadableStream<Uint8Array>({start(controller) {finish=()=>{controller.enqueue(bytes);controller.close();};}});
|
||||
const writing = uploadBrowserFile(version.id,file().path,{offset:0,size:bytes.length,sha256:sha(bytes)},body);
|
||||
try {
|
||||
state.handle!.prepare("UPDATE game_file_versions SET last_activity_at='2020-01-01T00:00:00.000Z' WHERE id=?").run(version.id);
|
||||
await cleanupControllerLibraries();
|
||||
expect(getLibraryVersion(version.id)?.filesRemoving).toBe(false);
|
||||
} finally {finish();await writing;}
|
||||
expect((await browserUploadStatus(version.id)).files[0].complete).toBe(true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user