@synopackageland/templates 0.9.5 → 0.9.7

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 (74) hide show
  1. package/README.md +27 -4
  2. package/package.json +2 -2
  3. package/templates/_shared/AGENTS.md +95 -18
  4. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  5. package/templates/native-go/backend/cmd/app/main.go +8 -1
  6. package/templates/native-go/backend/cmd/background-worker/main.go +5 -1
  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/backend/internal/app/background.go +19 -8
  10. package/templates/native-go/backend/internal/app/background_owner_test.go +2 -2
  11. package/templates/native-go/backend/internal/app/background_test.go +24 -0
  12. package/templates/native-go/backend/internal/app/background_work.go +94 -0
  13. package/templates/native-go/backend/internal/app/background_work_test.go +95 -0
  14. package/templates/native-go/backend/internal/app/uninstall.go +19 -0
  15. package/templates/native-go/backend/internal/app/uninstall_test.go +33 -0
  16. package/templates/native-go/dsm-package.yaml +18 -0
  17. package/templates/native-go-delegate/backend/cmd/background-worker/main.go +1 -1
  18. package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +4 -0
  19. package/templates/native-go-jobs/frontend/test/background-page.test.ts +21 -0
  20. package/templates/native-go-vue/frontend/package-lock.json +4 -4
  21. package/templates/native-go-vue/frontend/package.json +1 -1
  22. package/templates/native-rust/README.md +381 -0
  23. package/templates/native-rust/backend/Cargo.lock +125 -0
  24. package/templates/native-rust/backend/Cargo.toml +24 -0
  25. package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +207 -0
  26. package/templates/native-rust/backend/permissions.json +5 -0
  27. package/templates/native-rust/backend/rust-toolchain.toml +4 -0
  28. package/templates/native-rust/backend/src/app/background/mod.rs +424 -0
  29. package/templates/native-rust/backend/src/app/background/owner.rs +77 -0
  30. package/templates/native-rust/backend/src/app/background/owner_tests.rs +174 -0
  31. package/templates/native-rust/backend/src/app/background/schedule.rs +126 -0
  32. package/templates/native-rust/backend/src/app/background/schedule_tests.rs +108 -0
  33. package/templates/native-rust/backend/src/app/background/tests.rs +505 -0
  34. package/templates/native-rust/backend/src/app/background/work.rs +78 -0
  35. package/templates/native-rust/backend/src/app/background/work_tests.rs +123 -0
  36. package/templates/native-rust/backend/src/app/background_routes.rs +22 -0
  37. package/templates/native-rust/backend/src/app/delegate_config.rs +188 -0
  38. package/templates/native-rust/backend/src/app/delegate_routes.rs +22 -0
  39. package/templates/native-rust/backend/src/app/identity.rs +56 -0
  40. package/templates/native-rust/backend/src/app/migrations.rs +79 -0
  41. package/templates/native-rust/backend/src/app/mod.rs +30 -0
  42. package/templates/native-rust/backend/src/app/portal.rs +317 -0
  43. package/templates/native-rust/backend/src/app/root_routes.rs +23 -0
  44. package/templates/native-rust/backend/src/app/routes.rs +315 -0
  45. package/templates/native-rust/backend/src/app/session_files.rs +19 -0
  46. package/templates/native-rust/backend/src/app/shared_folder.rs +383 -0
  47. package/templates/native-rust/backend/src/app/test_support.rs +200 -0
  48. package/templates/native-rust/backend/src/app/uninstall.rs +38 -0
  49. package/templates/native-rust/backend/src/bin/api.rs +72 -0
  50. package/templates/native-rust/backend/src/bin/background-worker.rs +37 -0
  51. package/templates/native-rust/backend/src/build_env.rs +155 -0
  52. package/templates/native-rust/backend/src/lib.rs +6 -0
  53. package/templates/native-rust/backend/src/permissions.rs +54 -0
  54. package/templates/native-rust/backend/src/process.rs +91 -0
  55. package/templates/native-rust/dsm-package.yaml +89 -0
  56. package/templates/native-rust/gitignore +2 -0
  57. package/templates/native-rust/i18n/glossary.json +663 -0
  58. package/templates/native-rust/i18n/messages/de.json +29 -0
  59. package/templates/native-rust/i18n/messages/en.json +29 -0
  60. package/templates/native-rust/i18n/messages/es.json +29 -0
  61. package/templates/native-rust/i18n/messages/fr.json +29 -0
  62. package/templates/native-rust/i18n/messages/it.json +29 -0
  63. package/templates/native-rust/i18n/messages/ja.json +29 -0
  64. package/templates/native-rust/i18n/messages/ko.json +29 -0
  65. package/templates/native-rust/i18n/messages/zh-CN.json +29 -0
  66. package/templates/native-rust/i18n/messages/zh-TW.json +29 -0
  67. package/templates/native-rust/ui/app.css +45 -0
  68. package/templates/native-rust/ui/app.js +111 -0
  69. package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +92 -0
  70. package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +52 -0
  71. package/templates/native-rust-jobs/backend/src/app/background_routes.rs +109 -0
  72. package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +475 -0
  73. package/templates/native-rust-vue/README.md +55 -0
  74. package/templates/native-rust-vue/gitignore +3 -0
@@ -0,0 +1,207 @@
1
+ Native Rust package build dependencies (crates linked into the backend binaries)
2
+
3
+ syno-pkg-land-sdk v0.9.8 (MIT)
4
+
5
+ MIT License
6
+
7
+ Copyright (c) 2026 synopkgland
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ itoa v1.0.18 (MIT OR Apache-2.0; used under MIT)
28
+
29
+ Permission is hereby granted, free of charge, to any
30
+ person obtaining a copy of this software and associated
31
+ documentation files (the "Software"), to deal in the
32
+ Software without restriction, including without
33
+ limitation the rights to use, copy, modify, merge,
34
+ publish, distribute, sublicense, and/or sell copies of
35
+ the Software, and to permit persons to whom the Software
36
+ is furnished to do so, subject to the following
37
+ conditions:
38
+
39
+ The above copyright notice and this permission notice
40
+ shall be included in all copies or substantial portions
41
+ of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
44
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
45
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
46
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
47
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
48
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
49
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
50
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
51
+ DEALINGS IN THE SOFTWARE.
52
+
53
+ libc v0.2.190 (MIT OR Apache-2.0; used under MIT)
54
+
55
+ Copyright (c) The Rust Project Developers
56
+
57
+ Permission is hereby granted, free of charge, to any
58
+ person obtaining a copy of this software and associated
59
+ documentation files (the "Software"), to deal in the
60
+ Software without restriction, including without
61
+ limitation the rights to use, copy, modify, merge,
62
+ publish, distribute, sublicense, and/or sell copies of
63
+ the Software, and to permit persons to whom the Software
64
+ is furnished to do so, subject to the following
65
+ conditions:
66
+
67
+ The above copyright notice and this permission notice
68
+ shall be included in all copies or substantial portions
69
+ of the Software.
70
+
71
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
72
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
73
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
74
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
75
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
76
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
77
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
78
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
79
+ DEALINGS IN THE SOFTWARE.
80
+
81
+ memchr v2.8.3 (Unlicense OR MIT; used under MIT)
82
+
83
+ The MIT License (MIT)
84
+
85
+ Copyright (c) 2015 Andrew Gallant
86
+
87
+ Permission is hereby granted, free of charge, to any person obtaining a copy
88
+ of this software and associated documentation files (the "Software"), to deal
89
+ in the Software without restriction, including without limitation the rights
90
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
91
+ copies of the Software, and to permit persons to whom the Software is
92
+ furnished to do so, subject to the following conditions:
93
+
94
+ The above copyright notice and this permission notice shall be included in
95
+ all copies or substantial portions of the Software.
96
+
97
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
98
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
99
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
100
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
101
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
102
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
103
+ THE SOFTWARE.
104
+
105
+ serde v1.0.229 (MIT OR Apache-2.0; used under MIT)
106
+
107
+ Permission is hereby granted, free of charge, to any
108
+ person obtaining a copy of this software and associated
109
+ documentation files (the "Software"), to deal in the
110
+ Software without restriction, including without
111
+ limitation the rights to use, copy, modify, merge,
112
+ publish, distribute, sublicense, and/or sell copies of
113
+ the Software, and to permit persons to whom the Software
114
+ is furnished to do so, subject to the following
115
+ conditions:
116
+
117
+ The above copyright notice and this permission notice
118
+ shall be included in all copies or substantial portions
119
+ of the Software.
120
+
121
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
122
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
123
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
124
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
125
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
126
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
127
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
128
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
129
+ DEALINGS IN THE SOFTWARE.
130
+
131
+ serde_core v1.0.229 (MIT OR Apache-2.0; used under MIT)
132
+
133
+ Permission is hereby granted, free of charge, to any
134
+ person obtaining a copy of this software and associated
135
+ documentation files (the "Software"), to deal in the
136
+ Software without restriction, including without
137
+ limitation the rights to use, copy, modify, merge,
138
+ publish, distribute, sublicense, and/or sell copies of
139
+ the Software, and to permit persons to whom the Software
140
+ is furnished to do so, subject to the following
141
+ conditions:
142
+
143
+ The above copyright notice and this permission notice
144
+ shall be included in all copies or substantial portions
145
+ of the Software.
146
+
147
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
148
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
149
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
150
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
151
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
152
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
153
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
154
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
155
+ DEALINGS IN THE SOFTWARE.
156
+
157
+ serde_json v1.0.154 (MIT OR Apache-2.0; used under MIT)
158
+
159
+ Permission is hereby granted, free of charge, to any
160
+ person obtaining a copy of this software and associated
161
+ documentation files (the "Software"), to deal in the
162
+ Software without restriction, including without
163
+ limitation the rights to use, copy, modify, merge,
164
+ publish, distribute, sublicense, and/or sell copies of
165
+ the Software, and to permit persons to whom the Software
166
+ is furnished to do so, subject to the following
167
+ conditions:
168
+
169
+ The above copyright notice and this permission notice
170
+ shall be included in all copies or substantial portions
171
+ of the Software.
172
+
173
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
174
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
175
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
176
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
177
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
178
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
179
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
180
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
181
+ DEALINGS IN THE SOFTWARE.
182
+
183
+ zmij v1.0.23 (MIT)
184
+
185
+ Permission is hereby granted, free of charge, to any
186
+ person obtaining a copy of this software and associated
187
+ documentation files (the "Software"), to deal in the
188
+ Software without restriction, including without
189
+ limitation the rights to use, copy, modify, merge,
190
+ publish, distribute, sublicense, and/or sell copies of
191
+ the Software, and to permit persons to whom the Software
192
+ is furnished to do so, subject to the following
193
+ conditions:
194
+
195
+ The above copyright notice and this permission notice
196
+ shall be included in all copies or substantial portions
197
+ of the Software.
198
+
199
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
200
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
201
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
202
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
203
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
204
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
205
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
206
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
207
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ {
2
+ "schema": 1,
3
+ "default": "administrators",
4
+ "methods": {}
5
+ }
@@ -0,0 +1,4 @@
1
+ [toolchain]
2
+ channel = "1.97.1"
3
+ targets = ["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", "armv7-unknown-linux-gnueabihf"]
4
+ components = ["clippy", "rustfmt"]
@@ -0,0 +1,424 @@
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
+ }