Aug 11 - Update photo storage using r2
This commit is contained in:
@@ -227,6 +227,61 @@ Stripe per-plan subscription, fully implemented in `app/billing/` (`routes.py`,
|
||||
**MT-9 — iOS multi-tenant. 🔲 PENDING (server + client both unbuilt).**
|
||||
Planned server side: `GET /api/v1/discover?subdomain=acme` and `GET /api/v1/tenant` public endpoints — to be exempt from tenant middleware via `MULTI_TENANT_EXEMPT_PATHS`. **Neither endpoint exists in the code yet** — no `api_discovery` blueprint is registered. iOS side: also pending (web-first priority).
|
||||
|
||||
**MT-20 — R2 photo storage cutover. ✅ CODE DONE / 🔲 CUTOVER PENDING.**
|
||||
No schema change, no migration. The data-plane code was already fully on the
|
||||
storage seam (`storage.save()` / `media_url()` / `materialize_to_dir()` /
|
||||
`delete()` at every inspection and issue photo site) — MT served local purely
|
||||
because `STORAGE_BACKEND` had never been flipped, and it **could not** be:
|
||||
|
||||
- `boto3` was missing from `requirements.txt`, so `S3Backend.__init__`'s lazy
|
||||
import would have raised `ModuleNotFoundError` on the first upload after the
|
||||
flip — at request time, not at boot. Now pinned (`boto3>=1.34`, matching ST).
|
||||
- there was no cutover tooling, and ST's could not be reused (below).
|
||||
|
||||
**Why ST's sync script does not port.** The local backend deliberately does not
|
||||
prefix keys, so one shared `app/static/uploads/` holds every tenant's photos and
|
||||
a file on disk carries no ownership marker. ST's script walks the disk and
|
||||
uploads everything, which on MT writes objects with no tenant prefix — keys
|
||||
`S3Backend._object_key()` will never read. Ownership must instead be derived
|
||||
from each tenant DB's references, then written under `t<tenant_id>/`.
|
||||
|
||||
| Layer | Value |
|
||||
|---|---|
|
||||
| DB | `uploads/issue_photos/abc.jpg` |
|
||||
| Disk (local backend) | `app/static/uploads/issue_photos/abc.jpg` |
|
||||
| R2 object (s3 backend) | `t3/uploads/issue_photos/abc.jpg` |
|
||||
|
||||
Delivered:
|
||||
- `scripts/audit_photos.py` — read-only. Per tenant, collects every key its DB
|
||||
references (5 sources: `issues.photo_path`, `.mobile_photo_paths[]`,
|
||||
`.result_photos[]`, `inspections.form_data`, `inspection_results.photo_path`)
|
||||
and reconciles against disk. Emits the baseline count that must still resolve
|
||||
after cutover, plus orphans and any key claimed by more than one tenant.
|
||||
- `scripts/migrate_photos_to_r2.py` — copy-only, idempotent, resumable,
|
||||
MD5+size verified, tenant-prefixed. Exit 0 only on full verification.
|
||||
Orphans are **not** uploaded: no prefix could legitimately claim them.
|
||||
- `tests/test_storage_backend.py` — pins the prefix arithmetic, the bare key
|
||||
returned to the DB, the refusal to write unprefixed with no tenant bound, and
|
||||
the legacy-unprefixed read/delete fallback.
|
||||
|
||||
Both scripts read the control DB then each tenant DB via raw SQL (no Flask app
|
||||
context — the ORM's default bind is the wrong database for every tenant), and
|
||||
the sync imports `collect_referenced` from the audit script so the two key sets
|
||||
can never diverge. Neither writes to any database.
|
||||
|
||||
`STORAGE_BACKEND` is process-wide, so cutover is all-tenants-at-once; isolation
|
||||
comes from the prefix, not from separate backends. Per-tenant backend selection
|
||||
would require the resolver to carry a storage selector and `get_backend()` to
|
||||
cache per tenant rather than per app — do not half-build it.
|
||||
|
||||
Rollback is one env var (`STORAGE_BACKEND=local` + restart); the sync never
|
||||
deletes local files.
|
||||
|
||||
**Open, deliberately not done in MT-20:** `routes/tenant_settings.py::_save_logo`
|
||||
still writes tenant logos directly to `static/uploads/logos/` with `os.path.join`,
|
||||
bypassing the seam. After cutover it is the only remaining local-disk writer, so
|
||||
logos would sit outside whatever backs up R2.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tenant-zero (LT Services) migration
|
||||
|
||||
Reference in New Issue
Block a user