在使用者離線時代為工作
狀態:delegate(委派)已實作;真實裝置上的 device lifecycle acceptance 驗收尚待完成(pending)。預設的授權只允許一個操作:files.moveFromStaging。
delegate 讓你的套件在使用者關閉瀏覽器後,仍能繼續替該 DSM 使用者工作。管理員只需核准一次儲存範圍,每位使用者再在範圍內選一個目的地資料夾。之後你的背景工作就能用使用者自己的權限,把完成的檔案移進該資料夾,直到使用者或管理員撤銷為止。套件不會以 root 執行這些寫入,也不會覆蓋既有檔案。
建立含此能力的新專案,或把它加進既有的 native-go 專案:
synopkgland init -t native-go --with delegate my-appsynopkgland add delegate兩者都會把下列區塊寫入 dsm-package.yaml。宣告 delegate 也會一併啟用 jobs。
capabilities: delegate: scope: install-selected operations: [files.moveFromStaging] destinations: [save] lifetime: until-revoked| 欄位 | 允許的值 |
|---|---|
scope | install-selected:安裝後由管理員選 home 或某個共用資料夾。home:每位使用者自己的家目錄。shared:<name>:單一共用資料夾,例如 shared:photos。名稱不可含 /、\、: 或控制字元。 |
lifetime | until-revoked |
operations | [files.moveFromStaging] |
destinations | 1 到 64 個不重複的鍵。每個鍵須符合 ^[a-z][a-z0-9_-]{0,63}$。鍵是你的程式碼用的名稱,不是路徑。 |
install-selected 必須搭配 lifetime: until-revoked。只要你設定了 destinations 或 lifetime,或列出 files.moveFromStaging,表中四個欄位就都必須符合這些值。
add delegate 不會修改 backend/internal/api/permissions.json,但 init --with delegate 會。如果在 add delegate 之後仍要讓一般使用者看到 App,請在該檔設定 "app.status": "authenticated"。所有方法的預設值是 administrators。
delegate 需要 native-go 專案。binary 與 compose 專案不接受。armv7 的建置尚未在 NAS 上驗證。
各角色看到什麼
Section titled “各角色看到什麼”設定頁使用 BackgroundSaveSettings,每個目的地 key 只有一列「存放資料夾」,顯示已選資料夾,並提供「選擇資料夾」與「撤銷授權」。App 透過 delegate API 自行管理工作狀態及取消操作。
| 誰 | 會發生什麼 |
|---|---|
管理員,使用 install-selected | 選擇家目錄範圍或共用資料夾根目錄,輸入 DSM 密碼。一次送出就會透過 executor root task 核准範圍、讀回確認,並將該根目錄授權為這位管理員自己的存檔目的地。 |
| 一般使用者 | 透過限定範圍的 NAS picker,選擇核准範圍內的資料夾。確認即時 DSM 工作階段與寫入權限後,建立該使用者的授權;不需要管理員密碼,也沒有第二個同意對話框。 |
使用者,使用固定的 home 或 shared:<name> 範圍 | 在 manifest 指定的範圍內選擇目的地。 |
| 管理員,存檔已透過 API 停用 | 選「啟用背景存檔」,之後使用者重新選擇目的地。 |
開啟設定頁會保留既有目的地。管理員下一次選擇範圍時,會以所選根目錄作為自己的目的地。更換已確認的範圍會撤銷所有舊授權並取消待存檔工作,已存檔檔案保留。「撤銷授權」只影響目前登入使用者的授權。
plain UI 在 DSM 視窗自己的面板核准,管理員在面板內輸入密碼。那個面板核准時不帶範圍,所以只在 home 或 shared:<name>(範圍已固定在 manifest 中)時提供。使用 install-selected 時,核准必須帶上管理員選的範圍,所以請使用 Vue UI。
--ui plain 的頁面沒有目的地選擇器,也無法選範圍,所以 init --ui plain --with delegate 會以 CAPABILITY_COMBINATION_INVALID 失敗,check 也會拒絕 plain UI 搭配 scope: install-selected。請使用 Vue UI,或用下一節的方法自行建立控制項。
install-selected 專案第一次 synopkgland deploy 必須提供安裝精靈的範圍步驟:--param delegate_share=<共用資料夾> 或 --param delegate_home=true。未提供時,deploy 會在建置前以 DEPLOY_PARAM_REQUIRED 失敗。升級沿用已確認的範圍,兩者都不需要。
synopkgland status 會顯示管理員核准是 ready、needs-admin-approval 還是 blocked。使用 identity: standalone 時請用 https:// 主機;status 透過需要 HTTPS 的短期 App 工作階段讀取 executor。
backend/internal/app/delegate.go 以 Go 套件 github.com/synopackageland/syno-pkg-land-sdk/delegate 建立 delegate.Service(下文的 svc)並註冊其 route。SDK 有一個可運作的產生端 delegate/example.go(方法 delegate.example.export)。BackgroundSavePanel 可透過 opt-in 的 sampleSave producer 提供此範例。工作受理回覆不代表檔案已存妥。請沿用這個流程:
- 在使用者工作階段仍有效時,查詢目的地:
svc.Destination(ctx, caller, request, "save")。把Destination.ID存進你自己的工作紀錄。 - 把檔案產生在服務的暫存目錄
svc.StagingDir,檔案權限用0600。 - 之後不需要工作階段,再移動檔案:
svc.MoveFromStaging(ctx, delegate.MoveParams{DelegationID: id, JobID: jobID, Staging: name, Name: name, Size: size})。此函式拒絕以 root 執行。
selected, err := svc.Destination(ctx, caller, request, "save")// ... 把完成的檔案以 <name> 寫入 svc.StagingDir ..._, err = svc.MoveFromStaging(ctx, delegate.MoveParams{ DelegationID: selected.Destination.ID, JobID: jobID, Staging: name, Name: name, Size: size,})重複使用同一個 JobID 會回傳原本的操作,而不是開始新的。每個新檔案請用新的 JobID。
子資料夾與同名衝突
Section titled “子資料夾與同名衝突”Name 可以包含選定目的地底下的資料夾,例如 Album/CD1/01.flac。移動時以使用者自己的 DSM 權限建立缺少的資料夾,因此擁有者與繼承的 ACL 和使用者在 File Station 自己建立的資料夾一致。已存在的資料夾會沿用,所以重試或同一張專輯的下一個檔案不會建立重複的資料夾。資料夾只在檔案發布時才建立:被取消或撤銷的移動不會留下資料夾。
| 規則 | 值 |
|---|---|
| 分隔符號 | /。開頭與結尾不可有 /,不可有空白段、. 或 ..。 |
| 資料夾名稱 | 1 到 255 位元組。不可含控制字元,也不可含 \ : * ? " < > |。不可是 #recycle、#snapshot 或 @eaDir(不分大小寫)。 |
| 檔名 | 與不含資料夾的名稱規則相同,既有呼叫的行為不變。 |
| 資料夾層數 | 最多 16 層,所以一次移動最多建立 16 個資料夾。 |
| 整個名稱 | 最多 1024 位元組。 |
| Unicode | 照送來的位元組儲存,不做正規化。若要與 Windows 輸入的名稱在 File Station 中一致,請送 NFC。 |
| 符號連結與掛載點 | 名稱中的資料夾若是符號連結、檔案或另一個掛載的磁碟區,會以 destination-not-directory 拒絕。 |
用 Conflict 選擇檔名已被使用時的處理方式:
Conflict | 效果 |
|---|---|
fail 或空白 | 預設。移動以 destination-exists 失敗。 |
overwrite | 一次原子地取代既有檔案:新檔先以暫存名稱寫入,再改名蓋過舊檔。移動失敗時舊檔保持完整。只會取代使用者可寫入的一般檔案;資料夾、符號連結或裝置檔會以 destination-exists 失敗。 |
rename | 發布為 name (1).ext、name (2).ext,依此類推,最多到 name (99).ext。receipt.path 會回報實際使用的名稱。 |
保留種子目錄結構的 Gopeed 式下載:
for _, f := range task.Files { // f.Path is relative, e.g. "Album/CD1/01.flac" _, err = svc.MoveFromStaging(ctx, delegate.MoveParams{ DelegationID: selected.Destination.ID, JobID: task.ID + "-" + strconv.Itoa(f.Index), Staging: f.StagingName, Name: task.Name + "/" + f.Path, Size: f.Size, Conflict: "rename", })}你的 UI 用 ctx.call 呼叫這些後端方法。每個方法都需要已登入且 DSM 工作階段有效的使用者,否則回傳 session_required。
| 方法 | 誰能呼叫 | 結果 |
|---|---|---|
delegate.status | 使用者 | state(ready、needs-admin-approval、blocked、needs-scope-confirmation 或 disabled)、disabled、lifetime、destinations、scope_mode;install-selected 時另有 preselection、selection、mismatch |
delegate.destination | 使用者 | {key, selection, destination, status}。status 為 active、missing、revoked(此 key 的授權已撤銷,且沒有其他授權)、reselection-required、needs-scope-confirmation 或 disabled。 |
delegate.select_destination | 使用者 | 參數 {key, path}。檢查使用者可寫入該處後建立授權。使用者無法寫入的資料夾回傳 PERMISSION_DENIED。 |
delegate.move_from_staging | 使用者 | 參數 {delegation_id, job_id, staging, name, size, conflict?}。name 可以包含資料夾。回傳 {job_id, state}。呼叫者必須擁有該授權。 |
delegate.operations.list | 使用者 | 參數 {}。回傳該使用者的存檔。executor 已受理但尚未開始的存檔為 queued;uncertain 表示 executor 無法確定是否已受理。 |
delegate.operations.get、delegate.operations.cancel | 使用者 | 參數 {delegation_id, job_id}。回傳 {job_id, state, receipt?, code?, reason?}。 |
delegate.disable、delegate.enable | 管理員 | 參數 {}。兩者都會撤銷此 App 的所有授權。 |
delegate.audit | 使用者 | 管理員看得到此 App 的所有事件;其他使用者只看得到自己的。最多 256 筆。 |
delegate.example.export | 使用者 | 參數 {key, job_id}。執行範例產生端。 |
存檔的結果是 committed(附 receipt,即 {path, size})、canceled、interrupted 或 uncertain。請不要自動重試 uncertain 或 interrupted 的存檔,要先檢查目的地。取消時可能回報的原因有 operation-canceled-by-user、grant-revoked、scope-changed、integration-disabled 或 destination-replaced。由 executor 自行停止存檔時,原因是 grant-revoked、validation-denied、integration-disabled 或 storage-error,且即使使用者已失去寫入權限,executor 也會移除未完成的檔案。
與 daemon 並用
Section titled “與 daemon 並用”只有當 delegate 使用 lifetime: until-revoked 時,daemon 才能與 jobs 並存。daemon 的欄位請見Manifest 參考。
有時限的授權
Section titled “有時限的授權”如果你沒有設定 destinations、lifetime 與 files.moveFromStaging,delegate 會以有時限的模式運作。此時 operations 可以是下列檔案操作中任意不重複的子集,scope 是 home 或 shared:<name>。delegate.create 接受 1 到 3600 的 ttl_seconds,以及這一份授權的操作。產生的 worker 只處理下載與 SHA-256 工作,下載限制為 64 MiB 與 60 秒。預設授權不允許這些檔案操作:呼叫它們會回傳 POLICY_DENIED。
工作透過 job.Files() 呼叫這些操作。每個路徑都必須在授權的資料夾內。executor 會在呼叫前檢查每個路徑,並再次檢查 File Station 回報的路徑。除了 files.write 與 files.read,executor 都以該使用者身分透過 synowebapi 呼叫 File Station,因此 DSM 套用的是該使用者自己的權限。只有固定允許清單中的 File Station 方法能以這種方式呼叫。
| 操作 | Go 方法 | 作用 | 使用者需要的 DSM 權限 |
|---|---|---|---|
files.list、files.info | List、Info | 列出資料夾,或回傳單一項目。每個項目都包含 size。 | 讀取 |
files.createFolder | CreateFolder、CreateFolderAll | 建立資料夾。CreateFolderAll 也會建立缺少的上層資料夾,如同 mkdir -p。 | 上層資料夾的寫入 |
files.checkPermission | CheckPermission | 確認使用者能寫入資料夾。它會寫入再移除一個每次隨機命名的探測檔 .synopkgland-permission-probe-<32 hex>。 | 無 |
files.write | Write | 把 staging 中最多 64 MiB 的內容存成新檔案。 | 寫入 |
files.read | Read | 回傳最多 64 MiB 的一般檔案。executor 會視需要建立 staging 資料夾,再以使用者身分把檔案複製進去,且不跟隨符號連結。 | 讀取 |
files.rename | Rename | 原地重新命名檔案或資料夾。 | 修改 |
files.copy | Copy | 開始把檔案或資料夾複製到另一個資料夾。 | 來源的讀取、目的地的寫入 |
files.move | Move | 開始把檔案或資料夾移到另一個資料夾。 | 來源的刪除、目的地的寫入 |
files.delete | Delete | 開始刪除檔案或資料夾。 | 刪除 |
規則:
- 每個用到的操作都要宣告。寫入類操作不會隱含
files.delete,因此只存檔的 App 無法刪除檔案。核准的 policy 沒列出的操作回傳POLICY_DENIED;授權沒列出的操作回傳DELEGATION_DENIED。 - 授權的資料夾本身不能被重新命名、複製、移動或刪除(
SCOPE_DENIED)。資料夾外或經過符號連結的路徑也是SCOPE_DENIED;資料夾內不存在的路徑是not-found。 Copy與Move遇到已被使用的名稱會拒絕;delegate.ConflictOverwrite會取代它。它們沒有rename政策。- 刪除資料夾需要
Recursive: true,否則回傳recursive-required。刪除的項目會進入共用資料夾的資源回收筒。如果該共用資料夾沒有資源回收筒,Delete會回傳recycle-bin-disabled,除非你設定Permanent: true。資源回收筒開啟時,即使設定Permanent: true,DSM 仍會把項目留在回收筒。
長時間的檔案操作
Section titled “長時間的檔案操作”Copy、Move 與 Delete 會啟動 File Station 背景任務,並回傳帶有 TaskID 的 TaskState。持續呼叫 TaskStatus,直到 State 為 finished、failed(附 Error)或 canceled。Progress 是 0 到 1 的比例。任務結束後,TaskStatus 會持續回傳相同的最終狀態。CancelTask 會停止任務。只有啟動任務的工作能追蹤它。撤銷授權會停止其執行中的任務。
files := job.Files()task, err := files.Move(ctx, "/home/Downloads/album", "/home/Music", "")for err == nil && task.State == "running" { time.Sleep(time.Second) task, err = files.TaskStatus(ctx, task.TaskID)}File Station 會逐項回報拒絕原因,而不只是整個任務的結果。executor 會讀取這些原因並回傳該項目的錯誤碼:沒有權限是 PERMISSION_DENIED、名稱已被使用是 destination-exists、路徑不存在是 not-found、磁碟區拒絕過長的名稱是 name-too-long、磁碟區已滿是 NO_SPACE,在檔案底下 CreateFolder 是 destination-not-directory。這些錯誤碼不可重試。刪除後項目仍在時,會以 PERMISSION_DENIED 或 DSM_FAILED 失敗。
executor 執行期間最多追蹤 64 個任務。套件重新啟動後,TaskStatus 會回傳 task-not-found。DSM 已不認得該任務時,State 為 unknown;此時用 Info 確認結果。DSM 能否把任務狀態回報給之後另一個 synowebapi 程序,尚未在 NAS 上驗證。
| 限制 | 值 |
|---|---|
| 可移動的檔案大小 | 16 GiB |
| 排隊中的存檔 | 16 |
| 同時複製數 | 1 |
| 單次複製嘗試 | 24 小時 |
| 目的地鍵 | 1 到 64 個 |
| 目的地已有同名檔案 | 只有 conflict: overwrite 會取代 |
name 中的資料夾 | 16 層,總長 1024 位元組 |
files.read 讀取的檔案 | 64 MiB |
| 追蹤中的 File Station 任務 | 64 |
conflict: rename 的替代名稱 | name (1) 到 name (99) |
| 後端 | 僅 native-go |
| 架構 | x86_64、aarch64 與 armv7(armv7 尚未在 NAS 上驗證) |
delegate.audit 回傳的事件 | 256 筆 |
| DSM 7.1 上的核准 | 使用較舊的排程 API;見Root 操作的 DSM 7.1 |
生命週期的影響:
| 事件 | 影響 |
|---|---|
| 停止套件 | 存檔暫停。核准、授權與已確認的範圍都會保留。 |
| 升級 | 只有在已核准的策略仍然相符時才會恢復存檔(符合 INFO 錨點的新 executor 不需核准),否則 App 停在 needs-admin-approval。進行中的複製會以 interrupted 結束。 |
| 解除安裝 | 移除核准、授權、已確認的範圍、root 任務與這個 App 的應用程式權限規則,並停止 portal 網址。重新安裝需要重新核准。請見資料與移除。 |
變更 scope、destinations、operations、lifetime 或 entry 欄位 | 管理員必須重新核准。 |
| 症狀或錯誤碼 | 原因 | 解法 |
|---|---|---|
needs-admin-approval | 管理員尚未核准,或已核准的策略已改變。 | 管理員開啟設定並重新核准。 |
needs-scope-confirmation | scope: install-selected,但尚未確認範圍。 | 管理員在設定中選擇存放資料夾的範圍,並輸入 DSM 密碼確認。 |
disabled | 管理員停用了背景存檔。 | 管理員選「啟用背景存檔」。使用者重新選擇目的地。 |
revoked | 這個目的地的授權已被撤銷。 | 使用者重新選擇目的地。 |
reselection-required | 範圍已更換,或授權與已確認的範圍不符。 | 使用者重新選擇目的地。 |
invalid-destination | 該鍵不在 destinations 內,或使用者對它沒有有效授權。 | 修正鍵,或讓使用者選擇目的地。 |
destination-exists | 資料夾內已有同名檔案。 | 以新的 JobID 搭配新的檔名,或設定 conflict。 |
invalid-path、path-too-deep | name 中的資料夾違反上述規則,或超過 16 層。 | 修正名稱,並使用新的 JobID。 |
destination-not-directory | name 中的某個資料夾其實是檔案、符號連結或另一個磁碟區。 | 換一個資料夾名稱。 |
name-too-long | 磁碟區認為名稱過長而拒絕,例如加密共用資料夾的 143 位元組上限。 | 改用較短的名稱。 |
PERMISSION_DENIED | 使用者無法寫入目的地資料夾。 | 修正資料夾權限,之後使用者再選一次目的地。 |
NO_SPACE | 儲存空間已滿。 | 釋出空間後再開始新的工作。 |
queue-full | 已有 16 個存檔在排隊。 | 等待存檔完成。 |
recursive-required | 對資料夾呼叫 Delete 但沒有設定 Recursive。 | 設定 Recursive: true。 |
recycle-bin-disabled | 共用資料夾沒有資源回收筒。 | 在 DSM 開啟其資源回收筒,或設定 Permanent: true。 |
not-found、not-a-file | 路徑不存在,或 Read 遇到資料夾或特殊檔案。 | 檢查路徑。 |
task-not-found、too-many-tasks | 任務屬於其他工作或因重新啟動而遺失,或已追蹤 64 個任務。 | 用 Info 確認結果,或等待任務完成。 |
EXECUTOR_PAUSED | 套件已停止,或升級後尚未核准。 | 啟動套件,或重新核准。 |
session_required | 沒有有效的 DSM 工作階段。 | 使用者重新登入 DSM。 |
admin_required | 非管理員呼叫了僅限管理員的方法。 | 以管理員身分呼叫。 |
其他錯誤碼請見錯誤碼。