diff --git a/README.md b/README.md
index 27aa6e2..ce69a01 100644
--- a/README.md
+++ b/README.md
@@ -12,11 +12,13 @@
## 📖 概述
-一款專為 Android 設計的完整應用數據備份/恢復 Shell 腳本,支援 SSAID、運行時權限、OBB 數據包、WiFi 設定等完整備份,讓你換機換系統後能無縫還原所有應用狀態。
+一款專為 Android 設計的完整應用數據備份/恢復 Shell 腳本,支援 SSAID、運行時權限、OBB 數據包、WiFi 設定等完整備份,讓你換機換系統後能無縫還原所有應用狀態。
-> 作者為台灣人,預設發布繁體版本。CN 系統環境下腳本將自動翻譯為簡體中文。
+新版增加**完整的遠端備份系統**,支援 WebDAV / SMB 上傳到 NAS / 雲端 / 區網電腦,並可從遠端下載備份回手機直接恢復。
-**系統需求:** `Android 8+` · `arm64 架構` · `Root 權限(Magisk / KernelSU)`
+> 作者為台灣人,預設發布繁體版本。CN 系統環境下腳本將自動翻譯為簡體中文。
+
+**系統需求:** `Android 8+` · `arm64 架構` · `Root 權限(Magisk / KernelSU)`
---
@@ -24,26 +26,29 @@
| 功能 | 說明 |
|------|------|
-| 📦 完整數據備份 | 換機換系統後原有數據完整保留,無需重新登入或下載額外數據包 |
-| 🔑 SSAID 備份 | 支援 SSAID 備份,可完美備份 LINE 等依賴設備識別碼的應用 |
-| 🛡️ 權限備份 | 支援備份運行時權限(Runtime Permission)與 ops 權限 |
+| 📦 完整數據備份 | 換機換系統後原有數據完整保留,無需重新登入或下載額外數據包 |
+| 🔑 SSAID 備份 | 支援 SSAID 備份,可完美備份 LINE 等依賴設備識別碼的應用 |
+| 🛡️ 權限備份 | 支援備份運行時權限(Runtime Permission)與 ops 權限 |
| 📂 Split APK | 支援備份與恢復 Split APK 格式 |
-| 🎮 OBB 數據包 | 可選備份外部 OBB 數據(如原神、王者榮耀等大型遊戲) |
+| 🎮 OBB 數據包 | 可選備份外部 OBB 數據(如原神、王者榮耀等大型遊戲) |
| 📡 WiFi 備份 | 支援備份與恢復 WiFi 設定 |
| 📁 自定義資料夾備份 | 可備份 DCIM、Download、Music 等任意自定義目錄 |
-| 🗜️ 多種壓縮算法 | 支援 `tar`(僅打包)與 `zstd`(高壓縮率高速度) |
-| ⚡ 高速壓縮 | zstd 壓縮速率快速,優於鈦備份、Swift Backup |
+| 🗜️ 多種壓縮算法 | 支援 `tar`(僅打包)與 `zstd`(高壓縮率高速度) |
+| ⚡ 高速壓縮 | zstd 壓縮速率快速,優於鈦備份、Swift Backup |
| 🔒 完整性校驗 | 內建 tools SHA-256 校驗與壓縮包完整性驗證 |
-| 🔄 增量備份 | 比對上次備份大小,無變化則跳過,節省時間 |
-| 🖥️ 後台執行 | 支援後台執行模式,可完全關閉終端,log 持續刷新 |
-| 💡 偽裝亮屏 | 備份/恢復期間可偽裝亮屏,避免 IO 因息屏降速 |
-| 🌐 自動更新 | 聯網偵測最新版本,支援 CDN 節點(適合中國大陸用戶) |
-| 🌏 多語言 | 自動識別系統語言環境,支援繁體中文/簡體中文自動切換 |
-| 👥 多用戶支援 | 支援多用戶環境(user 0、999 等),可手動或自動選擇用戶 |
+| 🔄 增量備份 | 比對上次備份大小,無變化則跳過,節省時間 |
+| 🖥️ 後台執行 | 支援後台執行模式,可完全關閉終端,log 持續刷新 |
+| 💡 偽裝亮屏 | 備份/恢復期間可偽裝亮屏,避免 IO 因息屏降速 |
+| 🌐 自動更新 | 聯網偵測最新版本,支援 CDN 節點(適合中國大陸用戶) |
+| 🌏 多語言 | 自動識別系統語言環境,支援繁體中文/簡體中文自動切換 |
+| 👥 多用戶支援 | 支援多用戶環境(user 0、999 等),可手動或自動選擇用戶 |
| ⬛ 黑名單模式 | 黑名單應用可選「完全忽略」或「僅備份安裝包」 |
-| ⬜ 白名單支援 | 支援預裝應用白名單與系統應用白名單,可指定備份範圍 |
-| 📱 進程偵測 | 可設定忽略正在運行中的應用,避免備份數據不一致 |
-| ☁️ 遠程備份 | 支援 WebDAV / FTP / SMB / SCP 四種協議,備份完成後自動上傳到遠端伺服器 |
+| ⬜ 白名單支援 | 支援預裝應用白名單與系統應用白名單,可指定備份範圍 |
+| 📱 進程偵測 | 可設定忽略正在運行中的應用,避免備份數據不一致 |
+| ☁️ 遠程備份上傳 | 支援 WebDAV / SMB 兩種協議,備份完成自動上傳,智能範圍與失敗重試 |
+| 📥 遠程下載恢復 | 可從遠端直接下載備份回手機,點 start.sh 即可恢復 |
+| 🔍 區網掃描 | 自動掃描區網內所有 SMB 主機,免去手動找 IP |
+| 🧪 連線測試 | 三層測試(TCP / 認證 / 路徑),設定不需備份就能驗證 |
---
@@ -58,6 +63,10 @@
| 備份已更新應用 | 僅備份自上次備份以來有版本更新的應用 |
| 備份自定義資料夾 | 備份 `backup_settings.conf` 內設定的自定義目錄 |
| 備份 WiFi | 備份當前設備的 WiFi 設定 |
+| 測試遠端連線 | 驗證 WebDAV / SMB 設定,三層測試(TCP / 認證 / 路徑) |
+| 單獨上傳當前備份 | 上傳現有本地備份到遠端,不重新跑備份流程 |
+| 列出遠端備份 | 連線遠端、產生 `appList_network.txt` 讓你勾選要下載哪些 app |
+| 從遠端下載備份 | 依清單下載備份到本地,可直接執行恢復 |
| 殺死運行中腳本 | 安全終止正在執行的備份腳本 |
### 恢復模式
@@ -66,12 +75,12 @@
|------|------|
| 重新生成應用列表 | 刷新恢復資料夾內的 `appList.txt` |
| 恢復備份 | 根據列表完整恢復應用與數據 |
-| 僅恢復包含 SSAID 應用(含數據) | 只恢復有 SSAID 的應用及其完整數據 |
-| 僅恢復包含 SSAID 應用(不含數據) | 只套用 SSAID,不覆蓋現有數據 |
+| 僅恢復包含 SSAID 應用(含數據) | 只恢復有 SSAID 的應用及其完整數據 |
+| 僅恢復包含 SSAID 應用(不含數據) | 只套用 SSAID,不覆蓋現有數據 |
| 恢復自定義資料夾 | 恢復備份的自定義目錄 |
| 恢復 WiFi | 恢復已備份的 WiFi 設定 |
| 壓縮檔完整性檢查 | 驗證備份壓縮包是否完整無損 |
-| 轉換文件夾名稱 | 將備份資料夾名稱格式轉換(用於跨版本相容) |
+| 轉換文件夾名稱 | 將備份資料夾名稱格式轉換(用於跨版本相容) |
| 殺死運行中腳本 | 安全終止正在執行的恢復腳本 |
---
@@ -85,14 +94,14 @@ backup_script.zip
│ ├── busybox # 核心工具集
│ ├── zstd # zstd 壓縮工具
│ ├── tar # tar 打包工具
-│ ├── curl # 遠程傳輸工具 (WebDAV/FTP/SMB)
-│ ├── scp / ssh # SCP 遠程傳輸
+│ ├── curl # 遠程傳輸工具 (WebDAV)
+│ ├── smbclient # SMB 遠程傳輸
│ ├── jq # JSON 處理
│ ├── bc # 數學計算
│ ├── find # 文件搜索
│ ├── keycheck # 音量鍵監聽
│ ├── cmd # 系統指令橋接
-│ ├── classes.dex # Java 功能擴展(詳見下方說明)
+│ ├── classes.dex # Java 功能擴展(詳見下方說明)
│ ├── soc.json # 處理器資料庫
│ ├── Device_List # 設備型號資料庫
│ └── tools.sh # 核心腳本
@@ -101,66 +110,69 @@ backup_script.zip
└── start.sh # 主執行腳本
```
-> ⚠️ **重要:** 無論備份或恢復,都必須確保 `tools/` 目錄完整存在,否則腳本將無法正常運作。
+> ⚠️ **重要:** 無論備份或恢復,都必須確保 `tools/` 目錄完整存在,否則腳本將無法正常運作。
+
+備份完成後,每個 app 子目錄會額外生成 `upload.sh`,可單獨上傳該 app 到遠端,不需要重新備份。
---
-## ⚙️ 設定檔說明(backup_settings.conf)
+## ⚙️ 設定檔說明(backup_settings.conf)
| 設定項 | 說明 | 預設值 |
|--------|------|--------|
-| `Lo` | 操作方式:`0` 音量鍵 / `1` 音量鍵(強制) / `2` 鍵盤輸入 | `0` |
-| `background_execution` | 後台執行:`1` 可關閉終端 / `0` 需保持終端開啟 | `0` |
+| `Lo` | 操作方式:`0` 音量鍵 / `1` 音量鍵(強制) / `2` 鍵盤輸入 | `0` |
+| `background_execution` | 後台執行:`1` 可關閉終端 / `0` 需保持終端開啟 | `0` |
| `setDisplayPowerMode` | 備份期間偽裝亮屏防止 IO 降速 | `0` |
-| `Shell_LANG` | 語言:`0` 繁體中文 / `1` 簡體中文(留空自動偵測) | 自動 |
-| `Output_path` | 自定義備份輸出路徑,支援相對路徑(留空使用當前目錄) | 空 |
-| `list_location` | 自定義 appList.txt 位置(留空使用當前目錄) | 空 |
-| `update` | 自動更新:`1` 開啟 / `0` 關閉 | `1` |
-| `cdn` | 更新 CDN 節點:`0` 直連 / `1` ghfast.top / `2` workers.dev | `1` |
-| `mount_point` | 屏蔽外部掛載點(OTG、虛擬 SD 等),多個用 `\|` 分隔 | `rannki\|0000-1` |
-| `user` | 指定用戶 ID(留空自動選擇) | 空 |
-| `Backup_Mode` | 備份模式:`1` 應用+數據 / `0` 僅安裝包 | `1` |
-| `Backup_user_data` | 備份 user 數據:`1` 是 / `0` 否 | `1` |
-| `Backup_obb_data` | 備份 OBB 外部數據:`1` 是 / `0` 否 | `1` |
+| `Shell_LANG` | 語言:`0` 繁體中文 / `1` 簡體中文(留空自動偵測) | 自動 |
+| `Output_path` | 自定義備份輸出路徑,支援相對路徑(留空使用當前目錄) | 空 |
+| `list_location` | 自定義 appList.txt 位置(留空使用當前目錄) | 空 |
+| `update` | 自動更新:`1` 開啟 / `0` 關閉 | `1` |
+| `cdn` | 更新 CDN 節點:`0` 直連 / `1` ghfast.top / `2` workers.dev | `1` |
+| `mount_point` | 屏蔽外部掛載點(OTG、虛擬 SD 等),多個用 `\|` 分隔 | `rannki\|0000-1` |
+| `user` | 指定用戶 ID(留空自動選擇) | 空 |
+| `Backup_Mode` | 備份模式:`1` 應用+數據 / `0` 僅安裝包 | `1` |
+| `Backup_user_data` | 備份 user 數據:`1` 是 / `0` 否 | `1` |
+| `Backup_obb_data` | 備份 OBB 外部數據:`1` 是 / `0` 否 | `1` |
| `backup_media` | 備份完成後一併備份自定義資料夾 | `0` |
-| `Background_apps_ignore` | 忽略正在運行中的應用:`1` 忽略 / `0` 備份 | `0` |
-| `Custom_path` | 自定義備份目錄列表(絕對路徑,每行一個) | DCIM / Download 等 |
-| `blacklist_mode` | 黑名單模式:`1` 完全忽略 / `0` 僅備份安裝包 | `0` |
+| `Background_apps_ignore` | 忽略正在運行中的應用:`1` 忽略 / `0` 備份 | `0` |
+| `Custom_path` | 自定義備份目錄列表(絕對路徑,每行一個) | DCIM / Download 等 |
+| `blacklist_mode` | 黑名單模式:`1` 完全忽略 / `0` 僅備份安裝包 | `0` |
| `blacklist` | 黑名單應用包名列表 | 空 |
| `whitelist` | 預裝應用白名單包名列表 | 小米系列預裝 |
| `system` | 系統應用白名單包名列表 | Google 系列 |
-| `Compression_method` | 壓縮算法:`zstd` 或 `tar` | `zstd` |
-| `rgb_a` / `rgb_b` / `rgb_c` | 終端輸出主色/輔色(256 色代碼) | `226` / `123` / `177` |
-| `remote_type` | 遠程備份協議:`webdav` / `ftp` / `smb` / `scp`(留空不啟用) | 空 |
-| `remote_url` | 遠程伺服器地址(見下方格式說明) | 空 |
+| `Compression_method` | 壓縮算法:`zstd` 或 `tar` | `zstd` |
+| `rgb_a` / `rgb_b` / `rgb_c` | 終端輸出主色/輔色1/輔色2(256 色代碼) | `220` / `51` / `213` |
+| `remote_type` | 遠程備份協議:`webdav` / `smb`(留空不啟用) | 空 |
+| `remote_url` | 遠程伺服器地址(見下方格式說明) | 空 |
| `remote_user` | 遠程認證用戶名 | 空 |
| `remote_pass` | 遠程認證密碼 | 空 |
+| `remote_keep_local` | 上傳成功後本地檔案:`1` 保留 / `0` 刪除 | `0` |
---
## 🚀 使用方式
-> 推薦使用 [MT 管理器](https://www.coolapk.com/apk/bin.mt.plus) 執行腳本。若使用 Termux,請勿使用 `tsu`。
+> 推薦使用 [MT 管理器](https://www.coolapk.com/apk/bin.mt.plus) 執行腳本。若使用 Termux,請勿使用 `tsu`。
### 備份流程
**Step 1 — 生成應用列表**
-解壓腳本後執行 `start.sh`,選擇「**生成應用列表**」。執行完畢後,當前目錄會生成 `appList.txt`,內含所有已安裝的第三方應用(預裝應用預設屏蔽,可於 `backup_settings.conf` 加入白名單)。
+解壓腳本後執行 `start.sh`,選擇「**生成應用列表**」。執行完畢後,當前目錄會生成 `appList.txt`,內含所有已安裝的第三方應用(預裝應用預設屏蔽,可於 `backup_settings.conf` 加入白名單)。
**Step 2 — 編輯應用列表**
-打開 `appList.txt`,根據需求調整:
-- 行首加 `#`:注釋掉該應用,不備份
-- 行首加 `!`:僅備份安裝包,不備份數據
+打開 `appList.txt`,根據需求調整:
+- 行首加 `#`:注釋掉該應用,不備份
+- 行首加 `!`:僅備份安裝包,不備份數據
**Step 3 — 設置備份選項**
-打開 `backup_settings.conf`,根據上方設定說明調整各選項後儲存。
+打開 `backup_settings.conf`,根據上方設定說明調整各選項後儲存。
**Step 4 — 執行備份**
-執行 `start.sh`,選擇「**備份應用**」。備份完成後,當前目錄會生成 `Backup_<壓縮算法>_<用戶ID>/` 資料夾,將此資料夾完整保存至安全位置。
+執行 `start.sh`,選擇「**備份應用**」。備份完成後,當前目錄會生成 `Backup_<壓縮算法>_<用戶ID>/` 資料夾,將此資料夾完整保存至安全位置。
---
@@ -168,91 +180,156 @@ backup_script.zip
**Step 1 — 編輯恢復列表**
-進入備份資料夾,打開 `appList.txt`,刪除或注釋不需要恢復的應用行。
+進入備份資料夾,打開 `appList.txt`,刪除或注釋不需要恢復的應用行。
**Step 2 — 執行恢復**
-執行備份資料夾內的 `start.sh`,選擇「**恢復備份**」,等待腳本完成。
+執行備份資料夾內的 `start.sh`,選擇「**恢復備份**」,等待腳本完成。
**Step 3 — 注意 SSAID**
-若恢復結束後提示應用存在 SSAID,請**立刻重啟**後再開啟應用。若先開啟應用,Android 會生成新的 SSAID,導致應用白屏或需要重新登入。
+若恢復結束後提示應用存在 SSAID,請**立刻重啟**後再開啟應用。若先開啟應用,Android 會生成新的 SSAID,導致應用白屏或需要重新登入。
-> 💡 備份資料夾內每個應用子目錄都有獨立的 `backup.sh` 與 `recover.sh`,可單獨備份或恢復單一應用。
+> 💡 備份資料夾內每個應用子目錄都有獨立的 `backup.sh`、`recover.sh`、`upload.sh`,可單獨備份、恢復或上傳單一應用。
---
-### 遠程備份
+## ☁️ 遠程備份
-備份完成後自動將備份檔案上傳到遠端伺服器,支援四種協議:
+備份完成後自動將備份檔案上傳到遠端伺服器,支援 WebDAV 與 SMB:
-| 協議 | `remote_url` 格式 |
-|------|-------------------|
-| WebDAV | `http://192.168.1.100:8080/dav/backup/` |
-| FTP | `ftp://192.168.1.100/backup/` |
-| SMB | `smb://192.168.1.100/share/backup/` |
-| SCP | `192.168.1.100:/home/user/backup/` |
+| 協議 | `remote_url` 格式 | 適用場景 |
+|------|-------------------|---------|
+| WebDAV | `http://192.168.1.100:8080/dav/backup/` | NAS / Nextcloud / 雲端 / rclone serve |
+| SMB | `smb://192.168.1.100/share/` | Windows 共享 / Samba 伺服器 / NAS |
+
+**設定方式:** 編輯 `backup_settings.conf`:
-**設定方式:** 編輯 `backup_settings.conf`:
```conf
-remote_type=webdav
-remote_url=http://192.168.1.100:8080/dav/backup/
+remote_type=smb
+remote_url=smb://192.168.1.100/Backup
remote_user=用戶名
remote_pass=密碼
+remote_keep_local=0
```
-**SCP 注意事項:** SCP 優先使用 `sshpass` 進行密碼認證,若不支援則自動嘗試 SSH 密鑰認證。需確保遠端已安裝 SSH 伺服器。
+**遠端目錄結構:**
-**上傳範圍:** 僅上傳備份數據(應用檔案、WiFi、appList.txt),排除 `tools/`、`start.sh`、`restore_settings.conf` 等腳本文件。
+腳本會自動在 `remote_url` 後加 `Backup_<壓縮算法>_<用戶ID>/` 一層,結構與本地完全鏡像。例如 conf 設 `smb://NAS/Backup`,實際上傳到:
+
+```
+smb://NAS/Backup/
+ Backup_zstd_0/
+ 8591遊戲交易/...
+ Animeko/...
+ wifi/wifi.json
+ tools/
+ start.sh
+ restore_settings.conf
+```
+
+不同用戶(0、999)會自動分開到 `Backup_zstd_0/`、`Backup_zstd_999/`,互不衝突。
+
+**特性:**
+- **智能範圍上傳** — 只上傳本次備份的 app,不是整個資料夾
+- **進度與速度** — 每個目錄完成印「完成 X% (12.5 MB/s)」與總耗時
+- **失敗處理** — 累積失敗清單,完整成功才會刪本地,部分失敗則本地全保留
+- **連線預檢** — 沒網路時 3 秒內判斷並停用上傳,不卡死腳本
+- **HTTP code 顯示** — WebDAV 失敗時顯示具體狀態(401 / 403 / 404 / 423 等)
+
+---
+
+### 從遠端下載備份
+
+從 NAS / 雲端拉回備份,直接執行恢復:
+
+**Step 1 — 列出遠端備份**
+
+主選單選「**列出遠端備份**」。腳本會連線遠端,檢查必要檔案(`tools/`、`start.sh`、`restore_settings.conf`),產生 `appList_network.txt` 列出所有可下載的 app。
+
+**Step 2 — 編輯下載清單**
+
+打開 `appList_network.txt`,用 `#` 註解掉不要下載的 app。
+
+**Step 3 — 下載**
+
+主選單選「**從遠端下載備份**」。下載完成後會在當前目錄產生 `Backup_<壓縮算法>_<用戶ID>/`,可直接執行內附的 `start.sh` 恢復。
+
+---
+
+### 連線測試
+
+設定完 `backup_settings.conf` 後,主選單選「**測試遠端連線**」可驗證設定:
+
+```
+—————— TCP 連線測試 ——————
+目標: 192.168.1.100:445
+TCP 連線通過
+—————— 認證與列目錄測試 ——————
+SMB 認證通過, share 可存取
+全部測試通過, 可以開始備份
+```
+
+每個失敗階段都有對應錯誤訊息(認證失敗 / share 不存在 / 路徑不存在等)。
+
+---
+
+### 上傳範圍
+
+每次備份自動上傳:
+- 本次備份的 app(智能比對 appList.txt)
+- WiFi 配置(若有)
+- 自定義資料夾 Media/(若有設 Custom_path)
+- 固定 3 項:`tools/`、`start.sh`、`restore_settings.conf`(讓遠端能獨立恢復)
---
## 🔄 腳本更新方式
-支援以下四種更新方式:
+支援以下四種更新方式:
-1. **ZIP 放置更新**:將下載的 `.zip` 不解壓,直接放到腳本任意目錄(`tools/` 除外),執行任何腳本即自動更新。
-2. **聯網自動更新**:腳本執行時自動連線 GitHub API 檢查版本,發現新版本時提示下載(需設置 `update=1`)。
-3. **Download 目錄**:將 `.zip` 放置於 `/storage/emulated/0/Download/`,腳本自動偵測並更新。
-4. **QQ 群下載**:從 QQ 群下載的腳本不解壓,直接放置後執行即可自動更新。
+1. **ZIP 放置更新**:將下載的 `.zip` 不解壓,直接放到腳本任意目錄(`tools/` 除外),執行任何腳本即自動更新。
+2. **聯網自動更新**:腳本執行時自動連線 GitHub API 檢查版本,發現新版本時提示下載(需設置 `update=1`)。
+3. **Download 目錄**:將 `.zip` 放置於 `/storage/emulated/0/Download/`,腳本自動偵測並更新。
+4. **QQ 群下載**:從 QQ 群下載的腳本不解壓,直接放置後執行即可自動更新。
-> 🔒 腳本聯網**僅用於檢查更新**,無任何資料收集或非法操作。
+> 🔒 腳本聯網**僅用於檢查更新**,無任何資料收集或非法操作。
---
## ❓ 常見問題
-Q1:批量備份/恢復大量提示失敗?
+Q1:批量備份/恢復大量提示失敗?
-退出腳本,刪除 `/data/backup_tools/` 目錄後重新執行。若問題持續,請建立 [Issue](https://github.com/YAWAsau/backup_script/issues) 並附上截圖與 log。
+退出腳本,刪除 `/data/backup_tools/` 目錄後重新執行。若問題持續,請建立 [Issue](https://github.com/YAWAsau/backup_script/issues) 並附上截圖與 log。
-Q2:微信/QQ 能完美備份恢復嗎?
+Q2:微信/QQ 能完美備份恢復嗎?
-無法保證。建議同時使用其他你信賴的備份工具針對微信/QQ 額外備份,以防丟失重要數據。
+無法保證。建議同時使用其他你信賴的備份工具針對微信/QQ 額外備份,以防丟失重要數據。
-Q3:為什麼部分應用備份很久?
+Q3:為什麼部分應用備份很久?
-腳本會一同備份應用的 OBB 數據包,例如原神數據包超過 9GB,備份與恢復時間自然較長。可在 `backup_settings.conf` 設置 `Backup_obb_data=0` 跳過 OBB 備份。
+腳本會一同備份應用的 OBB 數據包,例如原神數據包超過 9GB,備份與恢復時間自然較長。可在 `backup_settings.conf` 設置 `Backup_obb_data=0` 跳過 OBB 備份。
-Q4:腳本每次都是全量備份嗎?
+Q4:腳本每次都是全量備份嗎?
-否。腳本會比對上次備份的檔案大小,若無差異則跳過該應用,節省時間與空間。
+否。腳本會比對上次備份的檔案大小,若無差異則跳過該應用,節省時間與空間。
-Q5:為什麼腳本內包含 .dex 檔案?
+Q5:為什麼腳本內包含 .dex 檔案?
-`classes.dex` 用於實現 Shell 腳本難以達成的功能,包含:
+`classes.dex` 用於實現 Shell 腳本難以達成的功能,包含:
- SSAID 備份與恢復
-- 運行時權限(Runtime Permission)與 ops 權限備份恢復
+- 運行時權限(Runtime Permission)與 ops 權限備份恢復
- GitHub API 更新版本檢查與下載
- 應用名稱與包名查詢
- 繁體中文 ↔ 簡體中文自動翻譯
@@ -262,33 +339,67 @@ remote_pass=密碼
-Q6:息屏後備份速度變慢?
+Q6:息屏後備份速度變慢?
-這是 Android 內核的 IO 節能機制導致的。建議在 `backup_settings.conf` 設置 `setDisplayPowerMode=1` 開啟偽裝亮屏,或在備份期間保持螢幕常亮。
+這是 Android 內核的 IO 節能機制導致的。建議在 `backup_settings.conf` 設置 `setDisplayPowerMode=1` 開啟偽裝亮屏,或在備份期間保持螢幕常亮。
-Q7:如何單獨備份或恢復單一應用?
+Q7:如何單獨備份/恢復/上傳單一應用?
-進入備份資料夾內對應的應用子目錄,直接執行 `backup.sh`(單獨備份)或 `recover.sh`(單獨恢復)即可。
+進入備份資料夾內對應的應用子目錄,直接執行:
+- `backup.sh` — 單獨備份該 app
+- `recover.sh` — 單獨恢復該 app
+- `upload.sh` — 單獨上傳該 app 到遠端(新)
+
+
+
+Q8:WebDAV 上傳顯示 HTTP 423 Locked?
+
+某些雲端網盤(例如 123 網盤)的 WebDAV 對大檔有單檔大小限制,失敗會把路徑標記為 locked。建議改用以下方案:
+- 自家 NAS / Windows SMB(無限制)
+- rclone serve webdav(無限制)
+- 群暉 / Nextcloud(無限制)
+
+
+
+Q9:WebDAV 上傳顯示 HTTP 404?
+
+腳本已強制 curl 使用 HTTP/1.1(`--http1.1`),避開部分 openresty / nginx 對 HTTP/2 PUT 的相容問題。如果仍 404,請檢查:
+- `remote_url` 路徑是否含正確的 webdav 端點(例如 `/dav/` 或 `/remote.php/webdav/`)
+- 帳號是否有寫入權限
+
+
+
+Q10:SMB 提示「找不到 share」?
+
+- Windows 端確認 SMB 共享已開啟,且網路設成「私人」而非「公用」
+- 防火牆放行 445 port
+- 主選單啟動時的 `scan_smb` 會自動列出區網 SMB 主機與 share 名,可對照確認
+
+
+
+Q11:沒網路會影響備份嗎?
+
+不會。腳本啟動時會做 TCP 預檢(3 秒內判斷),沒網路時自動停用遠端上傳但**完整保留本地備份**,流程繼續跑完。
---
## 📬 問題反饋
-遇到問題請攜帶截圖與 log 檔,透過以下方式反饋:
+遇到問題請攜帶截圖與 log 檔,透過以下方式反饋:
- 🐛 [GitHub Issues](https://github.com/YAWAsau/backup_script/issues)
- 💬 [Telegram 頻道](https://t.me/yawasau_script)
-- 🐧 QQ 群:`976613477`
-- 🧊 酷安:[@落葉淒涼TEL](http://www.coolapk.com/u/2277637)
+- 🐧 QQ 群:`976613477`
+- 🧊 酷安:[@落葉淒涼TEL](http://www.coolapk.com/u/2277637)
---
## ☕ 支持作者
-備份腳本耗費了大量時間與精力,如果你覺得好用,歡迎贊助支持!
+備份腳本耗費了大量時間與精力,如果你覺得好用,歡迎贊助支持!
[](https://paypal.me/YAWAsau?country.x=TW&locale.x=zh_TW)
@@ -298,12 +409,12 @@ remote_pass=密碼
| 貢獻者 | 貢獻內容 |
|--------|----------|
-| [kmou424](https://github.com/kmou424)(臭批老k) | 提供部分驗證函數思路 |
-| [雄氏老方](http://www.coolapk.com/u/665894)(屑老方) | 提供自動更新腳本方案 |
-| 雨季騷年(胖子老陳) | 協助測試 |
+| [kmou424](https://github.com/kmou424)(臭批老k) | 提供部分驗證函數思路 |
+| [雄氏老方](http://www.coolapk.com/u/665894)(屑老方) | 提供自動更新腳本方案 |
+| [sakuradairong](https://github.com/sakuradairong)(雨季騷年/胖子老陳) | 新增 WebDAV / SMB 功能與測試 |
| [XayahSuSuSu](https://github.com/XayahSuSuSu) | 提供 App 支持與 dex 功能支持 |
-`文檔編輯:Petit-Abba, YuKongA`
+`文檔編輯:Petit-Abba, YuKongA`
---
diff --git a/backup_settings.conf b/backup_settings.conf
index 4c08798..a8a2bc7 100644
--- a/backup_settings.conf
+++ b/backup_settings.conf
@@ -113,23 +113,23 @@ com.android.chrome"
#zstd擁有良好的壓縮率與速度
Compression_method=zstd
-#主色
-rgb_a=226
-#輔色
-rgb_b=123
-rgb_c=177
+#色彩設定 (256 色 ANSI 編號)
+#常用值: 39藍 51青 82綠 196紅 208橘 213粉 220黃 165紫
+#主色 (一般資訊, 預設亮黃)
+rgb_a=220
+#輔色1 (提示/進度, 預設亮青)
+rgb_b=51
+#輔色2 (強調/變數值, 預設粉紅)
+rgb_c=213
#遠程備份類型 (留空不啟用)
-#推薦 webdav (穩定) 或 ftp(可自動建目錄)
+#推薦 webdav (穩定)
#smb 僅支援 SMB1/CIFS,Windows Server 需手動開啟
-#scp 需 SSH 服務器,支援密碼或密鑰認證
remote_type=
#遠程地址
#WebDAV例: http://192.168.1.100:8080/dav/
-#FTP例: ftp://192.168.1.100/backup/
#SMB例: smb://192.168.1.100/backup/
-#SCP例: 192.168.1.100:/home/user/backup/
remote_url=
#遠程認證用戶名
diff --git a/start.sh b/start.sh
index 75a0373..6b9df03 100644
--- a/start.sh
+++ b/start.sh
@@ -1,19 +1,12 @@
#!/system/bin/sh
-if [ ! -f "${0%/*}/tools/tools.sh" ]; then
- echo "${0%/*}/tools/tools.sh遺失"
- exit 1
+if [ -f "${0%/*}/tools/tools.sh" ]; then
+ MODDIR="${0%/*}"
+ conf_path="${0%/*}/backup_settings.conf"
+ [ ! -f "${0%/*}/backup_settings.conf" ] && . "${0%/*}/tools/tools.sh"
+else
+ echo "${0%/*}/tools/tools.sh遺失"
fi
-
-MODDIR="${0%/*}"
-conf_path="${0%/*}/backup_settings.conf"
-
-# 若配置文件不存在,啟動腳本自動生成默認配置後退出
-if [ ! -f "$conf_path" ]; then
- . "${0%/*}/tools/tools.sh"
- exit 0
-fi
-
mkdir -p "${0%/*}/log" 2>/dev/null
logfile="${0%/*}/log/log_$(date +%Y-%m-%d_%H-%M).txt"
. "${0%/*}/tools/tools.sh" | tee "$logfile"
-sed -i $'s/\033\[[0-9;]*m//g' "$logfile"
+sed -i "$(printf 's/\[[0-9;]*m//g')" "$logfile"
diff --git a/tools/curl b/tools/curl
index 9467b63..393ce91 100644
Binary files a/tools/curl and b/tools/curl differ
diff --git a/tools/ssh b/tools/ssh
deleted file mode 100644
index 8e6fc80..0000000
Binary files a/tools/ssh and /dev/null differ
diff --git a/tools/tools.sh b/tools/tools.sh
index 94d25b2..d654fc8 100644
--- a/tools/tools.sh
+++ b/tools/tools.sh
@@ -10,6 +10,8 @@ tools_path="$MODDIR/tools"
script="${0##*/}"
backup_version="202508162209"
[[ $SHELL = *mt* ]] && echo "請勿使用MT管理器拓展包環境執行,請更換系統環境" && exit 2
+# 產生 backup_settings.conf 的內容模板 (寫到 stdout)
+# 透過重定向到檔案來生成或更新備份設定檔
update_backup_settings_conf() {
echo "#0關閉音量鍵選擇 (如選項未設置,則強制使用音量鍵選擇)
#1開啟音量鍵選擇 (如選項已設置,則跳過該選項提示)
@@ -126,23 +128,23 @@ com.android.chrome}"\"
#zstd擁有良好的壓縮率與速度
Compression_method=${Compression_method:-zstd}
-#主色
-rgb_a="${rgb_a:-226}"
-#輔色
-rgb_b="${rgb_b:-123}"
-rgb_c="${rgb_c:-177}"
+#色彩設定 (256 色 ANSI 編號)
+#常用值: 39藍 51青 82綠 196紅 208橘 213粉 220黃 165紫
+#主色 (一般資訊, 預設亮黃)
+rgb_a="${rgb_a:-220}"
+#輔色1 (提示/進度, 預設亮青)
+rgb_b="${rgb_b:-51}"
+#輔色2 (強調/變數值, 預設粉紅)
+rgb_c="${rgb_c:-213}"
#遠程備份類型 (留空不啟用)
-#推薦 webdav (穩定) 或 ftp(可自動建目錄)
+#推薦 webdav (穩定)
#smb 僅支援 SMB1/CIFS,Windows Server 需手動開啟
-#scp 需 SSH 服務器,支援密碼或密鑰認證
remote_type="${remote_type:-}"
#遠程地址
#WebDAV例: http://192.168.1.100:8080/dav/
-#FTP例: ftp://192.168.1.100/backup/
#SMB例: smb://192.168.1.100/backup/
-#SCP例: 192.168.1.100:/home/user/backup/
remote_url="${remote_url:-}"
#遠程認證用戶名
@@ -163,6 +165,8 @@ remote_keep_local="${remote_keep_local:-0}"
s/true/1/g;
s/false/0/g'
}
+# 產生 restore_settings.conf 的內容模板 (寫到 stdout)
+# 備份完成時呼叫此函數寫入備份目錄,讓恢復端有獨立的設定檔
update_Restore_settings_conf() {
echo "#0關閉音量鍵選擇 (如選項未設置,則強制使用音量鍵選擇)
#1開啟音量鍵選擇 (如選項已設置,則跳過該選項提示)
@@ -203,16 +207,21 @@ Background_apps_ignore="${Background_apps_ignore:-0}"
#使用者(如0 999等用戶,留空如存在多個用戶強制音量鍵選擇,無多用戶則默認0不詢問)
user="$user"
-#主色
-rgb_a="${rgb_a:-226}"
-#輔色
-rgb_b="${rgb_b:-123}"
-rgb_c="${rgb_c:-177}"" | sed 's/true/1/g ; s/false/0/g'
+#色彩設定 (256 色 ANSI 編號)
+#常用值: 39藍 51青 82綠 196紅 208橘 213粉 220黃 165紫
+#主色 (一般資訊, 預設亮黃)
+rgb_a="${rgb_a:-220}"
+#輔色1 (提示/進度, 預設亮青)
+rgb_b="${rgb_b:-51}"
+#輔色2 (強調/變數值, 預設粉紅)
+rgb_c="${rgb_c:-213}"" | sed 's/true/1/g ; s/false/0/g'
}
if [[ ! -d $tools_path ]]; then
tools_path="${MODDIR%/*}/tools"
[[ ! -d $tools_path ]] && echo "$tools_path二進制目錄遺失" && EXIT="true"
fi
+# 根據當前 conf_path 判斷類型,觸發對應模板重新寫入
+# 用於腳本版本升級時自動補齊新增的設定欄位
_update_conf() {
case $conf_path in
*backup_settings.conf) update_backup_settings_conf>"$conf_path" ;;
@@ -232,8 +241,15 @@ case $Shell_LANG in
0) LANG="TW" ;;
*) LANG="${LANG:="$(getprop "persist.sys.locale")"}" ;;
esac
+# 帶色彩輸出, 用法: echoRgb "訊息" [色碼]
+# 色碼:
+# 0 = 紅色 (197) - 錯誤/警告
+# 1 = 亮綠 (121) - 成功
+# 2 = rgb_c (213) - 強調/變數值 (粉紅, 預設)
+# 3 = rgb_b (51) - 提示/進度 (亮青, 預設)
+# 其他/省略 = rgb_a (220) - 一般資訊 (亮黃, 預設)
+# rgb_a/b/c 可在 conf 自訂, 全部都是 256 色 ANSI 編號
echoRgb() {
- #轉換echo顏色提高可讀性
local color
case $2 in
0) color=197 ;;
@@ -244,7 +260,7 @@ echoRgb() {
esac
echo -e "\e[38;5;${color}m -$1\e[0m"
}
-rgb_a="${rgb_a:=214}"
+rgb_a="${rgb_a:=220}"
abi="$(getprop ro.product.cpu.abi)"
sdk="$(getprop ro.build.version.sdk)"
release="$(getprop ro.build.version.release)"
@@ -363,6 +379,23 @@ fi
if [[ $quit -ne 0 ]]; then
exit "$quit"
fi
+# Logo
+echo -e "\e[38;5;51m"
+cat <<'LOGO'
+░██████╗██████╗░███████╗███████╗██████╗░
+██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗
+╚█████╗░██████╔╝█████╗░░█████╗░░██║░░██║
+░╚═══██╗██╔═══╝░██╔══╝░░██╔══╝░░██║░░██║
+██████╔╝██║░░░░░███████╗███████╗██████╔╝
+╚═════╝░╚═╝░░░░░╚══════╝╚══════╝╚═════╝░
+██████╗░░█████╗░░█████╗░██╗░░██╗██╗░░░██╗██████╗░
+██╔══██╗██╔══██╗██╔══██╗██║░██╔╝██║░░░██║██╔══██╗
+██████╦╝███████║██║░░╚═╝█████═╝░██║░░░██║██████╔╝
+██╔══██╗██╔══██║██║░░██╗██╔═██╗░██║░░░██║██╔═══╝░
+██████╦╝██║░░██║╚█████╔╝██║░╚██╗╚██████╔╝██║░░░░░
+╚═════╝░╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝░╚═════╝░╚═╝░░░░░
+LOGO
+echo -e "\e[38;5;213m » RESTORE // SYNC «\e[0m"
sleep 1 && clear
TMPDIR="/data/local/tmp"
rm -rf "$TMPDIR"/*
@@ -387,14 +420,19 @@ case $LANG in
esac
alias LS="toybox ls -Zd"
alias mv="$get_mv"
+# 給 trap / 流程控制用的 return 函數
+# Set_back_0 永遠回 0 (成功), Set_back_1 永遠回 1 (失敗)
Set_back_0() {
return 0
}
Set_back_1() {
return 1
}
+# 計算並輸出某段流程的耗時
+# 用法: endtime <計時編號> <名稱>
+# 計時編號: 1=讀 starttime1, 2=讀 starttime2 (需先在外面 set 變數)
+# 輸出格式: " -<名稱>用時:X天X時X分X秒"
endtime() {
- #計算總體切換時長耗費
case $1 in
1) starttime="$starttime1" ;;
2) starttime="$starttime2" ;;
@@ -404,6 +442,8 @@ endtime() {
[[ $duration != "" ]] && echo " -$2用時:$duration" || echo " -$2用時:0秒"
}
nskg=1
+# 從 GitHub Release API 取得腳本最新版本號
+# 透過 CDN (ghfast / cloudflare worker) 加速避免被牆
get_version() {
while :; do
keycheck
@@ -425,6 +465,8 @@ get_version() {
break
done
}
+# 規範化布林值,將 1/true/yes 等變成 true,其他變成 false
+# 用於 conf 讀進來的開關項統一格式
isBoolean() {
unset nsx
nsx="$1"
@@ -437,6 +479,9 @@ isBoolean() {
exit 2
fi
}
+# 根據上一條命令的退出碼輸出成功/失敗訊息
+# 用法: echo_log "操作名稱" [skip_success_msg]
+# 第二個參數非空時, 成功不輸出訊息 (只設變數)
echo_log() {
if [[ $? = 0 ]]; then
[[ $2 = "" ]] && echoRgb "$1成功" "1"
@@ -449,6 +494,8 @@ echo_log() {
Set_back_1
fi
}
+# 殺死先前殘留的腳本進程,並設置 lock 防止重複執行
+# trap EXIT 會清 lock 並觸發 remote_cleanup (若有遠端設定)
kill_Serve() {
local LOCK_DIR="/data/.backup_lock"
local MY_PID="$$"
@@ -495,8 +542,8 @@ remote_precheck() {
# 寫入遠端上傳 log (帶時間戳)
# 用法: remote_log "訊息"
remote_log() {
- [[ -z $Backup ]] && return
- local logf="$Backup/remote_upload.log"
+ [[ -z $MODDIR ]] && return
+ local logf="$MODDIR/log/remote_upload.log"
mkdir -p "${logf%/*}" 2>/dev/null
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$logf"
}
@@ -510,11 +557,17 @@ upload_summary() {
[[ -f $fail_list ]] && fail_count="$(wc -l < "$fail_list" 2>/dev/null)"
ok_count=${ok_count:-0}
fail_count=${fail_count:-0}
+ # 計算總耗時
+ local elapsed_str=""
+ if [[ -n $UPLOAD_START_TS ]]; then
+ local elapsed=$(( $(date +%s) - UPLOAD_START_TS ))
+ elapsed_str=" 用時${elapsed}秒"
+ fi
echoRgb "_______________________________________" "2"
- echoRgb "$proto 上傳完成: 成功 $ok_count / 失敗 $fail_count" "3"
- remote_log "$proto 上傳結束: 成功 $ok_count / 失敗 $fail_count"
+ echoRgb "$proto 上傳完成: 成功 $ok_count / 失敗 $fail_count${elapsed_str}" "3"
+ remote_log "$proto 上傳結束: 成功 $ok_count / 失敗 $fail_count${elapsed_str}"
if [[ $fail_count -gt 0 ]]; then
- echoRgb "失敗清單(已記錄到 remote_upload.log):" "0"
+ echoRgb "失敗清單(已記錄到 $MODDIR/log/remote_upload.log):" "0"
local n=0
while read -r line && [[ $n -lt 5 ]]; do
echoRgb " $line" "0"
@@ -538,9 +591,76 @@ upload_summary() {
echoRgb "remote_keep_local=1 本地檔案保留" "3"
fi
rm -f "$ok_list" "$fail_list" 2>/dev/null
+ unset UPLOAD_START_TS
[[ $fail_count -eq 0 ]]
}
+# URL 編碼 (處理 UTF-8 多 byte, 保留 / 不編碼以保持路徑結構)
+# 用法: url_encode_path
+url_encode_path() {
+ local s="$1"
+ # 用 od 把每個 byte 印成 hex, awk 處理 hex 字串轉換
+ # 避免 strtonum (busybox awk 不支援)
+ printf '%s' "$s" | od -An -tx1 -v | tr -s ' \n' ' ' | awk '
+ BEGIN {
+ # 建立 hex → dec 對照表
+ for (i=0; i<10; i++) hex2int[sprintf("%d",i)] = i
+ hex2int["a"]=10; hex2int["b"]=11; hex2int["c"]=12
+ hex2int["d"]=13; hex2int["e"]=14; hex2int["f"]=15
+ }
+ {
+ n = split($0, a, " ")
+ for (i=1; i<=n; i++) {
+ h = a[i]
+ if (h == "") continue
+ # 把兩個 hex 字元轉成 dec
+ val = hex2int[substr(h,1,1)] * 16 + hex2int[substr(h,2,1)]
+ # 不編碼: A-Z a-z 0-9 - _ . ~ /
+ if ((val>=48 && val<=57) || (val>=65 && val<=90) || (val>=97 && val<=122) \
+ || val==45 || val==95 || val==46 || val==126 || val==47) {
+ printf "%c", val
+ } else {
+ printf "%%%s", toupper(h)
+ }
+ }
+ }'
+}
+
+# URL 解碼 (處理 %XX, 含 UTF-8 多 byte)
+url_decode_path() {
+ local s="$1"
+ local converted
+ converted=$(echo "$s" | sed 's/%\([0-9a-fA-F][0-9a-fA-F]\)/\\x\1/g')
+ printf '%b' "$converted"
+}
+
+# 計算速度顯示字串
+# 用法: speed_calc <總bytes> <用時秒數>
+# 輸出: "8.5 MB/s" 或 "512 KB/s" 或 "" (時間=0時)
+speed_calc() {
+ local bytes="$1" secs="$2"
+ [[ -z $bytes || -z $secs || $secs -le 0 || $bytes -le 0 ]] && return
+ if [[ $bytes -ge 1048576 ]]; then
+ echo "$(echo "scale=2; $bytes / $secs / 1048576" | bc) MB/s"
+ elif [[ $bytes -ge 1024 ]]; then
+ echo "$(echo "scale=1; $bytes / $secs / 1024" | bc) KB/s"
+ else
+ echo "$((bytes / secs)) B/s"
+ fi
+}
+
+# 計算清單檔案總大小 (bytes)
+list_total_size() {
+ local list="$1"
+ [[ ! -f $list ]] && { echo 0; return; }
+ awk '{
+ cmd="stat -c%s \""$0"\" 2>/dev/null"
+ cmd | getline sz
+ close(cmd)
+ s+=sz+0
+ } END{print s+0}' "$list"
+}
+
# 收集本次需要上傳的清單 (而非整個Backup)
# 結果寫入 $1 指定的list_file
# 範圍由以下變數控制 (在各備份入口設定,只反映「本次執行」):
@@ -562,38 +682,96 @@ remote_collect_targets() {
[[ -z $name1 ]] && continue
local full="$Backup/$name1"
[[ -d $full ]] || continue
- find "$full" -type f ! -path "*/tools/*" ! -name 'start.sh' ! -name 'restore_settings.conf' > "$tmp_collect" 2>/dev/null
+ find "$full" -type f > "$tmp_collect" 2>/dev/null
[[ -s $tmp_collect ]] && cat "$tmp_collect" >> "$list_file"
done
fi
if [[ $REMOTE_UPLOAD_MEDIA = 1 && -d $Backup/Media ]]; then
- find "$Backup/Media" -type f ! -path "*/tools/*" ! -name 'start.sh' ! -name 'restore_settings.conf' > "$tmp_collect" 2>/dev/null
+ find "$Backup/Media" -type f > "$tmp_collect" 2>/dev/null
[[ -s $tmp_collect ]] && cat "$tmp_collect" >> "$list_file"
fi
if [[ $REMOTE_UPLOAD_WIFI = 1 && -d $Backup/wifi ]]; then
- find "$Backup/wifi" -type f ! -path "*/tools/*" ! -name 'start.sh' ! -name 'restore_settings.conf' > "$tmp_collect" 2>/dev/null
+ find "$Backup/wifi" -type f > "$tmp_collect" 2>/dev/null
[[ -s $tmp_collect ]] && cat "$tmp_collect" >> "$list_file"
fi
+ # 固定附加: tools/ 資料夾、start.sh、restore_settings.conf
+ # 只要 list_file 已經有內容(代表本次有東西要上傳)就一併帶上,讓遠端目錄能獨立還原
+ if [[ -s $list_file ]]; then
+ [[ -d $Backup/tools ]] && find "$Backup/tools" -type f >> "$list_file" 2>/dev/null
+ [[ -f $Backup/start.sh ]] && echo "$Backup/start.sh" >> "$list_file"
+ [[ -f $Backup/restore_settings.conf ]] && echo "$Backup/restore_settings.conf" >> "$list_file"
+ fi
rm -f "$tmp_collect" 2>/dev/null
}
-
+# 掃描區網內所有開放 SMB (445 port) 的主機
+# 50 個 IP 一批並行掃描, 然後 smbclient -L 列出每台的 share
+# 需要 nc 命令 (busybox 通常有)
+scan_smb() {
+ local my_ip
+ my_ip="$(ip route get 1 2>/dev/null | awk '{print $7; exit}')"
+ [[ -z $my_ip ]] && my_ip="$(ifconfig 2>/dev/null | grep -m1 'inet addr:192' | awk '{print $2}' | cut -d: -f2)"
+ [[ -z $my_ip ]] && { echoRgb "無法取得本機 IP" "0"; return 1; }
+ local subnet="${my_ip%.*}"
+ echoRgb "本機 IP: $my_ip" "2"
+ echoRgb "掃描 $subnet.0/24 上的 SMB 主機 (445 port)..." "3"
+ if ! command -v nc >/dev/null 2>&1; then
+ echoRgb "未找到 nc 命令,無法掃描" "0"
+ return 1
+ fi
+ local results="$TMPDIR/.smb_scan_results"
+ : > "$results"
+ local i pids=""
+ for i in $(seq 1 254); do
+ local target="$subnet.$i"
+ ( nc -z -w 1 "$target" 445 >/dev/null 2>&1 && echo "$target" >> "$results" ) &
+ pids="$pids $!"
+ if [[ $((i % 50)) -eq 0 ]]; then
+ wait $pids 2>/dev/null
+ pids=""
+ echoRgb " ...已掃描 $i/254" "2"
+ fi
+ done
+ wait $pids 2>/dev/null
+ if [[ ! -s $results ]]; then
+ echoRgb "未發現 SMB 主機" "0"
+ rm -f "$results"
+ return 1
+ fi
+ echoRgb "------- 掃描完成 -------" "3"
+ sort -t. -k4 -n "$results" | while read -r target; do
+ echoRgb "發現 SMB: $target" "1"
+ # 查主機名 (有 nmblookup 才查)
+ if command -v nmblookup >/dev/null 2>&1; then
+ local hn
+ hn="$(nmblookup -A "$target" 2>/dev/null | awk 'NR==2{print $1}' | tr -d '<>\t ')"
+ [[ -n $hn ]] && echoRgb " 主機名: $hn" "2"
+ fi
+ # 列 share — 用 awk 不用 grep,避開 busybox grep regex 限制
+ smbclient -L "//$target" -N -t 3 -s /dev/null 2>/dev/null \
+ | awk '/Disk/ {print " 共享: "$1}' \
+ | while read -r line; do echoRgb "$line" "2"; done
+ done
+ rm -f "$results"
+}
+# SMB 上傳實作 (使用 smbclient)
+# 流程: 解析 URL → 預檢 → 收集檔案 → 按目錄分組 → 每組一次 smbclient 批次傳輸
+# 跟 upload_remote 的差別: SMB 用獨立的 smbclient 二進制, 不走 curl
upload_smb() {
[[ -z $remote_url ]] && { echoRgb "remote_url未設置" "0"; return 1; }
+ UPLOAD_START_TS=$(date +%s)
echoRgb "使用: $filepath/smbclient" "2"
# 解析 smb://server/share/remotepath
- local url="${remote_url#smb://}"
- url="${url%/}"
- local server="${url%%/*}"
- local after_server="${url#$server/}"
- local share_name="${after_server%%/*}"
- local rem_path="/${after_server#$share_name}"
- rem_path="${rem_path%/}"
- [[ $rem_path = / ]] && rem_path=""
- local share="//$server/$share_name"
- # 拆出 host 和 port (server 可能是 host 或 host:port)
- local host="${server%%:*}"
- local port="${server#*:}"
- [[ $port = $server ]] && port=445
+ remote_parse_smb_url
+ local share="$SMB_SHARE"
+ local rem_path="$SMB_REM_PATH"
+ # 自動加上備份目錄前綴 (跟本地結構一致)
+ local backup_subdir="Backup_${Compression_method}_${user:-0}"
+ rem_path="${rem_path}/${backup_subdir}"
+ # 拆出 host 和 port (從 share 反推)
+ local _hp="${share#//}"; _hp="${_hp%%/*}"
+ local host="${_hp%%:*}"
+ local port="${_hp#*:}"
+ [[ $port = $_hp ]] && port=445
echoRgb "SMB: $share (路徑: ${rem_path:-/})" "2"
# 連線預檢
if ! remote_precheck "$host" "$port"; then
@@ -694,6 +872,8 @@ upload_smb() {
local is_wifi=0
[[ $rem_dir = */wifi || $rem_dir = wifi || $rem_dir = */wifi/* ]] && is_wifi=1
echoRgb "上傳目錄 $rem_dir ($file_count 檔)" "3"
+ local dir_start
+ dir_start=$(date +%s)
# 建立 smbclient batch script
local batch="$TMPDIR/.smb_batch"
echo "cd $rem_dir" > "$batch"
@@ -724,7 +904,14 @@ upload_smb() {
# 此目錄完成,印整體進度 (wifi 不算)
if [[ $is_wifi = 0 && $total_dirs -gt 0 ]]; then
let done_dirs++
- echoRgb "完成$((done_dirs * 100 / total_dirs))%" "3"
+ local dir_speed=""
+ local dir_bytes
+ dir_bytes=$(list_total_size "$gf")
+ local dir_elapsed=$(( $(date +%s) - dir_start ))
+ local sp
+ sp=$(speed_calc "$dir_bytes" "$dir_elapsed")
+ [[ -n $sp ]] && dir_speed=" ($sp)"
+ echoRgb "完成$((done_dirs * 100 / total_dirs))%${dir_speed}" "3"
fi
done
rm -rf "$group_dir" 2>/dev/null
@@ -732,32 +919,32 @@ upload_smb() {
upload_summary "SMB" "$ok_list" "$fail_list"
}
+# 遠端上傳分派器 + WebDAV 實作
+# $1=協議名 (webdav/smb), smb 會轉派給 upload_smb
+# WebDAV: 用 curl 逐檔 PUT, 預先 MKCOL 建好目錄結構
upload_remote() {
local proto="$1"
- [[ $proto = scp ]] && { upload_scp; return $?; }
[[ $proto = smb ]] && { upload_smb; return $?; }
[[ -z $remote_url ]] && { echoRgb "remote_url未設置" "0"; return 1; }
+ UPLOAD_START_TS=$(date +%s)
local base_url
case $proto in
webdav)
base_url="${remote_url%/}"
[[ $base_url != http://* && $base_url != https://* ]] && { echoRgb "WebDAV地址格式錯誤: $remote_url" "0"; return 1; }
;;
- ftp)
- base_url="$remote_url"
- [[ $base_url != ftp://* ]] && { echoRgb "FTP地址格式錯誤,需 ftp:// 開頭" "0"; return 1; }
- ;;
+ *) echoRgb "未支援的協議: $proto" "0"; return 1 ;;
esac
+ # 自動加上備份目錄前綴 (跟本地結構一致)
+ local backup_subdir="Backup_${Compression_method}_${user:-0}"
+ base_url="$base_url/$backup_subdir"
# 連線預檢: 從 base_url 解出 host:port
local _hp="${base_url#*://}"
_hp="${_hp%%/*}"
local _host="${_hp%%:*}"
local _port="${_hp#*:}"
if [[ $_port = $_hp ]]; then
- case $proto in
- webdav) [[ $base_url = https://* ]] && _port=443 || _port=80 ;;
- ftp) _port=21 ;;
- esac
+ [[ $base_url = https://* ]] && _port=443 || _port=80
fi
if ! remote_precheck "$_host" "$_port"; then
echoRgb "$proto伺服器無法連線: $_host:$_port (請檢查WiFi/位址/伺服器狀態)" "0"
@@ -780,25 +967,26 @@ upload_remote() {
total="$(wc -l < "$list_file")"
echoRgb "準備上傳 $total 個檔案" "3"
remote_log "$proto 開始: $base_url, 共 $total 檔"
- # WebDAV: 創建遠程目錄 (MKCOL), FTP: curl --ftp-create-dirs 自動處理
- if [[ $proto = webdav ]]; then
- while read -r f; do
- local d="${f#$Backup/}"
- d="${d%/*}"
- [[ -n $d && $d != "${f#$Backup/}" ]] && echo "$d"
- done < "$list_file" | sort -u | while read -r d; do
- local enc_d="$(echo -n "$d" | busybox sed 's/%/%25/g; s/ /%20/g; s/+/%2B/g; s/#/%23/g')"
- local cur="$base_url"
- local IFS='/'
- set -- $enc_d
- for seg; do
- cur="$cur/$seg"
- curl -sS -L -X MKCOL -u "$remote_user:$remote_pass" "$cur" 2>/dev/null
- done
+ # WebDAV: 先建初始目錄 (Backup_zstd_X 自己)
+ curl -sS -L --http1.1 -X MKCOL -u "$remote_user:$remote_pass" "$base_url" >/dev/null 2>&1
+ # WebDAV: 創建遠程子目錄 (MKCOL)
+ while read -r f; do
+ local d="${f#$Backup/}"
+ d="${d%/*}"
+ [[ -n $d && $d != "${f#$Backup/}" ]] && echo "$d"
+ done < "$list_file" | sort -u | while read -r d; do
+ local enc_d="$(url_encode_path "$d")"
+ local cur="$base_url"
+ local IFS='/'
+ set -- $enc_d
+ for seg; do
+ cur="$cur/$seg"
+ curl -sS -L --http1.1 -X MKCOL -u "$remote_user:$remote_pass" "$cur" >/dev/null 2>&1
done
- fi
+ done
# 預掃總目錄數 (排除 wifi, 不計入百分比)
local total_dirs done_dirs=0 last_dir="" cur_top_dir=""
+ local dir_start=0 dir_bytes_accum=0
while read -r f; do
local top="${f#$Backup/}"
top="${top%%/*}"
@@ -814,56 +1002,60 @@ upload_remote() {
let idx++
local rel="${f#$Backup/}"
local cur_top="${rel%%/*}"
+ # 判斷是「目錄內檔案」還是「根目錄檔案」
+ # rel 含 / → 目錄內檔案, cur_top 是目錄名
+ # rel 不含 / → 根目錄檔案, cur_top 是檔案名本身
+ local is_root_file=0
+ [[ $rel = "$cur_top" ]] && is_root_file=1
# 目錄切換時印上一個目錄的進度
if [[ -n $last_dir && $cur_top != "$last_dir" ]]; then
if [[ $last_dir != wifi && $total_dirs -gt 0 ]]; then
let done_dirs++
- echoRgb "完成$((done_dirs * 100 / total_dirs))%" "3"
+ local dir_speed=""
+ local dir_elapsed=$(( $(date +%s) - dir_start ))
+ local sp
+ sp=$(speed_calc "$dir_bytes_accum" "$dir_elapsed")
+ [[ -n $sp ]] && dir_speed=" ($sp)"
+ echoRgb "完成$((done_dirs * 100 / total_dirs))%${dir_speed}" "3"
fi
- echoRgb "上傳目錄 $cur_top" "3"
+ if [[ $is_root_file = 1 ]]; then
+ echoRgb "上傳檔案 $cur_top" "3"
+ else
+ echoRgb "上傳目錄 $cur_top" "3"
+ fi
+ dir_start=$(date +%s)
+ dir_bytes_accum=0
elif [[ -z $last_dir ]]; then
- echoRgb "上傳目錄 $cur_top" "3"
+ if [[ $is_root_file = 1 ]]; then
+ echoRgb "上傳檔案 $cur_top" "3"
+ else
+ echoRgb "上傳目錄 $cur_top" "3"
+ fi
+ dir_start=$(date +%s)
+ dir_bytes_accum=0
fi
last_dir="$cur_top"
+ # 累計這個目錄已上傳的 bytes
+ local _sz
+ _sz=$(stat -c%s "$f" 2>/dev/null)
+ dir_bytes_accum=$(( dir_bytes_accum + ${_sz:-0} ))
local target_url
if [[ $proto = webdav ]]; then
- local enc_rel="$(echo -n "$rel" | busybox sed 's/%/%25/g; s/ /%20/g; s/+/%2B/g; s/#/%23/g')"
+ local enc_rel="$(url_encode_path "$rel")"
target_url="$base_url/$enc_rel"
else
target_url="$base_url/$rel"
fi
local http_code curl_err
- if [[ $proto = ftp ]]; then
- # 構建 FTP 遠程路徑: /sdcard/backup/Backup_zstd_0/鱼泡网/
- local _ftp_path="${base_url#ftp://}"
- local _ftp_hp="${_ftp_path%%/*}"
- local _ftp_host="${_ftp_hp%%:*}"
- local _ftp_port="${_ftp_hp#*:}"; [[ $_ftp_port = $_ftp_hp ]] && _ftp_port=21
- local _ftp_rem="${_ftp_path#$_ftp_hp}" # /sdcard/backup/Backup_zstd_0
- local _ftp_dir="$_ftp_rem/$(dirname "$rel")"
- [[ -z $_ftp_dir ]] && _ftp_dir="$_ftp_rem"
- if ftpput -u "$remote_user" -p "$remote_pass" -P "$_ftp_port" "$_ftp_host" \
- "$_ftp_dir" "$f" 2>/dev/null; then
- echo "$f" >> "$ok_list"
- echoRgb "[$idx/$total] ✓ $rel" "1"
- else
- echo "$rel (FTP ERR)" >> "$fail_list"
- echoRgb "[$idx/$total] ✗ $rel (FTP ERR)" "0"
- fi
- continue
- fi
- if [[ $proto = webdav ]]; then
- http_code="$(curl -sS -L --connect-timeout 10 \
- -T "$f" -u "$remote_user:$remote_pass" -w '%{http_code}' -o /dev/null "$target_url" 2>"$TMPDIR/.curl_stderr")"
- elif [[ $proto = smb ]]; then
- http_code="$(curl -sS -L --connect-timeout 10 \
- -T "$f" -u "$remote_user:$remote_pass" -w '%{http_code}' -o /dev/null "$target_url" 2>"$TMPDIR/.curl_stderr")"
- else
- http_code="$(curl -sS -L --connect-timeout 10 \
- -T "$f" -u "$remote_user:$remote_pass" -w '%{http_code}' -o /dev/null "$target_url" 2>"$TMPDIR/.curl_stderr")"
- fi
- curl_err="$(cat "$TMPDIR/.curl_stderr" 2>/dev/null)"; rm -f "$TMPDIR/.curl_stderr"
- # http_code 2xx 視為成功;FTP 226/250 也是;0 表示連不上
+ # stderr → 檔案 (curl 自己的錯誤訊息)
+ # body → /dev/null (不需要)
+ # stdout → http_code 變數 (-w 的輸出)
+ http_code="$(curl -sS -L --http1.1 --retry 2 --retry-delay 3 --connect-timeout 10 \
+ -T "$f" -u "$remote_user:$remote_pass" -w '%{http_code}' \
+ -o /dev/null "$target_url" 2>"$TMPDIR/.curl_stderr")"
+ curl_err="$(cat "$TMPDIR/.curl_stderr" 2>/dev/null)"
+ rm -f "$TMPDIR/.curl_stderr"
+ # http_code 2xx 視為成功
case $http_code in
2*)
echo "$f" >> "$ok_list"
@@ -880,169 +1072,17 @@ upload_remote() {
# 最後一個目錄(非wifi)的進度
if [[ -n $last_dir && $last_dir != wifi && $total_dirs -gt 0 ]]; then
let done_dirs++
- echoRgb "完成$((done_dirs * 100 / total_dirs))%" "3"
+ local dir_speed=""
+ local dir_elapsed=$(( $(date +%s) - dir_start ))
+ local sp
+ sp=$(speed_calc "$dir_bytes_accum" "$dir_elapsed")
+ [[ -n $sp ]] && dir_speed=" ($sp)"
+ echoRgb "完成$((done_dirs * 100 / total_dirs))%${dir_speed}" "3"
fi
rm -f "$list_file" 2>/dev/null
upload_summary "$proto" "$ok_list" "$fail_list"
}
-upload_scp() {
- [[ -z $remote_url ]] && { echoRgb "remote_url未設置" "0"; return 1; }
- local use_sshpass
- command -v sshpass >/dev/null 2>&1 && use_sshpass=1
- local host="${remote_url#//}"
- local rpath
- if [[ $host = *:* ]]; then
- rpath="${host#*:}"
- host="${host%%:*}"
- elif [[ $host = */* ]]; then
- rpath="/${host#*/}"
- host="${host%%/*}"
- else
- rpath="/"
- fi
- [[ -z $host ]] && { echoRgb "SCP地址格式錯誤,例: 192.168.1.100:/path" "0"; return 1; }
- # 連線預檢
- if ! remote_precheck "$host" 22; then
- echoRgb "SCP伺服器無法連線: $host:22 (請檢查WiFi/位址/伺服器狀態)" "0"
- echoRgb "本地檔案已保留" "0"
- return 1
- fi
- local opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10"
- # 檢查連接
- if [[ -n $use_sshpass ]]; then
- sshpass -p "$remote_pass" ssh $opts "$remote_user@$host" "echo ok" >/dev/null 2>&1 \
- || { echoRgb "SCP密碼或主機錯誤" "0"; return 1; }
- elif [[ $remote_user != "" ]]; then
- ssh -o BatchMode=yes -o ConnectTimeout=5 $opts "$remote_user@$host" "echo ok" >/dev/null 2>&1 \
- || { echoRgb "SCP需密鑰認證或sshpass (未安裝)" "0"; return 1; }
- else
- echoRgb "remote_user未設置" "0"; return 1
- fi
- local list_file="$TMPDIR/.slist"
- local ok_list="$TMPDIR/.scpok"
- local fail_list="$TMPDIR/.scpfail"
- : > "$ok_list"; : > "$fail_list"
- remote_collect_targets "$list_file"
- if [[ ! -s $list_file ]]; then
- echoRgb "無檔案需上傳" "3"
- rm -f "$list_file" "$ok_list" "$fail_list" 2>/dev/null
- return 0
- fi
- local total
- total="$(wc -l < "$list_file")"
- echoRgb "準備上傳 $total 個檔案" "3"
- remote_log "SCP 開始: $remote_user@$host:$rpath, 共 $total 檔"
- # 先一次性建立所有需要的遠端目錄
- local dirs_file="$TMPDIR/.scp_dirs"
- while read -r f; do
- local rel="${f#$Backup/}"
- local d="${rel%/*}"
- [[ -n $d && $d != "$rel" ]] && echo "$rpath/$d"
- done < "$list_file" | sort -u > "$dirs_file"
- if [[ -s $dirs_file ]]; then
- local mkdir_cmd
- mkdir_cmd="$(awk '{printf "mkdir -p \"%s\"; ", $0}' "$dirs_file")"
- if [[ -n $use_sshpass ]]; then
- sshpass -p "$remote_pass" ssh $opts "$remote_user@$host" "$mkdir_cmd" 2>/dev/null
- else
- ssh $opts "$remote_user@$host" "$mkdir_cmd" 2>/dev/null
- fi
- fi
- rm -f "$dirs_file"
- # 嘗試用 tar pipeline 一次傳完 (比逐檔 scp 快很多)
- # 條件: tar 存在
- local use_tar=0
- command -v tar >/dev/null 2>&1 && use_tar=1
- if [[ $use_tar = 1 ]]; then
- echoRgb "使用 tar pipeline 批次傳輸" "3"
- # 用 -T 從清單讀檔, -C 切到 $Backup 讓相對路徑乾淨
- # 遠端用 tar x -C $rpath 解開
- local tar_rc=1
- # 把 list_file 轉成相對 $Backup 的相對路徑
- local rel_list="$TMPDIR/.scp_rel"
- while read -r f; do
- echo "${f#$Backup/}"
- done < "$list_file" > "$rel_list"
- if [[ -n $use_sshpass ]]; then
- tar -C "$Backup" -cf - -T "$rel_list" 2>/dev/null \
- | sshpass -p "$remote_pass" ssh $opts "$remote_user@$host" "tar -C '$rpath' -xf -" 2>&1
- tar_rc=$?
- else
- tar -C "$Backup" -cf - -T "$rel_list" 2>/dev/null \
- | ssh $opts "$remote_user@$host" "tar -C '$rpath' -xf -" 2>&1
- tar_rc=$?
- fi
- rm -f "$rel_list"
- if [[ $tar_rc -eq 0 ]]; then
- # 全部成功
- cat "$list_file" >> "$ok_list"
- local n=0
- while read -r f; do
- let n++
- echoRgb "[$n/$total] ✓ ${f#$Backup/}" "1"
- done < "$list_file"
- else
- # tar pipeline 失敗,退回逐檔 scp
- echoRgb "tar pipeline 失敗,退回逐檔 scp" "0"
- use_tar=0
- fi
- fi
- if [[ $use_tar = 0 ]]; then
- # 預掃總目錄數
- local total_dirs done_dirs=0 last_dir="" cur_top=""
- while read -r f; do
- local top="${f#$Backup/}"
- top="${top%%/*}"
- [[ $top = wifi ]] && continue
- echo "$top"
- done < "$list_file" | sort -u > "$TMPDIR/.scp_dirs_count"
- total_dirs="$(wc -l < "$TMPDIR/.scp_dirs_count" 2>/dev/null)"
- rm -f "$TMPDIR/.scp_dirs_count"
- local idx=0
- while read -r f; do
- [[ -z $f ]] && continue
- let idx++
- local rel="${f#$Backup/}"
- cur_top="${rel%%/*}"
- # 目錄切換時印上一個目錄的進度
- if [[ -n $last_dir && $cur_top != "$last_dir" ]]; then
- if [[ $last_dir != wifi && $total_dirs -gt 0 ]]; then
- let done_dirs++
- echoRgb "完成$((done_dirs * 100 / total_dirs))%" "3"
- fi
- echoRgb "上傳目錄 $cur_top" "3"
- elif [[ -z $last_dir ]]; then
- echoRgb "上傳目錄 $cur_top" "3"
- fi
- last_dir="$cur_top"
- local target="$remote_user@$host:$rpath/$rel"
- local scp_rc
- if [[ -n $use_sshpass ]]; then
- sshpass -p "$remote_pass" scp $opts "$f" "$target" >/dev/null 2>&1
- scp_rc=$?
- else
- scp $opts "$f" "$target" >/dev/null 2>&1
- scp_rc=$?
- fi
- if [[ $scp_rc -eq 0 ]]; then
- echo "$f" >> "$ok_list"
- echoRgb "[$idx/$total] ✓ $rel" "1"
- else
- echo "$rel" >> "$fail_list"
- echoRgb "[$idx/$total] ✗ $rel" "0"
- remote_log "FAIL SCP $rel"
- fi
- done < "$list_file"
- # 最後一個目錄(非wifi)的進度
- if [[ -n $last_dir && $last_dir != wifi && $total_dirs -gt 0 ]]; then
- let done_dirs++
- echoRgb "完成$((done_dirs * 100 / total_dirs))%" "3"
- fi
- fi
- rm -f "$list_file" 2>/dev/null
- upload_summary "SCP" "$ok_list" "$fail_list"
-}
# 從 remote_url 解析出 host 和 port (依 remote_type)
# 結果寫到全域變數 REMOTE_HOST 和 REMOTE_PORT
@@ -1058,20 +1098,42 @@ remote_parse_endpoint() {
REMOTE_HOST="${u%%:*}"; REMOTE_PORT="${u#*:}"
if [[ $REMOTE_PORT = $u ]]; then [[ $remote_url = https://* ]] && REMOTE_PORT=443 || REMOTE_PORT=80; fi
;;
- ftp)
- local u="${remote_url#ftp://}"; u="${u%%/*}"
- REMOTE_HOST="${u%%:*}"; REMOTE_PORT="${u#*:}"; [[ $REMOTE_PORT = $u ]] && REMOTE_PORT=21
- ;;
- scp)
- local u="${remote_url#//}"
- if [[ $u = *:* ]]; then REMOTE_HOST="${u%%:*}"
- elif [[ $u = */* ]]; then REMOTE_HOST="${u%%/*}"
- else REMOTE_HOST="$u"; fi
- REMOTE_PORT=22
- ;;
esac
}
+# 解析 SMB URL 並設定 SMB_SHARE / SMB_REM_PATH 全域變數
+# SMB_SHARE = //server/share_name (smbclient -L 用)
+# SMB_REM_PATH = /sub/path (空字串代表 share 根目錄, 不含結尾斜線)
+# 重複的 SMB URL 解析邏輯抽出 (原本有 4 個地方各自解析)
+remote_parse_smb_url() {
+ local url="${remote_url#smb://}"; url="${url%/}"
+ local server="${url%%/*}"
+ local after_server="${url#$server/}"
+ local share_name="${after_server%%/*}"
+ local rem_path="/${after_server#$share_name}"
+ rem_path="${rem_path%/}"
+ [[ $rem_path = / ]] && rem_path=""
+ SMB_SHARE="//$server/$share_name"
+ SMB_REM_PATH="$rem_path"
+}
+
+# 過濾 smbclient 輸出的雜訊行 (Try help / dos charset / OS= 等橫幅文字)
+# 用法: smb_filter_noise <輸入字串>
+smb_filter_noise() {
+ echo "$1" | grep -Ev '^Try "help"|^dos charset|^Can.t load|^Domain=|^OS=|^$'
+}
+
+# 判斷目錄是否含任何檔案 (非空)
+# 用法: dir_has_files <目錄路徑>
+# 回傳: 0=有檔案, 1=空目錄或不存在
+dir_has_files() {
+ [[ -d $1 ]] || return 1
+ [[ -n $(find "$1" -type f -print -quit 2>/dev/null) ]]
+}
+
+# 啟動時的遠端設定初始化
+# 規範化 remote_keep_local 值, 驗證 remote_type, TCP 預檢
+# 失敗時清空 remote_type 停用上傳但保留本地備份
remote_setup() {
[[ -z $remote_type ]] && return
# 規範化 remote_keep_local 成 true/false
@@ -1081,9 +1143,9 @@ remote_setup() {
esac
echoRgb "遠程備份: $remote_type -> $remote_url" "3"
case $remote_type in
- webdav|ftp|smb|scp)
+ webdav|smb)
;;
- *) echoRgb "未知遠程類型: $remote_type" "0"; remote_type=""; return 1 ;;
+ *) echoRgb "未知遠程類型: $remote_type (可選: webdav/smb)" "0"; remote_type=""; return 1 ;;
esac
[[ -z $remote_url ]] && { echoRgb "remote_url未設置,停用遠端上傳" "0"; remote_type=""; return 1; }
# 事前連線測試: 從各協議解出 host:port 做快速 TCP 探測
@@ -1108,6 +1170,91 @@ remote_setup() {
# 2. TCP 預檢
# 3. 嘗試認證 + list 遠端目錄
# 不會實際上傳任何東西
+# 單獨上傳某個 app 的備份 (給子目錄 upload.sh 用)
+# 假設呼叫時 Backup 是備份根目錄 (Backup_zstd_X)
+# $1 = app 名 (子目錄名)
+single_upload() {
+ local app_name="$1"
+ [[ -z $app_name ]] && { echoRgb "single_upload: 缺少 app 名" "0"; return 1; }
+ [[ -z $Backup ]] && Backup="$MODDIR"
+ local target="$Backup/$app_name"
+ [[ ! -d $target ]] && { echoRgb "找不到目錄: $target" "0"; return 1; }
+ dir_has_files "$target" || { echoRgb "$app_name 目錄為空,沒有東西可上傳" "0"; return 1; }
+ # 重置範圍 flag, 只標記這一個
+ unset REMOTE_APPLIST REMOTE_UPLOAD_MEDIA REMOTE_UPLOAD_WIFI
+ case $app_name in
+ Media) REMOTE_UPLOAD_MEDIA=1 ;;
+ wifi) REMOTE_UPLOAD_WIFI=1 ;;
+ *) REMOTE_APPLIST="$app_name" ;;
+ esac
+ REMOTE_TRIGGER=1
+ # 啟動時 remote_setup 已跑過, 這裡只檢查狀態
+ [[ -z $remote_type ]] && { echoRgb "遠端未設定或預檢失敗,終止" "0"; return 1; }
+ echoRgb "—————— 單獨上傳: $app_name ——————" "3"
+ case $remote_type in
+ smb) upload_smb ;;
+ webdav) upload_remote "webdav" ;;
+ esac
+ # 已主動上傳, 清旗標避免 trap EXIT 再跑一次
+ unset REMOTE_TRIGGER
+}
+
+# 主選單觸發: 讀 appList.txt + Custom_path, 直接上傳對應目錄
+# 不互動,等同於跑完整備份後的自動上傳,但不重新備份
+upload_current_backup() {
+ backup_path
+ [[ ! -d $Backup ]] && { echoRgb "本地備份目錄不存在: $Backup" "0"; return 1; }
+ echoRgb "本地備份: $Backup" "2"
+ # 讀 appList.txt (跟備份用同一個解析邏輯)
+ local applist=""
+ if [[ -n $list_location ]]; then
+ if [[ ${list_location:0:1} = / ]]; then
+ [[ -f $list_location ]] && applist="$list_location"
+ else
+ [[ -f $MODDIR/$list_location ]] && applist="$MODDIR/$list_location"
+ fi
+ fi
+ [[ -z $applist && -f $MODDIR/appList.txt ]] && applist="$MODDIR/appList.txt"
+ # 組裝 REMOTE_APPLIST (跟 backup() 用同一個變數,讓 collect_targets 認得)
+ unset REMOTE_APPLIST REMOTE_UPLOAD_MEDIA REMOTE_UPLOAD_WIFI
+ if [[ -n $applist ]]; then
+ REMOTE_APPLIST="$(cat "$applist")"
+ local app_count
+ app_count=$(echo "$REMOTE_APPLIST" | grep -cEv '^[[:space:]]*[##!]|^[[:space:]]*$')
+ echoRgb "讀取 $applist (有效 $app_count 個 app)" "2"
+ else
+ echoRgb "找不到 appList.txt" "0"
+ fi
+ # 讀 Custom_path: 有設就帶上 Media
+ if [[ -n $Custom_path ]]; then
+ if dir_has_files "$Backup/Media"; then
+ REMOTE_UPLOAD_MEDIA=1
+ echoRgb "Custom_path 已設, 將上傳 Media" "2"
+ fi
+ fi
+ # wifi 目錄存在就一併上傳
+ if dir_has_files "$Backup/wifi"; then
+ REMOTE_UPLOAD_WIFI=1
+ echoRgb "wifi 目錄存在, 將上傳 wifi" "2"
+ fi
+ if [[ -z $REMOTE_APPLIST && $REMOTE_UPLOAD_MEDIA != 1 && $REMOTE_UPLOAD_WIFI != 1 ]]; then
+ echoRgb "沒有可上傳項目 (appList 為空, Custom_path 未設, 無 wifi)" "0"
+ return 1
+ fi
+ REMOTE_TRIGGER=1
+ # 啟動時 remote_setup 已跑過, 這裡只檢查狀態
+ [[ -z $remote_type ]] && { echoRgb "遠端未設定或預檢失敗,終止" "0"; return 1; }
+ case $remote_type in
+ smb) upload_smb ;;
+ webdav) upload_remote "webdav" ;;
+ esac
+ # 已主動上傳, 清旗標避免 trap EXIT 再跑一次
+ unset REMOTE_TRIGGER
+}
+
+# 測試遠端連線完整性 (主選單第 6 項)
+# 三層測試: TCP 預檢 → 認證 → 路徑訪問
+# 每層失敗都有具體錯誤訊息 (帳密錯/路徑不存在/分享不存在等)
remote_test() {
echoRgb "============== 遠端連線測試 ==============" "3"
if [[ -z $remote_type ]]; then
@@ -1121,10 +1268,11 @@ remote_test() {
[[ -n $remote_pass ]] && echoRgb "密碼: ********" "2" || echoRgb "密碼: (未設)" "2"
echoRgb "保留本地: ${remote_keep_local:-0}" "2"
case $remote_type in
- webdav|ftp|smb|scp) ;;
- *) echoRgb "未知 remote_type: $remote_type (可選: webdav/ftp/smb/scp)" "0"; return 1 ;;
+ webdav|smb) ;;
+ *) echoRgb "未知 remote_type: $remote_type (可選: webdav/smb)" "0"; return 1 ;;
esac
[[ -z $remote_url ]] && { echoRgb "remote_url 未設置" "0"; return 1; }
+ scan_smb
# 第一關: TCP 預檢
remote_parse_endpoint
echoRgb "—————— TCP 連線測試 ——————" "3"
@@ -1143,17 +1291,13 @@ remote_test() {
echoRgb "—————— 認證與列目錄測試 ——————" "3"
case $remote_type in
smb)
- local url="${remote_url#smb://}"; url="${url%/}"
- local server="${url%%/*}"
- local after_server="${url#$server/}"
- local share_name="${after_server%%/*}"
- local rem_path="/${after_server#$share_name}"
- rem_path="${rem_path%/}"; [[ $rem_path = / ]] && rem_path=""
- local share="//$server/$share_name"
+ remote_parse_smb_url
+ local share="$SMB_SHARE"
+ local rem_path="$SMB_REM_PATH"
local out
out="$(smbclient "$share" -U "$remote_user%$remote_pass" -t 10 -s /dev/null \
- -c "cd ${rem_path:-/}; ls; exit" 2>&1 \
- | grep -Ev '^Try "help"|^dos charset|^Can.t load|^Domain=|^OS=' | sed '/^$/d')"
+ -c "cd ${rem_path:-/}; ls; exit" 2>&1)"
+ out="$(smb_filter_noise "$out")"
if echo "$out" | grep -qE 'NT_STATUS_LOGON_FAILURE'; then
echoRgb "認證失敗 (帳號或密碼錯誤)" "0"
return 1
@@ -1174,8 +1318,8 @@ remote_test() {
webdav)
local base_url="${remote_url%/}"
local code
- code="$(/system/bin/curl -sS -L --connect-timeout 10 -u "$remote_user:$remote_pass" \
- -X PROPFIND -H "Depth: 0" -w '%{http_code}' -o /dev/null "$base_url" 2>/dev/null)"
+ code="$(curl -sS -L --http1.1 --connect-timeout 10 -u "$remote_user:$remote_pass" \
+ -X PROPFIND -H "Depth: 0" -w '%{http_code}' -o /dev/null "$base_url" 2>&1)"
case $code in
2*|207) echoRgb "WebDAV 認證通過 (HTTP $code)" "1" ;;
401) echoRgb "認證失敗 (HTTP 401, 帳號或密碼錯誤)" "0"; return 1 ;;
@@ -1185,57 +1329,454 @@ remote_test() {
*) echoRgb "WebDAV 異常 (HTTP $code)" "0"; return 1 ;;
esac
;;
- ftp)
- # 系統 curl 無 FTP,用 tools/curl
- if "$filepath/curl" -sS --connect-timeout 15 --ftp-pasv --max-time 30 \
- -u "$remote_user:$remote_pass" --list-only "$remote_url" 2>/dev/null; then
- echoRgb "FTP 認證通過" "1"
- else
- echoRgb "FTP 異常 (逾時或無法列出目錄)" "0"; return 1
- fi
- ;;
- scp)
- local host="${remote_url#//}" rpath
- if [[ $host = *:* ]]; then rpath="${host#*:}"; host="${host%%:*}"
- elif [[ $host = */* ]]; then rpath="/${host#*/}"; host="${host%%/*}"
- else rpath="/"; fi
- local opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10"
- local out rc
- if command -v sshpass >/dev/null 2>&1; then
- out="$(sshpass -p "$remote_pass" ssh $opts "$remote_user@$host" "ls -d '$rpath' 2>/dev/null || echo NOPATH" 2>&1)"
- rc="$?"
- else
- out="$(ssh -o BatchMode=yes $opts "$remote_user@$host" "ls -d '$rpath' 2>/dev/null || echo NOPATH" 2>&1)"
- rc="$?"
- fi
- if [[ $rc -ne 0 ]]; then
- echoRgb "SSH 連線失敗" "0"
- echo "$out" | head -3
- [[ -z $(command -v sshpass) ]] && echoRgb "提示: 沒有 sshpass,僅支援密鑰認證" "3"
- return 1
- fi
- echoRgb "SSH 認證通過" "1"
- if echo "$out" | grep -q NOPATH; then
- echoRgb "遠端路徑不存在: $rpath (將在首次上傳時建立)" "3"
- else
- echoRgb "遠端路徑 $rpath 可存取" "1"
- fi
- ;;
esac
echoRgb "========================================" "3"
echoRgb "全部測試通過, 可以開始備份" "1"
return 0
}
+# 列出遠端可用的備份目錄並產生 appList_network.txt
+# 流程: 連遠端 → 列 Backup_*_* 目錄 → 讓使用者選 → 檢查必要檔案 → 掃 app 清單 → 輸出
+remote_list_backups() {
+ [[ -z $remote_type ]] && { echoRgb "remote_type 未設定" "0"; return 1; }
+ case $remote_type in
+ smb|webdav) ;;
+ *) echoRgb "下載功能僅支援 smb / webdav (目前 remote_type=$remote_type)" "0"; return 1 ;;
+ esac
+ # 規範化 remote_keep_local
+ case $remote_keep_local in
+ 1|true|True|TRUE) remote_keep_local=true ;;
+ *) remote_keep_local=false ;;
+ esac
+ # 目標目錄 = 跟本地備份一樣的命名規則
+ local target_dir="Backup_${Compression_method}_${user:-0}"
+ echoRgb "目標遠端目錄: $target_dir" "3"
+ # 連線預檢
+ remote_parse_endpoint
+ if ! remote_precheck "$REMOTE_HOST" "$REMOTE_PORT"; then
+ echoRgb "遠端連線失敗: $REMOTE_HOST:$REMOTE_PORT" "0"
+ return 1
+ fi
+ echoRgb "連線到 $remote_type://$REMOTE_HOST:$REMOTE_PORT" "1"
+ # 進入目標目錄, 列出檔案/子資料夾
+ local sub_listing="$TMPDIR/.remote_sub_listing"
+ : > "$sub_listing"
+ if [[ $remote_type = smb ]]; then
+ remote_parse_smb_url
+ local share="$SMB_SHARE"
+ local rem_path="$SMB_REM_PATH"
+ local smb_out
+ smb_out=$(smbclient "$share" -U "$remote_user%$remote_pass" -t 10 -s /dev/null \
+ -c "cd ${rem_path:-/}/$target_dir; ls; exit" 2>&1)
+ if echo "$smb_out" | grep -qE 'NT_STATUS_OBJECT_(PATH|NAME)_NOT_FOUND'; then
+ echoRgb "遠端目錄不存在: $target_dir" "0"
+ echoRgb "請確認遠端有此備份,或備份過至少一次" "3"
+ rm -f "$sub_listing"
+ return 1
+ fi
+ if echo "$smb_out" | grep -qE 'NT_STATUS|ERRSRV'; then
+ echoRgb "讀取遠端失敗:" "0"
+ echo "$smb_out" | grep -E 'NT_STATUS|ERR' | head -3
+ rm -f "$sub_listing"
+ return 1
+ fi
+ # 格式: "D dirname" 或 "N filename"
+ echo "$smb_out" | awk 'NF>=5 && $1 != "." && $1 != ".." {print $2, $1}' > "$sub_listing"
+ elif [[ $remote_type = webdav ]]; then
+ local base_url="${remote_url%/}"
+ local http_code
+ http_code=$(curl -sS -L --http1.1 --connect-timeout 10 -u "$remote_user:$remote_pass" \
+ -X PROPFIND -H "Depth: 1" -w '%{http_code}' -o "$TMPDIR/.wdav_out" \
+ "$base_url/$target_dir/" 2>/dev/null)
+ case $http_code in
+ 2*) ;;
+ 404) echoRgb "遠端目錄不存在: $target_dir (HTTP 404)" "0"
+ echoRgb "請確認遠端有此備份,或備份過至少一次" "3"
+ rm -f "$sub_listing" "$TMPDIR/.wdav_out"; return 1 ;;
+ *) echoRgb "讀取遠端失敗 (HTTP $http_code)" "0"
+ rm -f "$sub_listing" "$TMPDIR/.wdav_out"; return 1 ;;
+ esac
+ local propfind_out
+ propfind_out=$(cat "$TMPDIR/.wdav_out" 2>/dev/null)
+ rm -f "$TMPDIR/.wdav_out"
+ # 解析每個 response, 過濾掉「目錄自己」(href 跟 base 同名)
+ # 收集成 "D|encoded_name" 或 "N|encoded_name"
+ local raw_listing="$TMPDIR/.raw_wdav_listing"
+ echo "$propfind_out" | tr '><' '\n' | awk '
+ /^D:response$/ { in_resp=1; href=""; is_dir=0 }
+ /^\/D:response$/ {
+ if (in_resp && href != "") {
+ # 從 href 取最後一段 (URL 編碼狀態)
+ n = split(href, a, "/")
+ name = a[n]
+ if (name == "" && n > 1) name = a[n-1]
+ if (name != "" && name != "/") {
+ print (is_dir ? "D" : "N") "|" name
+ }
+ }
+ in_resp=0
+ }
+ /^D:href$/ { getline href }
+ /^D:collection/ { is_dir=1 }
+ ' > "$raw_listing"
+ # 過濾掉目錄自己 (encoded 或非 encoded 都比對)
+ # target_dir 是 "Backup_zstd_0" 純 ASCII,不需要編碼
+ grep -vE "\|${target_dir}\$" "$raw_listing" > "$sub_listing"
+ rm -f "$raw_listing"
+ # URL 解碼 (支援 UTF-8 中文)
+ # 用 printf 將 %XX 轉成實際字元
+ local decoded="$TMPDIR/.decoded_listing"
+ : > "$decoded"
+ while IFS='|' read -r typ name; do
+ [[ -z $name ]] && continue
+ # printf '%b' 不認 %XX, 要先轉成 \xXX
+ local converted
+ converted=$(echo "$name" | sed 's/%\([0-9a-fA-F][0-9a-fA-F]\)/\\x\1/g')
+ # 用 printf 把 \xXX 還原成真實字元
+ local real
+ real=$(printf '%b' "$converted")
+ echo "$typ $real" >> "$decoded"
+ done < "$sub_listing"
+ mv "$decoded" "$sub_listing"
+ fi
+ if [[ ! -s $sub_listing ]]; then
+ echoRgb "遠端目錄為空或讀取失敗" "0"
+ rm -f "$sub_listing"
+ return 1
+ fi
+ # 檢查必要檔案: tools/ start.sh restore_settings.conf
+ local has_tools=0 has_start=0 has_conf=0
+ while read -r type name; do
+ case "$name" in
+ tools) [[ $type = D ]] && has_tools=1 ;;
+ start.sh) [[ $type != D ]] && has_start=1 ;;
+ restore_settings.conf) [[ $type != D ]] && has_conf=1 ;;
+ esac
+ done < "$sub_listing"
+ local missing=""
+ [[ $has_tools = 0 ]] && missing="$missing tools/"
+ [[ $has_start = 0 ]] && missing="$missing start.sh"
+ [[ $has_conf = 0 ]] && missing="$missing restore_settings.conf"
+ if [[ -n $missing ]]; then
+ echoRgb "錯誤: 遠端 $target_dir 缺少必要檔案:$missing" "0"
+ echoRgb "此備份不完整,無法用於恢復" "0"
+ rm -f "$sub_listing"
+ return 1
+ fi
+ echoRgb "必要檔案檢查通過 (tools/ start.sh restore_settings.conf)" "1"
+ # 產生 appList_network.txt
+ # 規則:
+ # - 排除 tools, start.sh, restore_settings.conf (固定下載項)
+ # - 是目錄: wifi/Media → 特殊項; 其他 → app
+ # - 是檔案就忽略
+ local out="$MODDIR/appList_network.txt"
+ {
+ echo "# 遠端備份目錄: $target_dir"
+ echo "# 連線: $remote_type://$REMOTE_HOST/"
+ echo "# 用 # 註解掉不要下載的項目, 編輯完選 '從遠端下載備份' 即可"
+ echo ""
+ echo "# ---- 應用 (每行一個 app) ----"
+ local apps="$TMPDIR/.apps_list"
+ : > "$apps"
+ while read -r type name; do
+ [[ $type = D ]] || continue
+ case "$name" in
+ tools|wifi|Media) continue ;;
+ esac
+ echo "$name" >> "$apps"
+ done < "$sub_listing"
+ sort "$apps"
+ rm -f "$apps"
+ echo ""
+ echo "# ---- 特殊項目 (非 app, 有就會下載) ----"
+ while read -r type name; do
+ [[ $type = D ]] || continue
+ case "$name" in
+ wifi|Media) echo "$name" ;;
+ esac
+ done < "$sub_listing"
+ } > "$out"
+ rm -f "$sub_listing"
+ echoRgb "已輸出清單: $out" "1"
+ echoRgb "請編輯該檔案,留下你要下載的項目,然後選 '從遠端下載備份'" "3"
+}
+
+# 依 appList_network.txt 下載備份到 $MODDIR/Backup_*_$user
+remote_download_backup() {
+ [[ -z $remote_type ]] && { echoRgb "remote_type 未設定" "0"; return 1; }
+ case $remote_type in
+ smb|webdav) ;;
+ *) echoRgb "下載功能僅支援 smb / webdav (目前 remote_type=$remote_type)" "0"; return 1 ;;
+ esac
+ local list="$MODDIR/appList_network.txt"
+ if [[ ! -f $list ]]; then
+ echoRgb "找不到 $list" "0"
+ echoRgb "請先執行 '列出遠端備份' 產生清單" "3"
+ return 1
+ fi
+ local dl_start
+ dl_start=$(date +%s)
+ # 目標目錄 = 跟本地備份一樣的命名規則
+ local chosen="Backup_${Compression_method}_${user:-0}"
+ echoRgb "目標遠端目錄: $chosen" "3"
+ # 連線預檢
+ remote_parse_endpoint
+ if ! remote_precheck "$REMOTE_HOST" "$REMOTE_PORT"; then
+ echoRgb "遠端連線失敗: $REMOTE_HOST:$REMOTE_PORT" "0"
+ return 1
+ fi
+ # 解析清單 (去除註解/空行)
+ local items_file="$TMPDIR/.dl_items"
+ grep -Ev '^[[:space:]]*[##]|^[[:space:]]*$' "$list" > "$items_file"
+ if [[ ! -s $items_file ]]; then
+ echoRgb "清單為空,沒有東西需要下載" "0"
+ rm -f "$items_file"
+ return 1
+ fi
+ local item_count
+ item_count=$(wc -l < "$items_file")
+ echoRgb "將下載 $item_count 個項目 + 固定 3 項 (tools/ start.sh restore_settings.conf)" "3"
+ # 下載目標: $MODDIR/$chosen (例: $MODDIR/Backup_zstd_0)
+ local dest="$MODDIR/$chosen"
+ mkdir -p "$dest" 2>/dev/null
+ echoRgb "下載到: $dest" "2"
+ # 依協議分派
+ local fail=0
+ if [[ $remote_type = smb ]]; then
+ _remote_download_smb "$chosen" "$dest" "$items_file" || fail=1
+ elif [[ $remote_type = webdav ]]; then
+ _remote_download_webdav "$chosen" "$dest" "$items_file" || fail=1
+ fi
+ rm -f "$items_file"
+ local dl_elapsed=$(( $(date +%s) - dl_start ))
+ if [[ $fail -eq 0 ]]; then
+ echoRgb "_______________________________________" "2"
+ echoRgb "下載完成: $dest 用時${dl_elapsed}秒" "1"
+ echoRgb "可直接執行 $dest/start.sh 進行恢復" "3"
+ remote_log "下載完成: $dest 用時${dl_elapsed}秒"
+ else
+ echoRgb "下載過程有失敗,請檢查上方訊息 (用時${dl_elapsed}秒)" "0"
+ remote_log "下載失敗 用時${dl_elapsed}秒"
+ return 1
+ fi
+}
+
+# SMB 下載實作
+# 每個 item 一次 smbclient (用 -D 直接切目錄,避免 cd 路徑解析問題)
+# $1=遠端 Backup_zstd_X 目錄名, $2=本地目標, $3=要下載的項目清單檔
+_remote_download_smb() {
+ local chosen="$1" dest="$2" items_file="$3"
+ remote_parse_smb_url
+ local share="$SMB_SHARE"
+ local rem_path="$SMB_REM_PATH"
+ local SMB_OPTS="-t 30 -s /dev/null"
+ local base="${rem_path:+$rem_path/}$chosen"
+ local total_items
+ total_items=$(wc -l < "$items_file")
+ local idx=0 fail_total=0
+ # 下載每個項目 (用 -D 切到指定目錄, 再 mget *)
+ while read -r item; do
+ [[ -z $item ]] && continue
+ let idx++
+ echoRgb "[$idx/$total_items] 下載 $item" "3"
+ mkdir -p "$dest/$item" 2>/dev/null
+ local out
+ out=$(smbclient "$share" -U "$remote_user%$remote_pass" $SMB_OPTS \
+ -D "$base/$item" \
+ -c "lcd $dest/$item; prompt off; recurse on; mget *; exit" 2>&1)
+ out="$(smb_filter_noise "$out")"
+ if echo "$out" | grep -qE 'NT_STATUS_[A-Z_]+' \
+ || [[ -z "$(ls -A "$dest/$item" 2>/dev/null)" ]]; then
+ echoRgb " ✗ $item" "0"
+ echo "$out" | grep -E 'NT_STATUS' | head -3
+ let fail_total++
+ else
+ echoRgb " ✓ $item" "1"
+ fi
+ done < "$items_file"
+ # 固定 3 項: tools/ (獨立連線)
+ echoRgb "下載固定項目: tools/ start.sh restore_settings.conf" "3"
+ mkdir -p "$dest/tools" 2>/dev/null
+ local tools_out
+ tools_out=$(smbclient "$share" -U "$remote_user%$remote_pass" $SMB_OPTS \
+ -D "$base/tools" \
+ -c "lcd $dest/tools; prompt off; recurse on; mget *; exit" 2>&1)
+ tools_out="$(smb_filter_noise "$tools_out")"
+ # 固定 3 項: start.sh / restore_settings.conf (獨立連線)
+ local fix_out
+ fix_out=$(smbclient "$share" -U "$remote_user%$remote_pass" $SMB_OPTS \
+ -D "$base" \
+ -c "lcd $dest; prompt off; get start.sh; get restore_settings.conf; exit" 2>&1)
+ fix_out="$(smb_filter_noise "$fix_out")"
+ # 驗證
+ local fix_fail=0
+ [[ -z "$(ls -A "$dest/tools" 2>/dev/null)" ]] && fix_fail=1
+ [[ ! -s $dest/start.sh ]] && fix_fail=1
+ [[ ! -s $dest/restore_settings.conf ]] && fix_fail=1
+ if [[ $fix_fail = 1 ]]; then
+ echoRgb " 固定項目下載有錯誤" "0"
+ echo "$tools_out
+$fix_out" | grep -E 'NT_STATUS' | head -5
+ [[ -z "$(ls -A "$dest/tools" 2>/dev/null)" ]] && echoRgb " tools/ 為空" "0"
+ [[ ! -s $dest/start.sh ]] && echoRgb " start.sh 缺失或空檔" "0"
+ [[ ! -s $dest/restore_settings.conf ]] && echoRgb " restore_settings.conf 缺失或空檔" "0"
+ let fail_total++
+ else
+ echoRgb " ✓ 固定 3 項" "1"
+ fi
+ [[ -f $dest/start.sh ]] && chmod +x "$dest/start.sh" 2>/dev/null
+ [[ -d $dest/tools ]] && chmod -R +x "$dest/tools" 2>/dev/null
+ [[ $fail_total -eq 0 ]]
+}
+
+# WebDAV 下載實作 (並行模式: 先遞迴掃出所有檔案 url, 再 curl -Z 並行下載)
+_remote_download_webdav() {
+ local chosen="$1" dest="$2" items_file="$3"
+ local base_url="${remote_url%/}/$chosen"
+ local total_items
+ total_items=$(wc -l < "$items_file")
+ local fail_total=0
+ # 遞迴掃描 WebDAV 路徑, 把所有檔案 (含子目錄內) 寫入清單檔
+ # 清單格式: <遠端編碼URL>\t<本地完整路徑>
+ # $1=遠端 base url (已編碼), $2=本地目錄, $3=清單檔
+ _webdav_scan_files() {
+ local r_url="$1" l_dir="$2" out_list="$3"
+ mkdir -p "$l_dir" 2>/dev/null
+ local out
+ out=$(curl -sS -L --http1.1 --connect-timeout 10 -u "$remote_user:$remote_pass" \
+ -X PROPFIND -H "Depth: 1" "$r_url/" 2>/dev/null)
+ local parsed="$TMPDIR/.wdav_scan_$$"
+ echo "$out" | tr '><' '\n' | awk '
+ /^D:response$/ { in_resp=1; href=""; is_dir=0 }
+ /^\/D:response$/ {
+ if (in_resp && href != "") {
+ print (is_dir ? "D" : "F") "\t" href
+ }
+ in_resp=0
+ }
+ /^D:href$/ { getline href }
+ /^D:collection/ { is_dir=1 }
+ ' > "$parsed"
+ local r_url_basename_encoded r_url_basename
+ r_url_basename_encoded="$(echo "$r_url" | sed 's|/$||; s|.*/||')"
+ r_url_basename=$(url_decode_path "$r_url_basename_encoded")
+ local rc=0
+ while IFS=$'\t' read -r typ href; do
+ [[ -z $href ]] && continue
+ local encoded_name name
+ encoded_name="$(echo "$href" | sed 's|/$||; s|.*/||')"
+ name=$(url_decode_path "$encoded_name")
+ [[ -z $name ]] && continue
+ [[ $name = "$r_url_basename" ]] && continue
+ if [[ $typ = D ]]; then
+ _webdav_scan_files "$r_url/$encoded_name" "$l_dir/$name" "$out_list" || rc=1
+ else
+ # 寫入清單: URL\t本地路徑
+ echo -e "$r_url/$encoded_name\t$l_dir/$name" >> "$out_list"
+ fi
+ done < "$parsed"
+ rm -f "$parsed"
+ return $rc
+ }
+ # 用 curl -Z (--parallel) 批次下載清單內的所有檔案
+ # 每行 "url\tlocal_path", 並行度預設 4
+ _webdav_parallel_get() {
+ local list="$1"
+ [[ ! -s $list ]] && return 0
+ # 組裝 curl --config 格式: url + output 一對一
+ local cfg="$TMPDIR/.curl_cfg_$$"
+ : > "$cfg"
+ while IFS=$'\t' read -r url lpath; do
+ # curl config 格式: 每組 url + output
+ # 路徑要用引號避開空白
+ echo "url = \"$url\"" >> "$cfg"
+ echo "output = \"$lpath\"" >> "$cfg"
+ done < "$list"
+ curl -sS -L --http1.1 --connect-timeout 10 --retry 2 -Z --parallel-max 4 \
+ -u "$remote_user:$remote_pass" -K "$cfg" 2>/dev/null
+ local rc=$?
+ rm -f "$cfg"
+ return $rc
+ }
+ # 1. 遞迴掃描所有要下載的檔案
+ local all_files="$TMPDIR/.wdav_all_files"
+ : > "$all_files"
+ local idx=0
+ local scan_fail=0
+ # 1a. items_file 內每個項目
+ while read -r item; do
+ [[ -z $item ]] && continue
+ let idx++
+ local encoded_item
+ encoded_item=$(url_encode_path "$item")
+ [[ -z $encoded_item ]] && encoded_item="$item"
+ echoRgb "[$idx/$total_items] 掃描 $item" "3"
+ if ! _webdav_scan_files "$base_url/$encoded_item" "$dest/$item" "$all_files"; then
+ echoRgb " ✗ 掃描失敗: $item" "0"
+ scan_fail=1
+ let fail_total++
+ fi
+ done < "$items_file"
+ # 1b. 固定項目 tools/
+ echoRgb "掃描固定項目: tools/" "3"
+ if ! _webdav_scan_files "$base_url/tools" "$dest/tools" "$all_files"; then
+ echoRgb " ✗ 掃描失敗: tools/" "0"
+ scan_fail=1
+ let fail_total++
+ fi
+ # 1c. 固定檔案 start.sh / restore_settings.conf 直接加進清單
+ for f in start.sh restore_settings.conf; do
+ echo -e "$base_url/$f\t$dest/$f" >> "$all_files"
+ done
+ # 2. 並行下載
+ local total_files
+ total_files=$(wc -l < "$all_files")
+ echoRgb "並行下載 $total_files 個檔案 (4 路同時)" "3"
+ _webdav_parallel_get "$all_files"
+ rm -f "$all_files"
+ # 3. 事後驗證每個項目本地是否有檔案
+ while read -r item; do
+ [[ -z $item ]] && continue
+ if [[ -z "$(ls -A "$dest/$item" 2>/dev/null)" ]]; then
+ echoRgb " ✗ $item (本地為空)" "0"
+ let fail_total++
+ else
+ echoRgb " ✓ $item" "1"
+ fi
+ done < "$items_file"
+ # 固定項目驗證
+ local fix_fail=0
+ [[ -z "$(ls -A "$dest/tools" 2>/dev/null)" ]] && fix_fail=1
+ [[ ! -s $dest/start.sh ]] && fix_fail=1
+ [[ ! -s $dest/restore_settings.conf ]] && fix_fail=1
+ if [[ $fix_fail = 1 ]]; then
+ echoRgb " 固定項目下載有錯誤" "0"
+ [[ -z "$(ls -A "$dest/tools" 2>/dev/null)" ]] && echoRgb " tools/ 為空" "0"
+ [[ ! -s $dest/start.sh ]] && echoRgb " start.sh 缺失或空檔" "0"
+ [[ ! -s $dest/restore_settings.conf ]] && echoRgb " restore_settings.conf 缺失或空檔" "0"
+ let fail_total++
+ else
+ echoRgb " ✓ 固定 3 項" "1"
+ fi
+ [[ -f $dest/start.sh ]] && chmod +x "$dest/start.sh" 2>/dev/null
+ [[ -d $dest/tools ]] && chmod -R +x "$dest/tools" 2>/dev/null
+ [[ $fail_total -eq 0 ]]
+}
+
+# trap EXIT 觸發的遠端上傳函數
+# 只在 backup/backup_media/backup_update_apk 成功完成後才觸發上傳
+# 其他選項 (測試/列出/下載/退出) 不觸發, 由 REMOTE_TRIGGER 旗標控制
remote_cleanup() {
+ # 只有在 backup / backup_media / backup_update_apk 跑完後才上傳
+ # 其他功能 (測試連線、生成列表、檢查壓縮等) 不觸發上傳
+ [[ $REMOTE_TRIGGER != 1 ]] && return 0
case $remote_type in
webdav) upload_remote "webdav" ;;
- ftp) upload_remote "ftp" ;;
smb) upload_remote "smb" ;;
- scp) upload_remote "scp" ;;
*) return 0 ;;
esac
}
+# 從 /proc/uptime 算出開機時長並格式化成 X天X時X分X秒
Show_boottime() {
awk -F '.' '{run_days=$1 / 86400;run_hour=($1 % 86400)/3600;run_minute=($1 % 3600)/60;run_second=$1 % 60;printf("%d天%d時%d分%d秒",run_days,run_hour,run_minute,run_second)}' /proc/uptime 2>/dev/null
}
@@ -1300,6 +1841,8 @@ case $LANG in
Script_target_language="zh-CN" ;;
esac
echoRgb "$Script_target_language腳本"
+# 互動式輸入選項 (音量鍵或數字鍵)
+# 配合 keycheck 抓音量鍵, 沒音量鍵則退回鍵盤輸入
Enter_options() {
echoRgb "$1" "2"
unset option parameter
@@ -1319,6 +1862,8 @@ Enter_options() {
fi
done
}
+# 在生成 appList.txt 時, 為某個 app 補上額外資訊欄位
+# (例如版本號、apk 路徑等), 供後續使用
add_entry() {
app_name="$1"
package_name="$2"
@@ -1413,6 +1958,7 @@ alias Set_false_Permissions="app_process /system/bin com.xayah.dex.HiddenApiUtil
alias Set_Ops="app_process /system/bin com.xayah.dex.HiddenApiUtil setOpsMode $USER_ID $@"
alias setDisplay="app_process /system/bin com.xayah.dex.HiddenApiUtil setDisplayPowerMode $@"
find_tools_path="$(find "$path_hierarchy"/* -maxdepth 1 -name "tools" -type d ! -path "$path_hierarchy/tools")"
+# 備份 WiFi 密碼到指定目錄, 用 classes.dex 讀 system 內的 WifiConfigStore
backup_wifi() {
local wifi_dir="$1"
[[ -z $wifi_dir ]] && echoRgb "backup_wifi: 目錄參數為空" "0" && return 1
@@ -1424,6 +1970,7 @@ backup_wifi() {
echo_log "wifi備份"
fi
}
+# 從備份恢復 WiFi 密碼 (寫回 WifiConfigStore)
recover_wifi() {
if [[ -d $1 ]]; then
if [[ -f $1/wifi.json ]]; then
@@ -1460,6 +2007,9 @@ Rename_script () {
done
unset HT
}
+# 在指定路徑生成入口腳本 (start.sh / backup.sh / recover.sh / upload.sh)
+# $1=模式 (0/1/2/3/5), $2=目標檔案路徑
+# 模式對應的 MODDIR 路徑推算規則不同 (依腳本放置位置)
touch_shell() {
unset conf_path MODDIR_Path
case $1 in
@@ -1478,7 +2028,12 @@ touch_shell() {
3)
MODDIR_Path='${0%/*/*}'
MODDIR_Path1='${0%/*}'
- conf_path='${0%/*/*}/restore_settings.conf' ;;
+ conf_path='${0%/*/*}/restore_settings.conf' ;;
+ 5)
+ # upload.sh: 放在 Backup_zstd_X//, MODDIR 是 Backup_zstd_X 自己
+ MODDIR_Path='${0%/*/*/*}'
+ MODDIR_Path1='${0%/*/*}'
+ conf_path='${0%/*/*/*}/backup_settings.conf' ;;
esac
echo "#!/system/bin/sh
if [ -f \"$MODDIR_Path/tools/tools.sh\" ]; then
@@ -1493,6 +2048,7 @@ logfile=\"\${0%/*}/log/log_\$(date +%Y-%m-%d_%H-%M).txt\"
. \"$MODDIR_Path/tools/tools.sh\" | tee \"\$logfile\"
sed -i \"\$(printf 's/\033\[[0-9;]*m//g')\" \"\$logfile\"" > "$2"
}
+# 從 zip 檔自動更新腳本 (檢測 $MODDIR 內的 .zip 並提取 tools.sh)
update_script() {
[[ $zipFile = "" ]] && zipFile="$(find "$MODDIR" -maxdepth 1 -name "*.zip" -type f 2>/dev/null)"
if [[ $zipFile != "" ]]; then
@@ -1701,6 +2257,9 @@ else
[[ $update = true ]] && echoRgb "更新獲取失敗" "0"
fi
update_script
+# 計算本地備份目錄路徑
+# 格式: $Output_path/Backup_${Compression_method}_${user}
+# 並建立目錄, 設定 $Backup 全域變數供其他函數使用
backup_path() {
if [[ $Output_path != "" ]]; then
[[ ${Output_path: -1} = / ]] && Output_path="${Output_path%?}"
@@ -1757,6 +2316,7 @@ backup_path() {
echoRgb "$outshow" "2"
remote_setup
}
+# 計算指定目錄的總大小並輸出可讀字串 (KB/MB/GB)
Calculate_size() {
#計算出備份大小跟差異性
filesizee="$(find "$1" -type f -printf "%s\n" | awk '{s+=$1} END {print s}')"
@@ -1771,6 +2331,8 @@ Calculate_size() {
echoRgb "備份資料夾總體大小$(size "$filesizee")"
echoRgb "$NJL"
}
+# 把 bytes 轉成人類可讀格式 (B/KB/MB/GB)
+# 用法: size 或 size <檔案路徑> (會 stat 取大小)
size() {
local b_size get_size
case $1 in
@@ -1806,9 +2368,11 @@ partition_info() {
fi
Occupation_status="$(df -h "${Backup%/*}" | sed -n 's|% /.*|%|p' | awk '{print $(NF-1),$(NF)}')"
}
+# 取得指定 app 的後台運行 PID (用於跳過正在運行的 app)
Process_Information() {
dumpsys activity processes | awk -v key="$1" -v user="$user" 'function getUserFromUid(uid){return int(uid/100000)} /^ *user #[0-9]+ uid=/ {if($0 ~ /ISOLATED uid=[0-9]+/){uid="";pid="";pkg="";next} if(pkg!="" && uid!="" && pid!=""){if((key=="" || pkg==key) && (user=="" || getUserFromUid(uid)==user)){print pid}} uid="";pid="";pkg=""; if($0 ~ /uid=/ && uid==""){tmp=$0; sub(/^.*uid=/,"",tmp); sub(/ .*/,"",tmp); uid=tmp}} /packageList=\{/ {tmp=$0; sub(/^.*packageList=\{/,"",tmp); sub(/\}.*/,"",tmp); pkg=tmp} /pid=/ {tmp=$0; sub(/^.*pid=/,"",tmp); sub(/ .*/,"",tmp); pid=tmp} END {if(pkg!="" && uid!="" && pid!=""){if((key=="" || pkg==key) && (user=="" || getUserFromUid(uid)==user)){print pid}}}'
}
+# 強制終止指定 app (am force-stop + pkill 雙保險)
kill_app() {
process_Information="$(Process_Information "$name2")"
if [[ $name2 != bin.mt.plus && $name2 != com.termux && $name2 != bin.mt.plus.canary ]]; then
@@ -1822,6 +2386,7 @@ kill_app() {
fi
fi
}
+# 備份 app 的 apk 檔 (含 split apk, 用 tar/zstd 打包)
Backup_apk() {
#檢測apk狀態進行備份
#創建APP備份文件夾
@@ -1908,6 +2473,9 @@ Backup_apk() {
fi
[[ $name2 = bin.mt.plus && ! -f $Backup/$name1.apk ]] && cp -r "$apk_path" "$Backup/$name1.apk"
}
+# 備份 app 的 SSAID (應用識別碼)
+# 用 classes.dex 透過 app_process 讀 /data/system/users/$user/settings_ssaid.xml
+# 沒備份 SSAID 恢復後遊戲帳號會被當新裝置
Backup_ssaid() {
Ssaid="$(jq -r '.[] | select(.Ssaid != null).Ssaid' "$app_details")"
ssaid="$(awk -v pkg="$name2" '$1 == pkg {print $2}'<<<"$ssaid_info")"
@@ -1922,6 +2490,8 @@ Backup_ssaid() {
fi
[[ $ssaid = null ]] && ssaid=
}
+# 備份 app 的 runtime permissions (運行時權限)
+# 恢復時可一鍵還原所有授權, 不用再手動點
Backup_Permissions() {
get_Permissions="$(jq -r '.[] | select(.permissions != null).permissions' "$app_details")"
Get_Permissions="$(get_Permissions "$name2" | jq -nR '[inputs | select(. != "null" and length>0) | split(" ") | {(.[0]): (.[1:] | join(" "))}] | if length > 0 then add else empty end')"
@@ -2067,6 +2637,8 @@ Backup_data() {
[[ -f $data_path ]] && echoRgb "$1是一個文件 不支持備份" "0"
fi
}
+# 恢復 app 的 data 資料 (解壓 tar.zst 到 /data/data//)
+# 處理 selinux context、uid 綁定
Release_data() {
tar_path="$1"
X="$path2/$name2"
@@ -2187,6 +2759,7 @@ Release_data() {
esac
rm -rf "$TMPDIR"/*
}
+# 安裝 apk (含 split apk 處理), 自動繞過安裝驗證
installapk() {
apkfile="$(find "$Backup_folder" -maxdepth 1 -name "apk.*" -type f 2>/dev/null)"
if [[ $apkfile != "" ]]; then
@@ -2230,6 +2803,8 @@ installapk() {
esac
fi
}
+# 關閉 apk 安裝驗證 (verifier_verify_adb_installs)
+# 避免 Play Protect / 系統驗證攔截批次安裝
disable_verify() {
#禁用apk驗證
settings put global verifier_verify_adb_installs 0 2>/dev/null
@@ -2269,6 +2844,8 @@ disable_verify() {
fi
fi
}
+# 從 app 安裝資訊取得 app 名稱 / apk 路徑 / 版本等資料
+# 用 classes.dex 透過 hidden API 拿到完整 PackageInfo
get_name(){
txt="$MODDIR/appList.txt"
txt="${txt/'/storage/emulated/'/'/data/media/'}"
@@ -2432,11 +3009,14 @@ get_name(){
endtime 1
exit 0
}
+# 腳本自我檢測 (檢查工具完整性、權限、環境)
+# 啟動時呼叫, 出問題會提示並退出
self_test() {
if [[ $(dumpsys deviceidle get charging) = false && $(dumpsys battery | awk '/level/{print $2}' | grep -Eo '[0-9]+') -le 15 ]]; then
echoRgb "電量$(dumpsys battery | awk '/level/{print $2}' | grep -Eo '[0-9]+')%太低且未充電\n -為防止備份檔案或是恢復因低電量強制關機導致檔案損毀\n -請連接充電器後備份" "0" && exit 2
fi
}
+# 驗證單一檔案的 sha256 校驗碼 (對照 tools 目錄內預存的雜湊)
Validation_file() {
MODDIR_NAME="${1%/*}"
MODDIR_NAME="${MODDIR_NAME##*/}"
@@ -2448,6 +3028,8 @@ Validation_file() {
esac
echo_log "${FILE_NAME##*.}校驗"
}
+# 檢查壓縮檔完整性 (zstd -t / tar -t)
+# 主選單「壓縮檔完整性檢查」呼叫
Check_archive() {
starttime1="$(date -u "+%s")"
error_log="$TMPDIR/error_log"
@@ -2514,6 +3096,8 @@ restore_permissions () {
[[ $? != 0 ]] && echo_log "設置ops權限"
}
}
+# 取得當前正在後台運行的所有 app 列表
+# 配合「後台應用忽略」設定, 跳過正在運行的 app 不備份
Background_application_list() {
[[ $activity != false ]] && {
if [[ $Background_apps_ignore = true || $1 = debug ]]; then
@@ -2538,6 +3122,9 @@ else
echoRgb "後台應用獲取失敗" "0" activity=false
fi
unset Backstage
+# 主備份函數 - 對 appList.txt 內所有 app 執行完整備份
+# 流程: 讀清單 → 逐個 app → 備份 apk + data + user_de + obb → 備份 SSAID/權限
+# 結尾備份 wifi、生成 start.sh、設置 REMOTE_TRIGGER=1 觸發遠端上傳
backup() {
self_test
case $MODDIR in
@@ -2877,6 +3464,7 @@ backup() {
[[ -f $Backup_folder/${name2}.sh ]] && rm -rf "$Backup_folder/${name2}.sh"
[[ ! -f $Backup_folder/recover.sh ]] && touch_shell "3" "$Backup_folder/recover.sh"
[[ ! -f $Backup_folder/backup.sh ]] && touch_shell "1" "$Backup_folder/backup.sh"
+ [[ ! -f $Backup_folder/upload.sh ]] && touch_shell "5" "$Backup_folder/upload.sh"
fi
endtime 2 "$name1 備份" "3"
lxj="$(echo "$Occupation_status" | awk '{print $3}' | sed 's/%//g')"
@@ -2972,21 +3560,29 @@ backup() {
notification "105" "備份完成 $(endtime 1 "批量備份開始到結束")"
[[ -f $txt_path ]] && chown "$(stat -c '%u:%g' '/data/media/0/Download')" "$txt_path"
[[ -f $txt_path2 ]] && chown "$(stat -c '%u:%g' '/data/media/0/Download')" "$txt_path2"
+ REMOTE_TRIGGER=1
exit
}
+# 增量備份: 只備份版本號有更新的 app
+# 對照 app_details.json 內舊版本, 沒變動的跳過
backup_update_apk() {
Update_backup='true'
backup
}
+# 重新生成應用列表的 app 名稱欄位 (恢復模式選單用)
dumpname() {
get_name "Apkname"
}
+# 轉換 app 資料夾名稱 (舊格式 → 新格式)
convert() {
get_name "convert"
}
+# 對整個備份目錄做壓縮檔完整性檢查 (Check_archive 的批次入口)
check_file() {
Check_archive "$MODDIR"
}
+# 主恢復函數 - 安裝 apk + 恢復 data + 還原 SSAID/權限
+# ssaid_mode=true 時只恢復含 SSAID 的 app
Restore() {
self_test
disable_verify
@@ -3273,6 +3869,7 @@ Restore() {
notification "109" "恢復完成 $(endtime 1 "$DX開始到結束")"
rm -rf "$TMPDIR"/*
}
+# 恢復自定義資料夾 (Media 等)
Restore3() {
self_test
case $Lo in
@@ -3316,6 +3913,8 @@ Restore3() {
endtime 1 "恢復結束"
notification "108" "Media恢復完成 $(endtime 1 "Media恢復")"
}
+# 僅恢復包含 SSAID 應用 (不含數據,只裝 apk + 還原 SSAID)
+# 用於只想保留遊戲帳號識別、不要舊存檔的場景
Restore4() {
if [[ $ssaid_mode_1 = true ]]; then
while read -r; do
@@ -3379,6 +3978,8 @@ Restore4() {
done
fi
}
+# 生成應用列表 (掃描所有已安裝 user app, 輸出到 appList.txt)
+# 配合 blacklist/whitelist 過濾系統 app
Getlist() {
case $MODDIR in
/storage/emulated/0/Android/* | /data/media/0/Android/* | /sdcard/Android/*) echoRgb "請勿在$MODDIR內生成列表" "0" && exit 2 ;;
@@ -3535,6 +4136,9 @@ Getlist() {
chown "$(stat -c '%u:%g' '/data/media/0/Download')" "$MODDIR/appList.txt"
echoRgb "輸出包名結束 請查看$MODDIR/appList.txt"
}
+# 備份自定義資料夾 (來自 Custom_path 設定)
+# 例: Pictures / Download / DCIM / /data/adb 等
+# 結尾設 REMOTE_UPLOAD_MEDIA=1 + REMOTE_TRIGGER=1
backup_media() {
self_test
backup_path
@@ -3577,7 +4181,9 @@ backup_media() {
else
echoRgb "自定義路徑為空 無法備份" "0"
fi
+ REMOTE_TRIGGER=1
}
+# 從 tools/Device_List 對照表查詢設備識別資訊 (處理器型號、RAM 規格等)
Device_List() {
URL="https://raw.githubusercontent.com/KHwang9883/MobileModels/refs/heads/master/brands"
rm -rf "$tools_path/Device_List"
@@ -3634,6 +4240,8 @@ Device_List() {
echoRgb "下載機型失敗"
fi
}
+# 主選單「備份WiFi」入口
+# 建立備份目錄結構 + 複製 tools/ + 生成 start.sh + 備份 wifi.json
wifi() {
backup_path
[[ ! -d $Backup/tools ]] && cp -r "$tools_path" "$Backup"
@@ -3644,6 +4252,13 @@ wifi() {
}
if [[ $0 = *backup.sh ]]; then
start=backup
+elif [[ $0 = *upload.sh ]]; then
+ # upload.sh 位於 Backup_zstd_X//upload.sh
+ # MODDIR 已被入口腳本設成 Backup_zstd_X
+ # 取得 app 名 = upload.sh 所在的資料夾名
+ _upload_app="${0%/*}"
+ _upload_app="${_upload_app##*/}"
+ start="single_upload \"$_upload_app\""
else
[[ $0 = *recover.sh ]] && start=Restore
fi
@@ -3665,6 +4280,9 @@ else
"備份自定義資料夾"
"備份WiFi"
"測試遠端連線"
+ "單獨上傳當前備份"
+ "列出遠端備份(產生 appList_network.txt)"
+ "從遠端下載備份"
"殺死運行中腳本"
)
commands=(
@@ -3674,6 +4292,9 @@ else
"backup_media"
"wifi"
"remote_test"
+ "upload_current_backup"
+ "remote_list_backups"
+ "remote_download_backup"
"echoRgb '等待腳本停止中,請稍後.....' && echoRgb '腳本終止'; exit"
)
elif [[ -f $MODDIR/restore_settings.conf ]]; then
@@ -3711,7 +4332,7 @@ else
x|X)
echoRgb "已退出腳本" "0"
exit 0 ;;
- [1-9])
+ [0-9]*)
if (( choice >= 1 && choice <= ${#steps[@]} )); then
index="$((choice - 1))"
echo "執行:${steps[$index]}"
@@ -3728,4 +4349,4 @@ else
*)
echoRgb "輸入錯誤,請重新輸入有效的數字或輸入 x 離開。" "0" ;;
esac
-fi
\ No newline at end of file
+fi