mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
fix(storage): release a deleted journey's photos and a deleted list's images, and keep one bad key from stopping a category
Deleting a journey left its uploaded photos behind for good, and deleting a list queued neither its cover nor its place images. An object with a key the store refuses no longer fails every night's pass, and the admin view keeps the counts of a category skipped for a running backup.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-ADMIN-STOR-RECLAIM-001 to -007: the reclaim report as both storage panels show it.
|
||||
// FE-ADMIN-STOR-RECLAIM-001 to -009: the reclaim report as both storage panels show it.
|
||||
import { STORAGE_RECLAIM_CATEGORIES, type StorageReclaimCategoryReport, type StorageReclaimReport } from '@trek/shared';
|
||||
|
||||
import { reclaimView } from './reclaimModel';
|
||||
@@ -137,4 +137,84 @@ describe('reclaimView', () => {
|
||||
it('FE-ADMIN-STOR-RECLAIM-007: a running check reads as running', () => {
|
||||
expect(reclaimView({ deleteEnabled: false, running: true, lastRun: null, lastDeleteRun: null }).running).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-RECLAIM-008: a category held back by a backup or a failed delete still counts what was judged and not deleted', () => {
|
||||
const view = reclaimView({
|
||||
deleteEnabled: true,
|
||||
running: false,
|
||||
lastDeleteRun: null,
|
||||
lastRun: report(
|
||||
{
|
||||
covers: category({
|
||||
skipped: 'backup_running',
|
||||
unreferenced: { objects: 3_000, bytes: 3_000 },
|
||||
eligible: { objects: 3_000, bytes: 3_000 },
|
||||
}),
|
||||
places: category({
|
||||
skipped: 'backup_running',
|
||||
unreferenced: { objects: 1_500, bytes: 150 },
|
||||
eligible: { objects: 1_200, bytes: 120 },
|
||||
deleted: { objects: 1_000, bytes: 100 },
|
||||
}),
|
||||
files: category({
|
||||
skipped: 'delete_failed',
|
||||
unreferenced: { objects: 5, bytes: 50 },
|
||||
eligible: { objects: 4, bytes: 40 },
|
||||
deleted: { objects: 1, bytes: 10 },
|
||||
}),
|
||||
},
|
||||
'delete'
|
||||
),
|
||||
});
|
||||
const byCategory = Object.fromEntries(view.lines.map((line) => [line.category, line]));
|
||||
expect(byCategory.covers).toMatchObject({
|
||||
freeable: { objects: 3_000, bytes: 3_000 },
|
||||
waiting: none,
|
||||
skipped: 'backup_running',
|
||||
});
|
||||
expect(byCategory.places).toMatchObject({
|
||||
freeable: { objects: 200, bytes: 20 },
|
||||
waiting: { objects: 300, bytes: 30 },
|
||||
skipped: 'backup_running',
|
||||
});
|
||||
expect(byCategory.files).toMatchObject({
|
||||
freeable: { objects: 3, bytes: 30 },
|
||||
waiting: { objects: 1, bytes: 10 },
|
||||
skipped: 'delete_failed',
|
||||
});
|
||||
expect(view.freeable).toEqual({ objects: 3_203, bytes: 3_050 });
|
||||
expect(view.waiting).toEqual({ objects: 301, bytes: 40 });
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-RECLAIM-009: a category whose sources or listing failed shows its reason and counts nothing', () => {
|
||||
const view = reclaimView({
|
||||
deleteEnabled: true,
|
||||
running: false,
|
||||
lastDeleteRun: null,
|
||||
lastRun: report({
|
||||
covers: category({ skipped: 'source_failed', objects: 12 }),
|
||||
files: category({ skipped: 'list_failed' }),
|
||||
}),
|
||||
});
|
||||
expect(view.lines).toEqual([
|
||||
{
|
||||
category: 'files',
|
||||
freeable: none,
|
||||
waiting: none,
|
||||
confirmationRequired: false,
|
||||
unreferenced: 0,
|
||||
skipped: 'list_failed',
|
||||
},
|
||||
{
|
||||
category: 'covers',
|
||||
freeable: none,
|
||||
waiting: none,
|
||||
confirmationRequired: false,
|
||||
unreferenced: 0,
|
||||
skipped: 'source_failed',
|
||||
},
|
||||
]);
|
||||
expect(view.freeable).toEqual(none);
|
||||
expect(view.waiting).toEqual(none);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,11 +67,23 @@ function freedBy(run: StorageReclaimReport | null): ReclaimCount {
|
||||
);
|
||||
}
|
||||
|
||||
type SkipReason = StorageReclaimCategoryReport['skipped'];
|
||||
|
||||
/**
|
||||
* The reclaim state as the storage panels show it, desktop and phone alike.
|
||||
* `null` (a server from before the reclaim pass, or a state not loaded) reads
|
||||
* as deletion off, not running and never checked.
|
||||
* The reasons the pass gives when it left a category before judging it, so its
|
||||
* counts mean nothing. Every other reason (a backup running, a failed delete,
|
||||
* an interrupted pass) comes after the judgement and keeps its figures; a new
|
||||
* reason counts by default rather than hiding what the pass found.
|
||||
*/
|
||||
const NEVER_JUDGED: ReadonlySet<SkipReason> = new Set<SkipReason>([
|
||||
'sources_missing',
|
||||
'shared_location',
|
||||
'foreign_install',
|
||||
'owner_unknown',
|
||||
'list_failed',
|
||||
'source_failed',
|
||||
]);
|
||||
|
||||
/**
|
||||
* One category of the last check as a line, or null when it has nothing to
|
||||
* say. A category the pass stopped in still counts what it found; one it
|
||||
@@ -82,7 +94,7 @@ function lineOf(
|
||||
report: StorageReclaimCategoryReport | undefined
|
||||
): ReclaimCategoryLine | null {
|
||||
if (!report || report.skipped === 'not_reclaimed') return null;
|
||||
const judged = report.skipped === null || report.skipped === 'interrupted' || report.skipped === 'delete_failed';
|
||||
const judged = !NEVER_JUDGED.has(report.skipped);
|
||||
// What was deleted on this run no longer waits to be freed.
|
||||
const freeable = judged ? minus(report.eligible, report.deleted) : NONE;
|
||||
const waiting = judged ? minus(report.unreferenced, report.eligible) : NONE;
|
||||
|
||||
Reference in New Issue
Block a user