From 015f2da0f7326b4838b633c195bade7cf272a626 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Sun, 2 Aug 2026 14:49:35 +0200 Subject: [PATCH] refactor(backend): normalize naming convention to backend rather storage no ambiguity with the backend rather storage --- docs/config/admin-settings.md | 2 +- docs/plan/consistency-check.md | 2 +- docs/plan/job-registry.md | 4 +- docs/plan/storage-key-rotation.md | 42 ++++++------ docs/plan/storage-multi-entry.md | 6 +- frontend/src/lib/api/endpoints/admin.ts | 10 +-- frontend/src/lib/api/types.ts | 2 +- .../src/lib/components/ReadOnlyBanner.svelte | 4 +- .../src/lib/stores/serverStatus.svelte.ts | 4 +- .../src/routes/admin/[[tab]]/+page.svelte | 16 ++--- frontend/static/locales/en.json | 2 +- frontend/static/locales/fr.json | 2 +- src/application/dtos/settings_dto.rs | 2 +- src/application/ports/blob_storage_ports.rs | 4 +- src/common/config.rs | 8 +-- src/common/di.rs | 18 +++--- src/infrastructure/scheduler/recoverable.rs | 4 +- src/infrastructure/scheduler/types.rs | 2 +- .../services/azure_blob_backend.rs | 4 +- ...ervice.rs => backend_migration_service.rs} | 62 +++++++++--------- ...e_service.rs => backend_rotate_service.rs} | 58 ++++++++--------- .../services/blobs_consistency_service.rs | 4 +- .../services/encrypted_blob_backend.rs | 18 +++--- src/infrastructure/services/entry_backend.rs | 2 +- .../services/local_blob_backend.rs | 2 +- src/infrastructure/services/mod.rs | 4 +- .../services/s3_blob_backend.rs | 4 +- src/interfaces/api/handlers/admin_handler.rs | 64 +++++++++---------- src/main.rs | 6 +- tests/api/admin_jobs.hurl | 8 +-- 30 files changed, 185 insertions(+), 185 deletions(-) rename src/infrastructure/services/{storage_migration_service.rs => backend_migration_service.rs} (96%) rename src/infrastructure/services/{storage_rotate_service.rs => backend_rotate_service.rs} (92%) diff --git a/docs/config/admin-settings.md b/docs/config/admin-settings.md index e874f81e..0dc1f082 100644 --- a/docs/config/admin-settings.md +++ b/docs/config/admin-settings.md @@ -79,7 +79,7 @@ The admin storage tab operates on the **named storage entries** declared in `.en | `POST` | `/api/admin/storage/migration/resume` | Resume a paused run (target read from `params.target_name`, no body needed) | | `GET` | `/api/admin/storage/migration` | Poll the current run's progress | -Runs are recoverable — status, cursor, and per-blob failure findings all live in `jobs.recoverable_runs` / `jobs.run_findings`. The same run history is browsable via `GET /api/admin/jobs/storage_migration/runs`. +Runs are recoverable — status, cursor, and per-blob failure findings all live in `jobs.recoverable_runs` / `jobs.run_findings`. The same run history is browsable via `GET /api/admin/jobs/backend_migration/runs`. ### Cutover flow (moving the active pointer) diff --git a/docs/plan/consistency-check.md b/docs/plan/consistency-check.md index 7b864917..ad9fe1f7 100644 --- a/docs/plan/consistency-check.md +++ b/docs/plan/consistency-check.md @@ -565,7 +565,7 @@ CREATE SCHEMA IF NOT EXISTS admin; CREATE TABLE jobs.recoverable_runs ( id UUID PRIMARY KEY, - job_name TEXT NOT NULL, -- 'consistency_blobs', 'storage_migration', 'reextract_audio', ... + job_name TEXT NOT NULL, -- 'consistency_blobs', 'backend_migration', 'reextract_audio', ... status TEXT NOT NULL, -- Running / Paused / Completed / Failed / CancelRequested started_at TIMESTAMPTZ NOT NULL, last_progress_at TIMESTAMPTZ NOT NULL, diff --git a/docs/plan/job-registry.md b/docs/plan/job-registry.md index 44b87126..ebc30e6e 100644 --- a/docs/plan/job-registry.md +++ b/docs/plan/job-registry.md @@ -716,7 +716,7 @@ rationale + the merges/separations that fall out of the rule. | `files_consistency` | `storage.files` | file UUID | `parent_folder_trashed` (live file under trashed folder), `missing_blob` (severity `data_loss` — `blob_hash` present in neither `storage.blobs` nor `storage.chunk_manifests`), `chunk_missing` (severity `data_loss` — manifest exists but points at chunks absent from `storage.blobs`; typical dedup GC race), `blob_size_mismatch` (denormalised `files.size` diverges from the authoritative size — manifest first, blob fallback) | Shipped Slice 6, CDC-aware Slice 10. Handles both storage paths: `storage.chunk_manifests` (post-Apr-2026 FastCDC ingest, dominant path) and `storage.blobs` (pre-CDC whole-file blob, legacy fallback). Physical backend-existence checks (chunk bytes actually on disk) belong in `storage_consistency`. Room to grow: `drive_id_parent_mismatch`, mime-type reconciliation. | | `storage_consistency` | Storage backend (fs / S3) | object key / path | Each blob has a `storage.blobs` row (orphan detection) | `?deep=true` adds re-BLAKE3 + mime sniff. Orphan-side of the old bidirectional blob check + former `blob_integrity` + former `thumbnail_consistency`. | | `grants_consistency` (future) | `storage.role_grants` | grant UUID | subject/resource/granted_by exist | | -| `storage_migration` | `storage.blobs` (source) → target backend | blob hash | Copy bytes; failures → `stats.failed_blobs` (and eventually `jobs.run_findings`) | Retires `Arc>` in `migration_job.rs`. | +| `backend_migration` | `storage.blobs` (source) → target backend | blob hash | Copy bytes; failures → `stats.failed_blobs` (and eventually `jobs.run_findings`) | Retires `Arc>` in `migration_job.rs`. | | `reextract_audio` | `storage.files` where audio | file UUID | Re-run audio-tag parser, upsert `audio_metadata` | Retires synchronous admin-request execution. | | `reextract_image` | `storage.files` where image/video | file UUID | Re-run EXIF/container date parser, upsert capture date | Same shape as reextract_audio. | | `consistency_batch` (wrapper) | Iterates registered `*_consistency` jobs | — (JobHandler, not RecoverableJobHandler) | Sequentially triggers each sub-job; `?deep=true` propagates | Shipped Slice 5. One-click "run all" without per-job clicks; exclusivity via `job_name` prevents concurrent batches from stepping on each other. Batch itself always returns `Ok` — child failures land in `outcome.extra.per_check[].outcome`. | @@ -729,7 +729,7 @@ SELECT + one UPDATE. Kept as its own admin endpoint; do NOT fold into ### Verification (Part 2) 1. **Compile + schema-migration idempotence.** -2. **Fresh run:** `POST /api/admin/jobs/storage_migration/trigger` → new row with +2. **Fresh run:** `POST /api/admin/jobs/backend_migration/trigger` → new row with `status='Running'`, `cursor=NULL`. 3. **Concurrent trigger:** second `POST` while the first is running returns the SAME `run_id` (idempotent, DB unique index enforces). diff --git a/docs/plan/storage-key-rotation.md b/docs/plan/storage-key-rotation.md index b75098b2..7b908f3b 100644 --- a/docs/plan/storage-key-rotation.md +++ b/docs/plan/storage-key-rotation.md @@ -159,7 +159,7 @@ Properties this buys us: format-conversion work. * **Lazy conversion on hot paths.** Any COW overwrite (WebDAV MOVE, PUT-over, content-hash re-upload) naturally lands as v1 at the same object key. -* **Explicit conversion via `storage_rotate`.** The rotate job walks +* **Explicit conversion via `backend_rotate`.** The rotate job walks `storage.blobs`, reads each blob via the magic-byte dispatch, and if the blob is not already v1 with the head-pair key, PUTs it back as v1 with the head pair — in place, same object key. @@ -186,7 +186,7 @@ Guardrail: whatever consistency scan cadence the deployment has (weekly by default; on demand from the admin panel). * The *"Rotation complete — safe to remove the old key"* hint appears in the - entry card only when the last `storage_rotate` run completed with zero + entry card only when the last `backend_rotate` run completed with zero findings AND the most recent consistency scan reported zero legacy blobs. * Nothing enforces removal at code level — the admin is trusted, given a clear signal, and warned. @@ -226,11 +226,11 @@ disk pre-date the v1-code deployment. ### The rotation job -New `RecoverableJobHandler` tenant, `storage_rotate`. Mirrors -`storage_migration`'s shape: +New `RecoverableJobHandler` tenant, `backend_rotate`. Mirrors +`backend_migration`'s shape: * **Iterates `storage.blobs`** in hash-lex order. Cursor is the last-processed - hash (64 hex chars). Same cursor encoding as `storage_migration` and + hash (64 hex chars). Same cursor encoding as `backend_migration` and `blobs_consistency`. * **Per blob:** 1. Fetch `.blob` and dispatch via the standard read path. @@ -256,7 +256,7 @@ New `RecoverableJobHandler` tenant, `storage_rotate`. Mirrors * The v1 write is atomic at object-storage level (S3 replace, Local rename-into-place). A concurrent reader sees either state. * No readonly mode. This is a critical improvement over - `storage_migration`: rotation is per-blob idempotent, so we don't need + `backend_migration`: rotation is per-blob idempotent, so we don't need to freeze writes. * **Restart-survivable** — same boot-time sweep as every other recoverable handler. @@ -274,7 +274,7 @@ Preconditions: `OXICLOUD_STORAGE__ENCRYPTION_KEY` first, or wait for legacy blobs to accumulate — nothing to rotate right now."* -Clicking the button dispatches the `storage_rotate` job for that entry. The +Clicking the button dispatches the `backend_rotate` job for that entry. The job's progress rides on the same `X-Server-Status` header infrastructure the maintenance banner uses — but this time WITHOUT engaging read-only mode. Banner variant reads *"Rotating encryption key on `` — X% (Y / Z @@ -285,7 +285,7 @@ The entry card shows a **legacy-blob counter** sourced from the most recent next rotation)"*. Refresh-on-demand button next to it triggers a targeted `blobs_consistency` scan (already available via the admin surface). The *"Rotation complete — safe to remove the old key"* hint appears only when -N = 0 and the last `storage_rotate` run completed with zero findings. +N = 0 and the last `backend_rotate` run completed with zero findings. ### Removing the old pair @@ -307,7 +307,7 @@ Zero admin work required. On upgrade: key. * Existing legacy blobs stay readable via the magic-byte dispatch — the legacy read path is preserved verbatim. -* Admin can optionally trigger a `storage_rotate` run to consolidate every +* Admin can optionally trigger a `backend_rotate` run to consolidate every legacy blob into v1 format. Not required — legacy blobs migrate opportunistically via COW overwrites and stay readable indefinitely otherwise. @@ -347,7 +347,7 @@ The user-facing recipe (goes verbatim into `docs/guide/backend-storage.md`): 2. Add it AFTER `none`: OXICLOUD_STORAGE_local_main_ENCRYPTION_KEY=none:,aes-256-gcm: 3. Restart. New uploads are encrypted; existing plaintext blobs stay readable. -4. Run `storage_rotate` to encrypt existing blobs in place. +4. Run `backend_rotate` to encrypt existing blobs in place. 5. Remove `none:` from the list; restart. ``` @@ -357,7 +357,7 @@ The user-facing recipe (goes verbatim into `docs/guide/backend-storage.md`): 1. Add `none:` AFTER the current encryption key: OXICLOUD_STORAGE_local_main_ENCRYPTION_KEY=aes-256-gcm:,none: 2. Restart. New uploads are plaintext; existing encrypted blobs stay readable. -3. Run `storage_rotate` to decrypt existing blobs in place. +3. Run `backend_rotate` to decrypt existing blobs in place. 4. Remove the key pair, keep `none:` only (or drop `_ENCRYPTION_KEY` entirely); restart. ``` @@ -395,7 +395,7 @@ modules within source files"). never silent misread. Keeps the "collisions can't silently corrupt" claim in the plan honest. * **Rotation decision tree.** Unit tests on the per-blob `decide()` helper of - `storage_rotate_service.rs`. All six cases from *The rotation job* section + `backend_rotate_service.rs`. All six cases from *The rotation job* section as separate tests with clear names (`legacy_upgrades_to_v1`, `v1_encrypted_under_head_skips`, `v1_encrypted_under_older_pair_rewrites`, @@ -403,13 +403,13 @@ modules within source files"). `v1_plaintext_encrypts_when_head_is_cipher`, `v1_encrypted_decrypts_when_head_is_none`). * **Recoverable-job round-trip.** Integration test in - `storage_rotate_service::tests` using the existing recoverable-run harness: + `backend_rotate_service::tests` using the existing recoverable-run harness: seed N blobs (mix of legacy + v1-under-old-key), trigger rotation, assert every blob ends v1-with-head, `format_generation` (if we add it later) or the consistency-scan count reports zero legacy remaining, findings=0. * **Crash recovery.** Same harness: interrupt mid-run at cursor position K, restart, assert resume from K and eventual completion with correct final - state. Same discipline `storage_migration` already uses. + state. Same discipline `backend_migration` already uses. * **Concurrency safety.** Test that a `put_blob` call during a rotation targeting the same hash produces exactly one v1 blob at end-state (either the rotate's or the concurrent write's — both are head-format so the final @@ -496,19 +496,19 @@ the legacy-blob count. deployment reads existing legacy blobs and writes new v1 blobs at the same object-key. `blobs_consistency` reports a legacy-blob count in its run stats. -### Slice K3 — The `storage_rotate` recoverable job +### Slice K3 — The `backend_rotate` recoverable job **Scope.** New handler, admin-triggered, iterates blobs, per-blob decision tree (legacy → v1 upgrade, v1 with old key → v1 with head key, plaintext ↔ encrypted where applicable), records findings. -* New file `src/infrastructure/services/storage_rotate_service.rs`. -* Registered in `JobRegistry` as `storage_rotate`. Runs on the same +* New file `src/infrastructure/services/backend_rotate_service.rs`. +* Registered in `JobRegistry` as `backend_rotate`. Runs on the same recoverable-runs engine (crash recovery, cursor persistence, pause/resume). * Trigger endpoint: `POST /api/admin/storage/entries/{name}/rotate`. Requires admin. Refuses if no work would happen (all blobs already at - head format + head key). Refuses if a `storage_rotate` or - `storage_migration` run is already Active for any entry. + head format + head key). Refuses if a `backend_rotate` or + `backend_migration` run is already Active for any entry. * Per-blob decision tree per *The rotation job* section above. In-place atomic replace at the same `.blob` object key. * No readonly mode engaged. `X-Server-Status` header payload gains a @@ -566,7 +566,7 @@ changes. Reserved slots: v1 and v2 coexist in the same storage indefinitely — the magic-byte read dispatch handles arbitrary versions at position 5-6. Migration between -generations reuses `storage_rotate`'s pattern: rewrite each blob with the +generations reuses `backend_rotate`'s pattern: rewrite each blob with the new-generation writer, in-place at the same object key. ## Non-goals @@ -592,7 +592,7 @@ new-generation writer, in-place at the same object key. ## Open questions -* **Should we throttle the rotate job?** Same question `storage_migration` had. +* **Should we throttle the rotate job?** Same question `backend_migration` had. Answer: not in v1. If throughput bites, add a `_ROTATE_MAX_MB_PER_SEC` on the entry later. * **Should rotation be idempotent under repeat trigger?** Yes. Running it a diff --git a/docs/plan/storage-multi-entry.md b/docs/plan/storage-multi-entry.md index 584aab50..88bb9a6c 100644 --- a/docs/plan/storage-multi-entry.md +++ b/docs/plan/storage-multi-entry.md @@ -213,7 +213,7 @@ if permission.is_write() && self.migration_readonly.load(Ordering::Relaxed) { - **Reads are unaffected**. Users can still browse and download during migration. - **Boot-time clearing**: if boot detects `migration_readonly=true` AND no - in-flight `storage_migration` row (no `Running`/`Paused`) AND + in-flight `backend_migration` row (no `Running`/`Paused`) AND `active_backend_name` matches the entry the app booted onto → assume successful cutover completed on prior boot, clear the flag. Otherwise leave it set; admin knows they still need to finish something. @@ -230,7 +230,7 @@ if permission.is_write() && self.migration_readonly.load(Ordering::Relaxed) { strings — but the identity check still runs as a second-line defence against the encryption-in-place case) - Write admin_settings.storage.migration_readonly = true - - Trigger `storage_migration` recoverable job with + - Trigger `backend_migration` recoverable job with params = { source_name: "local_main", target_name: "s3_prod" } 3. Migration runs — target resolved fresh each batch by NAME lookup, so @@ -473,5 +473,5 @@ Per slice, plus these end-to-end scenarios in Hurl: `PgAclEngine::check_inner` is where write-permission short-circuits live; the new global read-only clause lands next to the per-drive one. - `docs/plan/job-registry.md` Part 2 — recoverable-run engine that - `storage_migration` runs on; `params` field, resume semantics, boot + `backend_migration` runs on; `params` field, resume semantics, boot sweep. diff --git a/frontend/src/lib/api/endpoints/admin.ts b/frontend/src/lib/api/endpoints/admin.ts index 231e64f3..3a9d8643 100644 --- a/frontend/src/lib/api/endpoints/admin.ts +++ b/frontend/src/lib/api/endpoints/admin.ts @@ -488,7 +488,7 @@ export interface StorageEncryptionPair { cipher: string; /** * SSH-style colon-hex 8-byte fingerprint of the key. Matches - * `storage_rotate`'s `head_key_fp` and the `oxicloud --fingerprint` + * `backend_rotate`'s `head_key_fp` and the `oxicloud --fingerprint` * CLI output — enables one-glance identification of which key is * which. `undefined` for `none:` pairs (no key material). */ @@ -578,15 +578,15 @@ export function migrationAction( } /** - * K4 (storage-key-rotation): trigger `storage_rotate` on a specific + * K4 (storage-key-rotation): trigger `backend_rotate` on a specific * storage entry. Normalises every blob on `` to that entry's * head-pair format: legacy → v1, plaintext ↔ encrypted, old-key → - * new-key. Fire-and-forget — poll `GET /api/admin/jobs/storage_rotate` + * new-key. Fire-and-forget — poll `GET /api/admin/jobs/backend_rotate` * for status. * * Backend: `POST /api/admin/storage/entries/{name}/rotate` - * (`admin_handler::trigger_storage_rotate`). Refuses (400) on unknown - * entry name or when a `storage_rotate` / `storage_migration` run is + * (`admin_handler::trigger_backend_rotate`). Refuses (400) on unknown + * entry name or when a `backend_rotate` / `backend_migration` run is * already in flight. */ export function rotateStorageEntry(name: string): Promise { diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index cfe95bc6..3845e86e 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -556,7 +556,7 @@ export interface JobSummary { * whether the row is expandable (drawer with run history + * findings) and to gate the retention/purge action — replaces * the pre-K3 name-based allowlist that missed newly-added - * recoverable tenants (`storage_rotate` shipped first without a + * recoverable tenants (`backend_rotate` shipped first without a * row-expand until this flag was added). */ recoverable: boolean; diff --git a/frontend/src/lib/components/ReadOnlyBanner.svelte b/frontend/src/lib/components/ReadOnlyBanner.svelte index 847ec41d..6a5aa61f 100644 --- a/frontend/src/lib/components/ReadOnlyBanner.svelte +++ b/frontend/src/lib/components/ReadOnlyBanner.svelte @@ -11,11 +11,11 @@ * * Rendered inside `AppShell` above `{children}` when the * `x-server-status` header says the whole server is in read-only - * mode — typically during a `storage_migration` cutover. + * mode — typically during a `backend_migration` cutover. * * ## `variant="rotating"` — background key rotation * - * K4 storage-key-rotation. `storage_rotate` walks blobs in place; + * K4 storage-key-rotation. `backend_rotate` walks blobs in place; * writes/reads continue normally throughout. Copy makes it clear * this is a background maintenance banner, not a freeze — the app * is fully usable. diff --git a/frontend/src/lib/stores/serverStatus.svelte.ts b/frontend/src/lib/stores/serverStatus.svelte.ts index aca71703..2b70a814 100644 --- a/frontend/src/lib/stores/serverStatus.svelte.ts +++ b/frontend/src/lib/stores/serverStatus.svelte.ts @@ -29,9 +29,9 @@ export interface ProgressStatus { /** * JSON shape emitted in the `x-server-status` header. * - * * `migration` — present only during a `storage_migration` run; + * * `migration` — present only during a `backend_migration` run; * engages `readonly = true` (all writes are refused). - * * `rotation` — present only during a `storage_rotate` run (K4 + * * `rotation` — present only during a `backend_rotate` run (K4 * storage-key-rotation); `readonly` stays false, uploads and * reads continue normally throughout. * diff --git a/frontend/src/routes/admin/[[tab]]/+page.svelte b/frontend/src/routes/admin/[[tab]]/+page.svelte index 560120e1..1900597a 100644 --- a/frontend/src/routes/admin/[[tab]]/+page.svelte +++ b/frontend/src/routes/admin/[[tab]]/+page.svelte @@ -473,7 +473,7 @@ if ( !confirm( t( - 'admin.storage_rotate_confirm', + 'admin.backend_rotate_confirm', { name }, 'Start a background rotation on `{{name}}`? Every existing blob is rewritten under the entry’s head pair (v1 header + head key). All operations continue normally during rotation — no read-only mode. Progress shows in the top banner and on the Jobs tab.' ) @@ -484,9 +484,9 @@ await rotateStorageEntry(name); ui.notify( t( - 'admin.storage_rotate_triggered', + 'admin.backend_rotate_triggered', { name }, - 'Rotation started on `{{name}}` — watch it on the Jobs tab (`storage_rotate`).' + 'Rotation started on `{{name}}` — watch it on the Jobs tab (`backend_rotate`).' ), 'success' ); @@ -2217,7 +2217,7 @@ onclick={() => doStorageConsistency(entry.name)} > - {t('admin.storage_backend_audit', 'Storage consistency')} + {t('admin.storage_backend_audit', 'Backend consistency')} {#if !entry.is_active && !migrationInFlight} {:else} {/if} diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index 275eba00..bb8dedba 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -1121,7 +1121,7 @@ "storage_size": "Stored", "storage_tab": "Storage", "storage_test": "Test", - "storage_backend_audit": "Storage consistency", + "storage_backend_audit": "Backend consistency", "storage_backend_audit_triggered": "backend_consistency triggered for `{{name}}` — watch it on the Jobs tab.", "time_day_ago": "{{n}} d ago", "time_hour_ago": "{{n}} h ago", diff --git a/frontend/static/locales/fr.json b/frontend/static/locales/fr.json index e18bcdb9..ccd8cae3 100644 --- a/frontend/static/locales/fr.json +++ b/frontend/static/locales/fr.json @@ -779,7 +779,7 @@ "storage_path_style": "Forcer le style de chemin", "storage_path_style_hint": "Requis pour MinIO et certains services compatibles S3", "storage_test": "Test", - "storage_backend_audit": "Cohérence du stockage", + "storage_backend_audit": "Cohérence du backend", "storage_backend_audit_triggered": "backend_consistency lancé pour `{{name}}` — suivez son avancement dans l’onglet Tâches.", "storage_test_connection": "Tester la connexion", "storage_test_success": "Connexion réussie", diff --git a/src/application/dtos/settings_dto.rs b/src/application/dtos/settings_dto.rs index dd5769f3..ba7328bf 100644 --- a/src/application/dtos/settings_dto.rs +++ b/src/application/dtos/settings_dto.rs @@ -259,7 +259,7 @@ pub struct StorageEntrySummaryDto { /// - See which pairs are configured + their SSH-style /// fingerprints without inspecting `.env`. /// - Cross-reference the head pair against the `head_key_fp` - /// from the last `storage_rotate` completion — if they + /// from the last `backend_rotate` completion — if they /// match AND `failed = 0`, every on-disk blob is under the /// head, and non-head pairs are safe to remove. #[serde(default)] diff --git a/src/application/ports/blob_storage_ports.rs b/src/application/ports/blob_storage_ports.rs index 724a5de3..262a1321 100644 --- a/src/application/ports/blob_storage_ports.rs +++ b/src/application/ports/blob_storage_ports.rs @@ -136,12 +136,12 @@ pub trait BlobStorageBackend: Send + Sync + 'static { /// that need the on-disk BYTES to change even when the CONTENT /// hash doesn't: /// - /// * `storage_rotate` — rewrites every blob under the head pair's + /// * `backend_rotate` — rewrites every blob under the head pair's /// format (legacy → v1 header, old key → new key, plaintext ↔ /// encrypted). If the target's `put_blob_from_bytes` silently /// skipped, rotation would report success while leaving the old /// format on disk. - /// * `storage_migration` — same story when a target already has a + /// * `backend_migration` — same story when a target already has a /// blob at that hash from an earlier state (Ed hit this on /// 2026-08-01 in the S3 → local migration test). /// diff --git a/src/common/config.rs b/src/common/config.rs index f6bb9004..77984a0a 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -510,7 +510,7 @@ impl KeyPair { /// SSH-style colon-hex fingerprint of the key material — 8 bytes /// of SHA-256 truncation rendered as `xx:yy:zz:...`. Same /// truncation as the v1 header's `` field and the - /// `head_key_fp` reported by `storage_rotate` on completion, so + /// `head_key_fp` reported by `backend_rotate` on completion, so /// operators can cross-reference the boot log against a rotate /// report or the CLI's `oxicloud --fingerprint ` /// output without any format conversion. @@ -725,7 +725,7 @@ pub fn parse_encryption_pair_list(entry_name: &str, raw: &str) -> Result` CLI subcommand so /// admins can identify which key in their `.env` corresponds to the -/// `head_key_fp` a `storage_rotate` run reported on completion — +/// `head_key_fp` a `backend_rotate` run reported on completion — /// see `docs/plan/storage-key-rotation.md`. /// /// Errors on non-base64 input or on decoded length ≠ 32 bytes (the @@ -911,7 +911,7 @@ impl NamedStorageEntry { /// The whole pair list, or an empty slice when the entry is /// unencrypted. Used by K2's read path to walk pairs and by - /// `storage_rotate` to enumerate legacy pairs. Callers that only + /// `backend_rotate` to enumerate legacy pairs. Callers that only /// need the write pair should prefer [`Self::head_key_material`]. pub fn encryption_pairs(&self) -> &[KeyPair] { self.encryption.as_deref().unwrap_or(&[]) @@ -3931,7 +3931,7 @@ mod tests { // K3.7: display fp switched from 12-char raw hex to // SSH-style 8-byte colon-hex (16 hex + 7 colons = 23 chars) // so operators can cross-reference against the v1 header's - // `` field + `storage_rotate`'s `head_key_fp` + // `` field + `backend_rotate`'s `head_key_fp` // output + the `oxicloud --fingerprint` CLI. let pairs = parse_encryption_pair_list("t", &format!("aes-256-gcm:{K1_B64},none:")).unwrap(); diff --git a/src/common/di.rs b/src/common/di.rs index 6197fd94..3673d977 100644 --- a/src/common/di.rs +++ b/src/common/di.rs @@ -2245,7 +2245,7 @@ impl AppServiceFactory { dyn crate::infrastructure::scheduler::JobStoreProvider, > = app_state.core.job_store_provider.clone(); let _ = Arc::new( - crate::infrastructure::services::storage_migration_service::StorageMigrationService::new( + crate::infrastructure::services::backend_migration_service::BackendMigrationService::new( app_state .maintenance_pool .clone() @@ -2262,13 +2262,13 @@ impl AppServiceFactory { .register_recoverable_job(&app_state.core.job_registry, &job_store_provider_dyn) .await; - // K3: `storage_rotate` recoverable-job tenant. Same - // pattern as `storage_migration` but without the + // K3: `backend_rotate` recoverable-job tenant. Same + // pattern as `backend_migration` but without the // cutover/readonly plumbing — rotation writes in place on // whichever entry the trigger endpoint names. Target name // comes from `params.target_name` per run. let _ = Arc::new( - crate::infrastructure::services::storage_rotate_service::StorageRotateService::new( + crate::infrastructure::services::backend_rotate_service::BackendRotateService::new( app_state .maintenance_pool .clone() @@ -2477,7 +2477,7 @@ impl AppServiceFactory { // Migration-readonly boot-clear rule. See // `docs/plan/storage-multi-entry.md` §"Read-only mode". // - // If the flag was set true at boot AND no storage_migration + // If the flag was set true at boot AND no backend_migration // run is currently non-terminal AND active_backend_name // matches the entry the app actually booted onto — that means // the cutover completed on a prior boot (the run reached @@ -2495,11 +2495,11 @@ impl AppServiceFactory { .migration_readonly .load(std::sync::atomic::Ordering::Relaxed) { - use crate::infrastructure::services::storage_migration_service::STORAGE_MIGRATION_JOB_NAME; + use crate::infrastructure::services::backend_migration_service::BACKEND_MIGRATION_JOB_NAME; let has_in_flight = match app_state .core .job_store_provider - .list_runs(STORAGE_MIGRATION_JOB_NAME, 5) + .list_runs(BACKEND_MIGRATION_JOB_NAME, 5) .await { Ok(runs) => runs.iter().any(|r| { @@ -2515,7 +2515,7 @@ impl AppServiceFactory { target: "oxicloud::scheduler", event = "storage.migration_readonly.clear_check_failed", error = %e, - "failed to list storage_migration runs during readonly-clear check; \ + "failed to list backend_migration runs during readonly-clear check; \ leaving migration_readonly flag as-is" ); // Play it safe: assume in-flight to avoid clearing prematurely. @@ -2816,7 +2816,7 @@ pub struct AppState { /// polling. See `MigrationProgress` for the field shape. pub migration_progress: Arc>>, /// Live progress snapshot for the storage-rotate handler - /// (`storage_rotate` — K3 of the storage-key-rotation plan). + /// (`backend_rotate` — K3 of the storage-key-rotation plan). /// `Some(_)` while a rotation is running; `None` otherwise. /// Held separately from `migration_progress` so the /// server-status header can broadcast the two states diff --git a/src/infrastructure/scheduler/recoverable.rs b/src/infrastructure/scheduler/recoverable.rs index f47010bb..47086284 100644 --- a/src/infrastructure/scheduler/recoverable.rs +++ b/src/infrastructure/scheduler/recoverable.rs @@ -124,7 +124,7 @@ pub enum RunOutcome { /// `extra_stats` is merged into the run row's `stats` JSONB /// alongside the engine-owned `scanned_count` + `finding_count` /// / `severity_counts`. Handlers use it to surface per-run - /// summary counters (e.g. `storage_rotate` reports + /// summary counters (e.g. `backend_rotate` reports /// `{"rewritten": N, "skipped": M, "failed": K}`) — the outcome /// message in `JobOutcome.extra` and every downstream reader /// of `RunSummary.stats` see the merged fields. @@ -304,7 +304,7 @@ pub trait JobStore: Send + Sync { /// Set an arbitrary string field on `params` (JSONB). Used by /// handlers on a Fresh run to persist per-run configuration that - /// must survive a mid-run restart — e.g. `storage_migration` + /// must survive a mid-run restart — e.g. `backend_migration` /// stamping `params.target_name` at run start so a resume can /// pick up the same target without the admin re-specifying it. /// diff --git a/src/infrastructure/scheduler/types.rs b/src/infrastructure/scheduler/types.rs index a2eac326..0cb22375 100644 --- a/src/infrastructure/scheduler/types.rs +++ b/src/infrastructure/scheduler/types.rs @@ -37,7 +37,7 @@ use serde::{Deserialize, Serialize}; /// /// Semantics of `storage`, per job (added for the multi-entry storage /// design — see `docs/plan/storage-multi-entry.md`): -/// - `storage_migration` — the NAME of the target storage entry to +/// - `backend_migration` — the NAME of the target storage entry to /// copy blobs INTO. Required on a Fresh run (handler refuses /// without it); ignored on a Resumed run (target read from the /// persisted `params.target_name`). diff --git a/src/infrastructure/services/azure_blob_backend.rs b/src/infrastructure/services/azure_blob_backend.rs index 75a2f568..622af000 100644 --- a/src/infrastructure/services/azure_blob_backend.rs +++ b/src/infrastructure/services/azure_blob_backend.rs @@ -173,8 +173,8 @@ impl BlobStorageBackend for AzureBlobBackend { }) } - /// Atomic overwrite path used by `storage_rotate` and - /// `storage_migration` when re-writing an already-present blob + /// Atomic overwrite path used by `backend_rotate` and + /// `backend_migration` when re-writing an already-present blob /// under a new head key/format. Trait default delegates to /// `put_blob_from_bytes` which `get_properties`-probes and /// silently skips — exactly wrong for the rotate/migrate use diff --git a/src/infrastructure/services/storage_migration_service.rs b/src/infrastructure/services/backend_migration_service.rs similarity index 96% rename from src/infrastructure/services/storage_migration_service.rs rename to src/infrastructure/services/backend_migration_service.rs index a80be76e..387568aa 100644 --- a/src/infrastructure/services/storage_migration_service.rs +++ b/src/infrastructure/services/backend_migration_service.rs @@ -67,7 +67,7 @@ use crate::infrastructure::services::entry_backend::{ build_entry_backend_typed, persist_active_backend_name, persist_migration_readonly, }; -pub const STORAGE_MIGRATION_JOB_NAME: &str = "storage_migration"; +pub const BACKEND_MIGRATION_JOB_NAME: &str = "backend_migration"; /// The `params` JSONB key under which the run's target entry name is /// stashed at Fresh-open time via `JobStore::set_string_param`. @@ -93,7 +93,7 @@ pub const SOURCE_NAME_PARAM: &str = "source_name"; /// every 100 rows too. Match `blobs_consistency` for consistency. const BATCH_SIZE: i64 = 100; -pub struct StorageMigrationService { +pub struct BackendMigrationService { pool: Arc, /// Backend the running app is bound to at handler-construction /// time. Refers to the hot-swap wrapper when multi-entry is @@ -140,7 +140,7 @@ pub struct StorageMigrationService { Arc>>, } -impl StorageMigrationService { +impl BackendMigrationService { #[allow(clippy::too_many_arguments)] pub fn new( pool: Arc, @@ -183,9 +183,9 @@ impl StorageMigrationService { } #[async_trait] -impl RecoverableJobHandler for StorageMigrationService { +impl RecoverableJobHandler for BackendMigrationService { fn name(&self) -> &str { - STORAGE_MIGRATION_JOB_NAME + BACKEND_MIGRATION_JOB_NAME } /// Definitive count — one row per blob. `SELECT COUNT(*) FROM @@ -200,7 +200,7 @@ impl RecoverableJobHandler for StorageMigrationService { Err(e) => { tracing::debug!( target: "oxicloud::migration", - event = "storage_migration.count_total_failed", + event = "backend_migration.count_total_failed", error = %e, "count_total failed — run will not surface a progress bar" ); @@ -234,7 +234,7 @@ impl RecoverableJobHandler for StorageMigrationService { let Some(name) = args.storage.clone() else { return RunOutcome::Failed { message: - "storage_migration requires `target_name` on a fresh run — trigger via \ + "backend_migration requires `target_name` on a fresh run — trigger via \ POST /api/admin/storage/migration/start with `{\"target_name\": \"\"}`." .to_string(), }; @@ -305,7 +305,7 @@ impl RecoverableJobHandler for StorageMigrationService { .clone(); tracing::warn!( target: "oxicloud::migration", - event = "storage_migration.legacy_paused_row_source_defaulted", + event = "backend_migration.legacy_paused_row_source_defaulted", run_id = %store.run_id(), fallback_source = %fallback, "resumed run has no source_name in params (pre-K3.8 row) — defaulting \ @@ -330,11 +330,11 @@ impl RecoverableJobHandler for StorageMigrationService { if target_name == active_backend_name { tracing::warn!( target: "audit", - event = "storage_migration.refused_noop", + event = "backend_migration.refused_noop", run_id = %store.run_id(), target_name = %target_name, active = %active_backend_name, - "storage_migration refused: target equals the currently-active entry" + "backend_migration refused: target equals the currently-active entry" ); return RunOutcome::Failed { message: format!( @@ -397,12 +397,12 @@ impl RecoverableJobHandler for StorageMigrationService { }; tracing::warn!( target: "audit", - event = "storage_migration.refused_same_physical_storage", + event = "backend_migration.refused_same_physical_storage", run_id = %store.run_id(), target_name = %target_name, source_name = %active_backend_name, encryption_differs = key_differs, - "storage_migration refused: named target differs from source but physical storage matches" + "backend_migration refused: named target differs from source but physical storage matches" ); return RunOutcome::Failed { message: format!( @@ -484,14 +484,14 @@ impl RecoverableJobHandler for StorageMigrationService { let target_kind = target.backend_type(); tracing::info!( target: "audit", - event = "storage_migration.run_started", + event = "backend_migration.run_started", run_id = %store.run_id(), source_name = %active_backend_name, target_name = %target_name, source_kind = source_kind, target_kind = target_kind, resuming = !is_fresh, - "storage_migration starting {active_backend_name} ({source_kind}) → {target_name} ({target_kind})" + "backend_migration starting {active_backend_name} ({source_kind}) → {target_name} ({target_kind})" ); // Cursor = the last-visited blob hash, UTF-8-encoded. On resume @@ -527,13 +527,13 @@ impl RecoverableJobHandler for StorageMigrationService { Ok(RunStatus::CancelRequested) => { tracing::info!( target: "oxicloud::migration", - event = "storage_migration.cancelled", + event = "backend_migration.cancelled", run_id = %store.run_id(), copied = copied_count, skipped = skipped_count, failed = failed_count, source_missing = source_missing_count, - "storage_migration cancelled cooperatively, pausing" + "backend_migration cancelled cooperatively, pausing" ); return RunOutcome::Paused { cursor: cursor @@ -604,7 +604,7 @@ impl RecoverableJobHandler for StorageMigrationService { source_missing_count += 1; tracing::warn!( target: "oxicloud::migration", - event = "storage_migration.source_missing", + event = "backend_migration.source_missing", run_id = %store.run_id(), hash = %hash, source = source_kind, @@ -612,7 +612,7 @@ impl RecoverableJobHandler for StorageMigrationService { ); record_or_log( store, - STORAGE_MIGRATION_JOB_NAME, + BACKEND_MIGRATION_JOB_NAME, "source_missing", "data_loss", None, @@ -635,7 +635,7 @@ impl RecoverableJobHandler for StorageMigrationService { // it. tracing::warn!( target: "oxicloud::migration", - event = "storage_migration.source_probe_error", + event = "backend_migration.source_probe_error", run_id = %store.run_id(), hash = %hash, error = %e, @@ -670,7 +670,7 @@ impl RecoverableJobHandler for StorageMigrationService { skipped_count += 1; tracing::debug!( target: "oxicloud::migration", - event = "storage_migration.blob_skipped_head_match", + event = "backend_migration.blob_skipped_head_match", run_id = %store.run_id(), hash = %hash, head_format = %target.head_format(), @@ -687,7 +687,7 @@ impl RecoverableJobHandler for StorageMigrationService { failed_count += 1; tracing::warn!( target: "oxicloud::migration", - event = "storage_migration.blob_failed", + event = "backend_migration.blob_failed", run_id = %store.run_id(), hash = %hash, error = %e, @@ -698,7 +698,7 @@ impl RecoverableJobHandler for StorageMigrationService { // where the admin UI reads it. record_or_log( store, - STORAGE_MIGRATION_JOB_NAME, + BACKEND_MIGRATION_JOB_NAME, "migration_failed", "data_loss", None, @@ -760,7 +760,7 @@ impl RecoverableJobHandler for StorageMigrationService { } } -impl StorageMigrationService { +impl BackendMigrationService { /// Terminal successful path — reached from both Completed sites /// in the batch loop (empty-first-batch and short-batch). /// @@ -831,7 +831,7 @@ impl StorageMigrationService { if !readonly_persisted { tracing::warn!( target: "oxicloud::migration", - event = "storage_migration.readonly_clear_persist_failed", + event = "backend_migration.readonly_clear_persist_failed", run_id = %store.run_id(), "cleared migration_readonly in memory (writes allowed against source) but the \ DB persist failed. Boot-clear rule will fix on next restart." @@ -839,7 +839,7 @@ impl StorageMigrationService { } tracing::info!( target: "audit", - event = "storage_migration.aborted", + event = "backend_migration.aborted", reason = "blobs_failed", run_id = %store.run_id(), active_backend_name = previous_active, @@ -848,7 +848,7 @@ impl StorageMigrationService { skipped = skipped, failed = failed, source_missing = source_missing, - "🛑 storage_migration aborted — {failed} blob(s) failed, active backend left at \ + "🛑 backend_migration aborted — {failed} blob(s) failed, active backend left at \ `{previous_active}`, readonly cleared. Inspect findings and retry, or accept \ the partial migration via `oxicloud --select-storage {target_name}`." ); @@ -907,7 +907,7 @@ impl StorageMigrationService { if !readonly_persisted { tracing::warn!( target: "oxicloud::migration", - event = "storage_migration.readonly_clear_persist_failed", + event = "backend_migration.readonly_clear_persist_failed", run_id = %store.run_id(), "cleared migration_readonly in memory (writes allowed) but the DB persist \ failed. If the server crashes before next boot, boot will re-seed the flag \ @@ -917,7 +917,7 @@ impl StorageMigrationService { tracing::info!( target: "audit", - event = "storage_migration.completed", + event = "backend_migration.completed", run_id = %store.run_id(), active_backend_name = target_name, previous_active = previous_active, @@ -925,11 +925,11 @@ impl StorageMigrationService { skipped = skipped, failed = failed, source_missing = source_missing, - "✅ storage_migration completed — hot-swapped runtime backend to `{target_name}`, \ + "✅ backend_migration completed — hot-swapped runtime backend to `{target_name}`, \ writes resumed. No restart required." ); // Per-run summary counters merged into `stats` for the admin - // UI drawer. Same shape as `storage_rotate`'s extras + one + // UI drawer. Same shape as `backend_rotate`'s extras + one // extra `source_missing` counter unique to migration. RunOutcome::completed_with(serde_json::json!({ "copied": copied, @@ -1022,7 +1022,7 @@ async fn collect_stream_bytes( let mut stream = std::pin::pin!(stream); while let Some(chunk) = stream.next().await { let bytes = chunk.map_err(|e| { - DomainError::internal_error("StorageMigration", format!("source stream read: {e}")) + DomainError::internal_error("BackendMigration", format!("source stream read: {e}")) })?; buf.extend_from_slice(&bytes); } diff --git a/src/infrastructure/services/storage_rotate_service.rs b/src/infrastructure/services/backend_rotate_service.rs similarity index 92% rename from src/infrastructure/services/storage_rotate_service.rs rename to src/infrastructure/services/backend_rotate_service.rs index 0edba7ef..e428cce2 100644 --- a/src/infrastructure/services/storage_rotate_service.rs +++ b/src/infrastructure/services/backend_rotate_service.rs @@ -15,13 +15,13 @@ //! //! ### No readonly, no cutover //! -//! `storage_rotate` is per-blob idempotent — repeat rewrites are +//! `backend_rotate` is per-blob idempotent — repeat rewrites are //! byte-safe (content-addressability holds; the wrapper always //! produces the head format). Concurrent user writes coexist: they //! land as head-format themselves, so when the walk reaches that //! hash the classifier reports "already at head format" and the //! decision tree collapses to `skip`. No app-wide read-only gate is -//! ever engaged — a critical improvement over `storage_migration`, +//! ever engaged — a critical improvement over `backend_migration`, //! whose target-different-from-source cutover forces one. //! //! ### Restart survival @@ -36,13 +36,13 @@ //! ### Design notes //! //! * **Cursor** — UTF-8 hex of the last-processed blob hash (64 -//! chars). Same encoding as `storage_migration` and +//! chars). Same encoding as `backend_migration` and //! `blobs_consistency`. //! * **Target lookup** — the entry NAME is stashed in `params` at //! Fresh-open time and re-read on Resume. The wrapper for that //! entry is rebuilt at the top of every run via //! `build_entry_backend_typed`; mid-run config changes are -//! ignored until the next run (mirrors `storage_migration`). +//! ignored until the next run (mirrors `backend_migration`). //! * **Per-blob failures don't fail the run** — each failure records //! a `rotation_failed` finding (severity `data_loss` — the bytes //! didn't get rewritten) and the walk continues. A run that @@ -68,20 +68,20 @@ use crate::infrastructure::scheduler::{ use crate::infrastructure::services::encrypted_blob_backend::BlobFormat; use crate::infrastructure::services::entry_backend::build_entry_backend_typed; -pub const STORAGE_ROTATE_JOB_NAME: &str = "storage_rotate"; +pub const BACKEND_ROTATE_JOB_NAME: &str = "backend_rotate"; /// The `params` JSONB key under which the run's target entry name is /// stashed at Fresh-open time via `JobStore::set_string_param`. -/// Kept identical to `storage_migration`'s TARGET_NAME_PARAM so +/// Kept identical to `backend_migration`'s TARGET_NAME_PARAM so /// operators grepping run rows see the same convention across both /// storage-touching tenants. pub const TARGET_NAME_PARAM: &str = "target_name"; -/// Rows per batch. Matches `storage_migration` / `blobs_consistency` +/// Rows per batch. Matches `backend_migration` / `blobs_consistency` /// so the checkpoint + cancel-poll cadence is uniform across tenants. const BATCH_SIZE: i64 = 100; -pub struct StorageRotateService { +pub struct BackendRotateService { pool: Arc, /// Immutable per-deploy snapshot; used to look up the target /// entry by name at run start. Matches `AppConfig.storage_entries`. @@ -99,7 +99,7 @@ pub struct StorageRotateService { rotation_progress: Arc>>, } -impl StorageRotateService { +impl BackendRotateService { pub fn new( pool: Arc, storage_entries: Vec, @@ -115,7 +115,7 @@ impl StorageRotateService { } /// Chainable self-registration — mirrors the `*_consistency` - /// tenants and `storage_migration`. On-demand only (no periodic + /// tenants and `backend_migration`. On-demand only (no periodic /// tick). pub async fn register_recoverable_job( self: Arc, @@ -130,13 +130,13 @@ impl StorageRotateService { } #[async_trait] -impl RecoverableJobHandler for StorageRotateService { +impl RecoverableJobHandler for BackendRotateService { fn name(&self) -> &str { - STORAGE_ROTATE_JOB_NAME + BACKEND_ROTATE_JOB_NAME } /// Definitive count — one row per blob. Same query as - /// `storage_migration::count_total`; the two walk the same rows. + /// `backend_migration::count_total`; the two walk the same rows. async fn count_total(&self) -> Option { let row: Result<(i64,), sqlx::Error> = sqlx::query_as("SELECT COUNT(*) FROM storage.blobs") .fetch_one(self.pool.as_ref()) @@ -146,7 +146,7 @@ impl RecoverableJobHandler for StorageRotateService { Err(e) => { tracing::debug!( target: "oxicloud::rotate", - event = "storage_rotate.count_total_failed", + event = "backend_rotate.count_total_failed", error = %e, "count_total failed — run will not surface a progress bar" ); @@ -161,12 +161,12 @@ impl RecoverableJobHandler for StorageRotateService { args: &JobRunArgs, resume_cursor: Option>, ) -> RunOutcome { - // Resolve target entry name — same shape as `storage_migration`. + // Resolve target entry name — same shape as `backend_migration`. let is_fresh = resume_cursor.is_none(); let target_name = if is_fresh { let Some(name) = args.storage.clone() else { return RunOutcome::Failed { - message: "storage_rotate requires `target_name` on a fresh run — trigger via \ + message: "backend_rotate requires `target_name` on a fresh run — trigger via \ POST /api/admin/storage/entries/{name}/rotate" .to_string(), }; @@ -230,7 +230,7 @@ impl RecoverableJobHandler for StorageRotateService { tracing::info!( target: "audit", - event = "storage_rotate.run_started", + event = "backend_rotate.run_started", run_id = %store.run_id(), target_name = %target_name, // `%` (Display) → SSH-style `encrypted-v1 key_fp=83:96:...` @@ -239,7 +239,7 @@ impl RecoverableJobHandler for StorageRotateService { // header bytes on disk. head_format = %head_format, resuming = !is_fresh, - "storage_rotate started on `{target_name}` (head_format = {head_format})" + "backend_rotate started on `{target_name}` (head_format = {head_format})" ); // Seed the progress snapshot. Total = count_total's estimate; @@ -280,12 +280,12 @@ impl RecoverableJobHandler for StorageRotateService { self.clear_progress(); tracing::info!( target: "oxicloud::rotate", - event = "storage_rotate.cancelled", + event = "backend_rotate.cancelled", run_id = %store.run_id(), rewritten = rewritten_count, skipped = skipped_count, failed = failed_count, - "storage_rotate cancelled cooperatively, pausing" + "backend_rotate cancelled cooperatively, pausing" ); return RunOutcome::Paused { cursor: cursor @@ -304,7 +304,7 @@ impl RecoverableJobHandler for StorageRotateService { } // Fetch the next batch. Same keyset pagination shape as - // `storage_migration` — `hash > $1` on the PK, index-only. + // `backend_migration` — `hash > $1` on the PK, index-only. let rows: Vec<(String,)> = match sqlx::query_as( r#" SELECT hash @@ -351,7 +351,7 @@ impl RecoverableJobHandler for StorageRotateService { failed_count += 1; tracing::warn!( target: "oxicloud::rotate", - event = "storage_rotate.read_failed", + event = "backend_rotate.read_failed", run_id = %store.run_id(), hash = %hash, error = %e, @@ -359,7 +359,7 @@ impl RecoverableJobHandler for StorageRotateService { ); record_or_log( store, - STORAGE_ROTATE_JOB_NAME, + BACKEND_ROTATE_JOB_NAME, "rotation_failed", "data_loss", None, @@ -402,7 +402,7 @@ impl RecoverableJobHandler for StorageRotateService { failed_count += 1; tracing::warn!( target: "oxicloud::rotate", - event = "storage_rotate.write_failed", + event = "backend_rotate.write_failed", run_id = %store.run_id(), hash = %hash, error = %e, @@ -410,7 +410,7 @@ impl RecoverableJobHandler for StorageRotateService { ); record_or_log( store, - STORAGE_ROTATE_JOB_NAME, + BACKEND_ROTATE_JOB_NAME, "rotation_failed", "data_loss", None, @@ -467,9 +467,9 @@ impl RecoverableJobHandler for StorageRotateService { } } -impl StorageRotateService { +impl BackendRotateService { /// Terminal successful path — clear the header snapshot and log a - /// final audit line. Unlike `storage_migration::finish_completed` + /// final audit line. Unlike `backend_migration::finish_completed` /// there's no cutover / hot-swap step: rotation writes in place /// on the entry that's already there. /// @@ -508,14 +508,14 @@ impl StorageRotateService { tracing::info!( target: "audit", - event = "storage_rotate.run_completed", + event = "backend_rotate.run_completed", run_id = %store.run_id(), target_name = %target_name, rewritten = rewritten, skipped = skipped, failed = failed, head_format = %head_display, - "storage_rotate completed on `{target_name}` — {rewritten} rewritten, {skipped} skipped, {failed} failed; head = {head_display}" + "backend_rotate completed on `{target_name}` — {rewritten} rewritten, {skipped} skipped, {failed} failed; head = {head_display}" ); // Surface the per-run summary counters as extras merged into diff --git a/src/infrastructure/services/blobs_consistency_service.rs b/src/infrastructure/services/blobs_consistency_service.rs index e5cedae7..5f0c3a0e 100644 --- a/src/infrastructure/services/blobs_consistency_service.rs +++ b/src/infrastructure/services/blobs_consistency_service.rs @@ -75,7 +75,7 @@ pub const BLOBS_CONSISTENCY_JOB_NAME: &str = "blobs_consistency"; /// `params` JSONB key under which the entry name being probed is /// stashed on a Fresh run (matches `TARGET_NAME_PARAM` on -/// `storage_migration`). Resumed runs re-read it so a paused audit +/// `backend_migration`). Resumed runs re-read it so a paused audit /// survives restart without the admin re-specifying the target. pub const PROBED_STORAGE_PARAM: &str = "probed_storage"; @@ -189,7 +189,7 @@ impl RecoverableJobHandler for BlobsConsistencyCheck { resume_cursor: Option>, ) -> RunOutcome { // Resolve the backend to probe. Two paths, mirroring the - // Fresh/Resumed split the storage_migration handler uses: + // Fresh/Resumed split the backend_migration handler uses: // // * Fresh + args.storage=Some — probe that named entry // instead of the live backend. Stamp probed_storage in diff --git a/src/infrastructure/services/encrypted_blob_backend.rs b/src/infrastructure/services/encrypted_blob_backend.rs index 2959cf6f..01b405be 100644 --- a/src/infrastructure/services/encrypted_blob_backend.rs +++ b/src/infrastructure/services/encrypted_blob_backend.rs @@ -142,7 +142,7 @@ pub struct EncryptedBlobBackend { /// * `read_dispatch` legacy-fallback path — iterates in order /// (oldest → newest) to try every real-cipher pair when a /// legacy blob's head-key decrypt fails. - /// * K3 `storage_rotate` — needs to walk pair indices. + /// * K3 `backend_rotate` — needs to walk pair indices. pairs: Vec, /// `` → per-pair cipher, for O(1) read dispatch on v1 /// blobs. Excludes any `none:` pair (nothing to build). Cloned @@ -218,7 +218,7 @@ impl EncryptedBlobBackend { key } - /// The format `storage_rotate` should normalise every blob TO — + /// The format `backend_rotate` should normalise every blob TO — /// derived from the wrapper's head pair. When /// `head_cipher.is_some()` we're writing encrypted-v1 with the /// head pair's `key_fp`; when it's `None` we're writing @@ -237,8 +237,8 @@ impl EncryptedBlobBackend { } } - /// Smart-skip probe used by `storage_migration` (and potentially - /// `storage_rotate` if it ever gains a fast-path). Reads the + /// Smart-skip probe used by `backend_migration` (and potentially + /// `backend_rotate` if it ever gains a fast-path). Reads the /// first [`HEADER_SIZE`] bytes of the on-disk blob and returns /// `true` iff: /// @@ -280,7 +280,7 @@ impl EncryptedBlobBackend { } /// Fetch, classify, and decrypt a blob in one round-trip. Used by - /// K3's `storage_rotate` per-blob step: it needs both the + /// K3's `backend_rotate` per-blob step: it needs both the /// plaintext (to re-encrypt under the head pair) AND the current /// on-disk format (to decide whether a rewrite is needed at all). /// @@ -317,7 +317,7 @@ impl EncryptedBlobBackend { } /// Classification of a raw blob's on-disk format. Exposed for K3's -/// `storage_rotate` decision tree; not used on the hot request path. +/// `backend_rotate` decision tree; not used on the hot request path. /// /// PartialEq is derived so `current == head_format` collapses the /// plan's six-case decision tree into a single equality check: @@ -541,7 +541,7 @@ fn read_dispatch( hash = %expected_hash, size = encrypted.len(), "🩹 legacy plaintext blob served via BLAKE3 rescue — no configured key \ - decrypted it, but content hash matched. Run storage_rotate to re-write \ + decrypted it, but content hash matched. Run backend_rotate to re-write \ under the current head." ); return Ok(Bytes::from(encrypted)); @@ -737,7 +737,7 @@ impl BlobStorageBackend for EncryptedBlobBackend { /// Frame the plaintext with the head pair's format (encrypted-v1 /// or plaintext-v1), then delegate the atomic replace to the - /// inner backend. Used by `storage_rotate` to actually change the + /// inner backend. Used by `backend_rotate` to actually change the /// on-disk bytes — `put_blob_from_bytes` would silently no-op on /// `LocalBlobBackend` when the object key already exists. fn put_blob_from_bytes_replace( @@ -1373,7 +1373,7 @@ mod tests { // ───────────────────────────────────────────────────────────── // K3 tests — BlobFormat classifier + head_format + read_and_classify. // - // These pin the format-inspection contract that `storage_rotate` + // These pin the format-inspection contract that `backend_rotate` // depends on. The rotate job's per-blob decision tree collapses // to `current != head_format ? rewrite : skip`, so any drift in // either helper would silently change rotation semantics. diff --git a/src/infrastructure/services/entry_backend.rs b/src/infrastructure/services/entry_backend.rs index 0832e2d5..d9f02afc 100644 --- a/src/infrastructure/services/entry_backend.rs +++ b/src/infrastructure/services/entry_backend.rs @@ -201,7 +201,7 @@ pub async fn resolve_active_entry<'a>( /// /// Same construction path as `build_entry_backend`; the trait-object /// version delegates through this. Preferred for job handlers -/// (`storage_rotate`) that need typed access. The trait-object +/// (`backend_rotate`) that need typed access. The trait-object /// version stays for the DI hot-path where the caller only needs /// the generic `BlobStorageBackend` contract. pub fn build_entry_backend_typed( diff --git a/src/infrastructure/services/local_blob_backend.rs b/src/infrastructure/services/local_blob_backend.rs index 157277e4..20ed4eaf 100644 --- a/src/infrastructure/services/local_blob_backend.rs +++ b/src/infrastructure/services/local_blob_backend.rs @@ -470,7 +470,7 @@ impl BlobStorageBackend for LocalBlobBackend { /// then `rename(2)` over the target. `write_blob_bytes`'s /// `O_CREAT|O_EXCL` idempotent-skip (the right choice for uploads) /// silently no-ops when the target already exists — wrong for - /// callers like `storage_rotate` that need the bytes to change. + /// callers like `backend_rotate` that need the bytes to change. /// See the trait doc for the full picture. /// /// Tempfile lives beside the target under the same shard directory diff --git a/src/infrastructure/services/mod.rs b/src/infrastructure/services/mod.rs index cc6261bf..20ce00aa 100644 --- a/src/infrastructure/services/mod.rs +++ b/src/infrastructure/services/mod.rs @@ -1,6 +1,8 @@ pub mod audio_metadata_service; pub mod azure_blob_backend; pub mod backend_consistency_service; +pub mod backend_migration_service; +pub mod backend_rotate_service; pub mod blobs_consistency_service; pub mod cached_blob_backend; pub mod chunked_upload_service; @@ -45,8 +47,6 @@ pub mod s3_blob_backend; pub mod search_index; pub mod share_unlock_cookie; pub mod smtp_email_sender; -pub mod storage_migration_service; -pub mod storage_rotate_service; pub mod swappable_blob_backend; pub mod thumbnail_service; #[cfg(test)] diff --git a/src/infrastructure/services/s3_blob_backend.rs b/src/infrastructure/services/s3_blob_backend.rs index 47e40449..c58ef2ce 100644 --- a/src/infrastructure/services/s3_blob_backend.rs +++ b/src/infrastructure/services/s3_blob_backend.rs @@ -237,8 +237,8 @@ impl BlobStorageBackend for S3BlobBackend { }) } - /// Atomic overwrite path used by `storage_rotate` and - /// `storage_migration` when re-writing an already-present blob + /// Atomic overwrite path used by `backend_rotate` and + /// `backend_migration` when re-writing an already-present blob /// under a new head key/format. Trait default delegates to /// `put_blob_from_bytes` which HEAD-probes and silently skips — /// exactly wrong for the rotate/migrate use case (the whole diff --git a/src/interfaces/api/handlers/admin_handler.rs b/src/interfaces/api/handlers/admin_handler.rs index 8967c6ca..638880fe 100644 --- a/src/interfaces/api/handlers/admin_handler.rs +++ b/src/interfaces/api/handlers/admin_handler.rs @@ -75,9 +75,9 @@ pub fn admin_routes() -> Router> { .route("/settings/storage", put(save_storage_settings)) .route("/settings/storage/test", post(test_storage_connection)) // Storage migration — thin shims over the recoverable-run - // engine (job_name = "storage_migration"). Retained under + // engine (job_name = "backend_migration"). Retained under // /storage/migration/* until the admin UI is rewired to - // /api/admin/jobs/storage_migration/*; both paths route to + // /api/admin/jobs/backend_migration/*; both paths route to // the same underlying JobRegistry dispatch. The old /complete // endpoint is retired — a finished run is a Completed row, // there's nothing to acknowledge. @@ -91,7 +91,7 @@ pub fn admin_routes() -> Router> { // new-key). No readonly mode; safe under normal traffic. .route( "/storage/entries/{name}/rotate", - post(trigger_storage_rotate), + post(trigger_backend_rotate), ) // NOTE: /storage/migration/verify retired in slice 7 (see the // comment near where `verify_migration` used to live). Use @@ -383,7 +383,7 @@ async fn test_storage_connection( /// GET /api/admin/storage/migration — current migration progress. /// /// Shim over the recoverable-run engine: reads the latest -/// `storage_migration` run from `jobs.recoverable_runs` (via the +/// `backend_migration` run from `jobs.recoverable_runs` (via the /// `JobStoreProvider`) and projects it into the legacy /// `MigrationStateDto` shape the admin storage tab expects. When no /// run has ever been triggered the response is an empty "idle" DTO — @@ -403,11 +403,11 @@ async fn test_storage_connection( pub async fn get_migration_status( State(state): State>, ) -> Result { - use crate::infrastructure::services::storage_migration_service::STORAGE_MIGRATION_JOB_NAME; + use crate::infrastructure::services::backend_migration_service::BACKEND_MIGRATION_JOB_NAME; let provider = state.core.job_store_provider.clone(); let latest = provider - .list_runs(STORAGE_MIGRATION_JOB_NAME, 1) + .list_runs(BACKEND_MIGRATION_JOB_NAME, 1) .await .map_err(AppError::from)? .into_iter() @@ -440,7 +440,7 @@ pub async fn get_migration_status( /// POST /api/admin/storage/migration/start — begin background migration. /// -/// Shim that forwards to `JobRegistry::trigger("storage_migration", +/// Shim that forwards to `JobRegistry::trigger("backend_migration", /// ...)`. `run_or_resume` (the RecoverableAdapter's inner dispatch) /// resumes a Paused run or starts a fresh one — one endpoint covers /// both. Exclusivity is enforced at the DB layer (the partial unique @@ -500,7 +500,7 @@ pub async fn start_migration( dto.target_name ))); } - trigger_storage_migration(state, Some(dto.target_name)).await + trigger_backend_migration(state, Some(dto.target_name)).await } /// POST /api/admin/storage/migration/pause — pause a running migration. @@ -524,18 +524,18 @@ pub async fn start_migration( pub async fn pause_migration( State(state): State>, ) -> Result { - use crate::infrastructure::services::storage_migration_service::STORAGE_MIGRATION_JOB_NAME; + use crate::infrastructure::services::backend_migration_service::BACKEND_MIGRATION_JOB_NAME; tracing::info!( target: "audit", - event = "storage_migration.pause_requested", - "👮🏻‍♂️ Admin requested storage_migration pause" + event = "backend_migration.pause_requested", + "👮🏻‍♂️ Admin requested backend_migration pause" ); let flipped = state .core .job_store_provider - .request_cancel(STORAGE_MIGRATION_JOB_NAME) + .request_cancel(BACKEND_MIGRATION_JOB_NAME) .await .map_err(AppError::from)?; @@ -576,7 +576,7 @@ pub async fn resume_migration( // it from `params.target_name` stamped on the original Fresh // open. Refuses gracefully via RunOutcome::Failed if there is // no Paused row to resume. - trigger_storage_migration(state, None).await + trigger_backend_migration(state, None).await } // verify_migration endpoint retired (slice 7 of @@ -596,18 +596,18 @@ pub async fn resume_migration( /// desync `current_run_start` from the actually-running task). The /// admin UI polls `GET /storage/migration` for progress; the trigger /// itself is fire-and-forget. -async fn trigger_storage_migration( +async fn trigger_backend_migration( state: Arc, target_name: Option, ) -> Result { use crate::infrastructure::scheduler::JobRunArgs; - use crate::infrastructure::services::storage_migration_service::STORAGE_MIGRATION_JOB_NAME; + use crate::infrastructure::services::backend_migration_service::BACKEND_MIGRATION_JOB_NAME; tracing::info!( target: "audit", - event = "storage_migration.trigger_requested", + event = "backend_migration.trigger_requested", target_name = target_name.as_deref().unwrap_or(""), - "👮🏻‍♂️ Admin triggered storage_migration" + "👮🏻‍♂️ Admin triggered backend_migration" ); let registry = state.core.job_registry.clone(); @@ -616,7 +616,7 @@ async fn trigger_storage_migration( ..JobRunArgs::default() }; tokio::spawn(async move { - registry.trigger(STORAGE_MIGRATION_JOB_NAME, &args).await; + registry.trigger(BACKEND_MIGRATION_JOB_NAME, &args).await; }); Ok(( @@ -630,7 +630,7 @@ async fn trigger_storage_migration( } /// POST /api/admin/storage/entries/{name}/rotate — trigger the -/// `storage_rotate` recoverable job on a specific entry. +/// `backend_rotate` recoverable job on a specific entry. /// /// Normalises every blob on `` to the entry's head-pair /// format: legacy → v1, plaintext ↔ encrypted, old-key → new-key. @@ -658,13 +658,13 @@ async fn trigger_storage_migration( security(("bearerAuth" = [])), tag = "admin" )] -pub async fn trigger_storage_rotate( +pub async fn trigger_backend_rotate( State(state): State>, axum::extract::Path(name): axum::extract::Path, ) -> Result { use crate::infrastructure::scheduler::JobRunArgs; - use crate::infrastructure::services::storage_migration_service::STORAGE_MIGRATION_JOB_NAME; - use crate::infrastructure::services::storage_rotate_service::STORAGE_ROTATE_JOB_NAME; + use crate::infrastructure::services::backend_migration_service::BACKEND_MIGRATION_JOB_NAME; + use crate::infrastructure::services::backend_rotate_service::BACKEND_ROTATE_JOB_NAME; // Synchronous entry-existence check — a bad name would fail the // run anyway, but returning 400 here spares the operator an @@ -699,7 +699,7 @@ pub async fn trigger_storage_rotate( .clone(); if name != active { return Err(AppError::bad_request(format!( - "storage_rotate refuses non-active entry `{name}` — the DB blob registry \ + "backend_rotate refuses non-active entry `{name}` — the DB blob registry \ describes the active entry (`{active}`), so walking it against a stale \ target produces spurious `rotation_failed` findings. Activate `{name}` \ first via `Migrate & activate`, then rotate." @@ -713,7 +713,7 @@ pub async fn trigger_storage_rotate( // hash. Cheap check — `list_runs` limit 1 with the status // filter is an index scan. let provider = state.core.job_store_provider.clone(); - for job_name in [STORAGE_ROTATE_JOB_NAME, STORAGE_MIGRATION_JOB_NAME] { + for job_name in [BACKEND_ROTATE_JOB_NAME, BACKEND_MIGRATION_JOB_NAME] { let in_flight = provider .list_runs(job_name, 5) .await @@ -729,7 +729,7 @@ pub async fn trigger_storage_rotate( }); if in_flight { return Err(AppError::bad_request(format!( - "cannot start storage_rotate on `{name}` — `{job_name}` is already Running / \ + "cannot start backend_rotate on `{name}` — `{job_name}` is already Running / \ Paused / CancelRequested. Wait for it to finish (or cancel via \ `POST /api/admin/jobs/{job_name}/cancel`)." ))); @@ -738,9 +738,9 @@ pub async fn trigger_storage_rotate( tracing::info!( target: "audit", - event = "storage_rotate.trigger_requested", + event = "backend_rotate.trigger_requested", target_name = %name, - "👮🏻‍♂️ Admin triggered storage_rotate on `{name}`" + "👮🏻‍♂️ Admin triggered backend_rotate on `{name}`" ); let registry = state.core.job_registry.clone(); @@ -749,13 +749,13 @@ pub async fn trigger_storage_rotate( ..JobRunArgs::default() }; tokio::spawn(async move { - registry.trigger(STORAGE_ROTATE_JOB_NAME, &args).await; + registry.trigger(BACKEND_ROTATE_JOB_NAME, &args).await; }); Ok(( StatusCode::ACCEPTED, Json(serde_json::json!({ - "message": format!("Rotation dispatched on `{name}` — poll GET /api/admin/jobs/{STORAGE_ROTATE_JOB_NAME} for progress"), + "message": format!("Rotation dispatched on `{name}` — poll GET /api/admin/jobs/{BACKEND_ROTATE_JOB_NAME} for progress"), "detached": true, })), ) @@ -2348,7 +2348,7 @@ pub struct TriggerJobQuery { pub deep: bool, /// Optional named storage entry to scope the run against — used by /// tenants that respect `JobRunArgs.storage` (currently - /// `storage_migration` for its target; `blobs_consistency` / + /// `backend_migration` for its target; `blobs_consistency` / /// `backend_consistency` will pick this up in slice 7 to probe a /// non-active entry). Ignored by tenants that don't declare a /// semantic for it. Unknown-name validation is per-tenant — the @@ -2405,7 +2405,7 @@ pub async fn trigger_job( storage: query.storage.clone(), }; - // Jobs that can run for hours (storage_migration, future + // Jobs that can run for hours (backend_migration, future // reextract_*) are detached: `tokio::spawn` the trigger so the // HTTP request returns immediately. Without this, browser HTTP // timeouts drop the request future mid-await → the SemaphorePermit @@ -2461,7 +2461,7 @@ pub async fn trigger_job( /// there's a second long-running tenant that justifies the plumbing. /// See the comment in `trigger_job` for why detach matters. fn is_detached_job(name: &str) -> bool { - matches!(name, "storage_migration") + matches!(name, "backend_migration") } /// `POST /api/admin/jobs/{name}/cancel` — cooperative cancel of the diff --git a/src/main.rs b/src/main.rs index dc71a35c..10a8c213 100644 --- a/src/main.rs +++ b/src/main.rs @@ -172,7 +172,7 @@ fn main() -> Result<(), Box> { // One-shot helper: compute the SSH-style colon-hex // fingerprint of a base64-encoded AES-256 key and // print to stdout. Same truncation used by the v1 - // header's `` field + the `storage_rotate` + // header's `` field + the `backend_rotate` // completion summary — so an admin can: // 1. Look at the `head_key_fp` reported by the // last rotate run. @@ -297,7 +297,7 @@ fn print_help() { println!(" oxicloud --fingerprint One-shot helper — print the SSH-style"); println!(" fingerprint of a base64 AES-256 key."); println!(" Same shape used by the v1 blob header"); - println!(" + `storage_rotate` completion summary."); + println!(" + `backend_rotate` completion summary."); println!(" Read stdin with `-` to keep keys out"); println!(" of shell history."); println!(); @@ -327,7 +327,7 @@ fn print_help() { println!(" --fingerprint "); println!(" Compute the SSH-style colon-hex fingerprint (16-hex, 8-byte"); println!(" truncation of sha256) of a base64-encoded AES-256 key. Matches the"); - println!(" `head_key_fp` field the `storage_rotate` job reports on completion,"); + println!(" `head_key_fp` field the `backend_rotate` job reports on completion,"); println!(" and the raw field embedded in every v1 blob header. Used"); println!(" to identify which key in `OXICLOUD_STORAGE__ENCRYPTION_KEY`"); println!(" corresponds to the current on-disk head — safe to drop any key"); diff --git a/tests/api/admin_jobs.hurl b/tests/api/admin_jobs.hurl index eeaa1b66..ee3c8ce3 100644 --- a/tests/api/admin_jobs.hurl +++ b/tests/api/admin_jobs.hurl @@ -96,8 +96,8 @@ jsonpath "$..interval_ms" count == 3 # wrapped by RecoverableAdapter so they appear here alongside the # periodics) + 1 coordinator (consistency_batch — a plain # JobHandler that dispatches every registered `*_consistency`) + -# 2 on-demand admin ops (storage_migration — the readonly-mode + -# cutover backend swap; storage_rotate — K3, in-place per-blob +# 2 on-demand admin ops (backend_migration — the readonly-mode + +# cutover backend swap; backend_rotate — K3, in-place per-blob # format normalisation, no readonly). # Bump when a new tenant registers. jsonpath "$..running" count == 12 @@ -105,8 +105,8 @@ jsonpath "$[*].name" contains "drives_consistency" jsonpath "$[*].name" contains "folders_consistency" jsonpath "$[*].name" contains "files_consistency" jsonpath "$[*].name" contains "consistency_batch" -jsonpath "$[*].name" contains "storage_migration" -jsonpath "$[*].name" contains "storage_rotate" +jsonpath "$[*].name" contains "backend_migration" +jsonpath "$[*].name" contains "backend_rotate" # ─────────────────────────────────────────────────────────────