@synopackageland/templates 0.9.9 → 0.10.1

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 (137) hide show
  1. package/README.md +29 -29
  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 +24 -3
  20. package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
  21. package/templates/native-go-root-ops/backend/internal/app/executor.go +2 -65
  22. package/templates/native-go-root-ops/backend/internal/app/executor_test.go +10 -82
  23. package/templates/native-go-root-ops/frontend/src/ExecutorGate.vue +16 -0
  24. package/templates/native-go-root-ops/frontend/test/executor-gate.test.ts +60 -0
  25. package/templates/native-go-vue/README.md +24 -8
  26. package/templates/native-go-vue/frontend/package-lock.json +9 -9
  27. package/templates/native-go-vue/frontend/package.json +2 -2
  28. package/templates/native-go-vue/frontend/src/App.vue +36 -36
  29. package/templates/native-go-vue/frontend/src/ExecutorGate.vue +10 -0
  30. package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
  31. package/templates/native-go-vue/frontend/test/context.ts +8 -0
  32. package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
  33. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
  34. package/templates/native-node/README.md +153 -0
  35. package/templates/native-node/backend/app/package.json +4 -0
  36. package/templates/native-node/backend/app/permissions.json +5 -0
  37. package/templates/native-node/backend/package-lock.json +59 -0
  38. package/templates/native-node/backend/package.json +17 -0
  39. package/templates/native-node/backend/src/delegate.ts +5 -0
  40. package/templates/native-node/backend/src/jobs.ts +5 -0
  41. package/templates/native-node/backend/src/main.ts +4 -0
  42. package/templates/native-node/backend/src/params.ts +11 -0
  43. package/templates/native-node/backend/src/root-ops.ts +5 -0
  44. package/templates/native-node/backend/src/routes.ts +97 -0
  45. package/templates/native-node/backend/src/service.ts +22 -0
  46. package/templates/native-node/backend/test/routes.test.ts +75 -0
  47. package/templates/native-node/backend/test/support.ts +67 -0
  48. package/templates/native-node/backend/tsconfig.build.json +8 -0
  49. package/templates/native-node/backend/tsconfig.json +18 -0
  50. package/templates/native-node/dsm-package.yaml +80 -0
  51. package/templates/native-node/gitignore +4 -0
  52. package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
  53. package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
  54. package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
  55. package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
  56. package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
  57. package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
  58. package/templates/native-node-vue/README.md +56 -0
  59. package/templates/native-node-vue/gitignore +4 -0
  60. package/templates/native-python/README.md +150 -0
  61. package/templates/native-python/backend/app/delegate.py +5 -0
  62. package/templates/native-python/backend/app/jobs.py +5 -0
  63. package/templates/native-python/backend/app/main.py +5 -0
  64. package/templates/native-python/backend/app/params.py +10 -0
  65. package/templates/native-python/backend/app/permissions.json +5 -0
  66. package/templates/native-python/backend/app/root_ops.py +5 -0
  67. package/templates/native-python/backend/app/routes.py +107 -0
  68. package/templates/native-python/backend/tests/support.py +72 -0
  69. package/templates/native-python/backend/tests/test_routes.py +73 -0
  70. package/templates/native-python/dsm-package.yaml +77 -0
  71. package/templates/native-python/gitignore +2 -0
  72. package/templates/native-python-delegate/backend/app/delegate.py +42 -0
  73. package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
  74. package/templates/native-python-jobs/backend/app/jobs.py +40 -0
  75. package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
  76. package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
  77. package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
  78. package/templates/native-python-vue/README.md +56 -0
  79. package/templates/native-python-vue/gitignore +3 -0
  80. package/templates/native-rust/README.md +74 -296
  81. package/templates/native-rust/backend/Cargo.lock +6 -6
  82. package/templates/native-rust/backend/Cargo.toml +2 -10
  83. package/templates/native-rust/backend/src/delegate.rs +6 -0
  84. package/templates/native-rust/backend/src/jobs.rs +6 -0
  85. package/templates/native-rust/backend/src/main.rs +12 -0
  86. package/templates/native-rust/backend/src/root_ops.rs +6 -0
  87. package/templates/native-rust/backend/src/routes.rs +352 -0
  88. package/templates/native-rust/backend/src/test_support.rs +91 -0
  89. package/templates/native-rust/dsm-package.yaml +10 -21
  90. package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
  91. package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
  92. package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
  93. package/templates/native-rust-vue/README.md +35 -34
  94. package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
  95. package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
  96. package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
  97. package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
  98. package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
  99. package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
  100. package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
  101. package/templates/native-rust/backend/src/app/background/work.rs +0 -78
  102. package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
  103. package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
  104. package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
  105. package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
  106. package/templates/native-rust/backend/src/app/identity.rs +0 -56
  107. package/templates/native-rust/backend/src/app/migrations.rs +0 -79
  108. package/templates/native-rust/backend/src/app/mod.rs +0 -30
  109. package/templates/native-rust/backend/src/app/portal.rs +0 -317
  110. package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
  111. package/templates/native-rust/backend/src/app/routes.rs +0 -315
  112. package/templates/native-rust/backend/src/app/session_files.rs +0 -19
  113. package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
  114. package/templates/native-rust/backend/src/app/test_support.rs +0 -202
  115. package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
  116. package/templates/native-rust/backend/src/bin/api.rs +0 -72
  117. package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
  118. package/templates/native-rust/backend/src/build_env.rs +0 -155
  119. package/templates/native-rust/backend/src/lib.rs +0 -6
  120. package/templates/native-rust/backend/src/permissions.rs +0 -54
  121. package/templates/native-rust/backend/src/process.rs +0 -91
  122. package/templates/native-rust/i18n/glossary.json +0 -665
  123. package/templates/native-rust/i18n/messages/de.json +0 -32
  124. package/templates/native-rust/i18n/messages/en.json +0 -32
  125. package/templates/native-rust/i18n/messages/es.json +0 -32
  126. package/templates/native-rust/i18n/messages/fr.json +0 -32
  127. package/templates/native-rust/i18n/messages/it.json +0 -32
  128. package/templates/native-rust/i18n/messages/ja.json +0 -32
  129. package/templates/native-rust/i18n/messages/ko.json +0 -32
  130. package/templates/native-rust/i18n/messages/zh-CN.json +0 -32
  131. package/templates/native-rust/i18n/messages/zh-TW.json +0 -32
  132. package/templates/native-rust/ui/app.css +0 -45
  133. package/templates/native-rust/ui/app.js +0 -111
  134. package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
  135. package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
  136. package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
  137. package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
@@ -1,505 +0,0 @@
1
- use std::sync::{Arc, Mutex};
2
-
3
- use serde_json::Value;
4
- use syno_pkg_land_sdk::BoxError;
5
- use syno_pkg_land_sdk::api::{self, Caller, Server};
6
- use syno_pkg_land_sdk::background::{
7
- JOB_TIMEOUT, JobResult, MAX_QUEUED, Outcome, State, Store, WorkerInfo,
8
- };
9
- use syno_pkg_land_sdk::ctx::Context;
10
- use syno_pkg_land_sdk::filestation::{Entry, Page};
11
- use syno_pkg_land_sdk::http::Request;
12
- use syno_pkg_land_sdk::time::Timestamp;
13
-
14
- use super::*;
15
- use crate::app::test_support::{Stub, TempDir, alice, api_request, bob, nobody, respond, rule};
16
-
17
- const POLICY: &[u8] = br#"{"schema":1,"default":"administrators","methods":{}}"#;
18
-
19
- #[derive(Default)]
20
- pub(super) struct Seen {
21
- pub calls: usize,
22
- pub folder: String,
23
- pub rel: String,
24
- pub offset: i64,
25
- }
26
-
27
- /// File Station stand-in that records what the routes asked of it.
28
- pub(super) struct FakeFiles {
29
- pub data: Mutex<Vec<u8>>,
30
- pub err: Mutex<Option<api::Error>>,
31
- pub page: Mutex<Page>,
32
- pub seen: Mutex<Seen>,
33
- }
34
-
35
- impl FakeFiles {
36
- fn answer<T>(&self, ok: T) -> Result<T, BoxError> {
37
- match self.err.lock().unwrap().clone() {
38
- Some(e) => Err(e.into()),
39
- None => Ok(ok),
40
- }
41
- }
42
-
43
- pub fn calls(&self) -> usize {
44
- self.seen.lock().unwrap().calls
45
- }
46
- }
47
-
48
- impl BackgroundFiles for FakeFiles {
49
- fn browse(
50
- &self,
51
- _: &Context,
52
- _: &Caller,
53
- _: &Request,
54
- folder: &str,
55
- offset: i64,
56
- ) -> Result<Page, BoxError> {
57
- {
58
- let mut seen = self.seen.lock().unwrap();
59
- seen.calls += 1;
60
- seen.folder = folder.into();
61
- seen.offset = offset;
62
- }
63
- self.answer(self.page.lock().unwrap().clone())
64
- }
65
-
66
- fn read_file(
67
- &self,
68
- _: &Context,
69
- _: &Caller,
70
- _: &Request,
71
- folder: &str,
72
- rel: &str,
73
- ) -> Result<Vec<u8>, BoxError> {
74
- {
75
- let mut seen = self.seen.lock().unwrap();
76
- seen.calls += 1;
77
- seen.folder = folder.into();
78
- seen.rel = rel.into();
79
- }
80
- self.answer(self.data.lock().unwrap().clone())
81
- }
82
- }
83
-
84
- type Lookup = Arc<dyn Fn(&str) -> String + Send + Sync>;
85
-
86
- pub(super) struct Env {
87
- pub store: Arc<Store>,
88
- pub files: Arc<FakeFiles>,
89
- uid: Arc<Mutex<Lookup>>,
90
- _dir: TempDir,
91
- }
92
-
93
- impl Env {
94
- pub fn new() -> Self {
95
- let dir = TempDir::new();
96
- let files = Arc::new(FakeFiles {
97
- data: Mutex::new(b"hello".to_vec()),
98
- err: Mutex::new(None),
99
- page: Mutex::new(Page::default()),
100
- seen: Mutex::new(Seen::default()),
101
- });
102
- Env {
103
- store: Arc::new(Store::new(dir.path().join("background"))),
104
- files,
105
- uid: Arc::new(Mutex::new(Arc::new(|_: &str| "1024".to_string()))),
106
- _dir: dir,
107
- }
108
- }
109
-
110
- /// Changes what the next request resolves DSM user names to.
111
- pub fn set_uid(&self, lookup: impl Fn(&str) -> String + Send + Sync + 'static) {
112
- *self.uid.lock().unwrap() = Arc::new(lookup);
113
- }
114
-
115
- pub fn api(&self) -> Arc<BackgroundApi> {
116
- let uid = self.uid.clone();
117
- BackgroundApi::new(
118
- self.store.clone(),
119
- self.files.clone(),
120
- Arc::new(move |name| (uid.lock().unwrap().clone())(name)),
121
- )
122
- }
123
-
124
- pub fn call(&self, auth: &Stub, method: &str, params: &str) -> (String, Value) {
125
- call_routes(self.api().routes(), auth, method, params)
126
- }
127
- }
128
-
129
- pub(super) fn call_routes(
130
- routes: Vec<api::Route>,
131
- auth: &Stub,
132
- method: &str,
133
- params: &str,
134
- ) -> (String, Value) {
135
- let server = Server::new_with_permissions(auth.clone(), POLICY, routes)
136
- .expect("the background routes and policy are valid");
137
- respond(&server, &mut api_request(method, params))
138
- }
139
-
140
- pub(super) fn data(out: &Value) -> &Value {
141
- &out["data"]
142
- }
143
-
144
- pub(super) fn id_params(id: &str) -> String {
145
- format!(r#"{{"id":"{id}"}}"#)
146
- }
147
-
148
- fn zeros() -> String {
149
- "0".repeat(32)
150
- }
151
-
152
- pub(super) fn sample_params(method: &str) -> String {
153
- let id = id_params(&"a".repeat(32));
154
- match method {
155
- "bg.schedules.list" | "bg.status" | "bg.jobs.list" => "{}".into(),
156
- "bg.schedules.create" => {
157
- r#"{"key":"schedule","jobId":"missing","intervalSeconds":3600,"consent":true}"#.into()
158
- }
159
- "bg.schedules.disable" | "bg.schedules.delete" => r#"{"id":"missing"}"#.into(),
160
- "bg.schedules.enable" => r#"{"id":"missing","consent":true}"#.into(),
161
- "bg.jobs.purge" => r#"{"owner":{"user":"bob","uid":"1025"}}"#.into(),
162
- "bg.browse" => r#"{"folder":"/share","offset":0}"#.into(),
163
- "bg.jobs.submit" => r#"{"key":"k1","folder":"/share","relative":"a.txt"}"#.into(),
164
- "bg.jobs.get" | "bg.jobs.cancel" | "bg.jobs.pause" | "bg.jobs.resume"
165
- | "bg.jobs.result" => id,
166
- other => panic!("no sample for {other}"),
167
- }
168
- }
169
-
170
- #[test]
171
- fn sdk_policies_authenticate_every_route() {
172
- let env = Env::new();
173
- let routes = env.api().routes();
174
- assert_eq!(routes.len(), 15);
175
- for route in &routes {
176
- let method = route.method();
177
- assert_ne!(rule(route), "public", "{method} must not be public");
178
- let params = sample_params(method);
179
- let (code, _) = env.call(&bob(), method, &params);
180
- if method == "bg.jobs.purge" || method == "bg.schedules.enable" {
181
- assert_eq!(code, "admin_required", "{method} as ordinary user");
182
- } else {
183
- assert!(
184
- !["admin_required", "unsupported", "disabled"].contains(&code.as_str()),
185
- "{method} unexpectedly blocked: {code}"
186
- );
187
- }
188
- assert_eq!(
189
- env.call(&nobody(), method, &params).0,
190
- "session_required",
191
- "{method} without a session"
192
- );
193
- }
194
- }
195
-
196
- #[test]
197
- fn status_reports_limits_and_worker() {
198
- let env = Env::new();
199
- let (code, out) = env.call(&alice(), "bg.status", "{}");
200
- let d = data(&out);
201
- assert_eq!(code, "", "{out}");
202
- assert_eq!(d["algorithm"], "sha256-v1");
203
- assert!(d["worker"].is_null());
204
- let limits = &d["limits"];
205
- assert_eq!(limits["maxInputBytes"], 512 * 1024);
206
- assert_eq!(limits["maxQueued"], 16);
207
- assert_eq!(limits["maxTotalBytes"], 16 << 20);
208
- assert_eq!(limits["jobTimeoutSeconds"], JOB_TIMEOUT.as_secs());
209
- env.store
210
- .heartbeat(
211
- &Context::background(),
212
- WorkerInfo {
213
- pid: 9,
214
- uid: 255853,
215
- username: "pkg".into(),
216
- ..WorkerInfo::default()
217
- },
218
- )
219
- .unwrap();
220
- let (_, out) = env.call(&alice(), "bg.status", "{}");
221
- let worker = &data(&out)["worker"];
222
- assert_eq!(worker["alive"], true);
223
- assert_eq!(worker["uid"], 255853);
224
- }
225
-
226
- #[test]
227
- fn submit_is_available_on_a_fresh_store() {
228
- let env = Env::new();
229
- let (code, _) = env.call(&alice(), "bg.jobs.submit", &sample_params("bg.jobs.submit"));
230
- assert_eq!((code.as_str(), env.files.calls()), ("", 1));
231
- }
232
-
233
- #[test]
234
- fn submit_imports_the_selected_file_and_is_idempotent() {
235
- let env = Env::new();
236
- let params = r#"{"key":"k1","folder":"/share","relative":"docs/a.txt"}"#;
237
- let (code, out) = env.call(&alice(), "bg.jobs.submit", params);
238
- let job = &data(&out)["job"];
239
- assert_eq!(code, "", "{out}");
240
- assert_eq!(job["state"], "queued");
241
- assert_eq!(job["name"], "a.txt");
242
- assert_eq!(data(&out)["duplicate"], false);
243
- {
244
- let seen = env.files.seen.lock().unwrap();
245
- assert_eq!(
246
- (seen.folder.as_str(), seen.rel.as_str()),
247
- ("/share", "docs/a.txt")
248
- );
249
- }
250
- assert_eq!(job["submitter"]["user"], "alice");
251
- assert_eq!(job["submitter"]["uid"], "1024");
252
- let (_, again) = env.call(&alice(), "bg.jobs.submit", params);
253
- assert_eq!(data(&again)["duplicate"], true);
254
- assert_eq!(data(&again)["job"]["id"], job["id"]);
255
- *env.files.data.lock().unwrap() = b"other".to_vec();
256
- assert_eq!(env.call(&alice(), "bg.jobs.submit", params).0, "conflict");
257
- }
258
-
259
- #[test]
260
- fn submit_passes_adapter_refusals_through() {
261
- let env = Env::new();
262
- *env.files.err.lock().unwrap() = Some(api::Error::new("limit-exceeded", "x"));
263
- let (code, _) = env.call(&alice(), "bg.jobs.submit", &sample_params("bg.jobs.submit"));
264
- assert_eq!(code, "limit-exceeded");
265
- let jobs = env.store.list(&Context::background()).unwrap();
266
- assert!(
267
- jobs.is_empty(),
268
- "a failed import must never enter the queue"
269
- );
270
- }
271
-
272
- #[test]
273
- fn submit_validates_params() {
274
- let env = Env::new();
275
- for params in [
276
- r#"{}"#,
277
- r#"{"key":"k","folder":"/share","relative":"a","extra":1}"#,
278
- r#"{"key":"bad key","folder":"/share","relative":"a"}"#,
279
- r#"{"key":"k","folder":"/share","relative":""}"#,
280
- ] {
281
- assert_eq!(
282
- env.call(&alice(), "bg.jobs.submit", params).0,
283
- "bad_request",
284
- "{params}"
285
- );
286
- }
287
- }
288
-
289
- #[test]
290
- fn queue_full_has_a_stable_code() {
291
- let env = Env::new();
292
- for i in 0..MAX_QUEUED {
293
- env.set_uid(move |_| (1024 + i / 2).to_string());
294
- let params = format!(
295
- r#"{{"key":"k{}","folder":"/share","relative":"a.txt"}}"#,
296
- (b'a' + i as u8) as char
297
- );
298
- let (code, _) = env.call(&alice(), "bg.jobs.submit", &params);
299
- assert_eq!(code, "", "{i}");
300
- }
301
- env.set_uid(|_| "9999".into());
302
- let params = r#"{"key":"more","folder":"/share","relative":"a.txt"}"#;
303
- assert_eq!(env.call(&alice(), "bg.jobs.submit", params).0, "queue-full");
304
- }
305
-
306
- fn submitted_id(env: &Env) -> String {
307
- let (_, out) = env.call(
308
- &alice(),
309
- "bg.jobs.submit",
310
- r#"{"key":"k1","folder":"/share","relative":"a.txt"}"#,
311
- );
312
- data(&out)["job"]["id"].as_str().unwrap().into()
313
- }
314
-
315
- #[test]
316
- fn list_get_cancel_and_result() {
317
- let env = Env::new();
318
- let id = submitted_id(&env);
319
- let params = id_params(&id);
320
-
321
- let (_, out) = env.call(&alice(), "bg.jobs.list", "{}");
322
- assert_eq!(data(&out)["jobs"].as_array().unwrap().len(), 1);
323
- let (code, out) = env.call(&alice(), "bg.jobs.get", &params);
324
- assert_eq!(
325
- (code.as_str(), &data(&out)["job"]["id"]),
326
- ("", &Value::from(id.clone()))
327
- );
328
- assert_eq!(
329
- env.call(&alice(), "bg.jobs.result", &params).0,
330
- "not_ready",
331
- "result of a queued job"
332
- );
333
- let ctx = Context::background();
334
- env.store.claim(&ctx).unwrap();
335
- let sum = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
336
- env.store
337
- .finish(
338
- &ctx,
339
- &id,
340
- Outcome {
341
- state: State::Succeeded,
342
- result: Some(JobResult {
343
- sha256: sum.into(),
344
- bytes: 5,
345
- }),
346
- ..Outcome::default()
347
- },
348
- )
349
- .unwrap();
350
- let (code, out) = env.call(&alice(), "bg.jobs.result", &params);
351
- let result = &data(&out)["result"];
352
- assert_eq!(code, "", "{out}");
353
- assert_eq!(result["sha256"], sum);
354
- assert_eq!(result["bytes"], 5);
355
- assert_eq!(result["worker"], "sha256-v1");
356
- assert_eq!(result["jobId"], id.as_str());
357
- assert_eq!(
358
- env.call(&alice(), "bg.jobs.cancel", &params).0,
359
- "conflict",
360
- "cancel after completion"
361
- );
362
- for method in ["bg.jobs.get", "bg.jobs.cancel", "bg.jobs.result"] {
363
- assert_eq!(
364
- env.call(&alice(), method, &id_params(&zeros())).0,
365
- "not_found",
366
- "{method} unknown id"
367
- );
368
- }
369
- }
370
-
371
- #[test]
372
- fn cancel_a_queued_job() {
373
- let env = Env::new();
374
- let id = submitted_id(&env);
375
- let (code, out) = env.call(&alice(), "bg.jobs.cancel", &id_params(&id));
376
- assert_eq!(code, "", "{out}");
377
- assert_eq!(data(&out)["job"]["state"], "canceled");
378
- }
379
-
380
- #[test]
381
- fn pause_and_resume_a_queued_job() {
382
- let env = Env::new();
383
- let params = id_params(&submitted_id(&env));
384
- for (method, state) in [("bg.jobs.pause", "paused"), ("bg.jobs.resume", "queued")] {
385
- let (code, out) = env.call(&alice(), method, &params);
386
- assert_eq!(code, "", "{method}: {out}");
387
- assert_eq!(data(&out)["job"]["state"], state, "{method}");
388
- }
389
- assert_eq!(env.call(&alice(), "bg.jobs.cancel", &params).0, "");
390
- for method in ["bg.jobs.pause", "bg.jobs.resume"] {
391
- assert_eq!(
392
- env.call(&alice(), method, &params).0,
393
- "conflict",
394
- "{method} after cancel"
395
- );
396
- let unknown = id_params(&"0".repeat(32));
397
- assert_eq!(
398
- env.call(&alice(), method, &unknown).0,
399
- "not_found",
400
- "{method} unknown id"
401
- );
402
- }
403
- }
404
-
405
- #[test]
406
- fn worker_alive_expires() {
407
- let at = |ago: i128| WorkerInfo {
408
- at: Timestamp::now().add_nanos(-ago * 1_000_000_000),
409
- ..WorkerInfo::default()
410
- };
411
- assert!(!worker_alive(&at(60), Timestamp::now()));
412
- assert!(worker_alive(&at(0), Timestamp::now()));
413
- }
414
-
415
- #[test]
416
- fn browse_lists_through_the_callers_session() {
417
- let env = Env::new();
418
- *env.files.page.lock().unwrap() = Page {
419
- files: vec![Entry {
420
- name: "a.txt".into(),
421
- path: "/share/a.txt".into(),
422
- ..Entry::default()
423
- }],
424
- total: 1,
425
- ..Page::default()
426
- };
427
- let (code, out) = env.call(&alice(), "bg.browse", r#"{"folder":"/share","offset":100}"#);
428
- assert_eq!(code, "", "{out}");
429
- {
430
- let seen = env.files.seen.lock().unwrap();
431
- assert_eq!((seen.folder.as_str(), seen.offset), ("/share", 100));
432
- }
433
- let files = data(&out)["files"].as_array().unwrap();
434
- assert_eq!(files.len(), 1);
435
- assert_eq!(files[0]["path"], "/share/a.txt");
436
- }
437
-
438
- #[test]
439
- fn browse_without_a_folder_lists_the_shares() {
440
- let env = Env::new();
441
- let (code, out) = env.call(&alice(), "bg.browse", "{}");
442
- assert_eq!(code, "", "{out}");
443
- let seen = env.files.seen.lock().unwrap();
444
- assert_eq!((seen.folder.as_str(), seen.offset), ("", 0));
445
- }
446
-
447
- #[test]
448
- fn browse_validates_params_and_passes_refusals_through() {
449
- let env = Env::new();
450
- for params in [
451
- r#"{"offset":-1}"#,
452
- r#"{"folder":"/share","extra":1}"#,
453
- r#"{"offset":"1"}"#,
454
- ] {
455
- assert_eq!(
456
- env.call(&alice(), "bg.browse", params).0,
457
- "bad_request",
458
- "{params}"
459
- );
460
- }
461
- assert_eq!(
462
- env.files.calls(),
463
- 0,
464
- "invalid params must not reach the NAS"
465
- );
466
- *env.files.err.lock().unwrap() = Some(api::Error::new("denied", "no"));
467
- assert_eq!(
468
- env.call(&alice(), "bg.browse", r#"{"folder":"/share"}"#).0,
469
- "denied"
470
- );
471
- }
472
-
473
- #[test]
474
- fn list_without_jobs_is_an_empty_array() {
475
- let env = Env::new();
476
- let (_, out) = env.call(&alice(), "bg.jobs.list", "{}");
477
- assert_eq!(data(&out)["jobs"], serde_json::json!([]));
478
- }
479
-
480
- #[test]
481
- fn base_name_follows_gos_path_base() {
482
- for (path, name) in [
483
- ("docs/a.txt", "a.txt"),
484
- ("a.txt", "a.txt"),
485
- ("a/b/", "b"),
486
- ("", "."),
487
- ("///", "/"),
488
- ] {
489
- assert_eq!(base_name(path), name, "{path:?}");
490
- }
491
- }
492
-
493
- #[test]
494
- fn background_error_maps_the_spool_and_keeps_context_errors() {
495
- let code = |err: BoxError| {
496
- api::find_error::<api::Error>(&*background_error(err))
497
- .map(|e| e.code.clone())
498
- .unwrap_or_default()
499
- };
500
- assert_eq!(code(Box::new(StoreError::Busy)), "jobs_busy");
501
- assert_eq!(code(Box::new(StoreError::NotRunning)), "unavailable");
502
- assert_eq!(code("disk on fire".into()), "unavailable");
503
- let canceled = background_error(Box::new(CtxError::Canceled));
504
- assert!(api::find_error::<CtxError>(&*canceled).is_some());
505
- }
@@ -1,78 +0,0 @@
1
- //! The App's own sha256-v1 Work, resumable after a pause or a package stop.
2
-
3
- use std::io::{self, ErrorKind, Read};
4
-
5
- use serde::{Deserialize, Serialize};
6
- use syno_pkg_land_sdk::BoxError;
7
- use syno_pkg_land_sdk::background::{JobResult, Progress, Task};
8
- use syno_pkg_land_sdk::ctx::Context;
9
- use syno_pkg_land_sdk::sha256::{Sha256, hex};
10
-
11
- /// Spaces [`hash_work`]'s checkpoints; a resumed job repeats at most this
12
- /// much hashing. A Work over larger input saves less often.
13
- pub(super) const HASH_CHECKPOINT_EVERY: i64 = 64 << 10;
14
-
15
- /// The checkpoint: the bytes already hashed and the hash state after them.
16
- #[derive(Serialize, Deserialize)]
17
- struct HashState {
18
- offset: i64,
19
- state: String,
20
- }
21
-
22
- /// Reports progress as it reads and checkpoints the hash state, so a job
23
- /// paused by its owner or by a package stop resumes where it left off
24
- /// instead of from the start.
25
- pub fn hash_work(ctx: &Context, t: &mut Task<'_>) -> Result<JobResult, BoxError> {
26
- let (mut h, mut done) = resume_hash(&t.job.checkpoint, t.input)?;
27
- let (total, mut saved) = (t.job.input_bytes, done);
28
- let mut buf = vec![0; 32 * 1024];
29
- loop {
30
- if let Some(e) = ctx.err() {
31
- return Err(Box::new(e));
32
- }
33
- let n = match t.input.read(&mut buf) {
34
- Ok(n) => n,
35
- Err(e) if e.kind() == ErrorKind::Interrupted => continue,
36
- Err(e) => return Err(Box::new(e)),
37
- };
38
- h.update(&buf[..n]);
39
- done += n as i64;
40
- t.report(Progress {
41
- bytes: done,
42
- total_bytes: total,
43
- ..Progress::default()
44
- });
45
- if n == 0 {
46
- return Ok(JobResult {
47
- sha256: hex(&h.finish()),
48
- bytes: done,
49
- });
50
- }
51
- // The hash state saves only after whole blocks; a short read waits for the next.
52
- if done - saved >= HASH_CHECKPOINT_EVERY
53
- && let Some(state) = save_hash(&h, done)
54
- {
55
- t.checkpoint(&state)?;
56
- saved = done;
57
- }
58
- }
59
- }
60
-
61
- pub(super) fn save_hash(h: &Sha256, offset: i64) -> Option<String> {
62
- let state = h.save()?;
63
- serde_json::to_string(&HashState { offset, state }).ok()
64
- }
65
-
66
- /// Continues the hash a checkpoint saved, past the input it covered.
67
- fn resume_hash(checkpoint: &str, input: &mut dyn Read) -> Result<(Sha256, i64), BoxError> {
68
- if checkpoint.is_empty() {
69
- return Ok((Sha256::new(), 0));
70
- }
71
- let saved: HashState = serde_json::from_str(checkpoint)?;
72
- let h = Sha256::restore(&saved.state).ok_or("checkpoint: invalid hash state")?;
73
- let offset = u64::try_from(saved.offset).map_err(|_| "checkpoint: negative offset")?;
74
- if io::copy(&mut input.take(offset), &mut io::sink())? != offset {
75
- return Err("checkpoint: input shorter than its offset".into());
76
- }
77
- Ok((h, saved.offset))
78
- }
@@ -1,123 +0,0 @@
1
- use std::io::{Read, empty};
2
- use std::sync::Arc;
3
- use std::time::Duration;
4
-
5
- use syno_pkg_land_sdk::background::{
6
- Job, Runner, State, Store, SubmitRequest, Submitter, Task, WorkerInfo,
7
- };
8
- use syno_pkg_land_sdk::ctx::{Cancel, Context};
9
- use syno_pkg_land_sdk::sha256::{Sha256, hex_digest};
10
-
11
- use super::work::{HASH_CHECKPOINT_EVERY, hash_work, save_hash};
12
- use crate::app::test_support::TempDir;
13
-
14
- #[test]
15
- fn hash_work_resumes_from_its_checkpoint() {
16
- let (head, tail) = (vec![b'h'; 1024], vec![b't'; 3000]);
17
- let mut h = Sha256::new();
18
- h.update(&head);
19
- let saved = save_hash(&h, head.len() as i64).unwrap();
20
- // The input's first bytes differ from what the checkpoint hashed: only a
21
- // Work that skips them and continues the saved hash reaches this digest.
22
- let mut input: &[u8] = &[vec![b'x'; head.len()], tail.clone()].concat();
23
- let job = Job {
24
- checkpoint: saved,
25
- ..Job::default()
26
- };
27
- let got = hash_work(&Context::background(), &mut Task::new(job, &mut input)).unwrap();
28
- assert_eq!(got.sha256, hex_digest(&[head, tail].concat()));
29
- assert_eq!(got.bytes, 4024);
30
- }
31
-
32
- #[test]
33
- fn hash_work_refuses_a_foreign_checkpoint() {
34
- for saved in [
35
- "{",
36
- r#"{"offset":-1,"state":""}"#,
37
- r#"{"offset":3,"state":"00"}"#,
38
- ] {
39
- let job = Job {
40
- checkpoint: saved.into(),
41
- ..Job::default()
42
- };
43
- let mut input: &[u8] = b"abc";
44
- let got = hash_work(&Context::background(), &mut Task::new(job, &mut input));
45
- assert!(got.is_err(), "{saved}: accepted");
46
- }
47
- }
48
-
49
- /// Ends the worker's context once `left` bytes have been read, like a
50
- /// package stop in the middle of a job.
51
- struct StopAfter<'a> {
52
- inner: &'a mut dyn Read,
53
- left: usize,
54
- stop: Arc<Cancel>,
55
- }
56
-
57
- impl Read for StopAfter<'_> {
58
- fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
59
- let n = self.inner.read(buf)?;
60
- self.left = self.left.saturating_sub(n);
61
- if self.left == 0 {
62
- self.stop.cancel();
63
- }
64
- Ok(n)
65
- }
66
- }
67
-
68
- #[test]
69
- fn hash_work_reports_progress_and_checkpoints_before_a_stop() {
70
- let dir = TempDir::new();
71
- let store = Arc::new(Store::new(dir.path().join("background")));
72
- let ctx = Context::background();
73
- let data = b"0123456789abcdef".repeat(16 << 10);
74
- let (job, _) = store
75
- .submit(
76
- &ctx,
77
- SubmitRequest {
78
- key: "k".into(),
79
- name: "a.bin".into(),
80
- data: data.clone(),
81
- submitter: Submitter {
82
- user: "alice".into(),
83
- uid: "1024".into(),
84
- },
85
- },
86
- )
87
- .unwrap();
88
- let (worker, stop) = ctx.with_cancel();
89
- let stop = Arc::new(stop);
90
- let mut runner = Runner {
91
- store: store.clone(),
92
- work: Arc::new(move |ctx: &Context, t: &mut Task<'_>| {
93
- // The task's input lives as long as the task; the test leaks its wrapper.
94
- let inner = std::mem::replace(&mut t.input, Box::leak(Box::new(empty())));
95
- t.input = Box::leak(Box::new(StopAfter {
96
- inner,
97
- left: 3 * HASH_CHECKPOINT_EVERY as usize / 2,
98
- stop: stop.clone(),
99
- }));
100
- hash_work(ctx, t)
101
- }),
102
- timeout: Duration::from_secs(60),
103
- cancel_poll: Duration::from_millis(10),
104
- ..Runner::new(dir.path().join("background"), WorkerInfo::default())
105
- };
106
- let claimed = store.claim(&ctx).unwrap().unwrap();
107
- runner.process(&worker, &claimed);
108
- let paused = store.get(&ctx, &job.id).unwrap();
109
- let progress = paused.progress.clone().unwrap();
110
- assert_eq!(paused.state, State::Paused, "{paused:?}");
111
- assert!(!paused.checkpoint.is_empty());
112
- assert!(progress.bytes >= HASH_CHECKPOINT_EVERY, "{progress:?}");
113
- assert_eq!(progress.total_bytes, data.len() as i64);
114
-
115
- store.recover(&ctx).unwrap();
116
- let claimed = store.claim(&ctx).unwrap().unwrap();
117
- assert_eq!(claimed.checkpoint, paused.checkpoint);
118
- runner.work = Arc::new(hash_work);
119
- runner.process(&ctx, &claimed);
120
- let done = store.get(&ctx, &job.id).unwrap();
121
- assert_eq!(done.state, State::Succeeded, "{done:?}");
122
- assert_eq!(done.progress.unwrap().bytes, data.len() as i64);
123
- }