visionA/docs/autoflow/04-architecture/adr/adr-004-storage-interface.md
jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類
共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git),
讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等
per-branch 筆記。

- 02-prd/        21 個檔(PRD、features、market-analysis 等)
- 03-design/     18 個檔(design-spec、wireframes、flows 等)
- 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等)
- 07-delivery/   3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup)

合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv,
但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
2026-05-04 16:55:55 +08:00

146 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-004模型儲存採用 S3-Compatible 介面
## 狀態
Accepted — 2026-04-21
## 背景 (Context)
visionA Cloud 需要儲存:
1. **使用者上傳的模型檔案**`.nef``.onnx` 等,幾 MB 幾百 MB
2. **模型 metadata**JSON幾 KB
3. **(未來)轉檔產物**kneron_model_converter 產生的 `.nef`
4. **未來推論紀錄、snapshots、截圖**
POCedge-ai-platform用本地檔案系統 + in-memory registry。local-tool 也一樣。這在單機環境 OK雲端不行
| 問題 | 本地檔案系統 | S3-compatible |
|------|------------|--------------|
| 多節點共用 | ❌ 各節點看不到彼此 | ✅ 共享 bucket |
| 持久性 | 單機磁碟壞 = 資料沒 | ✅ 11 個 9 的設計 |
| 容量彈性 | 單機磁碟有上限 | ✅ 幾乎無上限 |
| 成本效益 | 計算節點帶容量 = 貴 | 冷熱分層、便宜 |
| 直接給前端下載 | 要透過應用伺服器 | ✅ Presigned URL 直給 |
| Multipart upload | 自己實作 | ✅ 標準 |
同時,雛形階段不想立刻接真實 S3 / MinIO需架設、需 credentials、增加 setup 成本)。
## 決策 (Decision)
`internal/storage` 定義 **`Store` interface**,以 S3 的語義為基礎:
```go
package storage
import (
"context"
"io"
"time"
)
// Store 是模型 / 檔案儲存的抽象層。語義對齊 S3。
type Store interface {
// Put 上傳一個物件。size <0 表示未知大小streaming
Put(ctx context.Context, key string, r io.Reader, size int64, meta map[string]string) error
// Get 下載一個物件,回傳 readercaller 要負責 Close
Get(ctx context.Context, key string) (io.ReadCloser, *ObjectInfo, error)
// Stat 取得物件資訊但不下載 body。
Stat(ctx context.Context, key string) (*ObjectInfo, error)
// Delete 刪除一個物件。
Delete(ctx context.Context, key string) error
// List 列出 prefix 下的物件。
List(ctx context.Context, prefix string) ([]*ObjectInfo, error)
// PresignedGetURL 產生讓使用者直接下載的簽名 URL。
PresignedGetURL(ctx context.Context, key string, ttl time.Duration) (string, error)
// PresignedPutURL 產生讓使用者直接上傳的簽名 URL。
PresignedPutURL(ctx context.Context, key string, ttl time.Duration) (string, error)
}
type ObjectInfo struct {
Key string
Size int64
ContentType string
LastModified time.Time
ETag string
Metadata map[string]string
}
```
### 雛形提供兩個實作
1. **`LocalFSStore`**:把物件存在本地 `data/storage/`
- Key 對應檔案路徑(`models/user-123/yolov5.nef``data/storage/models/user-123/yolov5.nef`
- Metadata 存在同名 `.meta.json`
- `PresignedGetURL` / `PresignedPutURL``http://localhost:PORT/storage/...` 這種由 api-server 代理的 URL雛形的「假簽名」
- 啟動最快,開發者零成本
2. **`S3Store`**(可選,雛形後期加):用 `aws-sdk-go-v2`
- 連真實 AWS S3、MinIO、Cloudflare R2、Backblaze B2 等
- `PresignedGetURL` / `PresignedPutURL` 是真的 S3 presigned URL
- 需要 `S3_ENDPOINT``S3_BUCKET``S3_REGION``S3_ACCESS_KEY``S3_SECRET_KEY` env
### 選擇由 config 決定
```yaml
storage:
backend: localfs # or "s3"
localfs:
root: ./data/storage
base_url: http://localhost:3001/storage # api-server 代理路徑
s3:
endpoint: "" # 空字串用 AWS 原生;填 MinIO / R2 等用自訂
bucket: visiona-models
region: us-east-1
access_key: ${S3_ACCESS_KEY}
secret_key: ${S3_SECRET_KEY}
```
## 考慮過的替代方案
| 方案 | 優點 | 缺點 | 排除原因 |
|------|------|------|---------|
| **直接用 AWS SDK 呼叫 S3** | 簡單 | 綁定 AWSMinIO / R2 要 workaround | 綁定單一雲 |
| **用 MinIO SDK** | 輕量 | MinIO SDK 寫的 code 去接 AWS S3 要微調;綁定特定 SDK | 換雲成本 |
| **自己做儲存服務** | 完全掌控 | 重新發明輪子;費時;達不到 S3 等級的持久性 | 不值得 |
| **資料庫存 blob** | 交易一致性 | DB 不適合儲存大檔;昂貴 | 技術錯配 |
| **只用 LocalFS 不抽象** | 簡單 | 未來替換 = 重寫所有 model handler | 違反抽象原則 |
## 後果 (Consequences)
### 正面影響
- **雛形零 setup**`docker-compose up` 直接跑,不需 S3 / MinIO
- **切雲端零改動**:換 config 的 `backend: s3` 即可
- **支援多家雲**endpoint 可指向 AWS / Cloudflare R2 / Backblaze B2 / MinIO / 自建 Ceph 等
- **介面 S3-native**:未來加 multipart upload、server-side encryption 等 S3 進階特性無需變介面
### 負面影響(接受的取捨)
- **`PresignedGetURL` 在 LocalFS 是假的**:需 api-server 提供 `/storage/...` 代理端點。這段代理程式碼要寫,但不複雜
- **LocalFS 不支援多節點**雛形階段只有單節點可接受Phase 1 遷到 S3 後自動解決
- **Interface 比純 SDK 包裝更抽象**:略多一層呼叫,效能無感(本地 disk IO 遠大於 interface method dispatch
### 風險
- **LocalFS 的 metadata 靠 sidecar `.meta.json`**不是原子的雛形忽略Phase 1 改 S3 自然原子
- **PresignedPutURL 在 LocalFS 實作要處理 CSRF / auth**雛形可暫時接受裸上傳Phase 1 上 S3 後由 S3 presigned signature 保護
- **大檔上傳 OOM**`Put` 使用 `io.Reader` 已是 streamingLocalFS 實作用 `io.Copy` 避免 buffer 整個 body需要 code review 把關
## 合規性
- [x] Architect 確認
- [ ] 雛形測試:確保 `LocalFSStore` 通過 interface conformance tests
- [ ] Phase 1確保 `S3Store` 通過相同 tests
- [ ] 加入 `.gitignore``data/storage/` 不進 Git
## 相關文件
- Design Doc §3.3(外部依賴)
- TDD §8儲存層介面
- 相關 ADRADR-005雛形不接 DB / Auth