mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 08:11:03 +00:00
feat(storage): confirm the reclaim count the admin was shown, name a running backup as a skip, bound a trip cover
The confirm request now carries the unreferenced count the admin saw, so the server can refuse a count its last report no longer holds. A pass that found objects due while a backup read the uploads reports backup_running for them. A trip cover is bounded to 2048 characters on REST, RPC and the MCP tool, as a place image already is, since a replaced cover is queued for release by every upload path it spells out.
This commit is contained in:
@@ -225,6 +225,7 @@ export class TripsMcp {
|
||||
is_archived: z.boolean().optional().describe('Archive (true) or unarchive (false) the trip'),
|
||||
cover_image: z
|
||||
.string()
|
||||
.max(2048)
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe('Cover image path, e.g. /uploads/covers/abc.jpg; null removes the cover'),
|
||||
|
||||
@@ -224,7 +224,7 @@ describe('the reclaim contract', () => {
|
||||
expect(storageReclaimReportSchema.safeParse(old).success).toBe(false);
|
||||
});
|
||||
|
||||
it('names every skip reason a pass reports, the brake that used to skip a category gone', () => {
|
||||
it('names every skip reason a pass reports, a running backup included, the old brake gone', () => {
|
||||
expect([...STORAGE_RECLAIM_SKIP_REASONS]).toEqual([
|
||||
'not_reclaimed',
|
||||
'sources_missing',
|
||||
@@ -234,6 +234,7 @@ describe('the reclaim contract', () => {
|
||||
'list_failed',
|
||||
'source_failed',
|
||||
'delete_failed',
|
||||
'backup_running',
|
||||
'interrupted',
|
||||
]);
|
||||
for (const skipped of STORAGE_RECLAIM_SKIP_REASONS) {
|
||||
@@ -264,10 +265,20 @@ describe('the reclaim contract', () => {
|
||||
it('the requests are closed objects, and a started check answers started: true', () => {
|
||||
expect(storageReclaimSettingsRequestSchema.parse({ deleteEnabled: true })).toEqual({ deleteEnabled: true });
|
||||
expect(storageReclaimSettingsRequestSchema.safeParse({ deleteEnabled: true, extra: 1 }).success).toBe(false);
|
||||
expect(storageReclaimConfirmRequestSchema.parse({ category: 'covers' })).toEqual({ category: 'covers' });
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'nowhere' }).success).toBe(false);
|
||||
expect(storageReclaimConfirmRequestSchema.parse({ category: 'covers', unreferenced: 300 })).toEqual({
|
||||
category: 'covers',
|
||||
unreferenced: 300,
|
||||
});
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'nowhere', unreferenced: 3 }).success).toBe(false);
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'covers', count: 3 }).success).toBe(false);
|
||||
expect(storageReclaimRunStartedSchema.parse({ started: true })).toEqual({ started: true });
|
||||
expect(storageReclaimRunStartedSchema.safeParse({ started: false }).success).toBe(false);
|
||||
});
|
||||
it('a confirmation names the count the admin was shown, a whole number of objects', () => {
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'covers' }).success).toBe(false);
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'covers', unreferenced: -1 }).success).toBe(false);
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'covers', unreferenced: 1.5 }).success).toBe(false);
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'covers', unreferenced: '3' }).success).toBe(false);
|
||||
expect(storageReclaimConfirmRequestSchema.safeParse({ category: 'covers', unreferenced: 0 }).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -291,6 +291,8 @@ export const storageReclaimCategorySchema = z.enum(STORAGE_RECLAIM_CATEGORIES);
|
||||
* - `owner_unknown`: this installation's owner marker could not be written there, so a shared place cannot be ruled out;
|
||||
* - `list_failed` / `source_failed`: the listing or a reference lookup failed, so the pass stopped there;
|
||||
* - `delete_failed`: deleting failed on the backend, so the pass stopped deleting in this category;
|
||||
* - `backup_running`: a backup was reading the uploads when this category's objects were due, so the pass
|
||||
* deleted nothing there (from then on, for the rest of the pass); the next pass tries again;
|
||||
* - `interrupted`: the pass stopped (`stopped` says why) before it finished this category; its figures are partial.
|
||||
* A skip keeps the marks it found: only a pass that judges the category moves them.
|
||||
*/
|
||||
@@ -303,6 +305,7 @@ export const STORAGE_RECLAIM_SKIP_REASONS = [
|
||||
'list_failed',
|
||||
'source_failed',
|
||||
'delete_failed',
|
||||
'backup_running',
|
||||
'interrupted',
|
||||
] as const;
|
||||
|
||||
@@ -317,8 +320,9 @@ export const storageReclaimCategoryReportSchema = z.object({
|
||||
eligible: storageObjectCountSchema,
|
||||
deleted: storageObjectCountSchema,
|
||||
/**
|
||||
* More than a fifth of the category is unreferenced, and more than an admin
|
||||
* last confirmed: deletion waits for a confirmation (POST .../reclaim/confirm).
|
||||
* More than a fifth of the category is unreferenced and no admin confirmation
|
||||
* covers that count: deletion waits for one (POST .../reclaim/confirm). The
|
||||
* admin state reads false once a confirmation covers it, before the next pass.
|
||||
*/
|
||||
confirmationRequired: z.boolean(),
|
||||
skipped: z.enum(STORAGE_RECLAIM_SKIP_REASONS).nullable(),
|
||||
@@ -359,9 +363,16 @@ export const storageReclaimRunStartedSchema = z.object({ started: z.literal(true
|
||||
|
||||
/**
|
||||
* Lets the pass delete in a category whose unreferenced share is over a
|
||||
* fifth, up to the count the last report showed for it.
|
||||
* fifth. `unreferenced` is the count the admin was shown; the server refuses
|
||||
* (409 `reclaim_confirm_stale`) when its last report's count for the category
|
||||
* differs from it by more than a tenth or by more than 50 objects. The
|
||||
* confirmation then covers up to one and a half times that count (at least 100
|
||||
* more) for 14 days, and is used up once the share falls under a fifth again.
|
||||
*/
|
||||
export const storageReclaimConfirmRequestSchema = z.strictObject({ category: storageReclaimCategorySchema });
|
||||
export const storageReclaimConfirmRequestSchema = z.strictObject({
|
||||
category: storageReclaimCategorySchema,
|
||||
unreferenced: z.number().int().nonnegative(),
|
||||
});
|
||||
export type StorageReclaimConfirmRequest = z.infer<typeof storageReclaimConfirmRequestSchema>;
|
||||
|
||||
export const storageAdminStateSchema = z.object({
|
||||
|
||||
@@ -48,6 +48,13 @@ describe('tripUpdateRequestSchema', () => {
|
||||
expect(tripUpdateRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(tripUpdateRequestSchema.safeParse({ is_archived: 1, cover_image: null }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('bounds cover_image like a place image url: a replaced cover is queued for release', () => {
|
||||
expect(tripUpdateRequestSchema.safeParse({ cover_image: `/uploads/covers/${'a'.repeat(2000)}` }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(tripUpdateRequestSchema.safeParse({ cover_image: 'x'.repeat(2049) }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tripAddMemberRequestSchema', () => {
|
||||
|
||||
@@ -118,7 +118,9 @@ export const tripUpdateRequestSchema = z.object({
|
||||
reminder_days: z.number().optional(),
|
||||
day_count: z.number().optional(),
|
||||
is_archived: z.union([z.boolean(), z.number()]).optional(),
|
||||
cover_image: z.string().nullable().optional(),
|
||||
// Bounded like a place's image url: a replaced cover is queued for release,
|
||||
// and every upload path the old value spells out is one queued object.
|
||||
cover_image: z.string().max(2048).nullable().optional(),
|
||||
});
|
||||
export type TripUpdateRequest = z.infer<typeof tripUpdateRequestSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user