@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
|
@@ -120,6 +120,30 @@ test('the panels show a skeleton while the delegate status loads',async()=>{
|
|
|
120
120
|
const root=render({call,locale:'en'});await settle()
|
|
121
121
|
expect(root.querySelector('[data-ui-skeleton-line]')).not.toBeNull()
|
|
122
122
|
})
|
|
123
|
+
test('a failed status check offers Retry as a Callout action and recovers',async()=>{
|
|
124
|
+
let attempts=0,resolveStatus!:(value:unknown)=>void
|
|
125
|
+
const call=vi.fn(async(method:string)=>{
|
|
126
|
+
if(method==='delegate.status'){
|
|
127
|
+
if(++attempts===1)throw {code:'EXECUTOR_UNAVAILABLE'}
|
|
128
|
+
return new Promise(resolve=>{resolveStatus=resolve})
|
|
129
|
+
}
|
|
130
|
+
if(method==='delegate.destination')return {key:'save',selection:{scope:'home',root:'/home',generation:0},destination:null,status:'missing'}
|
|
131
|
+
throw {code:'unexpected'}
|
|
132
|
+
})
|
|
133
|
+
const root=render({call,locale:'en'});await settle()
|
|
134
|
+
const alert=root.querySelector('[role="alert"]')!
|
|
135
|
+
expect(alert).not.toBeNull()
|
|
136
|
+
const retry=button(root,'Retry')
|
|
137
|
+
expect(alert.querySelector('[data-ui-callout-actions]')?.contains(retry)).toBe(true)
|
|
138
|
+
expect(alert.querySelector('[data-ui-callout-body]')?.querySelector('button')).toBeNull()
|
|
139
|
+
retry.click();await settle()
|
|
140
|
+
expect(attempts).toBe(2)
|
|
141
|
+
expect(root.querySelector('[role="alert"]')).toBeNull()
|
|
142
|
+
expect(root.querySelector('[data-ui-skeleton-line]')).not.toBeNull()
|
|
143
|
+
resolveStatus({lifetime:'until-revoked',destinations:['save']});await settle()
|
|
144
|
+
expect(root.textContent).toContain('Background saves')
|
|
145
|
+
expect(root.querySelector('[data-ui-callout-actions]')).toBeNull()
|
|
146
|
+
})
|
|
123
147
|
// AppShell's ExecutorNotice: a delegate App's pages stay while the executor waits for approval.
|
|
124
148
|
// With root-ops, ExecutorGate holds the pages instead (its own test).
|
|
125
149
|
test.skipIf(holdsPages)('an executor awaiting approval adds a notice above the pages and holds none of them back',async()=>{
|
|
@@ -16,7 +16,6 @@ import (
|
|
|
16
16
|
|
|
17
17
|
func init() {
|
|
18
18
|
// The generic route tests need valid params for every background route.
|
|
19
|
-
// The SDK refuses retired backend session relays before reading a file.
|
|
20
19
|
// No jobs or schedules exist in its fresh data directory.
|
|
21
20
|
const id = `{"id":"missing"}`
|
|
22
21
|
sampleParams["bg.jobs.purge"] = `{"owner":{"user":"missing","uid":"9999"}}`
|
|
@@ -30,16 +29,14 @@ func init() {
|
|
|
30
29
|
sampleOutcome["bg.schedules.delete"] = "not_found"
|
|
31
30
|
sampleOutcome["bg.schedules.enable"] = "not_found"
|
|
32
31
|
sampleParams["bg.status"] = `{}`
|
|
33
|
-
sampleParams["bg.
|
|
34
|
-
sampleParams["bg.jobs.submit"] = `{"key":"k1","folder":"/share","relative":"a.bin"}`
|
|
32
|
+
sampleParams["bg.jobs.submit"] = `{"key":"k1","name":"a.bin","input":"/missing"}`
|
|
35
33
|
sampleParams["bg.jobs.list"] = `{}`
|
|
36
34
|
sampleParams["bg.jobs.get"] = id
|
|
37
35
|
sampleParams["bg.jobs.cancel"] = id
|
|
38
36
|
sampleParams["bg.jobs.pause"] = id
|
|
39
37
|
sampleParams["bg.jobs.resume"] = id
|
|
40
38
|
sampleParams["bg.jobs.result"] = id
|
|
41
|
-
sampleOutcome["bg.
|
|
42
|
-
sampleOutcome["bg.jobs.submit"] = "unsupported"
|
|
39
|
+
sampleOutcome["bg.jobs.submit"] = "bad_request"
|
|
43
40
|
sampleOutcome["bg.jobs.get"] = "not_found"
|
|
44
41
|
sampleOutcome["bg.jobs.cancel"] = "not_found"
|
|
45
42
|
sampleOutcome["bg.jobs.pause"] = "not_found"
|
|
@@ -72,7 +69,7 @@ func TestBackgroundRequestSubmitBusyAndProductionConstruction(t *testing.T) {
|
|
|
72
69
|
}
|
|
73
70
|
defer syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
|
74
71
|
start := time.Now()
|
|
75
|
-
code, out := e.call(alice, "bg.jobs.submit",
|
|
72
|
+
code, out := e.call(alice, "bg.jobs.submit", e.submitParams("k1"))
|
|
76
73
|
if code != "jobs_busy" || time.Since(start) > background.RequestLockTimeout+time.Second {
|
|
77
74
|
t.Fatalf("busy submit: %s %+v", code, out)
|
|
78
75
|
}
|
|
@@ -90,17 +87,3 @@ func TestBackgroundRequestSubmitBusyAndProductionConstruction(t *testing.T) {
|
|
|
90
87
|
t.Fatal(w.Body.String())
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
|
-
|
|
94
|
-
func TestProductionNASHashExampleRefusesWithoutCreatingJobs(t *testing.T) {
|
|
95
|
-
cfg := testConfig(t)
|
|
96
|
-
for _, method := range []string{"bg.browse", "bg.jobs.submit"} {
|
|
97
|
-
code, _ := call(t, cfg, alice, method, sampleParams[method])
|
|
98
|
-
if code != "unsupported" {
|
|
99
|
-
t.Fatalf("%s: %s", method, code)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
jobs, err := background.NewStore(filepath.Join(cfg.DataDir, "background")).List(context.Background())
|
|
103
|
-
if err != nil || len(jobs) != 0 {
|
|
104
|
-
t.Fatalf("disabled example created jobs: %v %v", jobs, err)
|
|
105
|
-
}
|
|
106
|
-
}
|
package/templates/native-go-jobs/frontend/test/{background-page.test.ts → background-shell.test.ts}
RENAMED
|
@@ -4,8 +4,7 @@ import {nextTick} from 'vue'
|
|
|
4
4
|
import {mount} from '../src/main'
|
|
5
5
|
import {dsmWindow} from './context'
|
|
6
6
|
async function settle(){for(let i=0;i<20;i++){await Promise.resolve();await nextTick()}}
|
|
7
|
-
|
|
8
|
-
test('a jobs project explains the disabled example without submitting or reading files',async()=>{
|
|
7
|
+
test('a jobs project opens its shell without starting a job',async()=>{
|
|
9
8
|
const call=vi.fn(async(method:string)=>{
|
|
10
9
|
if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
|
|
11
10
|
throw {code:'unknown_method'}
|
|
@@ -14,8 +13,7 @@ test('a jobs project explains the disabled example without submitting or reading
|
|
|
14
13
|
const app=mount(root,dsmWindow({call}))
|
|
15
14
|
try{
|
|
16
15
|
await settle()
|
|
17
|
-
expect(root.querySelector('[data-
|
|
18
|
-
expect(root.querySelector('[data-background-panel]')).toBeNull()
|
|
16
|
+
expect(root.querySelector('[data-app-status]')).not.toBeNull()
|
|
19
17
|
expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(false)
|
|
20
18
|
}finally{app.unmount();root.remove()}
|
|
21
19
|
})
|
|
@@ -43,16 +43,13 @@ Refresh and save expose busy state and localized errors. Reopen refreshes status
|
|
|
43
43
|
Each mount owns its state; unmount unsubscribes from host events and ignores late
|
|
44
44
|
responses. Backend route policies decide access.
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Background jobs
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
- **Errors and cleanup:** danger alerts map known error codes to localized safe copy, with a localized fallback rather than raw server text. The catalogs cover nine locales. Preserve per-mount disposal: stop polling, dispose the picker, revoke result download URLs and prevent pending responses from restoring disposed state. Deletion confirmation explains that queued jobs cancel, running jobs drain and finished results remain.
|
|
54
|
-
|
|
55
|
-
This layout is checked against the template's local component fixtures at the desktop size and at the DSM minimum window size, including the states and recovery copy above. That validates the component contract only; visual acceptance on a specific DSM release is the App developer's own check.
|
|
48
|
+
Job producers import their own input into the App data directory. The backend
|
|
49
|
+
keeps submission, progress, cancellation and private-copy scheduling contracts;
|
|
50
|
+
the shell does not pick or import NAS files. App-specific job controls can use
|
|
51
|
+
the installed SDK and UI components. Keep explicit consent for schedules and
|
|
52
|
+
user-and-UID ownership isolation, and stop polling on unmount.
|
|
56
53
|
|
|
57
54
|
## Do's and Don'ts
|
|
58
55
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"name": "native-dsm-vue-app",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@lucide/vue": "1.48.0",
|
|
10
|
-
"@synopackageland/sdk": "0.
|
|
11
|
-
"@synopackageland/ui": "0.14.
|
|
10
|
+
"@synopackageland/sdk": "0.28.0",
|
|
11
|
+
"@synopackageland/ui": "0.14.10",
|
|
12
12
|
"vue": "3.5.43"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
@@ -1152,9 +1152,9 @@
|
|
|
1152
1152
|
}
|
|
1153
1153
|
},
|
|
1154
1154
|
"node_modules/@synopackageland/sdk": {
|
|
1155
|
-
"version": "0.
|
|
1156
|
-
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.
|
|
1157
|
-
"integrity": "sha512-
|
|
1155
|
+
"version": "0.28.0",
|
|
1156
|
+
"resolved": "https://registry.npmjs.org/@synopackageland/sdk/-/sdk-0.28.0.tgz",
|
|
1157
|
+
"integrity": "sha512-SywxsuolhSKZKSk8HqOwu0m2BY577i8EvYojBUiN9UG8eSoQnJUTAEbfdvlLF1YbCZE6z5x2sV7DJvq8LYMCzg==",
|
|
1158
1158
|
"license": "MIT",
|
|
1159
1159
|
"peerDependencies": {
|
|
1160
1160
|
"@lucide/vue": "^1.48.0",
|
|
@@ -1171,9 +1171,9 @@
|
|
|
1171
1171
|
}
|
|
1172
1172
|
},
|
|
1173
1173
|
"node_modules/@synopackageland/ui": {
|
|
1174
|
-
"version": "0.14.
|
|
1175
|
-
"resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.14.
|
|
1176
|
-
"integrity": "sha512-
|
|
1174
|
+
"version": "0.14.10",
|
|
1175
|
+
"resolved": "https://registry.npmjs.org/@synopackageland/ui/-/ui-0.14.10.tgz",
|
|
1176
|
+
"integrity": "sha512-4xq1LvbBaM6HdDY2jcaQUe+nMfBdFb9GGq6ds/fsozfTrLnrZm0ZqUlmbosJuPqO8655ven2/FB0bKTnHj16mA==",
|
|
1177
1177
|
"license": "MIT",
|
|
1178
1178
|
"dependencies": {
|
|
1179
1179
|
"@internationalized/date": "3.12.4",
|
|
@@ -121,7 +121,6 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
|
|
|
121
121
|
<CapabilityPanels v-if="app" ref="panels" :key="app.user" :ctx="ctx" :admin="app.admin" />
|
|
122
122
|
<FilesPage v-if="page==='status'" :ctx="ctx" />
|
|
123
123
|
<div data-page="background" v-show="page==='background'">
|
|
124
|
-
<Callout v-if="backgroundEnabled && app" tone="warning" data-background-example-disabled>{{ copy.backgroundExampleUnavailable }}</Callout>
|
|
125
124
|
</div>
|
|
126
125
|
</div>
|
|
127
126
|
</ExecutorGate>
|
|
@@ -17,6 +17,5 @@
|
|
|
17
17
|
},
|
|
18
18
|
"packageName": "Paketname",
|
|
19
19
|
"user": "Benutzer",
|
|
20
|
-
"version": "Version"
|
|
21
|
-
"backgroundExampleUnavailable": "Das NAS-Datei-Hash-Beispiel ist bis zur Umstellung auf Browser-Eingaben deaktiviert. Bestehende APIs zur Auftragssteuerung bleiben verfügbar"
|
|
20
|
+
"version": "Version"
|
|
22
21
|
}
|
|
@@ -17,6 +17,5 @@
|
|
|
17
17
|
},
|
|
18
18
|
"packageName": "Package name",
|
|
19
19
|
"user": "User",
|
|
20
|
-
"version": "Version"
|
|
21
|
-
"backgroundExampleUnavailable": "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available"
|
|
20
|
+
"version": "Version"
|
|
22
21
|
}
|
|
@@ -17,6 +17,5 @@
|
|
|
17
17
|
},
|
|
18
18
|
"packageName": "Nombre del paquete",
|
|
19
19
|
"user": "Usuario",
|
|
20
|
-
"version": "Versión"
|
|
21
|
-
"backgroundExampleUnavailable": "El ejemplo de hash de archivos NAS está desactivado hasta migrar la entrada al navegador. Las API de control de trabajos siguen disponibles"
|
|
20
|
+
"version": "Versión"
|
|
22
21
|
}
|
|
@@ -17,6 +17,5 @@
|
|
|
17
17
|
},
|
|
18
18
|
"packageName": "Nom du paquet",
|
|
19
19
|
"user": "Utilisateur",
|
|
20
|
-
"version": "Version"
|
|
21
|
-
"backgroundExampleUnavailable": "L’exemple de hachage de fichiers NAS est désactivé en attendant la migration des entrées du navigateur. Les API de contrôle des tâches restent disponibles"
|
|
20
|
+
"version": "Version"
|
|
22
21
|
}
|
|
@@ -17,6 +17,5 @@
|
|
|
17
17
|
},
|
|
18
18
|
"packageName": "Nome pacchetto",
|
|
19
19
|
"user": "Utente",
|
|
20
|
-
"version": "Versione"
|
|
21
|
-
"backgroundExampleUnavailable": "L’esempio di hashing dei file NAS è disabilitato in attesa della migrazione dell’input dal browser. Le API di controllo dei processi restano disponibili"
|
|
20
|
+
"version": "Versione"
|
|
22
21
|
}
|
|
@@ -111,7 +111,7 @@ rebuilding the SPK.
|
|
|
111
111
|
|
|
112
112
|
| `--with` | Methods | Runtime services |
|
|
113
113
|
| --- | --- | --- |
|
|
114
|
-
| `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `
|
|
114
|
+
| `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | Submit an App-owned imported file with `{key, name, input}` via `bg.submit`; look up or cancel the caller's job via `bg.get` / `bg.cancel` |
|
|
115
115
|
| `root-ops` | `whoami.run` | `root.run` of `root/whoami.sh`, once an administrator approves the executor |
|
|
116
116
|
| `delegate` | `report.save` | `delegate.move_from_staging` into a folder the caller chose |
|
|
117
117
|
|
|
@@ -159,6 +159,12 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
|
|
|
159
159
|
| Target CPUs | x86_64 and aarch64 DSM models (one SPK each); armv7 is opt-in in `arches`, built but not NAS-verified |
|
|
160
160
|
| DSM | 7.1 or later declared (`os_min_ver`) |
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
## Background job input
|
|
163
|
+
|
|
164
|
+
Jobs process an existing file imported by the App's producer into its private
|
|
165
|
+
data directory. The submission params are `{key, name, input}`: an idempotency
|
|
166
|
+
key, a display name and an absolute path inside that directory. The SDK keeps a
|
|
167
|
+
bounded private copy for the worker; no browser session or NAS picker is used.
|
|
168
|
+
Submit with `hash.submit`; use `hash.get` for progress and `hash.cancel` to
|
|
169
|
+
cancel. These handlers call the existing `bg.submit`, `bg.get` and `bg.cancel`
|
|
170
|
+
services; the runtime checks the input path and binds ownership to the caller.
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
import { AppError, type App, type Context } from '@synopackageland/app'
|
|
1
|
+
// Background jobs over files already imported into the App data directory.
|
|
2
|
+
import type { App } from '@synopackageland/app'
|
|
4
3
|
import { strings } from './params.js'
|
|
5
4
|
import type { Config } from './routes.js'
|
|
6
5
|
import { service } from './service.js'
|
|
7
6
|
|
|
8
|
-
export function register(app: App,
|
|
9
|
-
app.method('hash.submit', (ctx, params) => submit(
|
|
7
|
+
export function register(app: App, _cfg: Config): void {
|
|
8
|
+
app.method('hash.submit', (ctx, params) => service(ctx, 'bg.submit', strings(params, 'key', 'name', 'input')))
|
|
10
9
|
app.method('hash.get', (ctx, params) => service(ctx, 'bg.get', strings(params, 'id')))
|
|
11
10
|
app.method('hash.cancel', (ctx, params) => service(ctx, 'bg.cancel', strings(params, 'id')))
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
async function submit(_cfg: Config, _ctx: Context, params: unknown) {
|
|
15
|
-
strings(params, 'key', 'path')
|
|
16
|
-
throw new AppError('unsupported', "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available.")
|
|
17
|
-
}
|
|
@@ -17,14 +17,15 @@ beforeEach(() => {
|
|
|
17
17
|
})
|
|
18
18
|
afterEach(() => rmSync(root, { recursive: true, force: true }))
|
|
19
19
|
|
|
20
|
-
test('
|
|
21
|
-
const
|
|
22
|
-
|
|
20
|
+
test('hash.submit passes imported input to the runtime', async () => {
|
|
21
|
+
const params = { key: 'k1', name: 'report.txt', input: join(cfg.dataDir, 'report.txt') }
|
|
22
|
+
const reply = await call(build(cfg), 'hash.submit', params, { services: { 'bg.submit': () => JOB } })
|
|
23
|
+
assert.deepEqual([reply.data, reply.services], [JOB, [['bg.submit', params]]])
|
|
23
24
|
assert.deepEqual(readdirSync(cfg.dataDir), [])
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
test('hash.submit refuses invalid params before any service', async () => {
|
|
27
|
-
for (const params of [null, { key: 'k1' }, { key: 1,
|
|
28
|
+
for (const params of [null, { key: 'k1' }, { key: 1, name: 'a', input: '/app/a' }, { key: 'k1', name: 'a', input: '/app/a', x: 1 }]) {
|
|
28
29
|
const reply = await call(build(cfg), 'hash.submit', params)
|
|
29
30
|
assert.deepEqual([reply.error?.code, reply.services], ['bad_request', []], JSON.stringify(params))
|
|
30
31
|
}
|
|
@@ -108,7 +108,7 @@ rebuilding the SPK.
|
|
|
108
108
|
|
|
109
109
|
| `--with` | Methods | Runtime services |
|
|
110
110
|
| --- | --- | --- |
|
|
111
|
-
| `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `
|
|
111
|
+
| `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | Submit an App-owned imported file with `{key, name, input}` via `bg.submit`; look up or cancel the caller's job via `bg.get` / `bg.cancel` |
|
|
112
112
|
| `root-ops` | `whoami.run` | `root.run` of `root/whoami.sh`, once an administrator approves the executor |
|
|
113
113
|
| `delegate` | `report.save` | `delegate.move_from_staging` into a folder the caller chose |
|
|
114
114
|
|
|
@@ -156,6 +156,12 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
|
|
|
156
156
|
| Target CPUs | x86_64 and aarch64 DSM models (one SPK each); armv7 is opt-in in `arches`, built but not NAS-verified |
|
|
157
157
|
| DSM | 7.1 or later declared (`os_min_ver`) |
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
## Background job input
|
|
160
|
+
|
|
161
|
+
Jobs process an existing file imported by the App's producer into its private
|
|
162
|
+
data directory. The submission params are `{key, name, input}`: an idempotency
|
|
163
|
+
key, a display name and an absolute path inside that directory. The SDK keeps a
|
|
164
|
+
bounded private copy for the worker; no browser session or NAS picker is used.
|
|
165
|
+
Submit with `hash.submit`; use `hash.get` for progress and `hash.cancel` to
|
|
166
|
+
cancel. These handlers call the existing `bg.submit`, `bg.get` and `bg.cancel`
|
|
167
|
+
services; the runtime checks the input path and binds ownership to the caller.
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
Caller-bound job controls remain available; no backend session replay is used.
|
|
4
|
-
"""
|
|
5
|
-
from synopkgland_app import Error
|
|
1
|
+
"""Background jobs over files already imported into the App data directory."""
|
|
6
2
|
from params import fields
|
|
7
3
|
|
|
8
4
|
|
|
9
5
|
def register(app, cfg):
|
|
10
|
-
app.method("hash.submit")(lambda ctx, params: submit(
|
|
6
|
+
app.method("hash.submit")(lambda ctx, params: ctx.runtime("bg.submit", fields(params, key=str, name=str, input=str)))
|
|
11
7
|
app.method("hash.get")(lambda ctx, params: ctx.runtime("bg.get", fields(params, id=str)))
|
|
12
8
|
app.method("hash.cancel")(lambda ctx, params: ctx.runtime("bg.cancel", fields(params, id=str)))
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def submit(cfg, ctx, params):
|
|
16
|
-
fields(params, key=str, path=str)
|
|
17
|
-
raise Error("unsupported", "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available.")
|
|
@@ -17,13 +17,14 @@ class JobsTest(unittest.TestCase):
|
|
|
17
17
|
os.mkdir(self.data)
|
|
18
18
|
self.app = routes.build(routes.Config("ExampleApp", self.data))
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
self.
|
|
20
|
+
def test_submit_passes_imported_input_to_the_runtime(self):
|
|
21
|
+
params = {"key": "k1", "name": "report.txt", "input": os.path.join(self.data, "report.txt")}
|
|
22
|
+
reply = call(self.app, "hash.submit", params, services={"bg.submit": lambda p: JOB})
|
|
23
|
+
self.assertEqual((reply.data, reply.services), (JOB, [("bg.submit", params)]))
|
|
23
24
|
self.assertEqual(os.listdir(self.data), [])
|
|
24
25
|
|
|
25
26
|
def test_submit_refuses_invalid_params_before_any_service(self):
|
|
26
|
-
for params in [None, {"key": "k1"}, {"key": 1, "
|
|
27
|
+
for params in [None, {"key": "k1"}, {"key": 1, "name": "a", "input": "/app/a"}, {"key": "k1", "name": "a", "input": "/app/a", "x": 1}]:
|
|
27
28
|
with self.subTest(params=params):
|
|
28
29
|
reply = call(self.app, "hash.submit", params)
|
|
29
30
|
self.assertEqual((reply.error["code"], reply.services), ("bad_request", []))
|
|
@@ -117,7 +117,7 @@ rebuilding the SPK.
|
|
|
117
117
|
|
|
118
118
|
| `--with` | Methods | Runtime services |
|
|
119
119
|
| --- | --- | --- |
|
|
120
|
-
| `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `
|
|
120
|
+
| `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | Submit an App-owned imported file with `{key, name, input}` via `bg.submit`; look up or cancel the caller's job via `bg.get` / `bg.cancel` |
|
|
121
121
|
| `root-ops` | `whoami.run` | `root.run` of `root/whoami.sh`, once an administrator approves the executor |
|
|
122
122
|
| `delegate` | `report.save` | `delegate.move_from_staging` into a folder the caller chose |
|
|
123
123
|
|
|
@@ -165,6 +165,12 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
|
|
|
165
165
|
| Target CPUs | x86_64 and aarch64 DSM models (one SPK each); armv7 is opt-in in `arches`, built but not NAS-verified |
|
|
166
166
|
| DSM | 7.1 or later declared (`os_min_ver`) |
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
## Background job input
|
|
169
|
+
|
|
170
|
+
Jobs process an existing file imported by the App's producer into its private
|
|
171
|
+
data directory. The submission params are `{key, name, input}`: an idempotency
|
|
172
|
+
key, a display name and an absolute path inside that directory. The SDK keeps a
|
|
173
|
+
bounded private copy for the worker; no browser session or NAS picker is used.
|
|
174
|
+
Submit with `hash.submit`; use `hash.get` for progress and `hash.cancel` to
|
|
175
|
+
cancel. These handlers call the existing `bg.submit`, `bg.get` and `bg.cancel`
|
|
176
|
+
services; the runtime checks the input path and binds ownership to the caller.
|
|
@@ -104,8 +104,8 @@ dependencies = [
|
|
|
104
104
|
|
|
105
105
|
[[package]]
|
|
106
106
|
name = "syno-pkg-land-app"
|
|
107
|
-
version = "0.
|
|
108
|
-
source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.
|
|
107
|
+
version = "0.28.0"
|
|
108
|
+
source = "git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=v0.28.0#a87bed308f80dbe99fd81f81e4e2071ae6ea432b"
|
|
109
109
|
dependencies = [
|
|
110
110
|
"libc",
|
|
111
111
|
"serde",
|
|
@@ -9,7 +9,7 @@ libc = "0.2"
|
|
|
9
9
|
serde = { version = "1", features = ["derive"] }
|
|
10
10
|
serde_json = "1"
|
|
11
11
|
# The SDK version is the tag; `synopkgland check` reads it from Cargo.lock.
|
|
12
|
-
syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.
|
|
12
|
+
syno-pkg-land-app = { git = "https://github.com/synopackageland/syno-pkg-land-sdk", tag = "v0.28.0" }
|
|
13
13
|
|
|
14
14
|
[profile.release]
|
|
15
15
|
# A handler panic becomes an "internal" reply; abort would end the App without one.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//! Background jobs over files already imported into the App data directory.
|
|
2
2
|
use crate::routes::Config;
|
|
3
3
|
use serde::Deserialize;
|
|
4
4
|
use serde_json::{Value, json};
|
|
@@ -8,7 +8,8 @@ use syno_pkg_land_app::{App, Context, Error, Params};
|
|
|
8
8
|
#[serde(deny_unknown_fields)]
|
|
9
9
|
struct Submit {
|
|
10
10
|
key: String,
|
|
11
|
-
|
|
11
|
+
name: String,
|
|
12
|
+
input: String,
|
|
12
13
|
}
|
|
13
14
|
#[derive(Deserialize)]
|
|
14
15
|
#[serde(deny_unknown_fields)]
|
|
@@ -17,11 +18,13 @@ struct JobId {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
pub fn register(app: &mut App, _cfg: &Config) {
|
|
20
|
-
app.method("hash.submit", |
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
21
|
+
app.method("hash.submit", |ctx, params| -> Result<Value, Error> {
|
|
22
|
+
let Submit { key, name, input } = params.decode()?;
|
|
23
|
+
ctx.runtime(
|
|
24
|
+
"bg.submit",
|
|
25
|
+
&json!({"key": key, "name": name, "input": input}),
|
|
26
|
+
)
|
|
27
|
+
});
|
|
25
28
|
app.method("hash.get", |ctx, params| job(ctx, "bg.get", params));
|
|
26
29
|
app.method("hash.cancel", |ctx, params| job(ctx, "bg.cancel", params));
|
|
27
30
|
}
|
|
@@ -49,17 +52,20 @@ mod tests {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
#[test]
|
|
52
|
-
fn
|
|
55
|
+
fn submit_passes_imported_input_to_the_runtime() {
|
|
53
56
|
let (_dir, cfg) = setup();
|
|
57
|
+
let params =
|
|
58
|
+
json!({"key":"k1","name":"report.txt","input":cfg.data_dir.join("report.txt")});
|
|
59
|
+
let services: Services = [("bg.submit", Box::new(|_: &Value| Ok(job())) as _)].into();
|
|
54
60
|
let reply = call(
|
|
55
|
-
crate::routes::build(cfg
|
|
61
|
+
crate::routes::build(cfg),
|
|
56
62
|
"hash.submit",
|
|
57
|
-
|
|
63
|
+
params.clone(),
|
|
58
64
|
"alice",
|
|
59
|
-
&
|
|
65
|
+
&services,
|
|
60
66
|
);
|
|
61
|
-
assert_eq!(
|
|
62
|
-
assert_eq!(
|
|
67
|
+
assert_eq!(reply.data, job());
|
|
68
|
+
assert_eq!(reply.services, vec![("bg.submit".to_string(), params)]);
|
|
63
69
|
}
|
|
64
70
|
#[test]
|
|
65
71
|
fn submit_refuses_invalid_params_before_any_service() {
|
|
@@ -67,8 +73,8 @@ mod tests {
|
|
|
67
73
|
for params in [
|
|
68
74
|
Value::Null,
|
|
69
75
|
json!({"key": "k1"}),
|
|
70
|
-
json!({"key": 1, "
|
|
71
|
-
json!({"key": "k1", "
|
|
76
|
+
json!({"key": 1, "name": "a", "input": "/app/a"}),
|
|
77
|
+
json!({"key": "k1", "name": "a", "input": "/app/a", "x": 1}),
|
|
72
78
|
] {
|
|
73
79
|
let reply = call(
|
|
74
80
|
crate::routes::build(cfg.clone()),
|