# Personal Media Hub [English](README.md) · **繁體中文** > 自架在家裡 Linux 主機上的私人照片、影片與雲端硬碟。閒置的 Android 手機和舊電腦可以加入成「**儲存節點**」,保存檔案的加密副本,**Nextcloud** 也能共用同一個儲存網路。 所有資料都留在你自己的機器上,不依賴外部雲端、CDN 或第三方帳號。預設只在區域網路內使用,**不需要、也不建議**對公開網際網路開放。

網頁版雲端硬碟(深色模式)

Android App:照片 Android App:自動備份相機照片 Android App:手機當作儲存節點

截圖來自測試環境(範例資料)。介面支援繁體中文與英文。 ## 目錄 - [功能](#功能) - [運作方式](#運作方式) - [Nextcloud](#nextcloud) - [系統需求](#系統需求) - [快速開始](#快速開始) - [加入儲存節點](#加入儲存節點) - [Android App](#android-app) - [開發](#開發) - [測試](#測試) - [文件](#文件) - [安全設計](#安全設計) - [已知限制](#已知限制) - [授權](#授權) ## 功能 ### 網頁版(手機、平板、電腦的瀏覽器都能用) - **照片**:依拍攝日期排列的圖庫、相簿、收藏、分享,支援 JPEG/PNG/WebP/GIF/HEIC。 - **影片**:影片庫。背景以 FFmpeg 產生縮圖與轉檔,播放器可以拖曳進度。 - **雲端硬碟**:資料夾、搜尋、拖放上傳、共享給其他使用者、垃圾桶與復原。大檔案走 [tus](https://tus.io/) 上傳,中斷後可以續傳。 - **帳號**:邀請制註冊、管理員後台、30 天滑動式登入期限,登入的裝置可以遠端登出。 - **介面**:深色模式;每個帳號可以選擇繁體中文或英文介面。 ### 儲存網路 - 每個檔案可以在多個**節點**(伺服器本身、Android 手機、其他電腦)各放一份副本。伺服器會追蹤每份副本的位置和健康狀態:完整、副本不足、暫時無法存取、損壞。 - 節點只會收到 **AES-256-GCM 密文**。金鑰由留在伺服器上的主金鑰包裝。 - 節點**主動**以 HTTPS 連到伺服器,伺服器和節點都不需要多開任何連接埠。 - 可以設定的策略: - 每個檔案保留幾份副本。 - 節點離線多久後在別處重建副本。 - 定期完整性巡檢。 - 選用的「本機卸載」(冷資料只留在節點上)。 - **安全移除**:節點退出前,會先把資料交給其他節點。撤銷遺失的手機前,會先列出哪些檔案*只*存在那支手機上。 - 內建 **S3 相容閘道**(SigV4),未修改的 Nextcloud 可以把檔案存進儲存網路。 ### Android App「Media Hub」(Android 8.0 以上) - 登入 **Media Hub 和/或 Nextcloud** 帳號(可以同時登入多個),瀏覽檔案、照片與影片。 - 從手機上傳,在背景執行並支援續傳:Media Hub 用 tus,Nextcloud 用 chunked upload v2。 - **自動備份相機照片**,有以下選項: - 只在連上 Wi-Fi 時備份。 - 只在充電時備份。 - 只備份相機拍的照片。 - 只備份新照片,或連同以前的照片一起備份。 - **掃 QR 碼登入**:在網站上顯示 QR 碼,手機掃描就登入,不用在手機上輸入密碼。 - 把手機變成**儲存節點**:借出一部分空間,保存加密副本。 ### Nextcloud - 讓 Nextcloud **跑在儲存網路上**,所有 Nextcloud 檔案都有同樣的副本與加密。 - Nextcloud App **`android_storage_nodes`**(Nextcloud 33–35): - Nextcloud 管理員可以直接在 Nextcloud 裡配對、監看、移除手機。 - 每位使用者會有一個存在儲存網路上的私人資料夾。 - Android App 也可以直接登入 Nextcloud。 詳見下方 [Nextcloud](#nextcloud)。 ## 運作方式 ``` 瀏覽器 / Android App ──HTTPS──▶ Caddy(前端 SPA、反向代理、私有 CA 憑證) ├─ /api ─────────▶ FastAPI「api」─┬─ PostgreSQL 17(中繼資料) │ └─ storage volume(原始檔、縮圖、轉檔) ├─ /node-api/v1 ◀─ 儲存節點(Android 手機、電腦 agent)主動連線 └─ :9443 ─▶ Nextcloud(選用)──S3──▶「s3gw」S3 閘道 ──▶ 同一批 blob 「worker」:縮圖/FFmpeg 工作 + 儲存網路複寫、完整性巡檢 ``` - **blob 與 file**:每個上傳的檔案只以一個 *blob* 存一次。網頁上的檔案、相簿項目、S3 物件(Nextcloud)都只是參照,所以複製或改名都不會搬動資料。 - **寫入**:新的 blob 先寫進伺服器的磁碟。worker 用每個 blob 各自的資料金鑰,為每個遠端節點加密出一份副本,排入佇列直到達到目標副本數。節點下載密文並回報 SHA-256,伺服器比對後標為已驗證。 - **讀取**:從本機磁碟讀取。如果檔案已經卸載,就從在線的節點取回,通過 GCM 驗證後才回傳。 - **完整性**:伺服器定期發出挑戰式驗證 `sha256(challenge ‖ 密文)`,節點無法假裝持有副本。 - **協定**:節點使用精簡的 JSON over HTTPS 協定([NODE_PROTOCOL.md](docs/NODE_PROTOCOL.md))。節點以長輪詢領取任務(存入、取回、驗證、刪除),資料以 1 MiB 為單位串流,中斷後可以續傳。 | 目錄 | 內容 | | --- | --- | | `backend/` | Python 3.12、FastAPI、SQLAlchemy 2、Alembic、argon2。`app/` 是 API、worker 和 S3 閘道;`nodeagent/` 是電腦用的儲存節點程式(`mediahub-node.pyz`);`nodesim/` 是測試用的模擬節點 | | `frontend/` | React 19、TypeScript、Vite、Tailwind CSS v4、TanStack Query、i18next | | `android/` | Kotlin + Jetpack Compose。`:core` 是節點協定,`:cloud` 是 Media Hub/Nextcloud 用戶端,`:app` 是 App 本體 | | `integrations/nextcloud/android_storage_nodes/` | Nextcloud App(PHP + 純 JS) | | `deploy/` | Caddy 映像與設定、Nextcloud 容器的 hooks | | `scripts/` | 初始化、備份/還原、建置、端對端測試 | | `docs/` | 完整文件 | ## Nextcloud Nextcloud 有三種搭配方式,可以同時使用,也可以都不用。 ### 1. 內建的 Nextcloud,檔案直接存在儲存網路上 `compose.nextcloud.yaml` 會在 Media Hub 旁邊跑一個 Nextcloud 34,並以 **S3 閘道作為它的主要物件儲存**。上傳到這個 Nextcloud 的每個檔案都會成為儲存網路裡的 blob,所以和 Media Hub 的檔案一樣,會在手機和電腦上有加密副本。 - 網址是 `https://<主機>:9443`。它和 Media Hub 是不同的網頁來源,登入狀態與 cookie 不會互通。 - Nextcloud、它的資料庫、Redis 和 S3 閘道都只在 compose 內部網路,對外只有 Caddy 的 9443。 - 每次啟動時,`deploy/nextcloud/hooks/` 會重新套用設定:信任網域、cron 模式、維護時段。 - `.env` 的 `COMPOSE_FILE=compose.yaml:compose.nextcloud.yaml` 會啟用它,這也是預設值。不需要 Nextcloud 就刪掉這一行。 - 主要物件儲存只能在**安裝時**選定。不要把已經把檔案存在本機磁碟的 Nextcloud 改接過來。 設定與升級見 [DEPLOYMENT.md §5.1](docs/DEPLOYMENT.md),概念驗證與結果見 [NEXTCLOUD_POC.md](docs/NEXTCLOUD_POC.md)。 ### 2. Nextcloud App `android_storage_nodes`(在 Nextcloud 裡管理手機) 適用於任何 Nextcloud **33、34、35**,包括上面內建的,或另一台機器上的 Nextcloud。完全不修改 Nextcloud 核心。 **管理員**可以在「管理設定 → Android 儲存節點」: - 設定**閘道連線**:位址、整合權杖、釘選的憑證指紋。只有在閘道憑證符合指紋、而且閘道接受權杖時才會儲存。 - **新增手機**:產生一次性 QR 碼,用 Android App 掃描。 - 查看**手機清單**:在線/離線、最後連線時間、已用與借出的空間、保管的副本數、最後錯誤。 - **安全移除**(先遷移資料)、**立即撤銷**(手機遺失時用,會先顯示有幾個檔案只存在那支手機上),以及從清單刪除。 - 查看整個儲存池的概況:在線節點與檔案健康狀態。 **一般使用者**在「檔案」裡會多一個**私人資料夾**(預設名稱「Android 儲存空間」),對應到閘道上他自己的 bucket。個人設定頁也能看到自己用了多少、檔案副本是否完整。這個資料夾需要內建的「External storage support」(`files_external`)App;只管理手機則不需要。 隔離與安全: - 每個 Nextcloud 在閘道上是獨立的**整合用戶端**,有自己的權杖和自己的 S3 金鑰。 - S3 金鑰只能存取自己的 bucket 前綴。它**只看得到、也只能管理自己配對的手機**,看不到從 Media Hub 或其他 Nextcloud 配對的。 - 權杖和 S3 secret **加密**存在 Nextcloud 的 app config 裡,不會送到瀏覽器,也不會寫進外部儲存的掛載表。 - 閘道的 CA 會被釘選並匯入 Nextcloud 的憑證庫。只有同一個網路裡的容器名稱(例如 `http://api:8000`)可以用 `http`。 - 管理操作需要 Nextcloud 的密碼確認。 安裝: ```bash # 閘道端(Media Hub 伺服器):建立整合用戶端,記下印出的權杖與憑證指紋 docker compose exec api mediahub integration create --name "家裡的 Nextcloud" # (也可以用網站「儲存網路 → 整合」;另有 integration list | rotate | revoke) # Nextcloud 端:把 App 複製到 custom_apps/(或解開 scripts/package-nextcloud-app.sh 產生的 tarball) occ app:enable files_external # 選用:只有「檔案」中的私人資料夾需要 occ app:enable android_storage_nodes ``` 接著打開「管理設定 → Android 儲存節點」,填入閘道資訊,按「儲存並測試」。 Nextcloud 在**另一台機器**時,還需要 S3 連接埠:把 `compose.gateway.yaml` 加進 `COMPOSE_FILE`,並設定 `S3_DOMAIN`、`S3_HTTPS_PORT`(預設 9445)。這個連接埠預設不啟用,啟用後綁在 `HUB_BIND`(區域網路),使用同一張私有 CA。 完整說明:[docs/nextcloud-integration/](docs/nextcloud-integration/README.md),包括[架構與設計取捨](docs/nextcloud-integration/ARCHITECTURE.md)、[安全設計](docs/nextcloud-integration/SECURITY.md)與[測試結果](docs/nextcloud-integration/TESTING.md)。 ### 3. 用 Android App 當 Nextcloud 用戶端 Media Hub Android App 可以登入任何 Nextcloud,登入方式有三種: - Nextcloud 官方的 Login Flow v2。 - 掃描應用程式密碼 QR 碼。 - 輸入密碼。App 會立刻換成應用程式密碼,不保存原本的密碼。 登入後可以瀏覽檔案與照片、上傳,也能把相機照片備份到 `Photos/Camera`。 ### 我需要哪一種? | 目的 | 使用 | | --- | --- | | Nextcloud 的檔案自動在手機上有副本 | **1**(內建 Nextcloud) | | 在 Nextcloud 管理頁配對、管理手機 | **2**(`android_storage_nodes`) | | 給每位 Nextcloud 使用者一個存在儲存網路上的額外資料夾 | **2** + `files_external` | | 另一台機器上現有的 Nextcloud 也要用你的手機 | **2** + `compose.gateway.yaml` | | 只是想在手機上瀏覽、備份到 Nextcloud | **3** | 一支手機一次只能配對**一台閘道**。但同一台閘道可以同時服務 Media Hub 和多個 Nextcloud,所以手機只要配對一次。 ## 系統需求 - **伺服器**:Linux,並安裝 Docker Engine 與 Docker Compose v2。 - 容器的記憶體上限:API 約 1 GiB,媒體 worker 約 3 GiB。 - 轉檔一次只跑一個,而且以低優先權執行。 - 磁碟要能放下原始檔、縮圖與轉檔結果。 - **Android App**:Android 8.0(API 26)以上。自己建置需要 Android SDK 和自己的簽章金鑰。 - **電腦節點**(選用):Python 3.10 以上。已在 Linux 上測試;macOS、Windows 可以在前景執行,但未經測試。 - **Nextcloud App**(選用):Nextcloud 33、34 或 35。 ## 快速開始 ```bash git clone https://github.com/Chihen-Tai/personal-media-hub.git && cd personal-media-hub ./scripts/init-env.sh # 產生 .env,所有密碼與主金鑰都是隨機產生 docker compose up -d --build --wait # db、api、worker、caddy(沒刪 COMPOSE_FILE 的話也包含 Nextcloud) docker compose exec api mediahub create-admin --username <帳號> # 互動輸入密碼 ``` 開啟 。設定區域網路存取之前,服務只會綁定 `127.0.0.1`。 **區域網路存取(手機等裝置):** 1. 在 `.env` 把 `HUB_BIND` 設成伺服器的區域網路 IP,並把 `https://` 加進 `HUB_SITES`。 2. 設定 `HUB_DEFAULT_SNI=` 和 `MEDIAHUB_PUBLIC_URL=https://`。 3. 執行 `docker compose up -d`。 4. 在各裝置上安裝本機根憑證。步驟和檢查腳本(`scripts/check-lan.sh`)見 [DEPLOYMENT.md §3](docs/DEPLOYMENT.md#3-區域網路存取)。 **開始存放真實資料之前:** - **另外備份 `.env` 裡的 `MEDIAHUB_MASTER_KEY`**。沒有它,遠端節點上的副本無法解密。 - 用 `./scripts/backup.sh [目標目錄]` 排程備份(cron 範例見 [OPERATIONS.md](docs/OPERATIONS.md)),並先熟悉 `./scripts/restore.sh <目錄>`。手機上的副本是冗餘,**不是**備份。 常用管理指令: ```bash docker compose exec api mediahub create-invite [--role user|admin] [--days 7] # 產生註冊邀請碼 docker compose exec api mediahub reset-password --username <帳號> docker compose exec api mediahub check [--fix] # 檢查資料庫與實體檔案是否一致 docker compose exec -u www-data nextcloud php occ # 內建 Nextcloud 的 occ ``` ## 加入儲存節點 1. 在網站「儲存網路 → 策略」,把**每個檔案的副本數**設為至少 2(伺服器一份,節點一份),否則節點不會收到任何資料。 2. 到「儲存網路 → 新增節點」: - **Android 手機**: 1. 安裝 App(APK 可以從網站的「設定」頁下載)。 2. 按「開始設定」,掃描 QR 碼。 3. 選要借多少空間(5 GB/20 GB/能借的全部)。手機永遠保留總容量的 10 % 給自己,最少 2 GB、最多 10 GB。 4. 預設只在充電中而且連上 Wi-Fi 時傳輸大量資料。 - **電腦**:下載 `mediahub-node.pyz`,執行網頁上顯示的指令,例如 `python3 mediahub-node.pyz pair --capacity 100GB --name '書房桌機' --payload '<…>'`。在 Linux 上可以用 `install-service` 安裝成使用者服務。 3. 在「儲存網路 → 節點」可以看到副本逐漸出現。每個節點會顯示容量、保管的副本數、電量與充電狀態,以及進行中的傳輸。 移除節點: - 一般情況用「退出並遷移資料」(電腦也可以用 `leave --wait`)。資料會先複製到其他地方,節點接著自動清空。 - 裝置遺失時用「立即撤銷」。 - 已退出或已撤銷的節點可以從清單刪除。 - 電腦節點程式可以用 `uninstall` 完整移除。 ## Android App App 有五個分頁:**檔案、照片、備份、儲存節點、帳號**。只借空間、沒有登入帳號的手機,打開時會直接進入「儲存節點」分頁。 | | | | --- | --- | | 登入 | Media Hub:掃描網站「設定 → 在手機上登入」的 QR 碼(5 分鐘內有效、只能用一次、會釘選伺服器憑證),或輸入帳號密碼。Nextcloud:Login Flow v2、應用程式密碼 QR 碼,或輸入密碼(會換成應用程式密碼) | | 檔案 | 資料夾、路徑列、搜尋、縮圖;可以用其他 App 開啟、分享、存到「下載」 | | 照片 | 依月份分組、捲動自動載入的照片牆;全螢幕左右滑動與縮放;影片播放(Media3),走同一條釘選的連線 | | 上傳/備份 | 由 WorkManager 在背景執行,App 關掉也會繼續,通知列顯示進度。相機備份每 15 分鐘檢查一次,有新照片時也會觸發 | | 儲存節點 | 狀態、借出的空間、充電/Wi-Fi 條件、暫停;另有各廠牌省電設定的檢查清單 | 帳號的密鑰用獨立的 Android Keystore 金鑰加密,和儲存節點的憑證分開。TLS **只**信任釘選的私有 CA。 **建置**:本倉庫不附 APK。 1. 請建立自己的簽章金鑰,寫進倉庫外的 `~/.config/mediahub/android-release.env`。 2. 執行 `./scripts/build-android.sh`,會產生 `android/dist/mediahub-node.apk`。 3. 伺服器會自動提供這個 APK 下載。 詳見 [ANDROID_NODE_APP.md](docs/ANDROID_NODE_APP.md)。 ## 開發 ```bash # 開發用 Postgres,綁在 127.0.0.1:55432(務必使用另一個專案名稱,見下方說明) docker compose -f compose.yaml -f compose.dev.yaml -p mediahub-dev up -d db cd backend && uv sync MEDIAHUB_DATABASE_URL=postgresql+psycopg://mediahub:@127.0.0.1:55432/mediahub \ MEDIAHUB_STORAGE_ROOT=../.devdata/storage MEDIAHUB_ENVIRONMENT=development MEDIAHUB_COOKIE_SECURE=false \ uv run sh -c "alembic upgrade head && uvicorn app.main:app --reload --port 8000" cd frontend && npm install && npm run dev # http://localhost:5173(/api 轉發到 :8000) cd integrations/nextcloud/android_storage_nodes && npm install && npm run build # Nextcloud App 的 JS 與翻譯 ``` > 已經有正式 stack 在跑的機器上,使用 `compose.dev.yaml` 時一定要加 `-p <其他名稱>`,否則會重建正式環境的 `db` 容器。 給貢獻者(以及 AI 助手)的開發慣例寫在 [CLAUDE.md](CLAUDE.md)。 - 資料庫結構變更一律寫成 `backend/alembic/versions/` 裡的 Alembic migration。 - 介面文字以繁體中文撰寫;英文翻譯在 `frontend/src/i18n/en.json`,由 `build_en.py` 產生。 ## 測試 ```bash ./scripts/test.sh # 後端 pytest(容器內,使用獨立的 mediahub_test 資料庫) cd frontend && npm run build # 型別檢查 + 建置 python3 scripts/i18n_check.py # 英文翻譯完整,沒有漏翻的中文 cd android && ./gradlew :core:test :cloud:test :app:lintDebug ``` 端對端測試會建立使用者與檔案,**只能對獨立的測試 stack 執行**(例如 `docker compose -p mediahub-e2e` 綁在 `127.0.0.1:8443`),不要對正式環境執行: ```bash ./scripts/make-test-media.sh # 測試媒體 → .devdata/testmedia uv run --no-project --with httpx python scripts/e2e_api.py https://localhost:8443 .devdata/testmedia uv run --no-project --with httpx python scripts/sim_e2e.py https://localhost:8443 .devdata/simnet cd frontend && E2E_ADMIN_PASSWORD= npx playwright test # 另外還有 scripts/nextcloud_app_e2e.py(Nextcloud App,NC 33/34/35 容器) # 與 scripts/android_cloud_e2e.py(只接受 Android 模擬器),參數寫在各檔案開頭 ``` [TEST_REPORT.md](docs/TEST_REPORT.md) 記錄了測過哪些項目、在哪些裝置與版本上測,以及哪些**還沒驗證**。 ## 文件 | 文件 | 內容 | | --- | --- | | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | 技術選型、資料模型、儲存與安全設計 | | [DEPLOYMENT.md](docs/DEPLOYMENT.md) | 安裝、區域網路與 HTTPS 憑證、環境變數、Nextcloud 部署、更新 | | [USER_GUIDE.md](docs/USER_GUIDE.md) | 使用者操作說明 | | [OPERATIONS.md](docs/OPERATIONS.md) | 備份、還原、一致性檢查、日誌、故障排除 | | [SECURITY.md](docs/SECURITY.md) | 威脅模型、防護措施、安全測試 | | [ANDROID_NODE_APP.md](docs/ANDROID_NODE_APP.md) | Android App:使用、建置、簽章、測試結果與未驗證項目 | | [NODE_PROTOCOL.md](docs/NODE_PROTOCOL.md) | 儲存節點通訊協定 v1 | | [nextcloud-integration/](docs/nextcloud-integration/README.md) | Nextcloud 整合:安裝、架構、安全、測試 | | [NEXTCLOUD_POC.md](docs/NEXTCLOUD_POC.md) | 概念驗證:未修改的 Nextcloud 透過 S3 閘道使用儲存網路 | | [PERFORMANCE.md](docs/PERFORMANCE.md) | 效能量測與最佳化 | | [RESEARCH.md](docs/RESEARCH.md) | 開源方案研究(Nextcloud、MeshHold、MinIO、Garage 等)與設計取捨 | | [TEST_REPORT.md](docs/TEST_REPORT.md) | 測試結果與驗收報告 | | [THIRD_PARTY_LICENSES.md](docs/THIRD_PARTY_LICENSES.md) | 第三方元件與授權 | ## 安全設計 - **只為區域網路設計**:由 Caddy 的私有 CA 簽發憑證。在你指定區域網路位址之前,服務只綁定 `127.0.0.1`;節點端也不需要開放任何連入的連接埠。 - **程式碼裡沒有機密**:密碼、主金鑰、節點憑證、整合權杖、S3 金鑰都在部署或配對時產生。伺服器只保存權杖的雜湊值,手機則把自己的憑證存在 Android Keystore。 - **加密**:遠端節點只保存 AES-256-GCM 密文,拿不到任何金鑰。但伺服器本身能解密,所以這是「傳輸加密與節點上的靜態加密」,**不是端對端加密**。 - **授權**: - 所有內容相關的路由都經過同一個權限層;檔案不存在和無權存取都回傳 404。 - 管理員能管理系統,但看不到使用者的內容。 - 敏感的管理操作需要再輸入一次密碼。 - 非 GET 的 API 請求必須帶 CSRF 標頭。 - 這是個人專案,**沒有**經過第三方安全稽核。發現問題請到 GitHub issues 回報。 ## 已知限制 - **節點提供的是冗餘,不是容量**:新資料先寫進伺服器的磁碟,節點保存額外的副本。只有啟用「本機卸載」時,冷資料才會只存在節點上,這時讀取需要節點在線。 - **這不是備份**:誤刪、勒索軟體、主金鑰遺失都不在副本能防的範圍。請另外做獨立備份。 - 一支手機一次只能配對一台閘道。 - Android 可能為了省電延後背景工作。App 在 Android 允許的範圍內使用前景服務和 WorkManager,並提示要打開哪些系統設定。 - 閘道曾經連不上時,Nextcloud 最多會等 **10 分鐘**才重試外部儲存資料夾。這是 Nextcloud 核心的行為,期間檔案不會被刪除。 - Nextcloud App 尚未上架 Nextcloud App Store。 ## 授權 本專案以 [GNU Affero General Public License v3.0 或更新版本](LICENSE)(AGPL-3.0-or-later)釋出,涵蓋後端、前端、Android App 與 Nextcloud App。 AGPL 的重點:你可以自由使用、修改與散布;但如果散布修改版,**或把修改版架成網路服務給別人使用**,就必須以同樣的授權提供對應的原始碼。 第三方元件的授權見 [THIRD_PARTY_LICENSES.md](docs/THIRD_PARTY_LICENSES.md)。容器映像裡的 FFmpeg 是 GPL 建置,散布映像時也要遵守 GPL。