@synopackageland/templates 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -20
- package/package.json +1 -1
- package/templates/_shared/AGENTS.md +5 -3
- package/templates/compose/dsm-package.yaml +1 -1
- package/templates/native-go/README.md +10 -3
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/backend/internal/app/background.go +52 -48
- package/templates/native-go/backend/internal/app/background_owner_test.go +2 -2
- package/templates/native-go/backend/internal/app/background_schedule_test.go +0 -4
- package/templates/native-go/backend/internal/app/background_test.go +59 -104
- package/templates/native-go-delegate/frontend/src/CapabilityPanels.vue +4 -1
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +24 -0
- package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +3 -20
- package/templates/native-go-jobs/frontend/test/{background-page.test.ts → background-shell.test.ts} +2 -4
- package/templates/native-go-vue/frontend/DESIGN.md +6 -9
- package/templates/native-go-vue/frontend/package-lock.json +8 -8
- package/templates/native-go-vue/frontend/package.json +2 -2
- package/templates/native-go-vue/frontend/src/App.vue +0 -1
- package/templates/native-go-vue/frontend/src/i18n/de.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/en.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/es.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/fr.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/it.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/ja.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/ko.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +1 -2
- package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +1 -2
- package/templates/native-node/README.md +10 -4
- package/templates/native-node/dsm-package.yaml +1 -1
- package/templates/native-node-jobs/backend/src/jobs.ts +4 -10
- package/templates/native-node-jobs/backend/test/jobs.test.ts +5 -4
- package/templates/native-python/README.md +10 -4
- package/templates/native-python/dsm-package.yaml +1 -1
- package/templates/native-python-jobs/backend/app/jobs.py +2 -11
- package/templates/native-python-jobs/backend/tests/test_jobs.py +5 -4
- package/templates/native-rust/README.md +10 -4
- package/templates/native-rust/backend/Cargo.lock +2 -2
- package/templates/native-rust/backend/Cargo.toml +1 -1
- package/templates/native-rust-jobs/backend/src/jobs.rs +21 -15
package/README.md
CHANGED
|
@@ -22,8 +22,8 @@ daemon:
|
|
|
22
22
|
window_seconds: 300
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Templates
|
|
26
|
-
Use CLI 0.
|
|
25
|
+
Templates pin UI 0.14.10 and SDK 0.28.0 in native and Compose projects.
|
|
26
|
+
Use CLI 0.28.0 or later. Go Apps select
|
|
27
27
|
their controller from their Go SDK requirement; an identity gateway selects it
|
|
28
28
|
from `identity_gateway.sdk`. The CLI rejects older selected versions, even for
|
|
29
29
|
an explicit `mode: off`, rather than dropping the declaration. A local SDK source
|
|
@@ -39,7 +39,7 @@ for declaration bounds, SDK selection, build/inspect checks and diagnostics.
|
|
|
39
39
|
|
|
40
40
|
An opt-in [Shared Folder permission example](examples/shared-folder-permission/README.md)
|
|
41
41
|
shows the Go status-handler registration and a Vue notice for startup checks and
|
|
42
|
-
storage failures. It uses the same SDK 0.
|
|
42
|
+
storage failures. It uses the same SDK 0.28.0 release pinned by the default scaffolds.
|
|
43
43
|
|
|
44
44
|
An opt-in [native UI plus DSM console example](examples/dual-entry-console/README.md)
|
|
45
45
|
shows the `console` declaration for an App that wraps a web service behind the
|
|
@@ -49,7 +49,7 @@ identity gateway and keeps its own settings in an administrator-only DSM window.
|
|
|
49
49
|
| --- | --- |
|
|
50
50
|
| `native-go` | Base: `dsm-package.yaml`, Go module (`backend/cmd/{app,background-worker}`, `backend/internal/app`, `backend/internal/api/permissions.json`), plain `ui/`, i18n |
|
|
51
51
|
| `native-go-vue` | Vue shell in `frontend/` (`App.vue`, `main.ts`, app i18n, `CapabilityPanels.vue`, `ExecutorGate.vue`) |
|
|
52
|
-
| `native-go-jobs` | Background job routes in `internal/app` and
|
|
52
|
+
| `native-go-jobs` | Background job routes in `internal/app` and a shell lifecycle test |
|
|
53
53
|
| `native-go-delegate` | Delegate routes, the delegate background worker, the delegate `CapabilityPanels.vue` |
|
|
54
54
|
| `native-go-root-ops` | The SDK's executor routes and `root.run` in `internal/app`, an example `root/whoami.sh`, and the Vue `ExecutorGate.vue` that holds the pages until the executor is approved |
|
|
55
55
|
| `native-python` | Base: `dsm-package.yaml` (`backend.runtime: python`), the App in `backend/app` (`routes.py`, stub capability modules, `permissions.json`), unittest tests |
|
|
@@ -79,21 +79,19 @@ calls each one from the generated script of the same name (`synopkgland skill sh
|
|
|
79
79
|
The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
|
|
80
80
|
`__BACKGROUND_ENABLED__` are substituted by `init`.
|
|
81
81
|
|
|
82
|
-
`backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.
|
|
82
|
+
`backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.28.0`; that line is
|
|
83
83
|
the single source of the SDK version and `frontend/package.json` pins
|
|
84
84
|
`@synopackageland/sdk` to the same version. Python and Node.js name it in
|
|
85
85
|
`backend.sdk`, Rust in the `syno-pkg-land-app` tag of `backend/Cargo.toml`
|
|
86
|
-
(and `Cargo.lock`); all of them pin 0.
|
|
87
|
-
`@synopackageland/ui` 0.14.
|
|
86
|
+
(and `Cargo.lock`); all of them pin 0.28.0. The Vue frontend pins
|
|
87
|
+
`@synopackageland/ui` 0.14.10. SDK 0.28.0 accepts UI `>=0.10.1 <0.15.0`,
|
|
88
88
|
so no UI peer override is needed. Tests use jsdom 30 for UI floating-layer styles.
|
|
89
89
|
|
|
90
90
|
SDK 0.27.0 moves interactive NAS file operations to same-origin browser calls.
|
|
91
|
-
|
|
92
|
-
`unsupported`; custom file handlers need the SDK's
|
|
91
|
+
Custom file handlers use the SDK's
|
|
93
92
|
[session transport migration](https://github.com/synopackageland/syno-pkg-land-sdk/blob/v0.27.0/docs/dsm-session-transport.md).
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
executors follow normal administrator approval. No NAS acceptance is claimed.
|
|
93
|
+
Use CLI 0.28.0 for the matching SDK 0.28.0 executor/lifecycle digests. Rebuilt
|
|
94
|
+
protected executors follow normal administrator approval. No NAS acceptance is claimed.
|
|
97
95
|
|
|
98
96
|
UI and i18n text, including the DSM glossary, omit terminal sentence periods.
|
|
99
97
|
Keep punctuation between sentences and intentional loading ellipses.
|
|
@@ -215,12 +213,17 @@ visible while reads run. The backend permissions remain authoritative.
|
|
|
215
213
|
|
|
216
214
|
Native Node Apps use the official [`Node.js_v22`](https://www.synology.com/en-us/dsm/packages/Node.js_v22) package. The official release history lists DSM 7 series; installation also depends on the model and DSM version supported by Package Center. This dependency does not certify all DSM 7 releases or install Node.js on unsupported models.
|
|
217
215
|
|
|
218
|
-
###
|
|
216
|
+
### Background jobs
|
|
219
217
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
218
|
+
The jobs overlays submit files the App's own producer has already imported into
|
|
219
|
+
its private data directory. Submit `{key, name, input}` through `bg.jobs.submit`
|
|
220
|
+
(Go) or `hash.submit` (Python, Node.js and Rust); `input` is the absolute path of
|
|
221
|
+
that App-owned file. The Go handler and the bridge runtime confine reads to the
|
|
222
|
+
App data directory and keep the SDK's bounded private-copy input contract.
|
|
223
|
+
There is no NAS browse/import example or automatic file submission in either UI shell.
|
|
224
|
+
|
|
225
|
+
Keep the job controls and package-account workers for progress, cancellation and
|
|
226
|
+
pause/resume. Existing Go schedule routes reprocess retained, successful job
|
|
227
|
+
copies with explicit consent; they do not read a fresh source file on each run.
|
|
228
|
+
The bridge examples expose submission, lookup and cancellation through
|
|
229
|
+
`bg.submit`, `bg.get` and `bg.cancel`; the SDK's existing service surface is unchanged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/templates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Project templates for synopkgland init: native-go, native-python, native-node and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -110,9 +110,11 @@ may redistribute; never copy Synology's.
|
|
|
110
110
|
|
|
111
111
|
## Background jobs (`jobs`)
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
Submit only files the App's own producer has already imported into its private
|
|
114
|
+
data directory, with `{key, name, input}`. The existing worker processes a
|
|
115
|
+
bounded private copy. Schedules reprocess that retained copy with explicit
|
|
116
|
+
consent; they never reread a source file. Keep submission, progress and
|
|
117
|
+
cancellation on the existing SDK contracts.
|
|
116
118
|
|
|
117
119
|
The worker runs the App's Work for one claimed job at a time, as the package
|
|
118
120
|
account. The Work receives the job's Task: it reads the private input copy,
|
|
@@ -14,7 +14,7 @@ compose:
|
|
|
14
14
|
web: {service: web, port: 8080, health: {path: /, status: 200}}
|
|
15
15
|
network: {egress: isolated}
|
|
16
16
|
identity_gateway:
|
|
17
|
-
sdk: 0.
|
|
17
|
+
sdk: 0.28.0
|
|
18
18
|
mode: none
|
|
19
19
|
listen: {http: 28000}
|
|
20
20
|
# Web Apps default to new-page through the shared app-launch resolver.
|
|
@@ -400,6 +400,13 @@ SDK's `docs/shared-folder.md` for the whole flow.
|
|
|
400
400
|
| DSM | 7.1 or later declared (`os_min_ver`, change it with `init --min-dsm` or `build --min-dsm`). Installed and tested on DSM 7.4 and 7.3.2 (x86_64). armv7 was installed and tested on DSM 7.1.1 (`alpine4k`, Linux 3.10, where the bridge runs from the package tmp fallback) with administrator, AppPriv and unauthenticated requests; `armada38x` and `monaco` have not been run on a NAS. No x86_64 Linux 3.10 model has been tested. aarch64 SPKs are built the same way but have not been run on a NAS. |
|
|
401
401
|
| Not supported | armv7 platforms only DSM 7.0/7.1 ship (`armada370`, `armada375`, `armadaxp`, `comcerto2k`); DSM 6, running anything as root without an administrator's approval |
|
|
402
402
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
403
|
+
## Background job input
|
|
404
|
+
|
|
405
|
+
Jobs process an existing file imported by the App's producer into its private
|
|
406
|
+
data directory. The submission params are `{key, name, input}`: an idempotency
|
|
407
|
+
key, a display name and an absolute path inside that directory. The SDK keeps a
|
|
408
|
+
bounded private copy for the worker; no browser session or NAS picker is used.
|
|
409
|
+
Submit with `bg.jobs.submit`; use `bg.jobs.get` for progress and
|
|
410
|
+
`bg.jobs.cancel` to cancel. Pause/resume and existing schedule routes continue
|
|
411
|
+
to operate on retained imported copies. Schedules require explicit consent and
|
|
412
|
+
reprocess the fixed private copy, without rereading the source file.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.
|
|
2
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.
|
|
1
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.28.0 h1:54DolNgwneslNufx1ntvV6BYt/eDlk58TbeTKet/Ovg=
|
|
2
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.28.0/go.mod h1:8GwREGbczc8mAk4Gv8FUHaukXXCUstjzkovQrNV59Cc=
|
|
3
3
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
4
4
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
@@ -4,16 +4,17 @@ import (
|
|
|
4
4
|
"context"
|
|
5
5
|
"encoding/json"
|
|
6
6
|
"errors"
|
|
7
|
+
"io"
|
|
7
8
|
"log"
|
|
8
9
|
"net/http"
|
|
10
|
+
"os"
|
|
9
11
|
"os/user"
|
|
10
|
-
"path"
|
|
11
12
|
"path/filepath"
|
|
13
|
+
"syscall"
|
|
12
14
|
"time"
|
|
13
15
|
|
|
14
16
|
"github.com/synopackageland/syno-pkg-land-sdk/api"
|
|
15
17
|
"github.com/synopackageland/syno-pkg-land-sdk/background"
|
|
16
|
-
"github.com/synopackageland/syno-pkg-land-sdk/filestation"
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
// workerFresh is how recent the worker heartbeat must be to count as running.
|
|
@@ -31,28 +32,20 @@ func osLookupUID(name string) string {
|
|
|
31
32
|
return u.Uid
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
type backgroundFiles interface {
|
|
35
|
-
Browse(ctx context.Context, c api.Caller, r *http.Request, folder string, offset int) (filestation.Page, error)
|
|
36
|
-
ReadFile(ctx context.Context, c api.Caller, r *http.Request, folder, rel string) ([]byte, error)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
35
|
type backgroundAPI struct {
|
|
40
|
-
store
|
|
41
|
-
|
|
36
|
+
store *background.Store
|
|
37
|
+
dataDir string
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
// BackgroundRoutes
|
|
45
|
-
// API. The NAS hashing example is disabled until browser-owned input is wired.
|
|
46
|
-
// Existing job control routes remain available. `synopkgland init --with jobs` wires it in through background.go's
|
|
47
|
-
// replacement of background_none.go; nothing else references it.
|
|
40
|
+
// BackgroundRoutes serves job controls for App-owned imported input.
|
|
48
41
|
func BackgroundRoutes(dataDir string) []api.Route {
|
|
49
|
-
return newBackgroundRoutes(background.NewRequestStore(filepath.Join(dataDir, "background")),
|
|
42
|
+
return newBackgroundRoutes(background.NewRequestStore(filepath.Join(dataDir, "background")), dataDir)
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
// Every request authenticates against DSM afresh. Owner routes are available
|
|
53
46
|
// to signed-in users; schedule reapproval and owner cleanup are admin-only.
|
|
54
|
-
func newBackgroundRoutes(store *background.Store,
|
|
55
|
-
b := backgroundAPI{store: store,
|
|
47
|
+
func newBackgroundRoutes(store *background.Store, dataDir string) []api.Route {
|
|
48
|
+
b := backgroundAPI{store: store, dataDir: dataDir}
|
|
56
49
|
return []api.Route{
|
|
57
50
|
api.HandleRequest("bg.status", b.owner(b.status)),
|
|
58
51
|
api.HandleRequest("bg.schedules.list", b.owner(b.schedules)),
|
|
@@ -61,7 +54,6 @@ func newBackgroundRoutes(store *background.Store, files backgroundFiles) []api.R
|
|
|
61
54
|
api.HandleRequest("bg.schedules.delete", b.owner(b.deleteSchedule)),
|
|
62
55
|
api.HandleRequest("bg.schedules.enable", b.admin(b.enableSchedule)),
|
|
63
56
|
api.HandleRequest("bg.jobs.purge", b.admin(b.purge)),
|
|
64
|
-
api.HandleRequest("bg.browse", b.owner(b.browse)),
|
|
65
57
|
api.HandleRequest("bg.jobs.submit", b.owner(b.submit)),
|
|
66
58
|
api.HandleRequest("bg.jobs.list", b.owner(b.list)),
|
|
67
59
|
api.HandleRequest("bg.jobs.get", b.owner(b.get)),
|
|
@@ -116,51 +108,28 @@ func workerAlive(w background.WorkerInfo, now time.Time) bool {
|
|
|
116
108
|
return now.Sub(w.At) <= workerFresh
|
|
117
109
|
}
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
// the panel can pick a file instead of typing a path. It grants nothing:
|
|
121
|
-
// submit still reads through the same session.
|
|
122
|
-
func (b backgroundAPI) browse(ctx context.Context, c api.Caller, r *http.Request, params json.RawMessage) (any, error) {
|
|
123
|
-
var in struct {
|
|
124
|
-
Folder string `json:"folder"`
|
|
125
|
-
Offset int `json:"offset"`
|
|
126
|
-
}
|
|
127
|
-
if err := decodeParams(params, &in); err != nil {
|
|
128
|
-
return nil, err
|
|
129
|
-
}
|
|
130
|
-
if in.Offset < 0 {
|
|
131
|
-
return nil, api.BadRequest("offset must not be negative")
|
|
132
|
-
}
|
|
133
|
-
if b.files == nil {
|
|
134
|
-
return nil, &api.Error{Code: "unsupported", Message: "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available."}
|
|
135
|
-
}
|
|
136
|
-
return b.files.Browse(ctx, c, r, in.Folder, in.Offset)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
func (b backgroundAPI) submit(ctx context.Context, c api.Caller, r *http.Request, params json.RawMessage) (any, error) {
|
|
111
|
+
func (b backgroundAPI) submit(ctx context.Context, _ api.Caller, _ *http.Request, params json.RawMessage) (any, error) {
|
|
140
112
|
var in struct {
|
|
141
|
-
Key
|
|
142
|
-
|
|
143
|
-
|
|
113
|
+
Key string `json:"key"`
|
|
114
|
+
Name string `json:"name"`
|
|
115
|
+
Input string `json:"input"`
|
|
144
116
|
}
|
|
145
117
|
if err := decodeParams(params, &in); err != nil {
|
|
146
118
|
return nil, err
|
|
147
119
|
}
|
|
148
|
-
if in.Key == "" || in.
|
|
149
|
-
return nil, api.BadRequest("key,
|
|
120
|
+
if in.Key == "" || in.Name == "" || in.Input == "" {
|
|
121
|
+
return nil, api.BadRequest("key, name and input are required")
|
|
150
122
|
}
|
|
151
123
|
access := accessFrom(ctx)
|
|
152
124
|
if !access.Owner.Valid() {
|
|
153
125
|
return nil, &api.Error{Code: "identity_unavailable", Message: "DSM user UID could not be resolved"}
|
|
154
126
|
}
|
|
155
|
-
|
|
156
|
-
return nil, &api.Error{Code: "unsupported", Message: "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available."}
|
|
157
|
-
}
|
|
158
|
-
data, err := b.files.ReadFile(ctx, c, r, in.Folder, in.Relative)
|
|
127
|
+
data, err := b.readInput(in.Input)
|
|
159
128
|
if err != nil {
|
|
160
129
|
return nil, err
|
|
161
130
|
}
|
|
162
131
|
job, duplicate, err := b.store.Submit(ctx, background.SubmitRequest{
|
|
163
|
-
Key: in.Key, Name:
|
|
132
|
+
Key: in.Key, Name: in.Name, Data: data, Submitter: access.Owner,
|
|
164
133
|
})
|
|
165
134
|
if err != nil {
|
|
166
135
|
return nil, backgroundError(err)
|
|
@@ -168,6 +137,41 @@ func (b backgroundAPI) submit(ctx context.Context, c api.Caller, r *http.Request
|
|
|
168
137
|
return map[string]any{"job": job, "duplicate": duplicate}, nil
|
|
169
138
|
}
|
|
170
139
|
|
|
140
|
+
// readInput accepts only an existing regular file inside the App data directory.
|
|
141
|
+
// The App's producer imports it first; jobs and schedules retain a private copy.
|
|
142
|
+
func (b backgroundAPI) readInput(input string) ([]byte, error) {
|
|
143
|
+
bad := api.BadRequest("input must be a regular file in the App data directory")
|
|
144
|
+
rel, err := filepath.Rel(b.dataDir, input)
|
|
145
|
+
if err != nil || !filepath.IsAbs(input) || !filepath.IsLocal(rel) {
|
|
146
|
+
return nil, bad
|
|
147
|
+
}
|
|
148
|
+
root, err := os.OpenRoot(b.dataDir)
|
|
149
|
+
if err != nil {
|
|
150
|
+
return nil, bad
|
|
151
|
+
}
|
|
152
|
+
defer root.Close()
|
|
153
|
+
f, err := root.OpenFile(rel, os.O_RDONLY|syscall.O_NONBLOCK, 0)
|
|
154
|
+
if err != nil {
|
|
155
|
+
return nil, bad
|
|
156
|
+
}
|
|
157
|
+
defer f.Close()
|
|
158
|
+
info, err := f.Stat()
|
|
159
|
+
if err != nil || !info.Mode().IsRegular() {
|
|
160
|
+
return nil, bad
|
|
161
|
+
}
|
|
162
|
+
if info.Size() > background.MaxInputBytes {
|
|
163
|
+
return nil, backgroundError(background.ErrTooLarge)
|
|
164
|
+
}
|
|
165
|
+
data, err := io.ReadAll(io.LimitReader(f, background.MaxInputBytes+1))
|
|
166
|
+
if err != nil {
|
|
167
|
+
return nil, bad
|
|
168
|
+
}
|
|
169
|
+
if len(data) > background.MaxInputBytes {
|
|
170
|
+
return nil, backgroundError(background.ErrTooLarge)
|
|
171
|
+
}
|
|
172
|
+
return data, nil
|
|
173
|
+
}
|
|
174
|
+
|
|
171
175
|
func (b backgroundAPI) list(ctx context.Context, _ api.Caller, _ *http.Request, params json.RawMessage) (any, error) {
|
|
172
176
|
if err := decodeParams(params, &struct{}{}); err != nil {
|
|
173
177
|
return nil, err
|
|
@@ -28,7 +28,7 @@ var carol = stubAuth{identity: api.Identity{User: "carol"}}
|
|
|
28
28
|
|
|
29
29
|
func ownerSubmit(t *testing.T, e *bgEnv, a api.Authenticator, key string) string {
|
|
30
30
|
t.Helper()
|
|
31
|
-
code, out := e.call(a, "bg.jobs.submit",
|
|
31
|
+
code, out := e.call(a, "bg.jobs.submit", e.submitParams(key))
|
|
32
32
|
if code != "" {
|
|
33
33
|
t.Fatalf("submit %s: %s %v", key, code, out)
|
|
34
34
|
}
|
|
@@ -103,7 +103,7 @@ func TestBackgroundPersonalQuotaAndAdminPurge(t *testing.T) {
|
|
|
103
103
|
}
|
|
104
104
|
ownerSubmit(t, e, bob, "one")
|
|
105
105
|
ownerSubmit(t, e, bob, "two")
|
|
106
|
-
if code, _ := e.call(bob, "bg.jobs.submit",
|
|
106
|
+
if code, _ := e.call(bob, "bg.jobs.submit", e.submitParams("three")); code != "owner-queue-full" {
|
|
107
107
|
t.Fatal(code)
|
|
108
108
|
}
|
|
109
109
|
ownerSubmit(t, e, carol, "one")
|
|
@@ -55,8 +55,4 @@ func TestBackgroundScheduleRoutesConsentOwnerAndAdmin(t *testing.T) {
|
|
|
55
55
|
if code, _ := e.call(bob, "bg.schedules.delete", fmt.Sprintf(`{"id":%q}`, sid)); code != "" {
|
|
56
56
|
t.Fatal(code)
|
|
57
57
|
}
|
|
58
|
-
// No scheduling route calls File Station, uses a root grant or stores a session.
|
|
59
|
-
if e.files.calls != 1 {
|
|
60
|
-
t.Fatalf("schedule read File Station %d", e.files.calls)
|
|
61
|
-
}
|
|
62
58
|
}
|
|
@@ -14,50 +14,30 @@ import (
|
|
|
14
14
|
|
|
15
15
|
"github.com/synopackageland/syno-pkg-land-sdk/api"
|
|
16
16
|
"github.com/synopackageland/syno-pkg-land-sdk/background"
|
|
17
|
-
"github.com/synopackageland/syno-pkg-land-sdk/filestation"
|
|
18
17
|
)
|
|
19
18
|
|
|
20
|
-
type fakeFiles struct {
|
|
21
|
-
data []byte
|
|
22
|
-
err error
|
|
23
|
-
calls int
|
|
24
|
-
folder string
|
|
25
|
-
rel string
|
|
26
|
-
offset int
|
|
27
|
-
page filestation.Page
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func (f *fakeFiles) Browse(_ context.Context, _ api.Caller, _ *http.Request, folder string, offset int) (filestation.Page, error) {
|
|
31
|
-
f.calls++
|
|
32
|
-
f.folder, f.offset = folder, offset
|
|
33
|
-
return f.page, f.err
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func (f *fakeFiles) ReadFile(_ context.Context, _ api.Caller, _ *http.Request, folder, rel string) ([]byte, error) {
|
|
37
|
-
f.calls++
|
|
38
|
-
f.folder, f.rel = folder, rel
|
|
39
|
-
return f.data, f.err
|
|
40
|
-
}
|
|
41
|
-
|
|
42
19
|
type bgEnv struct {
|
|
43
|
-
t
|
|
44
|
-
store
|
|
45
|
-
|
|
46
|
-
srv
|
|
20
|
+
t *testing.T
|
|
21
|
+
store *background.Store
|
|
22
|
+
dataDir string
|
|
23
|
+
srv map[string]*api.Server
|
|
47
24
|
}
|
|
48
25
|
|
|
49
26
|
func newBgEnv(t *testing.T) *bgEnv {
|
|
50
27
|
lookupSubmitterUID = func(string) string { return "1024" }
|
|
51
28
|
t.Cleanup(func() { lookupSubmitterUID = osLookupUID })
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
dir := t.TempDir()
|
|
30
|
+
store := background.NewStore(filepath.Join(dir, "background"))
|
|
31
|
+
if err := os.WriteFile(filepath.Join(dir, "input.txt"), []byte("hello"), 0600); err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
return &bgEnv{t: t, store: store, dataDir: dir, srv: map[string]*api.Server{}}
|
|
55
35
|
}
|
|
56
36
|
|
|
57
37
|
func (e *bgEnv) call(auth api.Authenticator, method, params string) (string, map[string]any) {
|
|
58
38
|
e.t.Helper()
|
|
59
39
|
// SDK policy provides owner routes immediately.
|
|
60
|
-
routes := newBackgroundRoutes(e.store, e.
|
|
40
|
+
routes := newBackgroundRoutes(e.store, e.dataDir)
|
|
61
41
|
srv, err := api.NewServerWithPermissions(auth, []byte(`{"schema":1,"default":"administrators","methods":{}}`), routes...)
|
|
62
42
|
if err != nil {
|
|
63
43
|
e.t.Fatal(err)
|
|
@@ -88,8 +68,7 @@ var bgSample = map[string]string{
|
|
|
88
68
|
"bg.schedules.enable": `{"id":"missing","consent":true}`,
|
|
89
69
|
"bg.status": `{}`,
|
|
90
70
|
"bg.jobs.purge": `{"owner":{"user":"bob","uid":"1025"}}`,
|
|
91
|
-
"bg.
|
|
92
|
-
"bg.jobs.submit": `{"key":"k1","folder":"/share","relative":"a.txt"}`,
|
|
71
|
+
"bg.jobs.submit": `{"key":"k1","name":"a.txt","input":"/missing"}`,
|
|
93
72
|
"bg.jobs.list": `{}`,
|
|
94
73
|
"bg.jobs.get": `{"id":"` + strings.Repeat("a", 32) + `"}`,
|
|
95
74
|
"bg.jobs.cancel": `{"id":"` + strings.Repeat("a", 32) + `"}`,
|
|
@@ -100,7 +79,7 @@ var bgSample = map[string]string{
|
|
|
100
79
|
|
|
101
80
|
func TestBackgroundSDKPoliciesAuthenticateEveryRoute(t *testing.T) {
|
|
102
81
|
e := newBgEnv(t)
|
|
103
|
-
routes := newBackgroundRoutes(e.store, e.
|
|
82
|
+
routes := newBackgroundRoutes(e.store, e.dataDir)
|
|
104
83
|
if len(routes) != len(bgSample) {
|
|
105
84
|
t.Fatalf("%d routes, %d samples", len(routes), len(bgSample))
|
|
106
85
|
}
|
|
@@ -149,55 +128,73 @@ func TestBackgroundStatusReportsLimitsAndWorker(t *testing.T) {
|
|
|
149
128
|
}
|
|
150
129
|
}
|
|
151
130
|
|
|
152
|
-
func
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
t.Fatalf("%q, NAS reads %d", code, e.files.calls)
|
|
131
|
+
func (e *bgEnv) submitParams(key string) string {
|
|
132
|
+
p, err := json.Marshal(map[string]string{"key": key, "name": "a.txt", "input": filepath.Join(e.dataDir, "input.txt")})
|
|
133
|
+
if err != nil {
|
|
134
|
+
e.t.Fatal(err)
|
|
157
135
|
}
|
|
136
|
+
return string(p)
|
|
158
137
|
}
|
|
159
138
|
|
|
160
|
-
func
|
|
139
|
+
func TestBackgroundSubmitImportsAppInputAndIsIdempotent(t *testing.T) {
|
|
161
140
|
e := newBgEnv(t)
|
|
162
|
-
params :=
|
|
141
|
+
params := e.submitParams("k1")
|
|
163
142
|
code, out := e.call(alice, "bg.jobs.submit", params)
|
|
164
|
-
|
|
165
|
-
if code != "" || job["state"] != "queued" || job["name"] != "a.txt" || data(out)["duplicate"] != false {
|
|
143
|
+
if code != "" {
|
|
166
144
|
t.Fatalf("%s %v", code, out)
|
|
167
145
|
}
|
|
168
|
-
|
|
169
|
-
|
|
146
|
+
job := data(out)["job"].(map[string]any)
|
|
147
|
+
if job["state"] != "queued" || job["name"] != "a.txt" || data(out)["duplicate"] != false {
|
|
148
|
+
t.Fatal(out)
|
|
170
149
|
}
|
|
171
150
|
submitter := job["submitter"].(map[string]any)
|
|
172
151
|
if submitter["user"] != "alice" || submitter["uid"] != "1024" {
|
|
173
|
-
t.
|
|
152
|
+
t.Fatal(submitter)
|
|
174
153
|
}
|
|
175
154
|
_, again := e.call(alice, "bg.jobs.submit", params)
|
|
176
155
|
if data(again)["duplicate"] != true || data(again)["job"].(map[string]any)["id"] != job["id"] {
|
|
177
|
-
t.
|
|
156
|
+
t.Fatal(again)
|
|
157
|
+
}
|
|
158
|
+
if err := os.WriteFile(filepath.Join(e.dataDir, "input.txt"), []byte("other"), 0600); err != nil {
|
|
159
|
+
t.Fatal(err)
|
|
178
160
|
}
|
|
179
|
-
e.files.data = []byte("other")
|
|
180
161
|
if code, _ := e.call(alice, "bg.jobs.submit", params); code != "conflict" {
|
|
181
|
-
t.Fatalf("
|
|
162
|
+
t.Fatalf("different bytes: %q", code)
|
|
182
163
|
}
|
|
183
164
|
}
|
|
184
165
|
|
|
185
|
-
func
|
|
166
|
+
func TestBackgroundSubmitRejectsExternalAndOversizedInput(t *testing.T) {
|
|
186
167
|
e := newBgEnv(t)
|
|
187
|
-
|
|
188
|
-
if
|
|
189
|
-
t.
|
|
168
|
+
outside := filepath.Join(t.TempDir(), "outside")
|
|
169
|
+
if err := os.WriteFile(outside, []byte("private"), 0600); err != nil {
|
|
170
|
+
t.Fatal(err)
|
|
190
171
|
}
|
|
191
|
-
if
|
|
192
|
-
t.Fatal(
|
|
172
|
+
if err := os.Symlink(outside, filepath.Join(e.dataDir, "escape")); err != nil {
|
|
173
|
+
t.Fatal(err)
|
|
174
|
+
}
|
|
175
|
+
for _, input := range []string{outside, "input.txt", filepath.Join(e.dataDir, "escape"), e.dataDir} {
|
|
176
|
+
p, _ := json.Marshal(map[string]string{"key": "bad", "name": "a.txt", "input": input})
|
|
177
|
+
if code, _ := e.call(alice, "bg.jobs.submit", string(p)); code != "bad_request" {
|
|
178
|
+
t.Fatalf("%s: %s", input, code)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if err := os.WriteFile(filepath.Join(e.dataDir, "input.txt"), make([]byte, background.MaxInputBytes+1), 0600); err != nil {
|
|
182
|
+
t.Fatal(err)
|
|
183
|
+
}
|
|
184
|
+
if code, _ := e.call(alice, "bg.jobs.submit", e.submitParams("big")); code != "limit-exceeded" {
|
|
185
|
+
t.Fatal(code)
|
|
186
|
+
}
|
|
187
|
+
jobs, err := e.store.List(context.Background())
|
|
188
|
+
if err != nil || len(jobs) != 0 {
|
|
189
|
+
t.Fatalf("invalid input queued: %v %v", jobs, err)
|
|
193
190
|
}
|
|
194
191
|
}
|
|
195
192
|
|
|
196
193
|
func TestBackgroundSubmitValidatesParams(t *testing.T) {
|
|
197
194
|
e := newBgEnv(t)
|
|
198
195
|
for _, p := range []string{
|
|
199
|
-
`{}`, `{"key":"k","
|
|
200
|
-
`{"key":"k","
|
|
196
|
+
`{}`, `{"key":"k","name":"a","input":"/a","extra":1}`, e.submitParams("bad key"),
|
|
197
|
+
`{"key":"k","name":"a","input":""}`,
|
|
201
198
|
} {
|
|
202
199
|
if code, _ := e.call(alice, "bg.jobs.submit", p); code != "bad_request" {
|
|
203
200
|
t.Errorf("%s: %q", p, code)
|
|
@@ -210,19 +207,19 @@ func TestBackgroundQueueFullAndQuotaErrorsHaveStableCodes(t *testing.T) {
|
|
|
210
207
|
for i := 0; i < background.MaxQueued; i++ {
|
|
211
208
|
lookupSubmitterUID = func(string) string { return fmt.Sprint(1024 + i/2) }
|
|
212
209
|
key := "k" + string(rune('a'+i))
|
|
213
|
-
if code, _ := e.call(alice, "bg.jobs.submit",
|
|
210
|
+
if code, _ := e.call(alice, "bg.jobs.submit", e.submitParams(key)); code != "" {
|
|
214
211
|
t.Fatalf("%d: %q", i, code)
|
|
215
212
|
}
|
|
216
213
|
}
|
|
217
214
|
lookupSubmitterUID = func(string) string { return "9999" }
|
|
218
|
-
if code, _ := e.call(alice, "bg.jobs.submit",
|
|
215
|
+
if code, _ := e.call(alice, "bg.jobs.submit", e.submitParams("more")); code != "queue-full" {
|
|
219
216
|
t.Fatalf("%q", code)
|
|
220
217
|
}
|
|
221
218
|
}
|
|
222
219
|
|
|
223
220
|
func TestBackgroundListGetCancelAndResult(t *testing.T) {
|
|
224
221
|
e := newBgEnv(t)
|
|
225
|
-
_, out := e.call(alice, "bg.jobs.submit",
|
|
222
|
+
_, out := e.call(alice, "bg.jobs.submit", e.submitParams("k1"))
|
|
226
223
|
id := data(out)["job"].(map[string]any)["id"].(string)
|
|
227
224
|
idp := `{"id":"` + id + `"}`
|
|
228
225
|
|
|
@@ -260,7 +257,7 @@ func TestBackgroundListGetCancelAndResult(t *testing.T) {
|
|
|
260
257
|
|
|
261
258
|
func TestBackgroundCancelQueuedJob(t *testing.T) {
|
|
262
259
|
e := newBgEnv(t)
|
|
263
|
-
_, out := e.call(alice, "bg.jobs.submit",
|
|
260
|
+
_, out := e.call(alice, "bg.jobs.submit", e.submitParams("k1"))
|
|
264
261
|
id := data(out)["job"].(map[string]any)["id"].(string)
|
|
265
262
|
code, out := e.call(alice, "bg.jobs.cancel", `{"id":"`+id+`"}`)
|
|
266
263
|
if code != "" || data(out)["job"].(map[string]any)["state"] != "canceled" {
|
|
@@ -270,7 +267,7 @@ func TestBackgroundCancelQueuedJob(t *testing.T) {
|
|
|
270
267
|
|
|
271
268
|
func TestBackgroundPauseAndResumeQueuedJob(t *testing.T) {
|
|
272
269
|
e := newBgEnv(t)
|
|
273
|
-
_, out := e.call(alice, "bg.jobs.submit",
|
|
270
|
+
_, out := e.call(alice, "bg.jobs.submit", e.submitParams("k1"))
|
|
274
271
|
idp := `{"id":"` + data(out)["job"].(map[string]any)["id"].(string) + `"}`
|
|
275
272
|
for method, state := range map[string]string{"bg.jobs.pause": "paused", "bg.jobs.resume": "queued"} {
|
|
276
273
|
if code, out := e.call(alice, method, idp); code != "" || data(out)["job"].(map[string]any)["state"] != state {
|
|
@@ -296,48 +293,6 @@ func TestBackgroundWorkerAliveExpires(t *testing.T) {
|
|
|
296
293
|
}
|
|
297
294
|
}
|
|
298
295
|
|
|
299
|
-
func TestBackgroundBrowseListsThroughTheCallersSession(t *testing.T) {
|
|
300
|
-
e := newBgEnv(t)
|
|
301
|
-
e.files.page = filestation.Page{Files: []filestation.Entry{{Name: "a.txt", Path: "/share/a.txt"}}, Total: 1}
|
|
302
|
-
code, out := e.call(alice, "bg.browse", `{"folder":"/share","offset":100}`)
|
|
303
|
-
if code != "" {
|
|
304
|
-
t.Fatalf("%s %v", code, out)
|
|
305
|
-
}
|
|
306
|
-
if e.files.folder != "/share" || e.files.offset != 100 {
|
|
307
|
-
t.Fatalf("browsed %q at %d", e.files.folder, e.files.offset)
|
|
308
|
-
}
|
|
309
|
-
files := data(out)["files"].([]any)
|
|
310
|
-
if len(files) != 1 || files[0].(map[string]any)["path"] != "/share/a.txt" {
|
|
311
|
-
t.Fatalf("page %v", out)
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
func TestBackgroundBrowseWithoutFolderListsTheShares(t *testing.T) {
|
|
316
|
-
e := newBgEnv(t)
|
|
317
|
-
if code, out := e.call(alice, "bg.browse", `{}`); code != "" {
|
|
318
|
-
t.Fatalf("%s %v", code, out)
|
|
319
|
-
}
|
|
320
|
-
if e.files.folder != "" || e.files.offset != 0 {
|
|
321
|
-
t.Fatalf("browsed %q at %d", e.files.folder, e.files.offset)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
func TestBackgroundBrowseValidatesParamsAndPassesRefusalsThrough(t *testing.T) {
|
|
326
|
-
e := newBgEnv(t)
|
|
327
|
-
for _, params := range []string{`{"offset":-1}`, `{"folder":"/share","extra":1}`, `{"offset":"1"}`} {
|
|
328
|
-
if code, _ := e.call(alice, "bg.browse", params); code != "bad_request" {
|
|
329
|
-
t.Errorf("%s: %q", params, code)
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if e.files.calls != 0 {
|
|
333
|
-
t.Fatal("invalid params must not reach the NAS")
|
|
334
|
-
}
|
|
335
|
-
e.files.err = &api.Error{Code: "denied", Message: "no"}
|
|
336
|
-
if code, _ := e.call(alice, "bg.browse", `{"folder":"/share"}`); code != "denied" {
|
|
337
|
-
t.Fatalf("got %q", code)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
296
|
func TestBackgroundListWithoutJobsIsAnEmptyArray(t *testing.T) {
|
|
342
297
|
e := newBgEnv(t)
|
|
343
298
|
_, out := e.call(alice, "bg.jobs.list", `{}`)
|
|
@@ -30,5 +30,8 @@ onBeforeUnmount(()=>{disposed=true;provider.dispose()})
|
|
|
30
30
|
<BackgroundSavePanel v-if="mode==='long'" ref="panel" :key="revision" :sdk="sdk" :admin="admin" :locale="ctx.locale" :picker="{provider,keys}" sample-save />
|
|
31
31
|
<DelegatePanel v-else-if="mode==='legacy'" ref="panel" :ctx="ctx" />
|
|
32
32
|
<SectionSkeleton v-else-if="mode==='loading'" />
|
|
33
|
-
<Callout v-else tone="danger" role="alert">
|
|
33
|
+
<Callout v-else tone="danger" role="alert">
|
|
34
|
+
{{ copy.error }}
|
|
35
|
+
<template #actions><Button variant="secondary" @click="detect">{{ copy.retry }}</Button></template>
|
|
36
|
+
</Callout>
|
|
34
37
|
</template>
|