@synopackageland/templates 0.9.9 → 0.10.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 (132) hide show
  1. package/README.md +27 -27
  2. package/package.json +2 -2
  3. package/templates/_shared/AGENTS.md +53 -15
  4. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  5. package/templates/native-go/backend/cmd/app/main.go +15 -1
  6. package/templates/native-go/backend/cmd/app/main_test.go +33 -0
  7. package/templates/native-go/backend/go.mod +1 -1
  8. package/templates/native-go/backend/go.sum +2 -2
  9. package/templates/native-go/i18n/messages/de.json +4 -0
  10. package/templates/native-go/i18n/messages/en.json +4 -0
  11. package/templates/native-go/i18n/messages/es.json +4 -0
  12. package/templates/native-go/i18n/messages/fr.json +4 -0
  13. package/templates/native-go/i18n/messages/it.json +4 -0
  14. package/templates/native-go/i18n/messages/ja.json +4 -0
  15. package/templates/native-go/i18n/messages/ko.json +4 -0
  16. package/templates/native-go/i18n/messages/zh-CN.json +4 -0
  17. package/templates/native-go/i18n/messages/zh-TW.json +4 -0
  18. package/templates/native-go/ui/app.js +2 -1
  19. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -2
  20. package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
  21. package/templates/native-go-vue/README.md +19 -7
  22. package/templates/native-go-vue/frontend/package-lock.json +9 -9
  23. package/templates/native-go-vue/frontend/package.json +2 -2
  24. package/templates/native-go-vue/frontend/src/App.vue +31 -36
  25. package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
  26. package/templates/native-go-vue/frontend/test/context.ts +8 -0
  27. package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
  28. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
  29. package/templates/native-node/README.md +153 -0
  30. package/templates/native-node/backend/app/package.json +4 -0
  31. package/templates/native-node/backend/app/permissions.json +5 -0
  32. package/templates/native-node/backend/package-lock.json +59 -0
  33. package/templates/native-node/backend/package.json +17 -0
  34. package/templates/native-node/backend/src/delegate.ts +5 -0
  35. package/templates/native-node/backend/src/jobs.ts +5 -0
  36. package/templates/native-node/backend/src/main.ts +4 -0
  37. package/templates/native-node/backend/src/params.ts +11 -0
  38. package/templates/native-node/backend/src/root-ops.ts +5 -0
  39. package/templates/native-node/backend/src/routes.ts +97 -0
  40. package/templates/native-node/backend/src/service.ts +22 -0
  41. package/templates/native-node/backend/test/routes.test.ts +75 -0
  42. package/templates/native-node/backend/test/support.ts +67 -0
  43. package/templates/native-node/backend/tsconfig.build.json +8 -0
  44. package/templates/native-node/backend/tsconfig.json +18 -0
  45. package/templates/native-node/dsm-package.yaml +80 -0
  46. package/templates/native-node/gitignore +4 -0
  47. package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
  48. package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
  49. package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
  50. package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
  51. package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
  52. package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
  53. package/templates/native-node-vue/README.md +56 -0
  54. package/templates/native-node-vue/gitignore +4 -0
  55. package/templates/native-python/README.md +150 -0
  56. package/templates/native-python/backend/app/delegate.py +5 -0
  57. package/templates/native-python/backend/app/jobs.py +5 -0
  58. package/templates/native-python/backend/app/main.py +5 -0
  59. package/templates/native-python/backend/app/params.py +10 -0
  60. package/templates/native-python/backend/app/permissions.json +5 -0
  61. package/templates/native-python/backend/app/root_ops.py +5 -0
  62. package/templates/native-python/backend/app/routes.py +107 -0
  63. package/templates/native-python/backend/tests/support.py +72 -0
  64. package/templates/native-python/backend/tests/test_routes.py +73 -0
  65. package/templates/native-python/dsm-package.yaml +77 -0
  66. package/templates/native-python/gitignore +2 -0
  67. package/templates/native-python-delegate/backend/app/delegate.py +42 -0
  68. package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
  69. package/templates/native-python-jobs/backend/app/jobs.py +40 -0
  70. package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
  71. package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
  72. package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
  73. package/templates/native-python-vue/README.md +56 -0
  74. package/templates/native-python-vue/gitignore +3 -0
  75. package/templates/native-rust/README.md +74 -296
  76. package/templates/native-rust/backend/Cargo.lock +6 -6
  77. package/templates/native-rust/backend/Cargo.toml +2 -10
  78. package/templates/native-rust/backend/src/delegate.rs +6 -0
  79. package/templates/native-rust/backend/src/jobs.rs +6 -0
  80. package/templates/native-rust/backend/src/main.rs +12 -0
  81. package/templates/native-rust/backend/src/root_ops.rs +6 -0
  82. package/templates/native-rust/backend/src/routes.rs +352 -0
  83. package/templates/native-rust/backend/src/test_support.rs +91 -0
  84. package/templates/native-rust/dsm-package.yaml +10 -21
  85. package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
  86. package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
  87. package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
  88. package/templates/native-rust-vue/README.md +35 -34
  89. package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
  90. package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
  91. package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
  92. package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
  93. package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
  94. package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
  95. package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
  96. package/templates/native-rust/backend/src/app/background/work.rs +0 -78
  97. package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
  98. package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
  99. package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
  100. package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
  101. package/templates/native-rust/backend/src/app/identity.rs +0 -56
  102. package/templates/native-rust/backend/src/app/migrations.rs +0 -79
  103. package/templates/native-rust/backend/src/app/mod.rs +0 -30
  104. package/templates/native-rust/backend/src/app/portal.rs +0 -317
  105. package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
  106. package/templates/native-rust/backend/src/app/routes.rs +0 -315
  107. package/templates/native-rust/backend/src/app/session_files.rs +0 -19
  108. package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
  109. package/templates/native-rust/backend/src/app/test_support.rs +0 -202
  110. package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
  111. package/templates/native-rust/backend/src/bin/api.rs +0 -72
  112. package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
  113. package/templates/native-rust/backend/src/build_env.rs +0 -155
  114. package/templates/native-rust/backend/src/lib.rs +0 -6
  115. package/templates/native-rust/backend/src/permissions.rs +0 -54
  116. package/templates/native-rust/backend/src/process.rs +0 -91
  117. package/templates/native-rust/i18n/glossary.json +0 -665
  118. package/templates/native-rust/i18n/messages/de.json +0 -32
  119. package/templates/native-rust/i18n/messages/en.json +0 -32
  120. package/templates/native-rust/i18n/messages/es.json +0 -32
  121. package/templates/native-rust/i18n/messages/fr.json +0 -32
  122. package/templates/native-rust/i18n/messages/it.json +0 -32
  123. package/templates/native-rust/i18n/messages/ja.json +0 -32
  124. package/templates/native-rust/i18n/messages/ko.json +0 -32
  125. package/templates/native-rust/i18n/messages/zh-CN.json +0 -32
  126. package/templates/native-rust/i18n/messages/zh-TW.json +0 -32
  127. package/templates/native-rust/ui/app.css +0 -45
  128. package/templates/native-rust/ui/app.js +0 -111
  129. package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
  130. package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
  131. package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
  132. package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
@@ -1,424 +0,0 @@
1
- //! The opt-in background jobs API: owner-scoped routes over the SDK's
2
- //! package-private spool. The overlay `background_routes.rs` decides whether
3
- //! a build serves them.
4
-
5
- mod owner;
6
- #[cfg(test)]
7
- mod owner_tests;
8
- mod schedule;
9
- #[cfg(test)]
10
- mod schedule_tests;
11
- #[cfg(test)]
12
- mod tests;
13
- mod work;
14
- #[cfg(test)]
15
- mod work_tests;
16
-
17
- pub use work::hash_work;
18
-
19
- use std::sync::Arc;
20
- use std::time::Duration;
21
-
22
- use serde::Deserialize;
23
- use serde_json::{Value, json};
24
- use syno_pkg_land_sdk::api::{self, Caller, Route};
25
- use syno_pkg_land_sdk::background::{
26
- Access, JOB_TIMEOUT, Job, MAX_INPUT_BYTES, MAX_OWNER_BYTES, MAX_OWNER_PENDING, MAX_QUEUED,
27
- MAX_SCHEDULES, MAX_TOTAL_BYTES, Store, StoreError, SubmitRequest, WorkerInfo,
28
- };
29
- use syno_pkg_land_sdk::ctx::{Context, CtxError};
30
- use syno_pkg_land_sdk::delegate;
31
- use syno_pkg_land_sdk::filestation::{Page, Service};
32
- use syno_pkg_land_sdk::http::Request;
33
- use syno_pkg_land_sdk::time::Timestamp;
34
- use syno_pkg_land_sdk::{BoxError, jsonx};
35
-
36
- use super::UidLookup;
37
- use super::routes::decode_params;
38
-
39
- /// How recent the worker heartbeat must be to count as running.
40
- const WORKER_FRESH: Duration = Duration::from_secs(15);
41
-
42
- /// Resolves the authenticated DSM username on every request. Unknown UIDs
43
- /// fail closed for owners and submission.
44
- pub fn system_uid_lookup() -> UidLookup {
45
- Arc::new(delegate::lookup_uid)
46
- }
47
-
48
- /// What the jobs API needs of File Station: browsing and importing one file
49
- /// through the caller's own DSM session.
50
- pub trait BackgroundFiles: Send + Sync {
51
- fn browse(
52
- &self,
53
- ctx: &Context,
54
- caller: &Caller,
55
- req: &Request,
56
- folder: &str,
57
- offset: i64,
58
- ) -> Result<Page, BoxError>;
59
-
60
- fn read_file(
61
- &self,
62
- ctx: &Context,
63
- caller: &Caller,
64
- req: &Request,
65
- folder: &str,
66
- rel: &str,
67
- ) -> Result<Vec<u8>, BoxError>;
68
- }
69
-
70
- impl BackgroundFiles for Service {
71
- fn browse(
72
- &self,
73
- ctx: &Context,
74
- caller: &Caller,
75
- req: &Request,
76
- folder: &str,
77
- offset: i64,
78
- ) -> Result<Page, BoxError> {
79
- Service::browse(self, ctx, caller, req, folder, offset)
80
- }
81
-
82
- fn read_file(
83
- &self,
84
- ctx: &Context,
85
- caller: &Caller,
86
- req: &Request,
87
- folder: &str,
88
- rel: &str,
89
- ) -> Result<Vec<u8>, BoxError> {
90
- Service::read_file(self, ctx, caller, req, folder, rel)
91
- }
92
- }
93
-
94
- /// A handler of one background route; the access it works under was derived
95
- /// from the authenticated caller, never from params.
96
- type Handler =
97
- fn(&BackgroundApi, &Context, &Caller, &Request, &Access, &[u8]) -> Result<Value, BoxError>;
98
-
99
- pub struct BackgroundApi {
100
- store: Arc<Store>,
101
- files: Arc<dyn BackgroundFiles>,
102
- uid_lookup: UidLookup,
103
- }
104
-
105
- impl BackgroundApi {
106
- pub fn new(
107
- store: Arc<Store>,
108
- files: Arc<dyn BackgroundFiles>,
109
- uid_lookup: UidLookup,
110
- ) -> Arc<Self> {
111
- Arc::new(BackgroundApi {
112
- store,
113
- files,
114
- uid_lookup,
115
- })
116
- }
117
-
118
- /// The production assembly: the request-bounded spool under the data
119
- /// directory and the caller's own File Station session. `synopkgland init
120
- /// --with jobs` wires it in through `background_routes.rs`.
121
- pub fn production(data_dir: &std::path::Path, uid_lookup: UidLookup) -> Arc<Self> {
122
- Self::new(
123
- Arc::new(Store::new_request(data_dir.join("background"))),
124
- Arc::new(Service::local(data_dir)),
125
- uid_lookup,
126
- )
127
- }
128
-
129
- /// Every request authenticates against DSM afresh. Owner routes are
130
- /// available to signed-in users; schedule reapproval and owner cleanup
131
- /// are admin-only.
132
- pub fn routes(self: &Arc<Self>) -> Vec<Route> {
133
- vec![
134
- self.owner("bg.status", Self::status),
135
- self.owner("bg.schedules.list", Self::schedules),
136
- self.owner("bg.schedules.create", Self::create_schedule),
137
- self.owner("bg.schedules.disable", Self::disable_schedule),
138
- self.owner("bg.schedules.delete", Self::delete_schedule),
139
- self.admin("bg.schedules.enable", Self::enable_schedule),
140
- self.admin("bg.jobs.purge", Self::purge),
141
- self.owner("bg.browse", Self::browse),
142
- self.owner("bg.jobs.submit", Self::submit),
143
- self.owner("bg.jobs.list", Self::list),
144
- self.owner("bg.jobs.get", Self::get),
145
- self.owner("bg.jobs.cancel", Self::cancel),
146
- self.owner("bg.jobs.pause", Self::pause),
147
- self.owner("bg.jobs.resume", Self::resume),
148
- self.owner("bg.jobs.result", Self::result),
149
- ]
150
- }
151
-
152
- fn status(
153
- &self,
154
- ctx: &Context,
155
- _: &Caller,
156
- _: &Request,
157
- access: &Access,
158
- params: &[u8],
159
- ) -> Result<Value, BoxError> {
160
- decode_params::<jsonx::Empty>(params)?;
161
- let st = self
162
- .store
163
- .status_for(ctx, access)
164
- .map_err(background_error)?;
165
- let worker = st.worker.as_ref().map(|w| {
166
- json!({
167
- "pid": w.pid, "uid": w.uid, "username": w.username,
168
- "revision": w.revision, "at": w.at,
169
- "alive": worker_alive(w, Timestamp::now()),
170
- })
171
- });
172
- Ok(json!({
173
- "owner": access.owner, "supported": true, "admin": access.admin,
174
- "algorithm": st.algorithm, "revision": st.revision,
175
- "queued": st.queued, "running": st.running, "worker": worker,
176
- "limits": {
177
- "maxInputBytes": MAX_INPUT_BYTES, "maxQueued": MAX_QUEUED,
178
- "maxSchedules": MAX_SCHEDULES, "intervalSeconds": [3600, 86400, 604800],
179
- "maxTotalBytes": MAX_TOTAL_BYTES, "maxOwnerPending": MAX_OWNER_PENDING,
180
- "maxOwnerBytes": MAX_OWNER_BYTES,
181
- "jobTimeoutSeconds": JOB_TIMEOUT.as_secs(),
182
- },
183
- }))
184
- }
185
-
186
- /// Lists what the caller's own File Station session can see, so the
187
- /// panel can pick a file instead of typing a path. It grants nothing:
188
- /// submit still reads through the same session.
189
- fn browse(
190
- &self,
191
- ctx: &Context,
192
- caller: &Caller,
193
- req: &Request,
194
- _: &Access,
195
- params: &[u8],
196
- ) -> Result<Value, BoxError> {
197
- #[derive(Default, Deserialize)]
198
- #[serde(deny_unknown_fields)]
199
- struct Params {
200
- #[serde(default)]
201
- folder: String,
202
- #[serde(default)]
203
- offset: i64,
204
- }
205
- let params: Params = decode_params(params)?;
206
- if params.offset < 0 {
207
- return Err(api::Error::bad_request("offset must not be negative").into());
208
- }
209
- let page = self
210
- .files
211
- .browse(ctx, caller, req, &params.folder, params.offset)?;
212
- Ok(serde_json::to_value(page)?)
213
- }
214
-
215
- fn submit(
216
- &self,
217
- ctx: &Context,
218
- caller: &Caller,
219
- req: &Request,
220
- access: &Access,
221
- params: &[u8],
222
- ) -> Result<Value, BoxError> {
223
- #[derive(Default, Deserialize)]
224
- #[serde(deny_unknown_fields)]
225
- struct Params {
226
- #[serde(default)]
227
- key: String,
228
- #[serde(default)]
229
- folder: String,
230
- #[serde(default)]
231
- relative: String,
232
- }
233
- let params: Params = decode_params(params)?;
234
- if params.key.is_empty() || params.folder.is_empty() || params.relative.is_empty() {
235
- return Err(api::Error::bad_request("key, folder and relative are required").into());
236
- }
237
- if !access.owner.valid() {
238
- return Err(identity_unavailable().into());
239
- }
240
- let data = self
241
- .files
242
- .read_file(ctx, caller, req, &params.folder, &params.relative)?;
243
- let (job, duplicate) = self
244
- .store
245
- .submit(
246
- ctx,
247
- SubmitRequest {
248
- key: params.key,
249
- name: base_name(&params.relative),
250
- data,
251
- submitter: access.owner.clone(),
252
- },
253
- )
254
- .map_err(background_error)?;
255
- Ok(json!({"job": job, "duplicate": duplicate}))
256
- }
257
-
258
- fn list(
259
- &self,
260
- ctx: &Context,
261
- _: &Caller,
262
- _: &Request,
263
- access: &Access,
264
- params: &[u8],
265
- ) -> Result<Value, BoxError> {
266
- decode_params::<jsonx::Empty>(params)?;
267
- let mut jobs = self.store.list_for(ctx, access).map_err(background_error)?;
268
- jobs.reverse();
269
- Ok(json!({"jobs": jobs}))
270
- }
271
-
272
- fn get(
273
- &self,
274
- ctx: &Context,
275
- _: &Caller,
276
- _: &Request,
277
- access: &Access,
278
- params: &[u8],
279
- ) -> Result<Value, BoxError> {
280
- self.by_id(ctx, access, params, Store::get_for)
281
- }
282
-
283
- fn cancel(
284
- &self,
285
- ctx: &Context,
286
- _: &Caller,
287
- _: &Request,
288
- access: &Access,
289
- params: &[u8],
290
- ) -> Result<Value, BoxError> {
291
- self.by_id(ctx, access, params, Store::cancel_for)
292
- }
293
-
294
- /// Holds a queued job at once; a running job pauses at its next checkpoint.
295
- fn pause(
296
- &self,
297
- ctx: &Context,
298
- _: &Caller,
299
- _: &Request,
300
- access: &Access,
301
- params: &[u8],
302
- ) -> Result<Value, BoxError> {
303
- self.by_id(ctx, access, params, Store::pause_for)
304
- }
305
-
306
- fn resume(
307
- &self,
308
- ctx: &Context,
309
- _: &Caller,
310
- _: &Request,
311
- access: &Access,
312
- params: &[u8],
313
- ) -> Result<Value, BoxError> {
314
- self.by_id(ctx, access, params, Store::resume_for)
315
- }
316
-
317
- fn by_id(
318
- &self,
319
- ctx: &Context,
320
- access: &Access,
321
- params: &[u8],
322
- act: fn(&Store, &Context, &str, &Access) -> Result<Job, BoxError>,
323
- ) -> Result<Value, BoxError> {
324
- let id = decode_id(params)?;
325
- let job = act(&self.store, ctx, &id, access).map_err(background_error)?;
326
- Ok(json!({"job": job}))
327
- }
328
-
329
- fn result(
330
- &self,
331
- ctx: &Context,
332
- _: &Caller,
333
- _: &Request,
334
- access: &Access,
335
- params: &[u8],
336
- ) -> Result<Value, BoxError> {
337
- let id = decode_id(params)?;
338
- let job = self
339
- .store
340
- .get_for(ctx, &id, access)
341
- .map_err(background_error)?;
342
- let result = match (&job.state, &job.result) {
343
- (syno_pkg_land_sdk::background::State::Succeeded, Some(result)) => result,
344
- _ => return Err(api::Error::new("not_ready", "the job has no result").into()),
345
- };
346
- Ok(json!({"job": job, "result": {
347
- "jobId": job.id, "worker": job.worker, "workerRevision": job.worker_revision,
348
- "algorithm": "sha256", "sha256": result.sha256, "bytes": result.bytes,
349
- "inputDigest": job.input_digest, "name": job.name,
350
- }}))
351
- }
352
- }
353
-
354
- fn decode_id(params: &[u8]) -> Result<String, api::Error> {
355
- #[derive(Default, Deserialize)]
356
- #[serde(deny_unknown_fields)]
357
- struct IdParams {
358
- #[serde(default)]
359
- id: String,
360
- }
361
- Ok(decode_params::<IdParams>(params)?.id)
362
- }
363
-
364
- fn identity_unavailable() -> api::Error {
365
- api::Error::new("identity_unavailable", "DSM user UID could not be resolved")
366
- }
367
-
368
- fn worker_alive(w: &WorkerInfo, now: Timestamp) -> bool {
369
- now.unix_nanos() - w.at.unix_nanos() <= WORKER_FRESH.as_nanos() as i128
370
- }
371
-
372
- /// The last element of a slash-separated path, as Go's path.Base.
373
- fn base_name(path: &str) -> String {
374
- if path.is_empty() {
375
- return ".".into();
376
- }
377
- let trimmed = path.trim_end_matches('/');
378
- if trimmed.is_empty() {
379
- return "/".into();
380
- }
381
- trimmed.rsplit('/').next().unwrap_or(trimmed).into()
382
- }
383
-
384
- /// Maps the spool's errors to the codes the panel knows; anything else is
385
- /// logged and reported as unavailable.
386
- fn background_error(err: BoxError) -> BoxError {
387
- let message = |code: &str, text: &str| -> BoxError { api::Error::new(code, text).into() };
388
- match api::find_error::<StoreError>(&*err) {
389
- Some(StoreError::Busy) => message("jobs_busy", "background jobs are busy; retry later"),
390
- Some(StoreError::ScheduleFull) => {
391
- message("schedule-full", "the schedule limit has been reached")
392
- }
393
- Some(StoreError::InputUnavailable) => {
394
- message("input-unavailable", "import the private copy again")
395
- }
396
- Some(StoreError::OwnerQueueFull) => message(
397
- "owner-queue-full",
398
- "your pending job limit has been reached",
399
- ),
400
- Some(StoreError::OwnerQuota) => message(
401
- "owner-quota-exceeded",
402
- "your background storage limit has been reached",
403
- ),
404
- Some(StoreError::Invalid) => api::Error::bad_request("invalid background request").into(),
405
- Some(StoreError::TooLarge) => message(
406
- "limit-exceeded",
407
- "the file is larger than the background limit",
408
- ),
409
- Some(StoreError::QueueFull) => message("queue-full", "the background queue is full"),
410
- Some(StoreError::Quota) => message("quota-exceeded", "the background spool is full"),
411
- Some(StoreError::KeyConflict | StoreError::NotCancelable | StoreError::NotPausable) => {
412
- message(
413
- "conflict",
414
- "the request conflicts with the job's current state",
415
- )
416
- }
417
- Some(StoreError::NotFound) => message("not_found", "job not found"),
418
- _ if api::find_error::<CtxError>(&*err).is_some() => err,
419
- _ => {
420
- eprintln!("background spool error: {err}");
421
- message("unavailable", "the background spool is unavailable")
422
- }
423
- }
424
- }
@@ -1,77 +0,0 @@
1
- //! Who a background request acts for: the authenticated DSM user and the UID
2
- //! looked up afresh for it.
3
-
4
- use std::sync::Arc;
5
-
6
- use serde::Deserialize;
7
- use serde_json::Value;
8
- use syno_pkg_land_sdk::BoxError;
9
- use syno_pkg_land_sdk::api::{self, Caller, Route};
10
- use syno_pkg_land_sdk::background::{Access, Submitter};
11
- use syno_pkg_land_sdk::ctx::Context;
12
- use syno_pkg_land_sdk::http::Request;
13
-
14
- use super::{BackgroundApi, Handler, background_error, identity_unavailable};
15
- use crate::app::routes::decode_params;
16
-
17
- impl BackgroundApi {
18
- pub(super) fn access(&self, caller: &Caller) -> Access {
19
- Access {
20
- admin: caller.admin(),
21
- owner: Submitter {
22
- user: caller.user().to_string(),
23
- uid: (self.uid_lookup)(caller.user()),
24
- },
25
- }
26
- }
27
-
28
- /// A route for any signed-in user whose UID resolves; administrators see
29
- /// every owner's jobs and need no UID.
30
- pub(super) fn owner(self: &Arc<Self>, method: &str, handler: Handler) -> Route {
31
- let api = self.clone();
32
- Route::handle_request(method, move |ctx, caller, req, params| {
33
- let access = api.access(caller);
34
- if !access.admin && !access.owner.valid() {
35
- return Err(identity_unavailable().into());
36
- }
37
- handler(&api, ctx, caller, req, &access, params)
38
- })
39
- }
40
-
41
- pub(super) fn admin(self: &Arc<Self>, method: &str, handler: Handler) -> Route {
42
- let api = self.clone();
43
- Route::handle_request(method, move |ctx, caller, req, params| {
44
- if !caller.admin() {
45
- return Err(api::Error::new(
46
- "unsupported",
47
- "background jobs need a DSM administrator",
48
- )
49
- .into());
50
- }
51
- handler(&api, ctx, caller, req, &api.access(caller), params)
52
- })
53
- }
54
-
55
- pub(super) fn purge(
56
- &self,
57
- ctx: &Context,
58
- caller: &Caller,
59
- _: &Request,
60
- _: &Access,
61
- params: &[u8],
62
- ) -> Result<Value, BoxError> {
63
- #[derive(Default, Deserialize)]
64
- #[serde(deny_unknown_fields)]
65
- struct Params {
66
- owner: Option<Submitter>,
67
- }
68
- let Some(owner) = decode_params::<Params>(params).ok().and_then(|p| p.owner) else {
69
- return Err(api::Error::bad_request("exact owner is required").into());
70
- };
71
- let audit = self
72
- .store
73
- .purge_owner(ctx, &owner, caller.user())
74
- .map_err(background_error)?;
75
- Ok(serde_json::to_value(audit)?)
76
- }
77
- }
@@ -1,174 +0,0 @@
1
- use serde_json::Value;
2
- use syno_pkg_land_sdk::background::{JobResult, Outcome, State};
3
- use syno_pkg_land_sdk::ctx::Context;
4
-
5
- use super::tests::{Env, data, id_params};
6
- use crate::app::test_support::{Stub, alice, bob, carol};
7
-
8
- fn ordinary_env() -> Env {
9
- let env = Env::new();
10
- env.set_uid(|name| match name {
11
- "alice" => "1024".into(),
12
- "bob" => "1025".into(),
13
- "carol" => "1026".into(),
14
- _ => String::new(),
15
- });
16
- env
17
- }
18
-
19
- fn owner_submit(env: &Env, auth: &Stub, key: &str) -> String {
20
- let params = format!(r#"{{"key":"{key}","folder":"/share","relative":"a.txt"}}"#);
21
- let (code, out) = env.call(auth, "bg.jobs.submit", &params);
22
- assert_eq!(code, "", "submit {key}: {out}");
23
- data(&out)["job"]["id"].as_str().unwrap().into()
24
- }
25
-
26
- fn job_count(env: &Env, auth: &Stub) -> usize {
27
- let (_, out) = env.call(auth, "bg.jobs.list", "{}");
28
- data(&out)["jobs"].as_array().unwrap().len()
29
- }
30
-
31
- #[test]
32
- fn owners_are_isolated_including_keys_and_counts() {
33
- let env = ordinary_env();
34
- let a = owner_submit(&env, &alice(), "same");
35
- let b = owner_submit(&env, &bob(), "same");
36
- let c = owner_submit(&env, &carol(), "same");
37
- assert!(a != b && b != c, "idempotency key leaked across owners");
38
- for auth in [bob(), carol()] {
39
- assert_eq!(job_count(&env, &auth), 1);
40
- let (_, out) = env.call(&auth, "bg.status", "{}");
41
- assert_eq!(data(&out)["queued"], 1, "status leaks: {out}");
42
- assert_eq!(data(&out)["admin"], false);
43
- }
44
- for method in [
45
- "bg.jobs.get",
46
- "bg.jobs.cancel",
47
- "bg.jobs.pause",
48
- "bg.jobs.resume",
49
- "bg.jobs.result",
50
- ] {
51
- for id in [&a, &c, &"0".repeat(32)] {
52
- let (code, out) = env.call(&bob(), method, &id_params(id));
53
- assert_eq!(code, "not_found", "{method} {id}: {out}");
54
- }
55
- }
56
- assert_eq!(job_count(&env, &alice()), 3, "admin must see all");
57
- assert_eq!(env.call(&alice(), "bg.jobs.cancel", &id_params(&b)).0, "");
58
- }
59
-
60
- #[test]
61
- fn owner_result_and_uid_change_fail_closed() {
62
- let env = ordinary_env();
63
- let id = owner_submit(&env, &bob(), "one");
64
- let ctx = Context::background();
65
- env.store.claim(&ctx).unwrap();
66
- env.store
67
- .finish(
68
- &ctx,
69
- &id,
70
- Outcome {
71
- state: State::Succeeded,
72
- result: Some(JobResult {
73
- sha256: "digest".into(),
74
- bytes: 5,
75
- }),
76
- ..Outcome::default()
77
- },
78
- )
79
- .unwrap();
80
- assert_eq!(env.call(&bob(), "bg.jobs.result", &id_params(&id)).0, "");
81
- assert_eq!(
82
- env.call(&carol(), "bg.jobs.result", &id_params(&id)).0,
83
- "not_found"
84
- );
85
- env.set_uid(|_| "2000".into());
86
- assert_eq!(
87
- job_count(&env, &bob()),
88
- 0,
89
- "same name with new UID saw old jobs"
90
- );
91
- assert_eq!(
92
- env.call(&bob(), "bg.jobs.get", &id_params(&id)).0,
93
- "not_found"
94
- );
95
- env.set_uid(|_| String::new());
96
- assert_eq!(
97
- env.call(&bob(), "bg.jobs.list", "{}").0,
98
- "identity_unavailable"
99
- );
100
- }
101
-
102
- #[test]
103
- fn personal_quota_and_admin_only_purge() {
104
- let env = ordinary_env();
105
- assert_eq!(env.call(&bob(), "bg.jobs.purge", "{}").0, "admin_required");
106
- owner_submit(&env, &bob(), "one");
107
- owner_submit(&env, &bob(), "two");
108
- let params = r#"{"key":"three","folder":"/share","relative":"a.txt"}"#;
109
- assert_eq!(
110
- env.call(&bob(), "bg.jobs.submit", params).0,
111
- "owner-queue-full"
112
- );
113
- owner_submit(&env, &carol(), "one");
114
- }
115
-
116
- #[test]
117
- fn purge_requires_an_admin_and_the_exact_owner() {
118
- let env = ordinary_env();
119
- let id = owner_submit(&env, &bob(), "one");
120
- owner_submit(&env, &carol(), "one");
121
- for params in [
122
- "{}",
123
- r#"{"owner":null}"#,
124
- r#"{"owner":{"user":"bob","uid":"1025"},"x":1}"#,
125
- ] {
126
- assert_eq!(
127
- env.call(&alice(), "bg.jobs.purge", params).0,
128
- "bad_request",
129
- "{params}"
130
- );
131
- }
132
- let (code, out) = env.call(
133
- &alice(),
134
- "bg.jobs.purge",
135
- r#"{"owner":{"user":"bob","uid":"1025"}}"#,
136
- );
137
- assert_eq!(code, "", "{out}");
138
- assert_eq!(data(&out)["removed"], 1);
139
- assert_eq!(
140
- env.call(&alice(), "bg.jobs.get", &id_params(&id)).0,
141
- "not_found"
142
- );
143
- assert_eq!(job_count(&env, &alice()), 1, "purge touched another owner");
144
- }
145
-
146
- #[test]
147
- fn fresh_ordinary_user_jobs_need_no_admin_setup() {
148
- let env = Env::new();
149
- env.set_uid(|name| if name == "bob" { "1025" } else { "1026" }.into());
150
- let (code, out) = env.call(&bob(), "bg.status", "{}");
151
- assert_eq!(code, "", "fresh status: {out}");
152
- for key in ["enabled", "allowOrdinaryUsers"] {
153
- assert!(
154
- data(&out).get(key).is_none(),
155
- "obsolete status switch {key}"
156
- );
157
- }
158
- let id = owner_submit(&env, &bob(), "fresh");
159
- for method in [
160
- "bg.jobs.get",
161
- "bg.jobs.cancel",
162
- "bg.jobs.pause",
163
- "bg.jobs.resume",
164
- ] {
165
- let (code, _) = env.call(&carol(), method, &id_params(&id));
166
- assert_eq!(code, "not_found", "other owner {method}");
167
- }
168
- assert_eq!(env.call(&bob(), "bg.jobs.get", &id_params(&id)).0, "");
169
- assert_eq!(env.call(&bob(), "bg.jobs.cancel", &id_params(&id)).0, "");
170
- for method in ["bg.mode.set", "bg.users.set"] {
171
- let (code, _): (String, Value) = env.call(&alice(), method, "{}");
172
- assert_eq!(code, "unknown_method", "obsolete route {method}");
173
- }
174
- }