# 上傳檔案集中化 + R2 遷移：安全推進策略

> 範圍：micro 專案（共用 `PHP7/pubs` + 多站台 `hosts/stations/*`）
> 目標：所有上傳檔集中到 `uploaded_files` 表，內文改存 `data-file-id`，並為日後搬 Cloudflare R2 鋪路
> 狀態：設計定案，尚未實作。先以 jyg 為 canary。

---

## 1. 為什麼這樣做

- **集中管理**：每個上傳檔在 `uploaded_files` 一列（file_id / name / host / path / info…），程式一律用 file_id 引用。
- **內文存 `data-file-id` 而非網址**：檔案搬家、改名、換 CDN/R2，只需更新 `uploaded_files`，**全站歷史內文零破圖、不需改寫**。
- 框架其實已備好骨架：`uploaded_files` 表、`Uploader\DB\{Files,Lists}`、`Uploader\Files`（只是表為空、未啟用）。

## 2. 資料形態：兩類

| 類別 | 對象欄位 | 集中後形態 |
|---|---|---|
| A 結構化 | `xxx_bind.albums`、`xxx_category_bind.albums`（articles/bulletins/products/services/videos/tags）、`pages_i18n.banner` | 值 = `["<file_id>"]` |
| B 內文 HTML | `content` / `description`（CKEditor 圖） | `<img data-file-id="<file_id>">` |

## 3. 還原掛點：單一咽喉（共用 base）

所有 module 的 DB 層都繼承 `PHP7/pubs/apps/Models/Base/Datatable/DB/*`，在既有的 `decode_all()` 之後接 `Media::resolve_all()`：

| 路徑 | 位置 | 動作 |
|---|---|---|
| 單筆 info | `Base/Datatable/DB/Main.php`（`decode_all($row)` 之後）| 接 `Media::resolve_all()` |
| 列表 list | `Base/Datatable/DB/Lists.php` 的 `output()` | 補 `decode_all` + `Media::resolve_all()` |

`Media::resolve_all($row)`：content/description 掃 `data-file-id` → 批次查表回填 `src`；albums/banner 的 `[file_id]` → 換成含 src 的物件。**一筆/一頁一條批次 SQL，無 N+1。** 模板完全不動。媒體欄位採「白名單」設定，不寫死。

---

## 4. 不讓共用 PHP7 一改就影響全部站台：三層保險

### 保險 1 — resolver 格式驅動（向後相容，部署本身零行為改變）
`Media::resolve_all` 只動新格式：看到 `data-file-id` 才還原、albums 是「純 file_id」才查表；舊的 `<img src="upload/...">`、舊路徑字串**原樣放行**。
→ 未遷移的站台沒有新格式 → resolver 形同 no-op → **把 hook 加進共用 base 並部署，對舊站台安全**。

### 保險 2 — 平行框架版本 PHP71 + 站台層切換（取代原 conf.media flag）
新版框架不直接改 PHP7，而是整包複製成 **PHP71**（`cp -a`，因全用 `dirname` 故自洽可跑）。
切換點在**每站台**的 `apps/conf/conf.weber.php`（原 `include .../PHP7/pubs/index.php` 那行）：
```php
// 測試者帶密鑰 → PHP71；其餘照舊 PHP7
$__pubs = (($_SERVER['HTTP_DEVELOPER'] ?? '') === '<密鑰>') ? "/PHP71/pubs" : "/PHP7/pubs";
include_once(dirname(SYSTEM).$__pubs."/index.php");
```
站台三狀態：① `header?PHP71:PHP7`（測試者可試）→ ② `"/PHP71/pubs"`（整站永久切）→ ③ 全站完成、PHP7 退役。
- **完全不碰共用核心**，其它站台 conf.weber 仍指 PHP7 → 零影響。
- header 在正式環境**可偽造** → 務必綁密鑰/IP，勿只看存在與否。
- 出事把該站 conf.weber 改回 `/PHP7/` 即瞬間還原。

### 保險 3 — canary 逐站推進
```
測試者 header 在 jyg 驗證 PHP71 → jyg conf.weber 永久切 PHP71(格式安全,舊資料仍正常)
                                → 遷移 jyg 資料 → 下一站 … 逐站 → PHP7 退役
```
**切框架版本 (PHP7→PHP71) 與遷移資料 (舊路徑→file_id) 是兩件事，靠保險1格式安全解耦，可分開進行。**

> 一句話：格式安全讓「舊資料在新碼上」不破圖；PHP71 站台切換讓「啟用」可控可回滾；canary 讓「擴散」漸進。

---

## 5. 日後上 Cloudflare R2：此設計已鋪好路

`uploaded_files` 已有 **`host` + `path`** 兩欄（逐檔記錄位置），前台 `_to_src()` = `host . path`。搬 R2：

| 步驟 | 動作 | 內文要改？ |
|---|---|---|
| 1 | 實體檔上傳到 R2 bucket | — |
| 2 | 更新該列 `host`（改 R2/CDN 網域） | 否 |
| 3 | `resolve_all` 自動吐新網址 | 否 |

- 內文永遠只存 file_id → **完全不動**。（若內文存網址，搬 R2 要再改寫全站，已避開。）
- `host` 逐檔存 → 可漸進搬：新檔直接寫 R2、舊檔慢慢搬，兩來源並存無妨。
- 實體寫檔集中在 `PHP7/pubs/apps/Models/IO/Uploader/Files.php`，加 storage adapter（local / R2，由 conf.media flag 控制）即可。R2 走 S3 API，屆時引一個乾淨 S3 client（repo 現只有 ckfinder 內附舊 aws-sdk）。
- **R2 設定檔**：跟 `conf.media.php` 同層、**每站台一份**（獨立 `conf.r2.php` 或併入 conf.media）。內容：`storage='r2'`、bucket、endpoint、公開 `host`（寫進 uploaded_files.host）、access_key/secret。
- **金鑰（已拍板）**：決定先**直接寫在 conf.r2.php**，求快、不外置。代價提醒：① 此 repo 日後若 `git init`，務必把 conf.r2.php 加進 `.gitignore`，否則金鑰進版控；② R2 token 用 **bucket 範圍限定**，勿用帳號級全權金鑰，限縮外洩影響面。

---

## 6. 實作順序（建議）

1. 寫 `Media::resolve_all`（**唯讀版**先）+ 媒體欄位白名單
2. PoC dry-run：挑 jyg 一筆有圖文章 + 一筆 pages_i18n banner，印「原始 row → resolve 後」對照
3. 寫遷移腳本（dry-run 出對照表 → 備份 → 執行）：A 類欄位 + B 類內文 + 補 `uploaded_files`
4. 改 base 掛 hook（保險1 格式安全）＋ jyg conf 加 flag（保險2）
5. canary：開 jyg → 驗證 → 遷移 jyg → 再逐站
6. （後續）編輯器上傳器改走 `Uploader\Files`；之後再做 R2 storage adapter

## 7. 注意事項
- B 類「之後新上傳」要改編輯器上傳器；先確認後台用哪套（ckeditor4 / ckeditor5 / tinymce 三套並存）。
- 跨目錄有同名檔（舊結構），遷移發 file_id 時以 file_id 命名即天然避開撞名。
- micro 專案目前**非 git repo**，任何遷移/改檔前先備份（孤兒檔清理已採此原則）。
