Add upload library management and ops diagnostics
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// Admin uploads: real ffmpeg-made files through the real routes.
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { mkdtempSync, rmSync, existsSync, readFileSync, copyFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Hono } from 'hono';
|
||||
@@ -32,7 +32,7 @@ beforeAll(async () => {
|
||||
'-shortest', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', fx('clip.mp4'));
|
||||
app = new Hono();
|
||||
uploads = registerUploadRoutes(app, {
|
||||
db, uploadDir: join(root, 'uploads'), parseLrc, sanitizeLyrics,
|
||||
db, uploadDir: join(root, 'uploads'), backupDir: join(root, 'backup-uploads'), parseLrc, sanitizeLyrics,
|
||||
requireAdminOrToken: async (c, next) => { if (c.req.header('x-admin') === 'yes') await next(); else return c.json({ ok: false, error: 'admin only' }, 401); },
|
||||
rangeFileResponse: (c, path, type) => {
|
||||
const body = readFileSync(path);
|
||||
@@ -108,11 +108,11 @@ describe('uploads', () => {
|
||||
expect(hit.uploads.map((u) => u.id)).toEqual([audioId]);
|
||||
const u = await db.getUpload(audioId);
|
||||
expect(uploads.streamsPayload(u)).toMatchObject({
|
||||
audioUrl: `/api/uploads/${audioId}`, kind: 'audio', upload: true, art: `/api/uploads/${audioId}/art`, qualities: [],
|
||||
audioUrl: `/api/uploads/${audioId}?g=1`, kind: 'audio', upload: true, art: `/api/uploads/${audioId}/art?g=1`, qualities: [],
|
||||
});
|
||||
expect(uploads.card(u)).toMatchObject({ id: audioId, title: 'Test Song', channel: 'Test Artist', kind: 'audio' });
|
||||
const v = uploads.streamsPayload(await db.getUpload(videoId));
|
||||
expect(v.qualities[0]).toMatchObject({ label: 'Original', hasAudio: true, url: `/api/uploads/${videoId}` });
|
||||
expect(v.qualities[0]).toMatchObject({ label: 'Original', hasAudio: true, url: `/api/uploads/${videoId}?g=1` });
|
||||
});
|
||||
|
||||
test('serving supports Range, and delete removes the files', async () => {
|
||||
@@ -162,7 +162,45 @@ describe('streaming upload (large / long files)', () => {
|
||||
const up = await (await put('/api/admin/uploads/stream?name=song.mp3', readFileSync(fx('song.mp3')))).json();
|
||||
const r = await put(`/api/admin/uploads/${up.upload.id}/art`, readFileSync(fx('cover.png')));
|
||||
expect((await r.json()).ok).toBe(true);
|
||||
expect((await db.getUpload(up.upload.id)).art).toBeTruthy();
|
||||
const updated = await db.getUpload(up.upload.id);
|
||||
expect(updated).toMatchObject({ art: 'file', gen: 1, artGen: 2 });
|
||||
expect(uploads.streamsPayload(updated).audioUrl).toBe(`/api/uploads/${up.upload.id}?g=1`);
|
||||
expect(uploads.streamsPayload(updated).art).toBe(`/api/uploads/${up.upload.id}/art?g=2`);
|
||||
expect((await app.request(`/api/uploads/${up.upload.id}/art?g=1`)).status).toBe(409);
|
||||
});
|
||||
|
||||
test('admin manager edits metadata, replaces bytes in place with a new generation, and pages orphan reports', async () => {
|
||||
const up = await (await put('/api/admin/uploads/stream?name=original.mp3', readFileSync(fx('song.mp3')))).json();
|
||||
const id = up.upload.id;
|
||||
expect(up.upload.gen).toBe(1);
|
||||
const edited = await app.request(`/api/admin/uploads/${id}`, { method: 'PATCH', headers: { 'x-admin': 'yes', 'content-type': 'application/json' }, body: JSON.stringify({ title: 'Changed title', artist: 'New artist', listed: false }) });
|
||||
expect((await edited.json()).upload).toMatchObject({ id, title: 'Changed title', artist: 'New artist', listed: false });
|
||||
const replaceBytes = readFileSync(fx('clip.mp4'));
|
||||
const replaced = await put(`/api/admin/uploads/${id}/replace?name=concert.mp4`, replaceBytes);
|
||||
const payload = await replaced.json();
|
||||
expect(replaced.status).toBe(200);
|
||||
expect(payload.upload).toMatchObject({ id, ext: 'mp4', kind: 'video', gen: 2, listed: false });
|
||||
expect((await app.request(`/api/uploads/${id}?g=1`)).status).toBe(409);
|
||||
expect((await app.request(`/api/uploads/${id}`, { headers: { Range: 'bytes=0-9' } })).status).toBe(206);
|
||||
expect(uploads.streamsPayload(payload.upload).qualities[0].url).toContain('?g=2');
|
||||
const listed = await app.request('/api/admin/uploads/bulk', { method: 'POST', headers: { 'x-admin': 'yes', 'content-type': 'application/json' }, body: JSON.stringify({ ids: [id], action: 'list' }) });
|
||||
expect(await listed.json()).toMatchObject({ ok: true, changed: 1 });
|
||||
const manage = await app.request('/api/admin/uploads/manage?q=Changed', { headers: { 'x-admin': 'yes' } });
|
||||
expect(await manage.json()).toMatchObject({ ok: true, total: 1, uploads: [expect.objectContaining({ id, listed: true, gen: 2 })] });
|
||||
const quota = await app.request('/api/admin/uploads/quotas/owner-device1', { method: 'PUT', headers: { 'x-admin': 'yes', 'content-type': 'application/json' }, body: JSON.stringify({ maxBytes: 5_000_000 }) });
|
||||
expect(await quota.json()).toMatchObject({ ok: true, maxBytes: 5_000_000 });
|
||||
expect(await db.getUploadQuota('owner-device1')).toBe(5_000_000);
|
||||
await db.setUploadQuota('owner-device1', null);
|
||||
});
|
||||
|
||||
test('same-extension replacement removes a stale fallback copy before publishing its new generation', async () => {
|
||||
const up = await (await put('/api/admin/uploads/stream?name=mirror.mp3', readFileSync(fx('song.mp3')))).json();
|
||||
const staleBackup = join(root, 'backup-uploads', `${up.upload.id}.mp3`);
|
||||
copyFileSync(join(root, 'uploads', `${up.upload.id}.mp3`), staleBackup);
|
||||
expect(existsSync(staleBackup)).toBe(true);
|
||||
const replaced = await put(`/api/admin/uploads/${up.upload.id}/replace?name=updated.mp3`, readFileSync(fx('song.mp3')));
|
||||
expect((await replaced.json()).upload.gen).toBe(2);
|
||||
expect(existsSync(staleBackup)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user