客戶證據檔案屬機敏資料,部分客戶不接受上傳到雲端託管。本案把「檔案儲存」拉出成客戶端 evidence-agent,雲端依 per-tenant 設定自動分流到各客戶 agent,binary 不存我們雲端磁碟。雲端業務碼幾乎不動。
jedi-file-upload 包成獨立 Docker 服務(自帶本地 Postgres、存 Local/MinIO);雲端新增 RemoteAgentAdapter(實作既有 IUploadFileProvider)依 tenant 呼叫 agent。業務碼幾乎不用動——現在就是 adapter 架構,且 per-tenant 路由已現成。
IUploadFileProvider,底下已有 Local / MinIO / GoogleDrive adapterRemoteAgentAdapter),不是搬整套STORAGE_CONFIG 存在 system_configs,該表繼承 TenantScopedMixinModel(RLS)→ 每客戶讀到自己那筆Factory,每 request 重讀 → 無 singleton 快取陷阱storage_type = remote_agent(value 放 agent base_url)RemoteAgentAdapter + RemoteAgentConfigDTOupload_files 加 tenant_id(參照表隔離)+ sha256(Phase 3 防竄改)關鍵問題是「檔案 metadata 真相來源放哪」。採 B,依據是最小化長期維護成本。
| 方案 | 重用 jedi-file-upload | metadata DB | 代價 |
|---|---|---|---|
| C | ✅ 完整 | agent 連回雲端 DB | 無重複,但破壞隔離:客戶端帶雲端 DB 帳密、吃網路延遲 |
| B採用 | ✅ 完整 | agent 本地小 Postgres | 重複幾欄不可變的 metadata(良性) |
| A | ❌ 自己寫薄服務 | 不需 DB | 無資料重複,但得 fork 檔案/轉檔邏輯 |
poetry update 就跟上。寧可重複一點不可變資料,也不要重複程式碼。灰=我們雲端託管;紫=客戶自有伺服器(binary 只存這)。雲端業務碼與介面不動,新增的只有 RemoteAgentAdapter。
路由完全沿用現有 RLS——RemoteAgentAdapter 裡沒有一行 if tenant。
@transaction 開 session_scope → 注入 RLS:app.user_id / allowed_tenant_paths_load_config 讀 STORAGE_CONFIG → RLS 自動回本 tenant 那筆(無 tenant filter,靠 RLS)storage_type=remote_agent → build RemoteAgentAdapter(base_url)POST /blob 把 binary 轉發給對應 agentGET /blob/<uid> 取回串流GET /blob/<uid>/pdf同一張 upload_files schema,部署在兩個 DB;uid 是跨邊界的鑰匙。範例:稽核報告.docx 落客戶 B 的 MinIO。
| 欄位 | 雲端 upload_files(參照) | 客戶 B agent upload_files(真檔) |
|---|---|---|
id | 5012 ← job_evidences.file_id | 88(agent 本地序號,雲端看不到) |
uid | f3a9… | f3a9… ← 兩邊相同,跨邊界鑰匙 |
| file_name / size / mime | 重疊、上傳當下寫一次、之後不變 | |
storage_type | REMOTE_AGENT | MINIO(agent 真後端) |
save_file_name | f3a9…(拿去跟 agent 要檔的 key) | f3a9….docx(MinIO 物件名) |
path | 空 / 不用 | <bucket>/<object key> |
tenant_id | 102(新增欄) | 不需要(單租戶機器) |
| binary 本體 | 無 | 在客戶 B 的 MinIO |
uid,用時 RLS 解析出本 tenant 的 base_url 再去要——endpoint 一個租戶存一次,不是每檔重複。縮小版主專案:照 core/api/di_containers/config 骨架,但無 domain/infra/app 空殼層(邏輯都在 jedi-file-upload)。
jedi-file-upload(檔案邏輯,連帶 jedi-common / minio / SQLAlchemy / psycopg)jedi-common(session_scope / init_db / BaseModel / @transaction)Flask / flask-restful / Flask-Cors / dependency-injector| Method | Path | 用途 |
|---|---|---|
POST | /blob | 上傳 → 回 uid / size / mimetype |
GET | /blob/{uid} | 下載 binary 串流 |
GET | /blob/{uid}/pdf | 取預覽 PDF(必要時即時轉、快取) |
DELETE | /blob/{uid} | 刪除 |
GET | /health | 健康檢查 |
FileUploadService + env 組的 config DTO(不搬 ManagedFileUploadService、不要 system_configs 表)→ agent DB 只有 upload_files 一張表。雲端多租戶才用 system_config;agent 單租戶用 env。upload_files 存 uid→落點,掉了 binary 全孤兒。compose 用 host bind-mount ./pgdata(連 down -v 都刪不掉)+ ./data,整包好備份。安全網:save_file_name 內嵌 uid,DB 全毀可從檔名重建。docker save 出貨;客戶端碰不到 Nexus,compose 改用 image: 不 build。binary 在客戶機房、不在我們控制下,需偵測「上傳後被竄改」。
sha256。由雲端在上傳串流時就地計算(不信 agent 回報值)。在 upload_files 加 sha256 欄(兩邊都存)。| 時機 | 動作 |
|---|---|
| 上傳 | 雲端邊串流邊算 SHA-256 存錨點;agent 落地獨立算一份 |
| 下載 | 雲端重算收到 bytes → 比對錨點:一致放行 / 不一致擋下 + 記稽核事件 |
| 預覽 | agent 轉檔前先驗 source 完整性 |
| 項目 | Demo | 後續 |
|---|---|---|
| binary 落客戶端、依 tenant 分流 | ✅ | — |
| Local / MinIO(客戶自選) | ✅ agent env | — |
| 預覽轉檔在客戶端 | ✅ | — |
| 路由設定存放 | 沿用 per-tenant STORAGE_CONFIG | (可選)拉專表給 admin UI |
| 完整性 SHA-256 | Phase 3 | 排程稽核對帳 |
| 傳輸/落地加密 | TLS | 落地加密、雲端全程不見明文 |
| 雲端↔agent 認證 | ❌ 略過 | 每客戶一組 key |
| agent DB 備份 | bind-mount 持久化 | pg_dump 排程 / 整包備份 |
| 驗證項 | 結果 |
|---|---|
| Phase 0 R-1 spike | ✅ jedi-common @transaction 不需 Redis/JWT 即可裸跑(init_db 後) |
| agent 本機 dev | ✅ 上傳/下載(內容一致)/刪除/404 envelope |
| docker compose(Nexus secret) | ✅ image build + 兩容器起 + health |
| 預覽轉檔(LibreOffice in image) | ✅ .rtf → PDF(%PDF-) |
| MinIO 後端(連真實 MinIO) | ✅ round-trip 一致 + 刪除 |
| 持久化(down/up) | ✅ 重啟後資料仍在(bind-mount pgdata/data) |
| 正式模式錯誤 envelope | ✅ 修 PROPAGATE_EXCEPTIONS(flask-restful 非 debug 吞 500 的雷) |
| Phase 2 雲端 RemoteAgentAdapter | ⏳ 待 FR-039 branch |