@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.
- package/README.md +12 -2
- package/package.json +1 -1
- package/templates/_shared/AGENTS.md +5 -2
- package/templates/native-go/README.md +4 -0
- package/templates/native-go/backend/internal/app/background.go +9 -2
- package/templates/native-go-delegate/backend/internal/app/delegate.go +0 -6
- package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +14 -0
- package/templates/native-go-jobs/frontend/test/background-page.test.ts +5 -43
- package/templates/native-go-vue/frontend/src/App.vue +3 -4
- package/templates/native-go-vue/frontend/src/i18n/de.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/en.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/es.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/fr.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/it.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/ja.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/ko.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +2 -1
- package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +2 -1
- package/templates/native-node/README.md +5 -1
- package/templates/native-node-jobs/backend/src/jobs.ts +5 -26
- package/templates/native-node-jobs/backend/test/jobs.test.ts +7 -41
- package/templates/native-python/README.md +5 -1
- package/templates/native-python-jobs/backend/app/jobs.py +4 -27
- package/templates/native-python-jobs/backend/tests/test_jobs.py +5 -37
- package/templates/native-rust/README.md +5 -1
- 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
|
-
|
|
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.
|
|
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)`: `
|
|
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.
|
|
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")),
|
|
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
|
|
8
|
-
test('a jobs project
|
|
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(
|
|
18
|
-
expect(root.querySelector('[data-
|
|
19
|
-
|
|
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,
|
|
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();
|
|
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
|
-
<
|
|
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
|
}
|
|
@@ -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` | `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
|
-
//
|
|
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(
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
2
|
+
import { mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join
|
|
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 {
|
|
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
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
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` | `
|
|
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
|
-
"""
|
|
1
|
+
"""The NAS hashing example is disabled until browser-owned input is wired.
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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` | `
|
|
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
|
-
//!
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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, ©).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(©);
|
|
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
|
-
(¶ms["key"], ¶ms["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
|
|
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":
|
|
57
|
+
json!({"key":"k1","path":"/home/report.txt"}),
|
|
185
58
|
"alice",
|
|
186
|
-
&
|
|
59
|
+
&Services::new(),
|
|
187
60
|
);
|
|
188
|
-
assert_eq!(reply.code(), "
|
|
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();
|