@synopackageland/templates 0.26.5 → 0.27.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.
Files changed (26) hide show
  1. package/README.md +12 -2
  2. package/package.json +1 -1
  3. package/templates/_shared/AGENTS.md +5 -2
  4. package/templates/native-go/README.md +4 -0
  5. package/templates/native-go/backend/internal/app/background.go +9 -2
  6. package/templates/native-go-delegate/backend/internal/app/delegate.go +0 -6
  7. package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +14 -0
  8. package/templates/native-go-jobs/frontend/test/background-page.test.ts +5 -43
  9. package/templates/native-go-vue/frontend/src/App.vue +3 -4
  10. package/templates/native-go-vue/frontend/src/i18n/de.json +2 -1
  11. package/templates/native-go-vue/frontend/src/i18n/en.json +2 -1
  12. package/templates/native-go-vue/frontend/src/i18n/es.json +2 -1
  13. package/templates/native-go-vue/frontend/src/i18n/fr.json +2 -1
  14. package/templates/native-go-vue/frontend/src/i18n/it.json +2 -1
  15. package/templates/native-go-vue/frontend/src/i18n/ja.json +2 -1
  16. package/templates/native-go-vue/frontend/src/i18n/ko.json +2 -1
  17. package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +2 -1
  18. package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +2 -1
  19. package/templates/native-node/README.md +5 -1
  20. package/templates/native-node-jobs/backend/src/jobs.ts +5 -26
  21. package/templates/native-node-jobs/backend/test/jobs.test.ts +7 -41
  22. package/templates/native-python/README.md +5 -1
  23. package/templates/native-python-jobs/backend/app/jobs.py +4 -27
  24. package/templates/native-python-jobs/backend/tests/test_jobs.py +5 -37
  25. package/templates/native-rust/README.md +5 -1
  26. package/templates/native-rust-jobs/backend/src/jobs.rs +14 -141
package/README.md CHANGED
@@ -22,8 +22,8 @@ daemon:
22
22
  window_seconds: 300
23
23
  ```
24
24
 
25
- Templates 0.26.5 pin SDK 0.27.0 in native and Compose projects.
26
- Daemon restart declarations need CLI 0.26.0 or later. Go Apps select
25
+ Templates 0.27.0 inherit UI 0.14.9 from 0.26.5 and pin SDK 0.27.0 in native and Compose projects.
26
+ Use CLI 0.27.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
@@ -214,3 +214,13 @@ the single-call path. Each batch result is handled separately; the shell stays
214
214
  visible while reads run. The backend permissions remain authoritative.
215
215
 
216
216
  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
+
218
+ ### Temporarily disabled NAS hashing example
219
+
220
+ SDK 0.27.0 no longer relays browser DSM sessions from a backend. This release
221
+ explicitly disables the template NAS file hashing example: Go production
222
+ `bg.browse`/`bg.jobs.submit` and bridge `hash.submit` return `unsupported` before
223
+ reading a file or creating a job. The Vue background example page displays an
224
+ unavailable notice. Existing job list/status/pause/resume/cancel/schedule APIs
225
+ and workers remain available for App-owned input; no SDK API or special option
226
+ is introduced. A safe browser-owned input flow is follow-up work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.26.5",
3
+ "version": "0.27.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,6 +110,10 @@ may redistribute; never copy Synology's.
110
110
 
111
111
  ## Background jobs (`jobs`)
112
112
 
113
+ The NAS hashing example is temporarily disabled pending browser-owned input
114
+ migration. Do not use backend DSM session replay or `files.read` to restore it.
115
+ Existing workers and job-control APIs remain available for App-owned input.
116
+
113
117
  The worker runs the App's Work for one claimed job at a time, as the package
114
118
  account. The Work receives the job's Task: it reads the private input copy,
115
119
  reports progress (`done`/`total`, `bytes`/`totalBytes`, `item`) and saves a
@@ -265,8 +269,7 @@ sweep forward the cancel instead of admitting it.
265
269
  The SDK runtime is the CGI: it checks the DSM session, the entry gate and
266
270
  permissions.json, then runs the App once per call, as the package account, and
267
271
  answers the browser itself for every refusal. The App only registers methods
268
- and asks the runtime for services with `ctx.runtime(op, params)`: `files.read`
269
- (the caller's file, `files: session`), `bg.submit`/`bg.get`/`bg.cancel` (the
272
+ and asks the runtime for services with `ctx.runtime(op, params)`: `bg.submit`/`bg.get`/`bg.cancel` (the
270
273
  caller's jobs, `jobs: true`, whose worker hashes each job's input), `root.run`
271
274
  (`rootOps`), `delegate.move_from_staging` (`delegate`) and `portal.status`. A
272
275
  service refusal is a code the browser may see; pass it on. The admin guard
@@ -399,3 +399,7 @@ SDK's `docs/shared-folder.md` for the whole flow.
399
399
  | Target CPUs | x86_64 and aarch64 DSM models (one SPK each). armv7 (DSM 7.2+ platforms `alpine`, `alpine4k`, `armada38x`, `monaco`) is opt-in: add it to `arches`; it covers the Go CGI (plain or Vue UI), jobs, daemon, root-ops and delegate. armv7 jobs, daemon and executor (root-ops, delegate) are built and checked locally but have not been verified on a NAS |
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
+
403
+ The NAS file hashing example is temporarily disabled pending browser-owned
404
+ input migration. NAS import submission returns `unsupported` without reading a
405
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -42,10 +42,11 @@ type backgroundAPI struct {
42
42
  }
43
43
 
44
44
  // BackgroundRoutes is the production assembly of the opt-in background jobs
45
- // API. `synopkgland init --with jobs` wires it in through background.go's
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
46
47
  // replacement of background_none.go; nothing else references it.
47
48
  func BackgroundRoutes(dataDir string) []api.Route {
48
- return newBackgroundRoutes(background.NewRequestStore(filepath.Join(dataDir, "background")), filestation.Local(dataDir))
49
+ return newBackgroundRoutes(background.NewRequestStore(filepath.Join(dataDir, "background")), nil)
49
50
  }
50
51
 
51
52
  // Every request authenticates against DSM afresh. Owner routes are available
@@ -129,6 +130,9 @@ func (b backgroundAPI) browse(ctx context.Context, c api.Caller, r *http.Request
129
130
  if in.Offset < 0 {
130
131
  return nil, api.BadRequest("offset must not be negative")
131
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
+ }
132
136
  return b.files.Browse(ctx, c, r, in.Folder, in.Offset)
133
137
  }
134
138
 
@@ -148,6 +152,9 @@ func (b backgroundAPI) submit(ctx context.Context, c api.Caller, r *http.Request
148
152
  if !access.Owner.Valid() {
149
153
  return nil, &api.Error{Code: "identity_unavailable", Message: "DSM user UID could not be resolved"}
150
154
  }
155
+ if b.files == nil {
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
+ }
151
158
  data, err := b.files.ReadFile(ctx, c, r, in.Folder, in.Relative)
152
159
  if err != nil {
153
160
  return nil, err
@@ -4,7 +4,6 @@ import (
4
4
  "github.com/synopackageland/syno-pkg-land-sdk/api"
5
5
  "github.com/synopackageland/syno-pkg-land-sdk/background"
6
6
  "github.com/synopackageland/syno-pkg-land-sdk/delegate"
7
- "github.com/synopackageland/syno-pkg-land-sdk/filestation"
8
7
  "path/filepath"
9
8
  )
10
9
 
@@ -16,10 +15,5 @@ func DelegateRoutes(cfg Config, scope string) []api.Route {
16
15
  if err == nil {
17
16
  service.Destinations, service.Operations, service.Lifetime = settings.Destinations, settings.Operations, settings.Lifetime
18
17
  }
19
- if cfg.SessionFiles != nil {
20
- service.SessionFiles = cfg.SessionFiles
21
- } else {
22
- service.SessionFiles = filestation.Local(cfg.DataDir)
23
- }
24
18
  return service.Routes()
25
19
  }
@@ -90,3 +90,17 @@ func TestBackgroundRequestSubmitBusyAndProductionConstruction(t *testing.T) {
90
90
  t.Fatal(w.Body.String())
91
91
  }
92
92
  }
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
+ }
@@ -4,8 +4,8 @@ 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
- // The CLI turns the background page on for jobs (and delegate) projects only.
8
- test('a jobs project mounts the background panel for the signed-in user',async()=>{
7
+ // The file-import example is explicitly unavailable until browser input is migrated.
8
+ test('a jobs project explains the disabled example without submitting or reading files',async()=>{
9
9
  const call=vi.fn(async(method:string)=>{
10
10
  if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
11
11
  throw {code:'unknown_method'}
@@ -14,46 +14,8 @@ test('a jobs project mounts the background panel for the signed-in user',async()
14
14
  const app=mount(root,dsmWindow({call}))
15
15
  try{
16
16
  await settle()
17
- expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(true)
18
- expect(root.querySelector('[data-page="background"]')).not.toBeNull()
19
- }finally{app.unmount();root.remove()}
20
- })
21
- test('the background panel pauses a running job and resumes a paused one',async()=>{
22
- const submitter={user:'alice',uid:'1024'}
23
- const jobs=[{id:'run',name:'a.bin',state:'running',submitter},{id:'held',name:'b.bin',state:'paused',pauseReason:'user',submitter}]
24
- const call=vi.fn(async(method:string):Promise<unknown>=>{
25
- if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
26
- if(method==='bg.status')return {owner:submitter,admin:false,revision:1,worker:{username:'pkg',alive:true},limits:{maxInputBytes:1024,maxQueued:8,maxOwnerPending:4,maxOwnerBytes:4096,jobTimeoutSeconds:60}}
27
- if(method==='bg.jobs.list')return {jobs}
28
- if(method==='bg.schedules.list')return {schedules:[]}
29
- return {}
30
- })
31
- const root=document.createElement('div');document.body.append(root)
32
- const app=mount(root,dsmWindow({call}))
33
- const button=(id:string,label:string)=>Array.from(root.querySelectorAll(`[data-job-id="${id}"] button`)).find(b=>b.textContent===label) as HTMLButtonElement|undefined
34
- try{
35
- await settle()
36
- button('run','Pause')!.click();await settle()
37
- expect(call).toHaveBeenCalledWith('bg.jobs.pause',{id:'run'})
38
- button('held','Resume')!.click();await settle()
39
- expect(call).toHaveBeenCalledWith('bg.jobs.resume',{id:'held'})
40
- }finally{app.unmount();root.remove()}
41
- })
42
- test('the header Refresh reloads the background panel',async()=>{
43
- const call=vi.fn(async(method:string):Promise<unknown>=>{
44
- if(method==='app.status')return {package:'Jobs',version:'1',user:'alice',admin:false}
45
- if(method==='bg.status')return {owner:{user:'alice',uid:'1024'},admin:false,revision:1,worker:{username:'pkg',alive:true},limits:{maxInputBytes:1024,maxQueued:8,maxOwnerPending:4,maxOwnerBytes:4096,jobTimeoutSeconds:60}}
46
- if(method==='bg.jobs.list')return {jobs:[]}
47
- if(method==='bg.schedules.list')return {schedules:[]}
48
- return {}
49
- })
50
- const root=document.createElement('div');document.body.append(root)
51
- const app=mount(root,dsmWindow({call}))
52
- const reads=()=>call.mock.calls.filter(([method])=>method==='bg.jobs.list').length
53
- try{
54
- await settle();const before=reads()
55
- // The header button comes first; the panels below may have their own.
56
- ;(Array.from(root.querySelectorAll('button')).find(b=>b.textContent==='Refresh') as HTMLButtonElement).click();await settle()
57
- expect(reads()).toBeGreaterThan(before)
17
+ expect(root.querySelector('[data-background-example-disabled]')?.textContent).toContain('disabled')
18
+ expect(root.querySelector('[data-background-panel]')).toBeNull()
19
+ expect(call.mock.calls.some(([method])=>method.startsWith('bg.'))).toBe(false)
58
20
  }finally{app.unmount();root.remove()}
59
21
  })
@@ -2,7 +2,7 @@
2
2
  import {computed, onMounted, onUnmounted, ref, watch} from 'vue'
3
3
  import {Button, Callout, Form, FormActions, FormItem, KeyValueRow, SettingsSection, Textarea} from '@synopackageland/ui'
4
4
  import {bridgeErrorCopy, sharedFolderAccessFor, type AppStatus, type Context, type ShellPage} from '@synopackageland/sdk'
5
- import {AppShell, BackgroundPanel, FilesPage, LoginPage, PortalSettings, SharedFolderSettings, createSession, shellPages} from '@synopackageland/sdk/vue'
5
+ import {AppShell, FilesPage, LoginPage, PortalSettings, SharedFolderSettings, createSession, shellPages} from '@synopackageland/sdk/vue'
6
6
  import {appCopy} from './i18n'
7
7
  import CapabilityPanels from './CapabilityPanels.vue'
8
8
  import ExecutorGate, {holdsPages} from './ExecutorGate.vue'
@@ -66,11 +66,10 @@ async function refresh() {
66
66
  }
67
67
  // The header Refresh also reloads the panels below and reads the portal again.
68
68
  const panels = ref<{refresh(): unknown}>()
69
- const background = ref<{refresh(): unknown}>()
70
69
  const portalReads = ref(0)
71
70
  const portalKey = computed(() => `${app.value?.user}:${portalReads.value}`)
72
71
  function refreshAll() {
73
- void refresh(); void panels.value?.refresh(); void background.value?.refresh(); portalReads.value++
72
+ void refresh(); void panels.value?.refresh(); portalReads.value++
74
73
  }
75
74
  async function save() {
76
75
  if(disposed || busy.value) return
@@ -122,7 +121,7 @@ onUnmounted(()=>{disposed=true;unsubscribe?.();login.dispose();text.value=''})
122
121
  <CapabilityPanels v-if="app" ref="panels" :key="app.user" :ctx="ctx" :admin="app.admin" />
123
122
  <FilesPage v-if="page==='status'" :ctx="ctx" />
124
123
  <div data-page="background" v-show="page==='background'">
125
- <BackgroundPanel v-if="backgroundEnabled && app" ref="background" :key="app.user" :ctx="ctx" />
124
+ <Callout v-if="backgroundEnabled && app" tone="warning" data-background-example-disabled>{{ copy.backgroundExampleUnavailable }}</Callout>
126
125
  </div>
127
126
  </div>
128
127
  </ExecutorGate>
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Paketname",
19
19
  "user": "Benutzer",
20
- "version": "Version"
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"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Package name",
19
19
  "user": "User",
20
- "version": "Version"
20
+ "version": "Version",
21
+ "backgroundExampleUnavailable": "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Nombre del paquete",
19
19
  "user": "Usuario",
20
- "version": "Versión"
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"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Nom du paquet",
19
19
  "user": "Utilisateur",
20
- "version": "Version"
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"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "Nome pacchetto",
19
19
  "user": "Utente",
20
- "version": "Versione"
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"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "パッケージ名",
19
19
  "user": "ユーザー",
20
- "version": "バージョン"
20
+ "version": "バージョン",
21
+ "backgroundExampleUnavailable": "NAS ファイルのハッシュ例はブラウザー入力への移行まで無効です。既存のジョブ制御 API は利用できます"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "패키지 이름",
19
19
  "user": "사용자",
20
- "version": "버전"
20
+ "version": "버전",
21
+ "backgroundExampleUnavailable": "브라우저 입력 전환이 완료될 때까지 NAS 파일 해시 예제를 사용할 수 없습니다. 기존 작업 제어 API는 계속 사용할 수 있습니다"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "套件名称",
19
19
  "user": "用户帐号",
20
- "version": "版本"
20
+ "version": "版本",
21
+ "backgroundExampleUnavailable": "本版本暂停 NAS 文件哈希示例,待浏览器输入流程完成后恢复。现有任务控制 API 仍可使用"
21
22
  }
@@ -17,5 +17,6 @@
17
17
  },
18
18
  "packageName": "套件名稱",
19
19
  "user": "使用者帳號",
20
- "version": "版本"
20
+ "version": "版本",
21
+ "backgroundExampleUnavailable": "本版本暫停 NAS 檔案雜湊範例,待瀏覽器輸入流程完成後恢復。既有工作控制 API 仍可使用"
21
22
  }
@@ -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` | `files.read` with the caller's session, `bg.submit`, `bg.get`, `bg.cancel` |
114
+ | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `hash.submit` is disabled pending browser input migration; `bg.get` and `bg.cancel` remain available |
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
 
@@ -158,3 +158,7 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
158
158
  | Build hosts | macOS, Linux |
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
+
162
+ The NAS file hashing example is temporarily disabled pending browser-owned
163
+ input migration. NAS import submission returns `unsupported` without reading a
164
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -1,38 +1,17 @@
1
- // Background jobs: hash a NAS file the caller may read.
2
- //
3
- // The SDK's worker runs each job as the package account: it hashes the job's
4
- // input (SHA-256) and keeps the result for the job's owner, the caller. The
5
- // runtime serves files.read with the caller's DSM session and bg.* for the
6
- // caller's own jobs only.
7
- import { copyFileSync, mkdirSync, unlinkSync } from 'node:fs'
8
- import { randomUUID } from 'node:crypto'
9
- import { basename, join } from 'node:path'
1
+ // The NAS hashing example is disabled until browser-owned input is wired.
2
+ // Keep caller-bound job controls; never relay a browser session from the backend.
10
3
  import { AppError, type App, type Context } from '@synopackageland/app'
11
4
  import { strings } from './params.js'
12
5
  import type { Config } from './routes.js'
13
6
  import { service } from './service.js'
14
7
 
15
- /** A job's input is at most 512 KiB, copied into the job when it is submitted. */
16
- const MAX_INPUT = 512 * 1024
17
-
18
8
  export function register(app: App, cfg: Config): void {
19
9
  app.method('hash.submit', (ctx, params) => submit(cfg, ctx, params))
20
10
  app.method('hash.get', (ctx, params) => service(ctx, 'bg.get', strings(params, 'id')))
21
11
  app.method('hash.cancel', (ctx, params) => service(ctx, 'bg.cancel', strings(params, 'id')))
22
12
  }
23
13
 
24
- async function submit(cfg: Config, ctx: Context, params: unknown) {
25
- const { key, path } = strings(params, 'key', 'path')
26
- const read = await service<{ path: string; size: number }>(ctx, 'files.read', { path })
27
- if (read.size > MAX_INPUT) throw new AppError('limit_exceeded', 'The file is larger than allowed.')
28
- // bg.submit takes its input from the data directory; the call's spool is elsewhere.
29
- const inputs = join(cfg.dataDir, 'hash-inputs')
30
- mkdirSync(inputs, { recursive: true, mode: 0o700 })
31
- const input = join(inputs, randomUUID())
32
- copyFileSync(read.path, input)
33
- try {
34
- return await service(ctx, 'bg.submit', { key, name: basename(path), input })
35
- } finally {
36
- unlinkSync(input)
37
- }
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.")
38
17
  }
@@ -1,60 +1,26 @@
1
1
  import assert from 'node:assert/strict'
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
- import { join, sep } from 'node:path'
4
+ import { join } from 'node:path'
5
5
  import { afterEach, beforeEach, test } from 'node:test'
6
6
  import { build, type Config } from '../src/routes.js'
7
- import { Refusal, call } from './support.js'
7
+ import { call } from './support.js'
8
8
 
9
9
  const JOB = { job: { id: 'j1', state: 'queued' }, duplicate: false }
10
10
  let root: string
11
11
  let cfg: Config
12
- let submitted: Record<string, unknown>[]
13
12
 
14
13
  beforeEach(() => {
15
14
  root = mkdtempSync(join(tmpdir(), 'app-'))
16
15
  cfg = { packageId: 'ExampleApp', dataDir: join(root, 'var'), packagesRoot: join(root, 'packages') }
17
16
  mkdirSync(cfg.dataDir)
18
- submitted = []
19
17
  })
20
18
  afterEach(() => rmSync(root, { recursive: true, force: true }))
21
19
 
22
- function spool(content: Buffer) {
23
- const path = join(root, 'spool')
24
- writeFileSync(path, content)
25
- return { path, size: content.length }
26
- }
27
-
28
- // The runtime reads the input file while it answers bg.submit.
29
- function submit(params: unknown) {
30
- const p = params as { input: string }
31
- submitted.push({ ...p, content: readFileSync(p.input, 'utf8') })
32
- return JOB
33
- }
34
-
35
- test('hash.submit hashes a copy of the caller\'s file', async () => {
36
- const services = { 'files.read': () => spool(Buffer.from('hello')), 'bg.submit': submit }
37
- const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/report.txt' }, { services })
38
- assert.deepEqual(reply.data, JOB)
39
- assert.deepEqual(reply.services.map(([op]) => op), ['files.read', 'bg.submit'])
40
- assert.deepEqual(reply.services[0]![1], { path: '/home/report.txt' })
41
- const [job] = submitted as [{ key: string; name: string; content: string; input: string }]
42
- assert.deepEqual([job.key, job.name, job.content], ['k1', 'report.txt', 'hello'])
43
- assert.ok(job.input.startsWith(cfg.dataDir + sep), job.input)
44
- assert.equal(existsSync(job.input), false, 'the copy is removed once submitted')
45
- })
46
-
47
- test('hash.submit refuses a file over the job input limit', async () => {
48
- const services = { 'files.read': () => spool(Buffer.alloc(512 * 1024 + 1)), 'bg.submit': submit }
49
- const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/big.bin' }, { services })
50
- assert.equal(reply.error?.code, 'limit_exceeded')
51
- assert.deepEqual(submitted, [])
52
- })
53
-
54
- test('hash.submit passes on the runtime\'s refusal', async () => {
55
- const services = { 'files.read': () => { throw new Refusal('read_failed', 'Unable to read the file.') } }
56
- const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/a' }, { services })
57
- assert.equal(reply.error?.code, 'read_failed')
20
+ test('the NAS hash example refuses without services or persistent writes', async () => {
21
+ const reply = await call(build(cfg), 'hash.submit', { key: 'k1', path: '/home/report.txt' })
22
+ assert.deepEqual([reply.error?.code, reply.services], ['unsupported', []])
23
+ assert.deepEqual(readdirSync(cfg.dataDir), [])
58
24
  })
59
25
 
60
26
  test('hash.submit refuses invalid params before any service', async () => {
@@ -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` | `files.read` with the caller's session, `bg.submit`, `bg.get`, `bg.cancel` |
111
+ | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `hash.submit` is disabled pending browser input migration; `bg.get` and `bg.cancel` remain available |
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
 
@@ -155,3 +155,7 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
155
155
  | Build hosts | macOS, Linux |
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
+
159
+ The NAS file hashing example is temporarily disabled pending browser-owned
160
+ input migration. NAS import submission returns `unsupported` without reading a
161
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -1,22 +1,10 @@
1
- """Background jobs: hash a NAS file the caller may read.
1
+ """The NAS hashing example is disabled until browser-owned input is wired.
2
2
 
3
- The SDK's worker runs each job as the package account: it hashes the job's
4
- input (SHA-256) and keeps the result for the job's owner, the caller. The
5
- runtime serves files.read with the caller's DSM session and bg.* for the
6
- caller's own jobs only.
3
+ Caller-bound job controls remain available; no backend session replay is used.
7
4
  """
8
-
9
- import os
10
- import shutil
11
- import tempfile
12
-
13
5
  from synopkgland_app import Error
14
-
15
6
  from params import fields
16
7
 
17
- # A job's input is at most 512 KiB, copied into the job when it is submitted.
18
- MAX_INPUT = 512 * 1024
19
-
20
8
 
21
9
  def register(app, cfg):
22
10
  app.method("hash.submit")(lambda ctx, params: submit(cfg, ctx, params))
@@ -25,16 +13,5 @@ def register(app, cfg):
25
13
 
26
14
 
27
15
  def submit(cfg, ctx, params):
28
- p = fields(params, key=str, path=str)
29
- read = ctx.runtime("files.read", {"path": p["path"]})
30
- if read["size"] > MAX_INPUT:
31
- raise Error("limit_exceeded", "The file is larger than allowed.")
32
- # bg.submit takes its input from the data directory; the call's spool is elsewhere.
33
- inputs = os.path.join(cfg.data_dir, "hash-inputs")
34
- os.makedirs(inputs, mode=0o700, exist_ok=True)
35
- with open(read["path"], "rb") as source, tempfile.NamedTemporaryFile(dir=inputs, delete=False) as copy:
36
- shutil.copyfileobj(source, copy)
37
- try:
38
- return ctx.runtime("bg.submit", {"key": p["key"], "name": os.path.basename(p["path"]), "input": copy.name})
39
- finally:
40
- os.unlink(copy.name)
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.")
@@ -2,7 +2,7 @@ import os
2
2
  import tempfile
3
3
  import unittest
4
4
 
5
- from support import Error, call
5
+ from support import call
6
6
 
7
7
  import routes
8
8
 
@@ -16,43 +16,11 @@ class JobsTest(unittest.TestCase):
16
16
  self.data = os.path.join(self.dir.name, "var")
17
17
  os.mkdir(self.data)
18
18
  self.app = routes.build(routes.Config("ExampleApp", self.data))
19
- self.submitted = []
20
19
 
21
- def spool(self, content):
22
- path = os.path.join(self.dir.name, "spool")
23
- with open(path, "wb") as f:
24
- f.write(content)
25
- return {"path": path, "size": len(content)}
26
-
27
- def submit(self, params):
28
- # The runtime reads the input file while it answers bg.submit.
29
- with open(params["input"], "rb") as f:
30
- self.submitted.append(dict(params, content=f.read()))
31
- return JOB
32
-
33
- def test_submit_hashes_a_copy_of_the_callers_file(self):
34
- services = {"files.read": lambda p: self.spool(b"hello"), "bg.submit": self.submit}
35
- reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/report.txt"}, services=services)
36
- self.assertEqual(reply.data, JOB)
37
- self.assertEqual([op for op, _ in reply.services], ["files.read", "bg.submit"])
38
- self.assertEqual(reply.services[0][1], {"path": "/home/report.txt"})
39
- [submitted] = self.submitted
40
- self.assertEqual((submitted["key"], submitted["name"], submitted["content"]), ("k1", "report.txt", b"hello"))
41
- self.assertTrue(submitted["input"].startswith(self.data + os.sep))
42
- self.assertFalse(os.path.exists(submitted["input"]), "the copy is removed once submitted")
43
-
44
- def test_submit_refuses_a_file_over_the_job_input_limit(self):
45
- services = {"files.read": lambda p: self.spool(b"x" * (512 * 1024 + 1)), "bg.submit": self.submit}
46
- reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/big.bin"}, services=services)
47
- self.assertEqual(reply.error["code"], "limit_exceeded")
48
- self.assertEqual(self.submitted, [])
49
-
50
- def test_submit_passes_on_the_runtimes_refusal(self):
51
- def unreadable(params):
52
- raise Error("read_failed", "Unable to read the file.")
53
-
54
- reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/a"}, services={"files.read": unreadable})
55
- self.assertEqual(reply.error["code"], "read_failed")
20
+ def test_nas_hash_example_refuses_without_services_or_writes(self):
21
+ reply = call(self.app, "hash.submit", {"key": "k1", "path": "/home/report.txt"})
22
+ self.assertEqual((reply.error["code"], reply.services), ("unsupported", []))
23
+ self.assertEqual(os.listdir(self.data), [])
56
24
 
57
25
  def test_submit_refuses_invalid_params_before_any_service(self):
58
26
  for params in [None, {"key": "k1"}, {"key": 1, "path": "/home/a"}, {"key": "k1", "path": "/home/a", "x": 1}]:
@@ -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` | `files.read` with the caller's session, `bg.submit`, `bg.get`, `bg.cancel` |
120
+ | `jobs` | `hash.submit`, `hash.get`, `hash.cancel` | `hash.submit` is disabled pending browser input migration; `bg.get` and `bg.cancel` remain available |
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
 
@@ -164,3 +164,7 @@ ticks **Also delete the app data listed above** in the uninstall wizard.
164
164
  | Build hosts | macOS, Linux |
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
+
168
+ The NAS file hashing example is temporarily disabled pending browser-owned
169
+ input migration. NAS import submission returns `unsupported` without reading a
170
+ file or creating a job; existing worker and job-control APIs remain available.
@@ -1,90 +1,38 @@
1
- //! Background jobs: hash a NAS file the caller may read.
2
- //!
3
- //! The SDK's worker runs each job as the package account: it hashes the job's
4
- //! input (SHA-256) and keeps the result for the job's owner, the caller. The
5
- //! runtime serves files.read with the caller's DSM session and bg.* for the
6
- //! caller's own jobs only.
7
-
8
- use crate::routes::{Config, internal};
1
+ //! NAS hashing is disabled until browser-owned input is wired; job controls remain.
2
+ use crate::routes::Config;
9
3
  use serde::Deserialize;
10
4
  use serde_json::{Value, json};
11
- use std::os::unix::fs::DirBuilderExt;
12
- use std::path::{Path, PathBuf};
13
5
  use syno_pkg_land_app::{App, Context, Error, Params};
14
6
 
15
- /// A job's input is at most 512 KiB, copied into the job when it is submitted.
16
- const MAX_INPUT: u64 = 512 * 1024;
17
-
18
7
  #[derive(Deserialize)]
19
8
  #[serde(deny_unknown_fields)]
20
9
  struct Submit {
21
10
  key: String,
22
11
  path: String,
23
12
  }
24
-
25
13
  #[derive(Deserialize)]
26
14
  #[serde(deny_unknown_fields)]
27
15
  struct JobId {
28
16
  id: String,
29
17
  }
30
18
 
31
- #[derive(Deserialize)]
32
- struct Read {
33
- path: PathBuf,
34
- size: u64,
35
- }
36
-
37
- pub fn register(app: &mut App, cfg: &Config) {
38
- let cfg = cfg.clone();
39
- app.method("hash.submit", move |ctx, params| submit(&cfg, ctx, params));
19
+ pub fn register(app: &mut App, _cfg: &Config) {
20
+ app.method("hash.submit", |_ctx, params| -> Result<Value, Error> {
21
+ let Submit { key, path } = params.decode()?;
22
+ let _ = (key, path);
23
+ Err(Error::new("unsupported", "The NAS file hashing example is disabled pending browser input migration. Existing job control APIs remain available."))
24
+ });
40
25
  app.method("hash.get", |ctx, params| job(ctx, "bg.get", params));
41
26
  app.method("hash.cancel", |ctx, params| job(ctx, "bg.cancel", params));
42
27
  }
43
-
44
28
  fn job(ctx: &Context, op: &str, params: &Params) -> Result<Value, Error> {
45
29
  let JobId { id } = params.decode()?;
46
30
  ctx.runtime(op, &json!({"id": id}))
47
31
  }
48
-
49
- fn submit(cfg: &Config, ctx: &Context, params: &Params) -> Result<Value, Error> {
50
- let p: Submit = params.decode()?;
51
- let read: Read = ctx.runtime("files.read", &json!({"path": p.path}))?;
52
- if read.size > MAX_INPUT {
53
- return Err(Error::new(
54
- "limit_exceeded",
55
- "The file is larger than allowed.",
56
- ));
57
- }
58
- // bg.submit takes its input from the data directory; the call's spool is elsewhere.
59
- let inputs = cfg.data_dir.join("hash-inputs");
60
- std::fs::DirBuilder::new()
61
- .recursive(true)
62
- .mode(0o700)
63
- .create(&inputs)
64
- .map_err(internal)?;
65
- let copy = inputs.join(&ctx.request_id);
66
- std::fs::copy(&read.path, &copy).map_err(internal)?;
67
- let name = Path::new(&p.path)
68
- .file_name()
69
- .map_or(String::new(), |n| n.to_string_lossy().into_owned());
70
- let submitted = ctx.runtime(
71
- "bg.submit",
72
- &json!({"key": p.key, "name": name, "input": copy}),
73
- );
74
- let _ = std::fs::remove_file(&copy);
75
- submitted
76
- }
77
-
78
32
  #[cfg(test)]
79
33
  mod tests {
80
34
  use super::*;
81
35
  use crate::test_support::{Services, TempDir, call};
82
- use serde_json::{Value, json};
83
- use std::sync::{Arc, Mutex};
84
-
85
- /// What bg.submit was asked: its params and the input it read.
86
- type Submitted = Arc<Mutex<Vec<(Value, Vec<u8>)>>>;
87
-
88
36
  fn setup() -> (TempDir, Config) {
89
37
  let dir = TempDir::new();
90
38
  let cfg = Config {
@@ -100,94 +48,19 @@ mod tests {
100
48
  json!({"job": {"id": "j1", "state": "queued"}, "duplicate": false})
101
49
  }
102
50
 
103
- /// files.read answers a spool file with content; bg.submit records its params and the input it read.
104
- fn services(dir: &TempDir, content: Vec<u8>, submitted: &Submitted) -> Services {
105
- let spool = dir.path().join("spool");
106
- std::fs::write(&spool, &content).unwrap();
107
- let read = json!({"path": spool, "size": content.len()});
108
- let submitted = submitted.clone();
109
- [
110
- (
111
- "files.read",
112
- Box::new(move |_: &Value| Ok(read.clone())) as _,
113
- ),
114
- (
115
- "bg.submit",
116
- Box::new(move |p: &Value| {
117
- // The runtime reads the input file while it answers bg.submit.
118
- let input = std::fs::read(p["input"].as_str().unwrap()).unwrap();
119
- submitted.lock().unwrap().push((p.clone(), input));
120
- Ok(job())
121
- }) as _,
122
- ),
123
- ]
124
- .into()
125
- }
126
-
127
- #[test]
128
- fn submit_hashes_a_copy_of_the_callers_file() {
129
- let (dir, cfg) = setup();
130
- let submitted = Arc::new(Mutex::new(Vec::new()));
131
- let services = services(&dir, b"hello".to_vec(), &submitted);
132
- let reply = call(
133
- crate::routes::build(cfg.clone()),
134
- "hash.submit",
135
- json!({"key": "k1", "path": "/home/report.txt"}),
136
- "alice",
137
- &services,
138
- );
139
- assert_eq!(reply.data, job());
140
- let ops: Vec<_> = reply.services.iter().map(|(op, _)| op.as_str()).collect();
141
- assert_eq!(ops, ["files.read", "bg.submit"]);
142
- assert_eq!(reply.services[0].1, json!({"path": "/home/report.txt"}));
143
- let submitted = submitted.lock().unwrap();
144
- let (params, content) = &submitted[0];
145
- assert_eq!(
146
- (&params["key"], &params["name"], content.as_slice()),
147
- (&json!("k1"), &json!("report.txt"), b"hello".as_slice())
148
- );
149
- let input = params["input"].as_str().unwrap();
150
- assert!(input.starts_with(cfg.data_dir.to_str().unwrap()), "{input}");
151
- assert!(
152
- !std::path::Path::new(input).exists(),
153
- "the copy is removed once submitted"
154
- );
155
- }
156
-
157
51
  #[test]
158
- fn submit_refuses_a_file_over_the_job_input_limit() {
159
- let (dir, cfg) = setup();
160
- let submitted = Arc::new(Mutex::new(Vec::new()));
161
- let services = services(&dir, vec![0; MAX_INPUT as usize + 1], &submitted);
162
- let reply = call(
163
- crate::routes::build(cfg),
164
- "hash.submit",
165
- json!({"key": "k1", "path": "/home/big.bin"}),
166
- "alice",
167
- &services,
168
- );
169
- assert_eq!(reply.code(), "limit_exceeded");
170
- assert!(submitted.lock().unwrap().is_empty());
171
- }
172
-
173
- #[test]
174
- fn submit_passes_on_the_runtimes_refusal() {
52
+ fn nas_hash_example_refuses_without_services_or_writes() {
175
53
  let (_dir, cfg) = setup();
176
- let services: Services = [(
177
- "files.read",
178
- Box::new(|_: &Value| Err(Error::new("read_failed", "Unable to read the file."))) as _,
179
- )]
180
- .into();
181
54
  let reply = call(
182
- crate::routes::build(cfg),
55
+ crate::routes::build(cfg.clone()),
183
56
  "hash.submit",
184
- json!({"key": "k1", "path": "/home/a"}),
57
+ json!({"key":"k1","path":"/home/report.txt"}),
185
58
  "alice",
186
- &services,
59
+ &Services::new(),
187
60
  );
188
- assert_eq!(reply.code(), "read_failed");
61
+ assert_eq!((reply.code(), reply.services.len()), ("unsupported", 0));
62
+ assert_eq!(std::fs::read_dir(&cfg.data_dir).unwrap().count(), 0);
189
63
  }
190
-
191
64
  #[test]
192
65
  fn submit_refuses_invalid_params_before_any_service() {
193
66
  let (_dir, cfg) = setup();