@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.
- package/README.md +27 -27
- package/package.json +2 -2
- package/templates/_shared/AGENTS.md +53 -15
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +15 -1
- package/templates/native-go/backend/cmd/app/main_test.go +33 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/i18n/messages/de.json +4 -0
- package/templates/native-go/i18n/messages/en.json +4 -0
- package/templates/native-go/i18n/messages/es.json +4 -0
- package/templates/native-go/i18n/messages/fr.json +4 -0
- package/templates/native-go/i18n/messages/it.json +4 -0
- package/templates/native-go/i18n/messages/ja.json +4 -0
- package/templates/native-go/i18n/messages/ko.json +4 -0
- package/templates/native-go/i18n/messages/zh-CN.json +4 -0
- package/templates/native-go/i18n/messages/zh-TW.json +4 -0
- package/templates/native-go/ui/app.js +2 -1
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -2
- package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
- package/templates/native-go-vue/README.md +19 -7
- package/templates/native-go-vue/frontend/package-lock.json +9 -9
- package/templates/native-go-vue/frontend/package.json +2 -2
- package/templates/native-go-vue/frontend/src/App.vue +31 -36
- package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
- package/templates/native-go-vue/frontend/test/context.ts +8 -0
- package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
- package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
- package/templates/native-node/README.md +153 -0
- package/templates/native-node/backend/app/package.json +4 -0
- package/templates/native-node/backend/app/permissions.json +5 -0
- package/templates/native-node/backend/package-lock.json +59 -0
- package/templates/native-node/backend/package.json +17 -0
- package/templates/native-node/backend/src/delegate.ts +5 -0
- package/templates/native-node/backend/src/jobs.ts +5 -0
- package/templates/native-node/backend/src/main.ts +4 -0
- package/templates/native-node/backend/src/params.ts +11 -0
- package/templates/native-node/backend/src/root-ops.ts +5 -0
- package/templates/native-node/backend/src/routes.ts +97 -0
- package/templates/native-node/backend/src/service.ts +22 -0
- package/templates/native-node/backend/test/routes.test.ts +75 -0
- package/templates/native-node/backend/test/support.ts +67 -0
- package/templates/native-node/backend/tsconfig.build.json +8 -0
- package/templates/native-node/backend/tsconfig.json +18 -0
- package/templates/native-node/dsm-package.yaml +80 -0
- package/templates/native-node/gitignore +4 -0
- package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
- package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
- package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
- package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
- package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
- package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
- package/templates/native-node-vue/README.md +56 -0
- package/templates/native-node-vue/gitignore +4 -0
- package/templates/native-python/README.md +150 -0
- package/templates/native-python/backend/app/delegate.py +5 -0
- package/templates/native-python/backend/app/jobs.py +5 -0
- package/templates/native-python/backend/app/main.py +5 -0
- package/templates/native-python/backend/app/params.py +10 -0
- package/templates/native-python/backend/app/permissions.json +5 -0
- package/templates/native-python/backend/app/root_ops.py +5 -0
- package/templates/native-python/backend/app/routes.py +107 -0
- package/templates/native-python/backend/tests/support.py +72 -0
- package/templates/native-python/backend/tests/test_routes.py +73 -0
- package/templates/native-python/dsm-package.yaml +77 -0
- package/templates/native-python/gitignore +2 -0
- package/templates/native-python-delegate/backend/app/delegate.py +42 -0
- package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
- package/templates/native-python-jobs/backend/app/jobs.py +40 -0
- package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
- package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
- package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
- package/templates/native-python-vue/README.md +56 -0
- package/templates/native-python-vue/gitignore +3 -0
- package/templates/native-rust/README.md +74 -296
- package/templates/native-rust/backend/Cargo.lock +6 -6
- package/templates/native-rust/backend/Cargo.toml +2 -10
- package/templates/native-rust/backend/src/delegate.rs +6 -0
- package/templates/native-rust/backend/src/jobs.rs +6 -0
- package/templates/native-rust/backend/src/main.rs +12 -0
- package/templates/native-rust/backend/src/root_ops.rs +6 -0
- package/templates/native-rust/backend/src/routes.rs +352 -0
- package/templates/native-rust/backend/src/test_support.rs +91 -0
- package/templates/native-rust/dsm-package.yaml +10 -21
- package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
- package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
- package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
- package/templates/native-rust-vue/README.md +35 -34
- package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
- package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
- package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
- package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
- package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
- package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
- package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
- package/templates/native-rust/backend/src/app/background/work.rs +0 -78
- package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
- package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
- package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/identity.rs +0 -56
- package/templates/native-rust/backend/src/app/migrations.rs +0 -79
- package/templates/native-rust/backend/src/app/mod.rs +0 -30
- package/templates/native-rust/backend/src/app/portal.rs +0 -317
- package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
- package/templates/native-rust/backend/src/app/routes.rs +0 -315
- package/templates/native-rust/backend/src/app/session_files.rs +0 -19
- package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
- package/templates/native-rust/backend/src/app/test_support.rs +0 -202
- package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
- package/templates/native-rust/backend/src/bin/api.rs +0 -72
- package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
- package/templates/native-rust/backend/src/build_env.rs +0 -155
- package/templates/native-rust/backend/src/lib.rs +0 -6
- package/templates/native-rust/backend/src/permissions.rs +0 -54
- package/templates/native-rust/backend/src/process.rs +0 -91
- package/templates/native-rust/i18n/glossary.json +0 -665
- package/templates/native-rust/i18n/messages/de.json +0 -32
- package/templates/native-rust/i18n/messages/en.json +0 -32
- package/templates/native-rust/i18n/messages/es.json +0 -32
- package/templates/native-rust/i18n/messages/fr.json +0 -32
- package/templates/native-rust/i18n/messages/it.json +0 -32
- package/templates/native-rust/i18n/messages/ja.json +0 -32
- package/templates/native-rust/i18n/messages/ko.json +0 -32
- package/templates/native-rust/i18n/messages/zh-CN.json +0 -32
- package/templates/native-rust/i18n/messages/zh-TW.json +0 -32
- package/templates/native-rust/ui/app.css +0 -45
- package/templates/native-rust/ui/app.js +0 -111
- package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
- package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
- package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
- 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, ¶ms.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, ¶ms.folder, ¶ms.relative)?;
|
|
243
|
-
let (job, duplicate) = self
|
|
244
|
-
.store
|
|
245
|
-
.submit(
|
|
246
|
-
ctx,
|
|
247
|
-
SubmitRequest {
|
|
248
|
-
key: params.key,
|
|
249
|
-
name: base_name(¶ms.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", ¶ms);
|
|
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
|
-
}
|