feat(admin): manual cleanup of stale tus upload sessions + visibility
Adds an admin-driven sweep on top of the existing 24h auto-expire so
operators can see and reap abandoned `.tus/` sessions without waiting.
- storageJanitor: extract `walkTusDir(predicate)` helper, add
`getStaleTusInfo` + `cleanupStaleTusSessions(thresholdMs, dryRun)`;
refactor `cleanupTusStragglers` to delegate while preserving its
janitor-tick `{ removed }` contract.
- StorageStats gains `staleTusSessions` + `staleTusSize` (fixed 1h
display threshold).
- New `POST /api/admin/storage/cleanup-tus` route with
`maxAgeHours` validation (positive finite number, default 1) and
`dryRun` support; admin-gated.
- StoragePanel: 6th overview card "Stale Uploads" + new cleanup
subsection mirroring the media-cleanup pattern (preview-then-clean
with shared result panel styling).
- Tests: 8 new janitor tests covering empty dir, threshold filtering,
dry-run vs live, oldest-mtime tracking, subdir skipping, and the
override path on the existing straggler sweep. New
`routes/admin.test.ts` covers auth/admin gates, validation (zero,
negative, NaN), default `maxAgeHours`, dry-run vs live unlink.
- Docs: `uploads.md` §Janitor expanded to the full lifecycle (cancel
DELETE, discard DELETE, auto-expire, straggler sweep, admin route);
`admin.md` Storage Management updated with the new endpoint and
StorageStats fields.
This commit is contained in:
@@ -2,7 +2,7 @@ import type { FastifyInstance } from 'fastify';
|
||||
import crypto from 'crypto';
|
||||
import { eq, like, or, and, ne, sql, isNull, isNotNull, gte, lte, asc, desc } from 'drizzle-orm';
|
||||
import { authenticate, requireAdmin, hashPassword } from '../utils/auth.js';
|
||||
import { getStorageStats, getOrphanedFiles, cleanupStorage, cleanupOldMedia } from '../utils/storageJanitor.js';
|
||||
import { getStorageStats, getOrphanedFiles, cleanupStorage, cleanupOldMedia, cleanupStaleTusSessions } from '../utils/storageJanitor.js';
|
||||
import { getDb, schema } from '../db/index.js';
|
||||
import { connectionManager } from '../ws/handler.js';
|
||||
import { tombstoneUser, collectDeletionBroadcastTargets } from '../utils/userDeletion.js';
|
||||
@@ -74,6 +74,32 @@ export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/admin/storage/cleanup-tus — admin-driven sweep of stale tus
|
||||
// upload sessions. Defaults: maxAgeHours=1 (matches the staleTusSessions
|
||||
// display threshold), dryRun=false. Per-file unlink errors are surfaced via
|
||||
// CleanupResult.errors. No DB rows touched — `.tus/` is filesystem-only.
|
||||
app.post<{ Body: { maxAgeHours?: number; dryRun?: boolean } }>('/api/admin/storage/cleanup-tus', { preHandler: [authenticate, requireAdmin] }, async (request, reply) => {
|
||||
try {
|
||||
const rawAge = request.body?.maxAgeHours;
|
||||
const maxAgeHours = rawAge === undefined ? 1 : Number(rawAge);
|
||||
if (!Number.isFinite(maxAgeHours) || maxAgeHours <= 0) {
|
||||
return reply.code(400).send({ error: 'maxAgeHours must be a positive finite number', statusCode: 400 });
|
||||
}
|
||||
const dryRun = request.body?.dryRun ?? false;
|
||||
const thresholdMs = maxAgeHours * 60 * 60 * 1000;
|
||||
const result = cleanupStaleTusSessions(thresholdMs, dryRun);
|
||||
return reply.code(200).send({
|
||||
dryRun,
|
||||
deletedFiles: result.deletedFiles,
|
||||
freedBytes: result.freedBytes,
|
||||
deletedAttachmentRecords: 0,
|
||||
errors: result.errors,
|
||||
});
|
||||
} catch (err: any) {
|
||||
return reply.code(500).send({ error: `Tus cleanup failed: ${err.message}`, statusCode: 500 });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── User Management ────────────────────────────────────────────────────
|
||||
|
||||
// GET /api/admin/users — paginated user list with search
|
||||
|
||||
Reference in New Issue
Block a user