Skip to content
Syno Package Land
Language: English

Work for a user while they are offline

Status: delegate is implemented; device lifecycle acceptance on real hardware is pending. The default grant allows one operation, files.moveFromStaging.

A delegate lets your package keep working for a DSM user after that user closes the browser. An administrator approves a save scope once. Each user then picks a destination folder inside that scope. From then on, your jobs can move finished files into that folder with the user’s own permissions, until the user or an administrator revokes access. The package never runs these writes as root, and it never replaces an existing file.

Create a project with the capability, or add it to a native-go project:

Terminal window
synopkgland init -t native-go --with delegate my-app
Terminal window
synopkgland add delegate

Both write this block to dsm-package.yaml. Declaring delegate also turns on jobs.

capabilities:
delegate:
scope: install-selected
operations: [files.moveFromStaging]
destinations: [save]
lifetime: until-revoked
FieldAllowed values
scopeinstall-selected: an administrator picks home or one Shared Folder after install. home: each user’s home folder. shared:<name>: one Shared Folder, for example shared:photos. The name has no /, \, : or control characters.
lifetimeuntil-revoked
operations[files.moveFromStaging]
destinations1 to 64 unique keys. Each key matches ^[a-z][a-z0-9_-]{0,63}$. A key is a name your code uses, not a path.

install-selected requires lifetime: until-revoked. If you set destinations or lifetime, or list files.moveFromStaging, all four fields in the table must follow these values.

add delegate does not change backend/internal/api/permissions.json, but init --with delegate does. If ordinary users must see the app after add delegate, set "app.status": "authenticated" there. The default for every method is administrators.

delegate needs a native-go project. binary and compose projects reject it. armv7 builds are not yet verified on a NAS.

The settings page uses BackgroundSaveSettings, with one Save folder row for each destination key. The row shows the selected folder and offers Choose folder and Revoke access. The app manages its own job status and cancellation through the delegate API.

WhoWhat happens
Administrator, install-selectedChooses the home scope or a Shared Folder root and enters the DSM password. One submission approves the scope through the executor root task, reads it back, and grants that root as this administrator’s save destination.
Ordinary userChooses a folder inside the approved scope through the scoped NAS picker. This creates that user’s grant after checking the live DSM session and write permission; no administrator password or second consent dialog is needed.
User, fixed home or shared:<name> scopeChooses a destination inside the scope fixed by the manifest.
Administrator, saving disabled through the APISelects Enable background saves, then users choose their destinations again.

Opening settings leaves existing destinations as they are. An administrator’s next scope selection uses the selected root as their own destination. Changing the confirmed scope revokes all old grants and cancels pending saves; saved files stay in place. Revoke access affects only the signed-in user’s grant.

The plain UI approves in the DSM window’s own panel, where the administrator enters the password. That panel approves without a scope, so it is offered only with home or shared:<name>, where the manifest fixes the scope. With install-selected, the approval must carry the scope the administrator picks, so use a Vue UI.

The --ui plain page has no destination picker and cannot select a scope, so init --ui plain --with delegate fails with CAPABILITY_COMBINATION_INVALID, and check refuses scope: install-selected with a plain UI. Use the Vue UI, or build the controls yourself with the methods in the next section.

The first synopkgland deploy of an install-selected project must pass the installer’s scope step: --param delegate_share=<Shared Folder> or --param delegate_home=true. Without it, deploy fails with DEPLOY_PARAM_REQUIRED before it builds. An upgrade keeps the confirmed scope and needs neither.

synopkgland status shows whether the administrator approval is ready, needs-admin-approval or blocked. With identity: standalone, use an https:// host; status reads the executor through a short-lived App session that needs HTTPS.

backend/internal/app/delegate.go builds a delegate.Service (svc below) from the Go package github.com/synopackageland/syno-pkg-land-sdk/delegate and registers its routes. The SDK has one working producer, delegate/example.go (method delegate.example.export). BackgroundSavePanel can expose it through the opt-in sampleSave producer. A job acknowledgement does not mean the file has been saved. Copy its flow:

  1. While the user’s session is live, look up the destination: svc.Destination(ctx, caller, request, "save"). Keep Destination.ID with your own job record.
  2. Produce the file in the service’s staging directory, svc.StagingDir. Create it with mode 0600.
  3. Later, with no session, move it: svc.MoveFromStaging(ctx, delegate.MoveParams{DelegationID: id, JobID: jobID, Staging: name, Name: name, Size: size}). This refuses to run as root.
selected, err := svc.Destination(ctx, caller, request, "save")
// ... write the finished file into svc.StagingDir as <name> ...
_, err = svc.MoveFromStaging(ctx, delegate.MoveParams{
DelegationID: selected.Destination.ID,
JobID: jobID,
Staging: name,
Name: name,
Size: size,
})

A repeated JobID returns the original operation instead of starting a new one. Use a new JobID for each new file.

Name can name folders below the chosen destination, for example Album/CD1/01.flac. The move creates missing folders as the user, with the user’s own DSM permissions, so their owner and inherited ACL match a folder the user creates in File Station. Existing folders are reused, so a retry or the next file of the same album does not create duplicates. Folders are created only when the file is published: a canceled or revoked move leaves none behind.

RuleValue
Separator/. No leading or trailing /, no empty, . or .. part.
Folder names1 to 255 bytes. No control characters and none of \ : * ? " < > |. Not #recycle, #snapshot or @eaDir (any case).
File nameThe same rule as a name without folders, so existing calls behave as before.
FoldersAt most 16 levels, so one move creates at most 16 folders.
Whole nameAt most 1024 bytes.
UnicodeStored as sent, not normalized. Send NFC if you want the name File Station shows for names typed on Windows.
Symlinks and mountsA folder in the name that is a symlink, a file, or another mounted volume is refused with destination-not-directory.

Set Conflict to choose what happens when the file name is taken:

ConflictEffect
fail or emptyDefault. The move fails with destination-exists.
overwriteReplaces the existing file in one atomic step: the new file is written under a temporary name and then renamed over the old one. A failed move leaves the old file whole. Only a regular file the user can write is replaced; a folder, symlink or device fails with destination-exists.
renamePublishes name (1).ext, name (2).ext and so on, up to name (99).ext. The receipt.path reports the name used.

A Gopeed-style download that keeps the torrent layout:

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",
})
}

Your UI calls these backend methods with ctx.call. Every method needs a signed-in user with a live DSM session, otherwise it returns session_required.

MethodWhoResult
delegate.statususerstate (ready, needs-admin-approval, blocked, needs-scope-confirmation or disabled), disabled, lifetime, destinations, scope_mode, and for install-selected: preselection, selection, mismatch
delegate.destinationuser{key, selection, destination, status}. status is active, missing, revoked (the grant for the key was revoked and no other remains), reselection-required, needs-scope-confirmation or disabled.
delegate.select_destinationuserTakes {key, path}. Checks that the user can write there, then creates the grant. A folder the user cannot write to returns PERMISSION_DENIED.
delegate.move_from_staginguserTakes {delegation_id, job_id, staging, name, size, conflict?}. name may contain folders. Returns {job_id, state}. The caller must own the grant.
delegate.operations.listuserTakes {}. Returns the user’s saves. A save the executor has admitted but not started is queued; uncertain means the executor cannot tell whether it was admitted.
delegate.operations.get, delegate.operations.canceluserTake {delegation_id, job_id}. Return {job_id, state, receipt?, code?, reason?}.
delegate.disable, delegate.enableadministratorTake {}. Both revoke all grants of the app.
delegate.audituserAdministrators see all events of the app. Other users see only their own. At most 256 events.
delegate.example.exportuserTakes {key, job_id}. Runs the example producer.

A save ends as committed with a receipt ({path, size}), canceled, interrupted or uncertain. Do not retry an uncertain or interrupted save automatically: check the destination first. A cancel can report the reasons operation-canceled-by-user, grant-revoked, scope-changed, integration-disabled or destination-replaced. When the executor stops a save itself, the reason is grant-revoked, validation-denied, integration-disabled or storage-error, and the executor removes the partial file even after the user lost write access.

A daemon can run next to jobs only when the delegate uses lifetime: until-revoked. See Manifest reference for the daemon fields.

If you set none of destinations, lifetime and files.moveFromStaging, the delegate runs in a time-limited mode. Then operations is any unique subset of the file operations below, and scope is home or shared:<name>. delegate.create takes a ttl_seconds from 1 to 3600 and the operations of that one grant. The generated worker handles only download and SHA-256 jobs, and downloads are limited to 64 MiB and 60 seconds. The default grant does not allow these file operations: calls to them return POLICY_DENIED.

A job calls them through job.Files(). Every path must be inside the folder of the grant. The executor checks each path before the call, and checks again the paths that File Station reports back. For every operation except files.write and files.read, the executor calls File Station through synowebapi as the user, so DSM applies that user’s own permissions. Only a fixed allowlist of File Station methods can be called this way.

OperationGo methodWhat it doesDSM permission the user needs
files.list, files.infoList, InfoLists a folder, or returns one entry. Each entry includes its size.Read
files.createFolderCreateFolder, CreateFolderAllCreates a folder. CreateFolderAll also creates missing parent folders, like mkdir -p.Write on the parent
files.checkPermissionCheckPermissionChecks that the user can write to a folder. It writes and removes a probe file with a fresh random name, .synopkgland-permission-probe-<32 hex>.None
files.writeWriteSaves at most 64 MiB from staging as a new file.Write
files.readReadReturns a regular file of at most 64 MiB. The executor creates the staging folder if needed and copies the file into it as the user, without following symlinks.Read
files.renameRenameRenames a file or folder in place.Modify
files.copyCopyStarts copying a file or folder into another folder.Read on the source, write on the destination
files.moveMoveStarts moving a file or folder into another folder.Delete on the source, write on the destination
files.deleteDeleteStarts deleting a file or folder.Delete

Rules:

  • Declare each operation you use. A write operation never implies files.delete, so an app that only saves files cannot delete them. An operation that the approved policy does not list returns POLICY_DENIED. An operation that the grant does not list returns DELEGATION_DENIED.
  • The folder of the grant itself cannot be renamed, copied, moved or deleted (SCOPE_DENIED). A path outside the folder or through a symlink is SCOPE_DENIED too; a path inside it that does not exist is not-found.
  • Copy and Move refuse a name that is already taken. delegate.ConflictOverwrite replaces it. They have no rename policy.
  • Delete of a folder needs Recursive: true, otherwise it returns recursive-required. Deleted items go to the recycle bin of the Shared Folder. If that Shared Folder has no recycle bin, Delete returns recycle-bin-disabled unless you set Permanent: true. When the recycle bin is on, DSM keeps the item there even with Permanent: true.

Copy, Move and Delete start a File Station background task and return a TaskState with a TaskID. Call TaskStatus until State is finished, failed (with Error) or canceled. Progress is a fraction from 0 to 1. Once a task has ended, TaskStatus keeps returning the same final state. CancelTask stops the task. Only the job that started a task can follow it. Revoking the grant stops its running tasks.

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 reports a refusal per item, not only for the whole task. The executor reads those and returns the item’s code: no permission is PERMISSION_DENIED, a taken name is destination-exists, a missing path is not-found, a name the volume refuses is name-too-long, a full volume is NO_SPACE, and CreateFolder below a file is destination-not-directory. These codes are not retryable. A delete that leaves the item behind fails with PERMISSION_DENIED or DSM_FAILED.

The executor tracks at most 64 tasks while it runs. After the package restarts, TaskStatus returns task-not-found. State is unknown when DSM has dropped the task; then check the result with Info. Whether DSM reports a task to a later synowebapi process is not yet verified on a NAS.

LimitValue
File size moved16 GiB
Queued saves16
Concurrent copies1
One copy attempt24 hours
Destination keys1 to 64
Existing destination fileReplaced only with conflict: overwrite
Folders in name16 levels, 1024 bytes in total
File read by files.read64 MiB
File Station tasks tracked64
conflict: rename alternativesname (1) to name (99)
Backendnative-go only
Architecturesx86_64, aarch64 and armv7 (armv7 not yet verified on a NAS)
Audit events returned by delegate.audit256
Approval on DSM 7.1Uses older scheduler APIs; see Root operations, DSM 7.1

Lifecycle effects:

EventEffect
Stop the packageSaving pauses. Approval, grants and the confirmed scope are kept.
UpgradeSaving resumes only if the approved policy still matches. A new executor that matches the INFO anchor needs no approval. Otherwise the app stays at needs-admin-approval. A copy in progress ends as interrupted.
UninstallRemoves the approval, grants, confirmed scope, root tasks and the app’s Application Privileges rules, and stops a portal address. A reinstall needs a new approval. See Data and removal.
Change scope, destinations, operations or lifetime, or the entry fieldThe administrator must approve again.
Symptom or codeCauseFix
needs-admin-approvalNo administrator approval yet, or the approved policy changed.An administrator opens settings and approves again.
needs-scope-confirmationscope: install-selected and no scope is confirmed.An administrator chooses the save-folder scope in settings and confirms it with the DSM password.
disabledAn administrator disabled background saves.An administrator selects Enable background saves. Users choose destinations again.
revokedThe grant for this destination was revoked.The user chooses the destination again.
reselection-requiredThe scope changed, or the grant does not match the confirmed scope.The user chooses the destination again.
invalid-destinationThe key is not in destinations, or the user has no active grant for it.Fix the key, or let the user choose a destination.
destination-existsA file with that name already exists in the folder.Use a new file name with a new JobID, or set conflict.
invalid-path, path-too-deepA folder in name breaks the rules above, or there are more than 16.Fix the name; use a new JobID.
destination-not-directoryA folder in name exists as a file, a symlink or another volume.Choose another folder name.
name-too-longThe volume refused a name as too long, for example 143 bytes on an encrypted Shared Folder.Use a shorter name.
PERMISSION_DENIEDThe user cannot write to the destination folder.Fix the folder permission, then the user selects the destination again.
NO_SPACEThe volume is full.Free space, then start a new job.
queue-full16 saves are already queued.Wait for saves to finish.
recursive-requiredDelete was called on a folder without Recursive.Set Recursive: true.
recycle-bin-disabledThe Shared Folder has no recycle bin.Turn on its recycle bin in DSM, or set Permanent: true.
not-found, not-a-fileThe path does not exist, or Read met a folder or a special file.Check the path.
task-not-found, too-many-tasksThe task belongs to another job or was lost in a restart, or 64 tasks are tracked.Check the result with Info, or wait for tasks to finish.
EXECUTOR_PAUSEDThe package is stopped, or an upgrade has not been approved.Start the package, or approve again.
session_requiredNo live DSM session.The user signs in to DSM again.
admin_requiredAn administrator-only method was called by another user.Call it as an administrator.

Other codes are listed in Errors.