@synopackageland/templates 0.9.8 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +27 -27
  2. package/package.json +2 -2
  3. package/templates/_shared/AGENTS.md +53 -15
  4. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  5. package/templates/native-go/backend/cmd/app/main.go +15 -1
  6. package/templates/native-go/backend/cmd/app/main_test.go +33 -0
  7. package/templates/native-go/backend/go.mod +1 -1
  8. package/templates/native-go/backend/go.sum +2 -2
  9. package/templates/native-go/i18n/glossary.json +3 -1
  10. package/templates/native-go/i18n/messages/de.json +6 -0
  11. package/templates/native-go/i18n/messages/en.json +6 -0
  12. package/templates/native-go/i18n/messages/es.json +6 -0
  13. package/templates/native-go/i18n/messages/fr.json +6 -0
  14. package/templates/native-go/i18n/messages/it.json +6 -0
  15. package/templates/native-go/i18n/messages/ja.json +6 -0
  16. package/templates/native-go/i18n/messages/ko.json +6 -0
  17. package/templates/native-go/i18n/messages/zh-CN.json +6 -0
  18. package/templates/native-go/i18n/messages/zh-TW.json +6 -0
  19. package/templates/native-go/ui/app.js +2 -1
  20. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -2
  21. package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
  22. package/templates/native-go-vue/README.md +19 -7
  23. package/templates/native-go-vue/frontend/package-lock.json +9 -9
  24. package/templates/native-go-vue/frontend/package.json +2 -2
  25. package/templates/native-go-vue/frontend/src/App.vue +31 -36
  26. package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
  27. package/templates/native-go-vue/frontend/test/context.ts +8 -0
  28. package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
  29. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
  30. package/templates/native-node/README.md +153 -0
  31. package/templates/native-node/backend/app/package.json +4 -0
  32. package/templates/native-node/backend/app/permissions.json +5 -0
  33. package/templates/native-node/backend/package-lock.json +59 -0
  34. package/templates/native-node/backend/package.json +17 -0
  35. package/templates/native-node/backend/src/delegate.ts +5 -0
  36. package/templates/native-node/backend/src/jobs.ts +5 -0
  37. package/templates/native-node/backend/src/main.ts +4 -0
  38. package/templates/native-node/backend/src/params.ts +11 -0
  39. package/templates/native-node/backend/src/root-ops.ts +5 -0
  40. package/templates/native-node/backend/src/routes.ts +97 -0
  41. package/templates/native-node/backend/src/service.ts +22 -0
  42. package/templates/native-node/backend/test/routes.test.ts +75 -0
  43. package/templates/native-node/backend/test/support.ts +67 -0
  44. package/templates/native-node/backend/tsconfig.build.json +8 -0
  45. package/templates/native-node/backend/tsconfig.json +18 -0
  46. package/templates/native-node/dsm-package.yaml +80 -0
  47. package/templates/native-node/gitignore +4 -0
  48. package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
  49. package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
  50. package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
  51. package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
  52. package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
  53. package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
  54. package/templates/native-node-vue/README.md +56 -0
  55. package/templates/native-node-vue/gitignore +4 -0
  56. package/templates/native-python/README.md +150 -0
  57. package/templates/native-python/backend/app/delegate.py +5 -0
  58. package/templates/native-python/backend/app/jobs.py +5 -0
  59. package/templates/native-python/backend/app/main.py +5 -0
  60. package/templates/native-python/backend/app/params.py +10 -0
  61. package/templates/native-python/backend/app/permissions.json +5 -0
  62. package/templates/native-python/backend/app/root_ops.py +5 -0
  63. package/templates/native-python/backend/app/routes.py +107 -0
  64. package/templates/native-python/backend/tests/support.py +72 -0
  65. package/templates/native-python/backend/tests/test_routes.py +73 -0
  66. package/templates/native-python/dsm-package.yaml +77 -0
  67. package/templates/native-python/gitignore +2 -0
  68. package/templates/native-python-delegate/backend/app/delegate.py +42 -0
  69. package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
  70. package/templates/native-python-jobs/backend/app/jobs.py +40 -0
  71. package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
  72. package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
  73. package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
  74. package/templates/native-python-vue/README.md +56 -0
  75. package/templates/native-python-vue/gitignore +3 -0
  76. package/templates/native-rust/README.md +74 -296
  77. package/templates/native-rust/backend/Cargo.lock +6 -6
  78. package/templates/native-rust/backend/Cargo.toml +2 -10
  79. package/templates/native-rust/backend/src/delegate.rs +6 -0
  80. package/templates/native-rust/backend/src/jobs.rs +6 -0
  81. package/templates/native-rust/backend/src/main.rs +12 -0
  82. package/templates/native-rust/backend/src/root_ops.rs +6 -0
  83. package/templates/native-rust/backend/src/routes.rs +352 -0
  84. package/templates/native-rust/backend/src/test_support.rs +91 -0
  85. package/templates/native-rust/dsm-package.yaml +10 -21
  86. package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
  87. package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
  88. package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
  89. package/templates/native-rust-vue/README.md +35 -34
  90. package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
  91. package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
  92. package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
  93. package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
  94. package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
  95. package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
  96. package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
  97. package/templates/native-rust/backend/src/app/background/work.rs +0 -78
  98. package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
  99. package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
  100. package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
  101. package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
  102. package/templates/native-rust/backend/src/app/identity.rs +0 -56
  103. package/templates/native-rust/backend/src/app/migrations.rs +0 -79
  104. package/templates/native-rust/backend/src/app/mod.rs +0 -30
  105. package/templates/native-rust/backend/src/app/portal.rs +0 -317
  106. package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
  107. package/templates/native-rust/backend/src/app/routes.rs +0 -315
  108. package/templates/native-rust/backend/src/app/session_files.rs +0 -19
  109. package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
  110. package/templates/native-rust/backend/src/app/test_support.rs +0 -202
  111. package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
  112. package/templates/native-rust/backend/src/bin/api.rs +0 -72
  113. package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
  114. package/templates/native-rust/backend/src/build_env.rs +0 -155
  115. package/templates/native-rust/backend/src/lib.rs +0 -6
  116. package/templates/native-rust/backend/src/permissions.rs +0 -54
  117. package/templates/native-rust/backend/src/process.rs +0 -91
  118. package/templates/native-rust/i18n/glossary.json +0 -663
  119. package/templates/native-rust/i18n/messages/de.json +0 -30
  120. package/templates/native-rust/i18n/messages/en.json +0 -30
  121. package/templates/native-rust/i18n/messages/es.json +0 -30
  122. package/templates/native-rust/i18n/messages/fr.json +0 -30
  123. package/templates/native-rust/i18n/messages/it.json +0 -30
  124. package/templates/native-rust/i18n/messages/ja.json +0 -30
  125. package/templates/native-rust/i18n/messages/ko.json +0 -30
  126. package/templates/native-rust/i18n/messages/zh-CN.json +0 -30
  127. package/templates/native-rust/i18n/messages/zh-TW.json +0 -30
  128. package/templates/native-rust/ui/app.css +0 -45
  129. package/templates/native-rust/ui/app.js +0 -111
  130. package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
  131. package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
  132. package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
  133. package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
@@ -1,317 +0,0 @@
1
- use std::collections::BTreeMap;
2
- use std::sync::Arc;
3
-
4
- use syno_pkg_land_sdk::api::{self, Caller, Route};
5
- use syno_pkg_land_sdk::ctx::Context;
6
- use syno_pkg_land_sdk::delegate::{Client, ExecutorClient};
7
- use syno_pkg_land_sdk::http::Request;
8
- use syno_pkg_land_sdk::jsonx::Empty;
9
- use syno_pkg_land_sdk::{BoxError, portal};
10
-
11
- use super::Config;
12
- use super::routes::decode_params;
13
-
14
- /// What changes the executor's entry: `portal::register` or `portal::unregister`.
15
- type Change =
16
- fn(&Context, &dyn ExecutorClient, &BTreeMap<String, String>) -> Result<String, BoxError>;
17
-
18
- impl Config {
19
- /// portal.status when dsm-package.yaml declares `portal`. An administrator
20
- /// sets the alias with DSM's own AppPortal API from the UI; the package
21
- /// only reads DSM's saved setting. Below DSM 7.2 the executor registers
22
- /// the App's entry (`portal_executor`): portal.status then reports it, and
23
- /// portal.register and portal.unregister change it.
24
- pub(super) fn portal_routes(self: &Arc<Self>) -> Vec<Route> {
25
- if !self.portal {
26
- return Vec::new();
27
- }
28
- let mut routes = vec![self.handle("portal.status", Config::portal_status)];
29
- if self.portal_executor {
30
- routes.push(self.handle_request("portal.register", Config::portal_register));
31
- routes.push(self.handle_request("portal.unregister", Config::portal_unregister));
32
- }
33
- routes
34
- }
35
-
36
- /// The executor at its fixed socket, unless a test supplies one.
37
- fn portal_client(&self) -> Arc<dyn ExecutorClient> {
38
- self.executor
39
- .clone()
40
- .unwrap_or_else(|| Arc::new(Client::local(&self.package_id)))
41
- }
42
-
43
- fn portal_status(
44
- &self,
45
- ctx: &Context,
46
- _: &Caller,
47
- params: &[u8],
48
- ) -> Result<portal::Status, BoxError> {
49
- decode_params::<Empty>(params)?;
50
- Ok(self.read_portal(ctx))
51
- }
52
-
53
- fn read_portal(&self, ctx: &Context) -> portal::Status {
54
- let path = match &self.portal_config {
55
- Some(path) => path.to_string_lossy().into_owned(),
56
- None => portal::config_path(&self.package_id),
57
- };
58
- let status = portal::read(path, &format!("SYNO.SDS.{}.Instance", self.package_id));
59
- if !self.portal_executor {
60
- return status;
61
- }
62
- // An unreadable entry is not a known one: with_entry reports unavailable.
63
- let entry = portal::executor_entry(ctx, &*self.portal_client()).unwrap_or_default();
64
- portal::with_entry(status, &entry)
65
- }
66
-
67
- fn portal_register(
68
- &self,
69
- ctx: &Context,
70
- caller: &Caller,
71
- req: &Request,
72
- params: &[u8],
73
- ) -> Result<portal::Status, BoxError> {
74
- self.portal_change(ctx, caller, req, params, portal::register)
75
- }
76
-
77
- fn portal_unregister(
78
- &self,
79
- ctx: &Context,
80
- caller: &Caller,
81
- req: &Request,
82
- params: &[u8],
83
- ) -> Result<portal::Status, BoxError> {
84
- self.portal_change(ctx, caller, req, params, portal::unregister)
85
- }
86
-
87
- /// Forwards the administrator's DSM session; the executor checks it again
88
- /// and keeps its refusal codes, such as PORTAL_FAILED.
89
- fn portal_change(
90
- &self,
91
- ctx: &Context,
92
- caller: &Caller,
93
- req: &Request,
94
- params: &[u8],
95
- change: Change,
96
- ) -> Result<portal::Status, BoxError> {
97
- api::require_admin(caller)?;
98
- decode_params::<Empty>(params)?;
99
- let auth = api::dsm_session(req)?;
100
- change(ctx, &*self.portal_client(), &auth)?;
101
- Ok(self.read_portal(ctx))
102
- }
103
- }
104
-
105
- #[cfg(test)]
106
- mod tests {
107
- use std::path::PathBuf;
108
- use std::sync::Mutex;
109
-
110
- use serde_json::Value;
111
- use serde_json::value::RawValue;
112
- use syno_pkg_land_sdk::delegate::Auth;
113
-
114
- use super::super::test_support::*;
115
- use super::*;
116
-
117
- const SAVED: &str = r#"{"alias":"hello-rust","id":"SYNO.SDS.HelloRust.Instance","version":3}"#;
118
-
119
- fn portal_fixture(saved: &str) -> Fixture {
120
- let mut fixture = Fixture::new();
121
- let config: PathBuf = fixture.scratch().join("AppPortal.json");
122
- if !saved.is_empty() {
123
- std::fs::write(&config, saved).unwrap();
124
- }
125
- fixture.cfg.portal = true;
126
- fixture.cfg.portal_config = Some(config);
127
- fixture
128
- }
129
-
130
- #[test]
131
- fn portal_status_tells_every_signed_in_user_the_alias() {
132
- let fixture = portal_fixture(SAVED);
133
- let (code, out) = call(&fixture.cfg, &bob(), "portal.status", "{}");
134
- assert_eq!(code, "", "{out}");
135
- assert_eq!(out["data"]["state"], "active");
136
- assert_eq!(out["data"]["alias"], "hello-rust");
137
- let (code, _) = call(&fixture.cfg, &nobody(), "portal.status", "{}");
138
- assert_eq!(code, "session_required");
139
- let (code, _) = call(&fixture.cfg, &bob(), "portal.status", r#"{"alias":"x"}"#);
140
- assert_eq!(code, "bad_request");
141
- }
142
-
143
- #[test]
144
- fn portal_status_fails_closed_on_another_apps_setting() {
145
- let fixture = portal_fixture(r#"{"alias":"hello-rust","id":"SYNO.SDS.Other.Instance"}"#);
146
- let (_, out) = call(&fixture.cfg, &bob(), "portal.status", "{}");
147
- assert_eq!(out["data"]["state"], "unavailable");
148
- assert!(out["data"].get("alias").is_none());
149
- let (_, out) = call(&portal_fixture("").cfg, &bob(), "portal.status", "{}");
150
- assert_eq!(out["data"]["state"], "inactive");
151
- }
152
-
153
- #[test]
154
- fn portal_status_exists_only_for_a_portal_build() {
155
- let (code, _) = call(&Fixture::new().cfg, &alice(), "portal.status", "{}");
156
- assert_eq!(code, "unknown_method");
157
- }
158
-
159
- enum Failure {
160
- Down,
161
- Refused(&'static str),
162
- }
163
-
164
- /// The executor as the portal routes see it.
165
- #[derive(Default)]
166
- struct FakeEntry {
167
- state: Mutex<FakeState>,
168
- }
169
-
170
- #[derive(Default)]
171
- struct FakeState {
172
- entry: String,
173
- failure: Option<Failure>,
174
- calls: Vec<String>,
175
- auth: Auth,
176
- }
177
-
178
- impl ExecutorClient for FakeEntry {
179
- fn call(
180
- &self,
181
- _: &Context,
182
- op: &str,
183
- auth: &Auth,
184
- _: &RawValue,
185
- ) -> Result<Box<RawValue>, BoxError> {
186
- let mut state = self.state.lock().unwrap();
187
- state.calls.push(op.to_string());
188
- match &state.failure {
189
- Some(Failure::Down) => return Err("executor down".into()),
190
- Some(Failure::Refused(code)) => return Err(api::Error::new(code, "x").into()),
191
- None => {}
192
- }
193
- match op {
194
- "portal.register" => {
195
- (state.entry, state.auth) = (portal::ENTRY_REGISTERED.into(), auth.clone())
196
- }
197
- "portal.unregister" => {
198
- (state.entry, state.auth) = (portal::ENTRY_UNREGISTERED.into(), auth.clone())
199
- }
200
- _ => {}
201
- }
202
- let key = if op == "status" { "portal" } else { "entry" };
203
- let reply = serde_json::json!({ key: state.entry }).to_string();
204
- Ok(RawValue::from_string(reply)?)
205
- }
206
- }
207
-
208
- fn executor_fixture(saved: &str, executor: &Arc<FakeEntry>) -> Fixture {
209
- let mut fixture = portal_fixture(saved);
210
- fixture.cfg.portal_executor = true;
211
- fixture.cfg.executor = Some(executor.clone());
212
- fixture
213
- }
214
-
215
- fn fake(entry: &str, failure: Option<Failure>) -> Arc<FakeEntry> {
216
- Arc::new(FakeEntry {
217
- state: Mutex::new(FakeState {
218
- entry: entry.into(),
219
- failure,
220
- ..FakeState::default()
221
- }),
222
- })
223
- }
224
-
225
- /// An administrator's call with a DSM session.
226
- fn session_call(cfg: &Config, method: &str) -> (String, Value) {
227
- let mut req = api_request(method, "{}");
228
- req.header.set("Cookie", "id=session");
229
- req.header.set("X-SYNO-TOKEN", "token");
230
- serve(
231
- cfg,
232
- &alice(),
233
- req,
234
- crate::permissions::permissions().unwrap(),
235
- )
236
- }
237
-
238
- #[test]
239
- fn executor_portal_status_reports_the_entry() {
240
- for (entry, failure, state, alias) in [
241
- (
242
- portal::ENTRY_REGISTERED,
243
- false,
244
- "active",
245
- Some("hello-rust"),
246
- ),
247
- (
248
- portal::ENTRY_CLEANUP_REQUIRED,
249
- false,
250
- "active",
251
- Some("hello-rust"),
252
- ),
253
- (portal::ENTRY_UNREGISTERED, false, "inactive", None),
254
- ("", true, "unavailable", None),
255
- ] {
256
- let executor = fake(entry, failure.then_some(Failure::Down));
257
- let fixture = executor_fixture(SAVED, &executor);
258
- let (code, out) = call(&fixture.cfg, &bob(), "portal.status", "{}");
259
- let data = &out["data"];
260
- assert_eq!(code, "", "{entry}: {out}");
261
- assert_eq!(data["state"], state, "{entry}");
262
- assert_eq!(data["alias"].as_str(), alias, "{entry}");
263
- if !failure {
264
- assert_eq!(data["entry"], entry);
265
- }
266
- }
267
- }
268
-
269
- #[test]
270
- fn executor_portal_register_and_unregister_return_the_fresh_status() {
271
- let executor = fake(portal::ENTRY_UNREGISTERED, None);
272
- let fixture = executor_fixture(SAVED, &executor);
273
- let (code, out) = session_call(&fixture.cfg, "portal.register");
274
- assert_eq!(code, "", "register: {out}");
275
- assert_eq!(out["data"]["entry"], portal::ENTRY_REGISTERED);
276
- assert_eq!(out["data"]["state"], "active");
277
- assert_eq!(
278
- executor.state.lock().unwrap().auth["dsm_cookie"],
279
- "session",
280
- "register did not forward the DSM session"
281
- );
282
- let (code, out) = session_call(&fixture.cfg, "portal.unregister");
283
- assert_eq!(code, "", "unregister: {out}");
284
- assert_eq!(out["data"]["entry"], portal::ENTRY_UNREGISTERED);
285
- assert_eq!(out["data"]["state"], "inactive");
286
- }
287
-
288
- #[test]
289
- fn executor_portal_changes_need_an_administrators_dsm_session() {
290
- let executor = fake(portal::ENTRY_UNREGISTERED, None);
291
- let fixture = executor_fixture("", &executor);
292
- for method in ["portal.register", "portal.unregister"] {
293
- let (code, _) = call(&fixture.cfg, &bob(), method, "{}");
294
- assert_ne!(code, "", "{method}: an ordinary user changed the entry");
295
- let (code, _) = call(&fixture.cfg, &alice(), method, "{}");
296
- assert_eq!(code, "session_required", "{method} without a DSM session");
297
- }
298
- let calls = executor.state.lock().unwrap().calls.clone();
299
- assert!(calls.is_empty(), "the executor was asked: {calls:?}");
300
- }
301
-
302
- #[test]
303
- fn executor_portal_refusal_keeps_the_executors_code() {
304
- let executor = fake("", Some(Failure::Refused("PORTAL_FAILED")));
305
- let fixture = executor_fixture("", &executor);
306
- let (code, _) = session_call(&fixture.cfg, "portal.register");
307
- assert_eq!(code, "PORTAL_FAILED");
308
- }
309
-
310
- #[test]
311
- fn portal_changes_exist_only_in_an_executor_portal_build() {
312
- for method in ["portal.register", "portal.unregister"] {
313
- let (code, _) = call(&portal_fixture("").cfg, &alice(), method, "{}");
314
- assert_eq!(code, "unknown_method", "{method}");
315
- }
316
- }
317
- }
@@ -1,23 +0,0 @@
1
- use std::sync::Arc;
2
-
3
- use syno_pkg_land_sdk::api::Route;
4
-
5
- use super::Config;
6
- #[cfg(test)]
7
- use super::test_support::Sample;
8
-
9
- impl Config {
10
- /// Empty: this package declares privilege: none and never runs anything
11
- /// as root. `synopkgland init -t native-rust --privilege root` overlays
12
- /// this file with one that reads the results of administrator-approved
13
- /// root operations.
14
- pub(super) fn root_routes(self: &Arc<Self>) -> Vec<Route> {
15
- Vec::new()
16
- }
17
- }
18
-
19
- /// The root routes' samples: none.
20
- #[cfg(test)]
21
- pub(super) fn samples() -> Vec<Sample> {
22
- Vec::new()
23
- }
@@ -1,315 +0,0 @@
1
- use std::path::{Path, PathBuf};
2
- use std::sync::Arc;
3
-
4
- use serde::{Deserialize, Serialize, de::DeserializeOwned};
5
- use serde_json::{Value, json};
6
- use syno_pkg_land_sdk::api::{self, Caller, Route};
7
- use syno_pkg_land_sdk::ctx::Context;
8
- use syno_pkg_land_sdk::delegate::ExecutorClient;
9
- use syno_pkg_land_sdk::filestation;
10
- use syno_pkg_land_sdk::http::Request;
11
- use syno_pkg_land_sdk::time::Timestamp;
12
- use syno_pkg_land_sdk::{BoxError, jsonx};
13
-
14
- /// Resolves a DSM user name to its UID; empty when the account is unknown.
15
- pub type UidLookup = Arc<dyn Fn(&str) -> String + Send + Sync>;
16
-
17
- /// Config is fixed at build time and startup, never taken from a request.
18
- #[derive(Clone, Default)]
19
- pub struct Config {
20
- pub delegate_scope: String,
21
- pub delegate_operations_json: String,
22
- pub delegate_destinations_json: String,
23
- pub delegate_lifetime: String,
24
- pub executor_digest: String,
25
- pub policy_digest: String,
26
-
27
- /// The interactive File Station adapter; None keeps the legacy refusal.
28
- pub session_files: Option<Arc<filestation::Service>>,
29
- pub package_id: String,
30
- pub version: String,
31
- /// The package's persistent, package-owned directory
32
- /// (/var/packages/<package>/var). It survives upgrades.
33
- pub data_dir: PathBuf,
34
- /// Where /volumeN lives; None means "/". Tests set it.
35
- pub volume_root: Option<PathBuf>,
36
- /// Serves portal.status; `portal_executor` adds the executor's entry
37
- /// below DSM 7.2. `portal_config` overrides DSM's saved AppPortal.json
38
- /// path in tests.
39
- pub portal: bool,
40
- pub portal_executor: bool,
41
- pub portal_config: Option<PathBuf>,
42
-
43
- /// Test seams: the production values are the system's user database and
44
- /// the executor's socket under /run/synopkgland.
45
- pub uid_lookup: Option<UidLookup>,
46
- pub executor: Option<Arc<dyn ExecutorClient>>,
47
- }
48
-
49
- pub fn routes(cfg: Config) -> Vec<Route> {
50
- let cfg = Arc::new(cfg);
51
- let mut routes = vec![
52
- cfg.handle("app.status", Config::status),
53
- cfg.handle("greeting.get", Config::get_greeting),
54
- cfg.handle("greeting.set", Config::set_greeting),
55
- ];
56
- if let Some(files) = &cfg.session_files {
57
- routes.extend(files.routes());
58
- }
59
- routes.extend(cfg.portal_routes());
60
- routes.extend(cfg.background_routes());
61
- routes.extend(cfg.root_routes());
62
- routes.extend(cfg.delegate_routes());
63
- routes
64
- }
65
-
66
- type PlainFn<T> = fn(&Config, &Context, &Caller, &[u8]) -> Result<T, BoxError>;
67
- type RequestFn<T> = fn(&Config, &Context, &Caller, &Request, &[u8]) -> Result<T, BoxError>;
68
-
69
- impl Config {
70
- /// A route whose handler needs only the caller and its params.
71
- pub(super) fn handle<T: Serialize + 'static>(
72
- self: &Arc<Self>,
73
- method: &str,
74
- handler: PlainFn<T>,
75
- ) -> Route {
76
- let cfg = self.clone();
77
- Route::handle(method, move |ctx, caller, params| {
78
- handler(&cfg, ctx, caller, params)
79
- })
80
- }
81
-
82
- /// A route whose handler also needs the authenticated HTTP request.
83
- pub(super) fn handle_request<T: Serialize + 'static>(
84
- self: &Arc<Self>,
85
- method: &str,
86
- handler: RequestFn<T>,
87
- ) -> Route {
88
- let cfg = self.clone();
89
- Route::handle_request(method, move |ctx, caller, req, params| {
90
- handler(&cfg, ctx, caller, req, params)
91
- })
92
- }
93
-
94
- fn status(&self, _: &Context, caller: &Caller, _: &[u8]) -> Result<Value, BoxError> {
95
- // SAFETY: getuid and getgid take no arguments and cannot fail.
96
- let (uid, gid) = unsafe { (libc::getuid(), libc::getgid()) };
97
- Ok(self.with_shared_folders(json!({
98
- "package": self.package_id,
99
- "version": self.version,
100
- "user": caller.user(),
101
- "admin": caller.admin(),
102
- "process": {"uid": uid, "gid": gid},
103
- "language": "rust",
104
- "platform": format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH),
105
- "time": Timestamp::from_unix_nanos(i128::from(Timestamp::now().unix()) * 1_000_000_000).to_string(),
106
- })))
107
- }
108
-
109
- fn greeting_path(&self) -> PathBuf {
110
- self.data_dir.join("greeting.json")
111
- }
112
-
113
- fn get_greeting(&self, _: &Context, _: &Caller, _: &[u8]) -> Result<Greeting, BoxError> {
114
- match std::fs::read(self.greeting_path()) {
115
- Ok(data) => Ok(serde_json::from_slice(&data)?),
116
- Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Greeting {
117
- text: "Hello".into(),
118
- updated_by: String::new(),
119
- }),
120
- Err(e) => Err(e.into()),
121
- }
122
- }
123
-
124
- fn set_greeting(
125
- &self,
126
- _: &Context,
127
- caller: &Caller,
128
- params: &[u8],
129
- ) -> Result<Greeting, BoxError> {
130
- #[derive(Default, Deserialize)]
131
- #[serde(deny_unknown_fields)]
132
- struct Params {
133
- #[serde(default)]
134
- text: String,
135
- }
136
- let params: Params = decode_params(params)?;
137
- let length = params.text.chars().count();
138
- if !(1..=200).contains(&length) {
139
- return Err(api::Error::bad_request("text must be 1 to 200 characters").into());
140
- }
141
- let greeting = Greeting {
142
- text: params.text,
143
- updated_by: caller.user().to_string(),
144
- };
145
- write_file_atomic(&self.greeting_path(), &jsonx::to_vec(&greeting)?)?;
146
- Ok(greeting)
147
- }
148
- }
149
-
150
- #[derive(Serialize, Deserialize)]
151
- struct Greeting {
152
- text: String,
153
- #[serde(default, skip_serializing_if = "String::is_empty")]
154
- updated_by: String,
155
- }
156
-
157
- /// Decodes params strictly: unknown fields are an error.
158
- pub(super) fn decode_params<T: DeserializeOwned + Default>(params: &[u8]) -> Result<T, api::Error> {
159
- let params = if params.is_empty() { b"{}" } else { params };
160
- jsonx::strict(params).map_err(|e| api::Error::bad_request(&format!("invalid params: {e}")))
161
- }
162
-
163
- /// Replaces path in one rename, so readers never see half a file.
164
- pub(super) fn write_file_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> {
165
- use std::io::Write;
166
- use std::os::unix::fs::OpenOptionsExt;
167
- use std::sync::atomic::{AtomicU64, Ordering};
168
-
169
- static NEXT: AtomicU64 = AtomicU64::new(0);
170
- let tmp = path.with_file_name(format!(
171
- ".tmp-{}-{}",
172
- std::process::id(),
173
- NEXT.fetch_add(1, Ordering::Relaxed)
174
- ));
175
- let written = std::fs::OpenOptions::new()
176
- .write(true)
177
- .create_new(true)
178
- .mode(0o600)
179
- .open(&tmp)
180
- .and_then(|mut f| f.write_all(data).and_then(|()| f.flush()))
181
- .and_then(|()| std::fs::rename(&tmp, path));
182
- if written.is_err() {
183
- let _ = std::fs::remove_file(&tmp);
184
- }
185
- written
186
- }
187
-
188
- #[cfg(test)]
189
- mod tests {
190
- use super::super::test_support::*;
191
- use super::*;
192
- use syno_pkg_land_sdk::api::ADMINISTRATOR_PERMISSION;
193
-
194
- #[test]
195
- fn every_route_has_sample_params_and_none_is_public() {
196
- let fixture = Fixture::new();
197
- let routes = routes(fixture.cfg.clone());
198
- assert_eq!(
199
- routes.len(),
200
- samples().len(),
201
- "add the new route to test_support::samples"
202
- );
203
- for r in &routes {
204
- params_of(r.method());
205
- assert_ne!(
206
- rule(r),
207
- "public",
208
- "route {} is public; this template ships none",
209
- r.method()
210
- );
211
- }
212
- }
213
-
214
- #[test]
215
- fn every_route_refuses_callers_without_a_session_and_changes_nothing() {
216
- for r in routes(Fixture::new().cfg) {
217
- let fixture = Fixture::new();
218
- let (code, _) = call(&fixture.cfg, &nobody(), r.method(), &params_of(r.method()));
219
- assert_eq!(code, "session_required", "{}", r.method());
220
- fixture.assert_data_dir_empty();
221
- }
222
- }
223
-
224
- #[test]
225
- fn administrator_routes_refuse_ordinary_users_and_change_nothing() {
226
- for r in routes(Fixture::new().cfg) {
227
- if rule(&r) != ADMINISTRATOR_PERMISSION {
228
- continue;
229
- }
230
- let fixture = Fixture::new();
231
- let (code, _) = call(&fixture.cfg, &bob(), r.method(), &params_of(r.method()));
232
- assert_eq!(code, "admin_required", "{}", r.method());
233
- fixture.assert_data_dir_empty();
234
- }
235
- }
236
-
237
- #[test]
238
- fn every_route_succeeds_for_an_admin() {
239
- for sample in samples() {
240
- let (code, out) = call(&Fixture::new().cfg, &alice(), sample.method, &sample.params);
241
- assert_eq!(code, sample.outcome, "{}: {out}", sample.method);
242
- }
243
- }
244
-
245
- #[test]
246
- fn file_station_routes_exist_only_with_the_session_adapter() {
247
- let mut fixture = Fixture::new();
248
- let (code, _) = call(&fixture.cfg, &alice(), "nas.session.status", "{}");
249
- assert_eq!(code, "unknown_method");
250
- fixture.cfg.session_files = Some(Arc::new(filestation::Service::default()));
251
- let (code, _) = call(&fixture.cfg, &nobody(), "nas.session.status", "{}");
252
- assert_eq!(code, "session_required");
253
- let (code, _) = call(&fixture.cfg, &alice(), "nas.session.status", "{}");
254
- assert_eq!(
255
- code, "unavailable",
256
- "an adapter without a DSM client refuses"
257
- );
258
- }
259
-
260
- #[test]
261
- fn status_reports_package_and_caller() {
262
- let (code, out) = call(&Fixture::new().cfg, &alice(), "app.status", "{}");
263
- let data = &out["data"];
264
- assert_eq!(code, "");
265
- assert_eq!(data["package"], "HelloRust");
266
- assert_eq!(data["version"], "0.1.0-0001");
267
- assert_eq!(data["user"], "alice");
268
- assert_eq!(data["admin"], true);
269
- assert!(data["process"]["uid"].is_u64() && data["process"]["gid"].is_u64());
270
- assert_eq!(data["language"], "rust");
271
- assert!(data["time"].as_str().unwrap().ends_with('Z'));
272
- }
273
-
274
- #[test]
275
- fn greeting_defaults_and_admin_can_change_it() {
276
- let fixture = Fixture::new();
277
- let (_, out) = call(&fixture.cfg, &alice(), "greeting.get", "{}");
278
- assert_eq!(out["data"]["text"], "Hello");
279
- let (code, out) = call(
280
- &fixture.cfg,
281
- &alice(),
282
- "greeting.set",
283
- r#"{"text":"Hi from DSM"}"#,
284
- );
285
- assert_eq!(code, "", "{out}");
286
- let (_, out) = call(&fixture.cfg, &alice(), "greeting.get", "{}");
287
- assert_eq!(out["data"]["text"], "Hi from DSM");
288
- assert_eq!(out["data"]["updated_by"], "alice");
289
- }
290
-
291
- #[test]
292
- fn greeting_set_validates_text() {
293
- let too_long = format!(r#"{{"text":"{}"}}"#, "x".repeat(201));
294
- for params in [
295
- "{}",
296
- r#"{"text":""}"#,
297
- too_long.as_str(),
298
- r#"{"text":7}"#,
299
- r#"{"text":"ok","extra":1}"#,
300
- ] {
301
- let fixture = Fixture::new();
302
- let (code, _) = call(&fixture.cfg, &alice(), "greeting.set", params);
303
- assert_eq!(code, "bad_request", "{params}");
304
- fixture.assert_data_dir_empty();
305
- }
306
- }
307
-
308
- #[test]
309
- fn greeting_text_counts_characters_not_bytes() {
310
- let fixture = Fixture::new();
311
- let params = format!(r#"{{"text":"{}"}}"#, "\u{5b57}".repeat(200));
312
- let (code, out) = call(&fixture.cfg, &alice(), "greeting.set", &params);
313
- assert_eq!(code, "", "{out}");
314
- }
315
- }
@@ -1,19 +0,0 @@
1
- /// Selects the interactive File Station adapter using the caller's DSM session.
2
- pub const FILE_STATION_SESSION: &str = "session";
3
-
4
- /// Accepts only the fixed build-time session setting.
5
- pub fn session_files_enabled(file_station: &str) -> bool {
6
- file_station == FILE_STATION_SESSION
7
- }
8
-
9
- #[cfg(test)]
10
- mod tests {
11
- use super::*;
12
-
13
- #[test]
14
- fn session_files_are_enabled_by_the_trusted_build_setting() {
15
- for (value, want) in [("", false), ("session", true), ("yes", false)] {
16
- assert_eq!(session_files_enabled(value), want, "{value:?}");
17
- }
18
- }
19
- }