feat(storage): wire choice of storage

This commit is contained in:
Edouard Vanbelle
2026-08-01 13:54:00 +02:00
parent 2de71b6d9a
commit d7c19570a5
10 changed files with 574 additions and 286 deletions
+2 -9
View File
@@ -104,15 +104,8 @@ describe('admin test/probe endpoints', () => {
await expect(admin.testStorage({ backend: 's3' })).resolves.toMatchObject({ connected: true });
});
it('verifyMigration fills defaults and throws on error', async () => {
fetchMock.mockResolvedValue(okRes({ passed: true }));
await expect(admin.verifyMigration(10)).resolves.toMatchObject({
passed: true,
sample_checked: 0
});
fetchMock.mockResolvedValue(errRes(500, {}));
await expect(admin.verifyMigration()).rejects.toThrow(/verify failed/);
});
// verifyMigration retired in slice 7 of docs/plan/storage-multi-entry.md.
// Superseded by `POST /api/admin/jobs/blobs_consistency/trigger?storage=<name>`.
it('installPlugin posts a FormData bundle', async () => {
fetchMock.mockResolvedValue(okRes({ id: 'com.example.hello' }));
+36 -39
View File
@@ -466,6 +466,15 @@ export function saveOidc(body: Record<string, unknown>): Promise<void> {
// ── Storage settings + migration ───────────────────────────────────────────
export interface StorageEntrySummary {
name: string;
backend: string;
is_active: boolean;
encryption_enabled: boolean;
/** Human-readable physical hint (root_dir / bucket / container). */
location_hint?: string | null;
}
export interface StorageSettings {
backend: string;
s3_endpoint_url?: string | null;
@@ -479,6 +488,11 @@ export interface StorageSettings {
total_blobs?: number;
total_bytes_stored?: number;
dedup_ratio?: number;
// Multi-entry view (slice 6 of docs/plan/storage-multi-entry.md).
// `entries` is empty for the legacy zero-entries path.
entries?: StorageEntrySummary[];
active_entry_name?: string;
migration_readonly?: boolean;
}
export function getStorageSettings(): Promise<StorageSettings> {
@@ -512,50 +526,33 @@ export function getMigration(): Promise<MigrationStatus> {
return apiJson<MigrationStatus>('/api/admin/storage/migration', { credentials: 'same-origin' });
}
export function migrationAction(action: 'start' | 'pause' | 'resume'): Promise<void> {
export function migrationAction(
action: 'start' | 'pause' | 'resume',
targetName?: string
): Promise<void> {
// `complete` was retired when the migration became a recoverable
// job — Completed is the terminal `RunSummary.status`; there's
// nothing left to acknowledge. Post-migration cutover now happens
// via .env + restart, prompted by an inline hint on the admin
// storage tab (see `cutoverPending` in +page.svelte).
const body = action === 'start' ? { concurrency: 4 } : {};
// nothing left to acknowledge. Post-migration cutover happens on
// operator restart (server re-boots on active_backend_name = the
// new entry; the boot-clear rule drops migration_readonly).
//
// `start` REQUIRES `targetName` in multi-entry mode — the backend
// rejects an unnamed start with 400 (see StartMigrationDto).
// `pause` and `resume` take no body (resume reads target_name
// from the paused run's params).
const body: Record<string, unknown> =
action === 'start' ? { target_name: targetName ?? '', concurrency: 4 } : {};
return mutate(`/api/admin/storage/migration/${action}`, 'POST', body);
}
/** Result of a `verify` integrity check (POST .../migration/verify). */
export interface MigrationVerifyResult {
passed: boolean;
sample_checked: number;
pg_blob_count: number;
missing_in_target: string[];
size_mismatches: string[];
}
/**
* Run an integrity verification pass over a sample of migrated blobs. Unlike
* the other migration actions this returns a structured result that the caller
* renders (passed / sample-checked / missing / size-mismatch counts).
*/
export async function verifyMigration(sampleSize = 100): Promise<MigrationVerifyResult> {
const res = await apiFetch('/api/admin/storage/migration/verify', {
method: 'POST',
credentials: 'same-origin',
headers: { ...JSON_HEADERS, ...getCsrfHeaders() },
body: JSON.stringify({ sample_size: sampleSize })
});
if (!res.ok) {
const e = (await res.json().catch(() => ({}))) as { message?: string };
throw new Error(e.message || `verify failed: ${res.status}`);
}
const r = (await res.json()) as Partial<MigrationVerifyResult>;
return {
passed: r.passed ?? false,
sample_checked: r.sample_checked ?? 0,
pg_blob_count: r.pg_blob_count ?? 0,
missing_in_target: r.missing_in_target ?? [],
size_mismatches: r.size_mismatches ?? []
};
}
// verifyMigration + MigrationVerifyResult retired in slice 7 of
// docs/plan/storage-multi-entry.md — the corresponding backend
// endpoint's sample-based check is superseded by
// `POST /api/admin/jobs/blobs_consistency/trigger?storage=<name>`,
// which does a full walk against any named entry and integrates
// with the standard runs / findings admin surface. Trigger from
// the Jobs tab; the Storage tab drops the "Verify integrity"
// button.
// ── Plugins ─────────────────────────────────────────────────────────────