@synopackageland/templates 0.15.1 → 0.16.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 (46) hide show
  1. package/README.md +6 -4
  2. package/package.json +1 -1
  3. package/templates/_shared/AGENTS.md +3 -2
  4. package/templates/binary/ui/app.css +21 -4
  5. package/templates/compose/dsm-package.yaml +1 -1
  6. package/templates/native-go/README.md +15 -0
  7. package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
  8. package/templates/native-go/backend/cmd/app/main.go +4 -0
  9. package/templates/native-go/backend/go.mod +1 -1
  10. package/templates/native-go/backend/go.sum +2 -2
  11. package/templates/native-go/backend/internal/app/routes.go +12 -2
  12. package/templates/native-go/backend/internal/app/routes_test.go +4 -3
  13. package/templates/native-go/backend/internal/app/shared_folder.go +37 -127
  14. package/templates/native-go/backend/internal/app/shared_folder_test.go +95 -108
  15. package/templates/native-go/i18n/messages/de.json +4 -3
  16. package/templates/native-go/i18n/messages/en.json +4 -3
  17. package/templates/native-go/i18n/messages/es.json +4 -3
  18. package/templates/native-go/i18n/messages/fr.json +4 -3
  19. package/templates/native-go/i18n/messages/it.json +4 -3
  20. package/templates/native-go/i18n/messages/ja.json +4 -3
  21. package/templates/native-go/i18n/messages/ko.json +4 -3
  22. package/templates/native-go/i18n/messages/zh-CN.json +4 -3
  23. package/templates/native-go/i18n/messages/zh-TW.json +4 -3
  24. package/templates/native-go/ui/app.css +57 -25
  25. package/templates/native-go/ui/app.js +7 -4
  26. package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -3
  27. package/templates/native-go-root-ops/backend/internal/app/executor_test.go +1 -1
  28. package/templates/native-go-vue/frontend/DESIGN.md +1 -1
  29. package/templates/native-go-vue/frontend/package-lock.json +118 -193
  30. package/templates/native-go-vue/frontend/package.json +3 -3
  31. package/templates/native-go-vue/frontend/src/App.vue +13 -5
  32. package/templates/native-go-vue/frontend/src/i18n/de.json +5 -2
  33. package/templates/native-go-vue/frontend/src/i18n/en.json +5 -2
  34. package/templates/native-go-vue/frontend/src/i18n/es.json +5 -2
  35. package/templates/native-go-vue/frontend/src/i18n/fr.json +5 -2
  36. package/templates/native-go-vue/frontend/src/i18n/it.json +5 -2
  37. package/templates/native-go-vue/frontend/src/i18n/ja.json +5 -2
  38. package/templates/native-go-vue/frontend/src/i18n/ko.json +5 -2
  39. package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +5 -2
  40. package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +5 -2
  41. package/templates/native-go-vue/frontend/src/style.css +2 -2
  42. package/templates/native-go-vue/frontend/test/apps.test.ts +22 -2
  43. package/templates/native-node/dsm-package.yaml +1 -1
  44. package/templates/native-python/dsm-package.yaml +1 -1
  45. package/templates/native-rust/backend/Cargo.lock +2 -2
  46. package/templates/native-rust/backend/Cargo.toml +1 -1
package/README.md CHANGED
@@ -38,12 +38,13 @@ calls each one from the generated script of the same name (`synopkgland skill sh
38
38
  The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
39
39
  `__BACKGROUND_ENABLED__` are substituted by `init`.
40
40
 
41
- `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.15.1`; that line is
41
+ `backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.16.0`; that line is
42
42
  the single source of the SDK version and `frontend/package.json` pins
43
43
  `@synopackageland/sdk` to the same version. Python and Node.js name it in
44
44
  `backend.sdk`, Rust in the `syno-pkg-land-app` tag of `backend/Cargo.toml`
45
- (and `Cargo.lock`); all of them pin 0.15.1. The Vue frontend pins
46
- `@synopackageland/ui` 0.11.0.
45
+ (and `Cargo.lock`); all of them pin 0.16.0. The Vue frontend pins
46
+ `@synopackageland/ui` 0.12.0. SDK 0.16.0 accepts UI `>=0.10.1 <0.13.0`,
47
+ so no UI peer override is needed. Tests use jsdom 30 for UI floating-layer styles.
47
48
 
48
49
  ## Notice writer
49
50
 
@@ -76,7 +77,8 @@ Without `SYNOPKGLAND_SDK_DIR` the combinations use the released SDK that
76
77
  `backend/go.mod` and `frontend/package.json` pin. With it, `assemble.sh` adds
77
78
  `replace github.com/synopackageland/syno-pkg-land-sdk => $SYNOPKGLAND_SDK_DIR`
78
79
  and installs `$SYNOPKGLAND_SDK_DIR/ts` as an `npm pack` tarball (not a link, so Vue
79
- resolves once). With `SYNOPKGLAND_UI_TARBALL` set it installs that tarball as
80
+ resolves once). Set `SYNOPKGLAND_SDK_TARBALL` to a published tarball to test that
81
+ release without rebuilding the SDK frontend. With `SYNOPKGLAND_UI_TARBALL` set it installs that tarball as
80
82
  `@synopackageland/ui`, for a UI version not yet on npm.
81
83
  The Python and Node.js combinations need `SYNOPKGLAND_SDK_DIR` (its `python/`,
82
84
  and `node/` built with `npm ci && npm run build`) for their App SDK, and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/templates",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Project templates for synopkgland init: native-go, native-python, native-node and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
5
5
  "license": "MIT",
6
6
  "repository": {"type": "git", "url": "git+https://github.com/synopackageland/syno-pkg-land-templates.git"},
@@ -234,8 +234,9 @@ sweep forward the cancel instead of admitting it.
234
234
  repeats the original request on repeated IDs. Review it in the SDK's delegate
235
235
  package to adapt a real producer; show Saved only from a protected committed
236
236
  receipt.
237
- - Wizard support is measured on vt-nas DSM 7.4 via a session-only SSH local
238
- forward. This is not proof of other DSM releases or full writer/reboot behavior.
237
+ - Verify the wizard on your own DSM release over an SSH local forward. The
238
+ template ships the wizard contract; it is not proof of every DSM release or of
239
+ full writer/reboot behavior.
239
240
 
240
241
  ## Go backend
241
242
 
@@ -1,4 +1,21 @@
1
- :root { font-family: system-ui, sans-serif; color: #1e293b; background: #f8fafc; }
2
- .binary-app { padding: 2rem; }
3
- .binary-app h1 { font-size: 1.5rem; }
4
- .binary-app p { line-height: 1.6; }
1
+ /* Static UI subset from @synopackageland/ui 0.12.0 dsm7-embedded theme. */
2
+ .binary-app {
3
+ --ui-font-sans: Verdana, Arial, "Microsoft JhengHei", "Microsoft YaHei", Meiryo, sans-serif;
4
+ --ui-font-size: 13px;
5
+ --ui-font-size-title: 16px;
6
+ --ui-line-height: 20px;
7
+ --ui-text: #414b55;
8
+ --ui-surface: #fff;
9
+ --ui-space-2: 8px;
10
+ --ui-space-5: 20px;
11
+ --ui-space-6: 24px;
12
+ box-sizing: border-box;
13
+ min-height: 100%;
14
+ padding: var(--ui-space-5) var(--ui-space-6);
15
+ font: var(--ui-font-size)/var(--ui-line-height) var(--ui-font-sans);
16
+ color: var(--ui-text);
17
+ background: var(--ui-surface);
18
+ overflow-wrap: anywhere;
19
+ }
20
+ .binary-app h1 { margin: 0 0 var(--ui-space-2); font-size: var(--ui-font-size-title); font-weight: 600; }
21
+ .binary-app p { margin: 0; }
@@ -14,7 +14,7 @@ compose:
14
14
  web: {service: web, port: 8080, health: {path: /, status: 200}}
15
15
  network: {egress: isolated}
16
16
  identity_gateway:
17
- sdk: 0.15.1
17
+ sdk: 0.16.0
18
18
  mode: none
19
19
  listen: {http: 28000}
20
20
  # Web Apps default to new-page through the shared app-launch resolver.
@@ -378,6 +378,21 @@ account or other packages. An upgrade never deletes anything. Do not write your
378
378
  `synopkgland inspect` flags a deleting script that lacks the upgrade guard
379
379
  or deletes outside the package's own folders.
380
380
 
381
+ ## Shared Folder access
382
+
383
+ A `install.sharedFolder` in `dsm-package.yaml` lets the administrator pick a folder in the
384
+ install wizard, but DSM does not give the package account access to a picked folder. The
385
+ backend reports the real state: the `sharedfolder.status` operation (administrators only
386
+ unless you change `permissions.json`) returns each folder with what the App needs, what the
387
+ package account has (`none`, `read`, `write`) and a `state` (`ready`, `pending`, `missing`,
388
+ `ambiguous`). `cfg.SharedFolderPath(key)` returns the path or an error that names the
389
+ Control Panel step instead of failing on a file read. Keep `SharedFolderRequired` in
390
+ `cmd/app/main.go` equal to the permission in `dsm-package.yaml`. Set `StorageAdapter` to
391
+ apply a changed path (a re-picked folder, a moved volume) once; it runs only after access is
392
+ granted. The Vue template shows the `SharedFolderSettings` section, which offers
393
+ **Grant access** to an administrator and otherwise explains the Control Panel steps. See the
394
+ SDK's `docs/shared-folder.md` for the whole flow.
395
+
381
396
  ## Support matrix
382
397
 
383
398
  | | Status |
@@ -1,6 +1,6 @@
1
1
  Native Go package build dependencies
2
2
 
3
- github.com/synopackageland/syno-pkg-land-sdk v0.15.1 (MIT)
3
+ github.com/synopackageland/syno-pkg-land-sdk v0.16.0 (MIT)
4
4
 
5
5
  Copyright (c) 2026 synopkgland
6
6
 
@@ -20,6 +20,7 @@ import (
20
20
  "github.com/synopackageland/syno-pkg-land-sdk/dsm"
21
21
  "github.com/synopackageland/syno-pkg-land-sdk/filestation"
22
22
  standaloneidentity "github.com/synopackageland/syno-pkg-land-sdk/identity"
23
+ "github.com/synopackageland/syno-pkg-land-sdk/sharedfolder"
23
24
 
24
25
  appapi "__PACKAGE__/internal/api"
25
26
  "__PACKAGE__/internal/app"
@@ -100,6 +101,9 @@ func main() {
100
101
  DataDir: "/var/packages/" + packageID + "/var",
101
102
  Portal: portal != "",
102
103
  PortalExecutor: portal == "executor",
104
+ // install.sharedFolder in dsm-package.yaml: `synopkgland init --shared-folder`
105
+ // writes key "shared" with permission rw. Keep this equal to it.
106
+ SharedFolderRequired: map[string]sharedfolder.Access{"shared": sharedfolder.Write},
103
107
  }
104
108
  if _, err := app.ParseDelegateConfig(cfg); err != nil {
105
109
  log.Fatal(err)
@@ -4,6 +4,6 @@ go 1.25.0
4
4
 
5
5
  toolchain go1.26.9
6
6
 
7
- require github.com/synopackageland/syno-pkg-land-sdk v0.15.1
7
+ require github.com/synopackageland/syno-pkg-land-sdk v0.16.0
8
8
 
9
9
  require golang.org/x/sys v0.44.0 // indirect
@@ -1,4 +1,4 @@
1
- github.com/synopackageland/syno-pkg-land-sdk v0.15.1 h1:RxG9aWn6PpAe/0o4jkbfDekrVImuIMSfx21X3lcnn24=
2
- github.com/synopackageland/syno-pkg-land-sdk v0.15.1/go.mod h1:msR4MhJQnVZ8NKLb0E/1K79ldZUvcOJmWhKq+gPEa+4=
1
+ github.com/synopackageland/syno-pkg-land-sdk v0.16.0 h1:9MYFliraGfCE+1TXyFQD6s/YPMBMhGbj4bY+HHh9Y08=
2
+ github.com/synopackageland/syno-pkg-land-sdk v0.16.0/go.mod h1:vixf5Lo4PssQDGU5zam0Mb5sFu9wPUP4Ck36BMAVbNc=
3
3
  golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
4
4
  golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -20,6 +20,7 @@ import (
20
20
 
21
21
  "github.com/synopackageland/syno-pkg-land-sdk/api"
22
22
  "github.com/synopackageland/syno-pkg-land-sdk/filestation"
23
+ "github.com/synopackageland/syno-pkg-land-sdk/sharedfolder"
23
24
  )
24
25
 
25
26
  // Config is fixed at build time and startup, never taken from a request.
@@ -37,6 +38,14 @@ type Config struct {
37
38
  DataDir string
38
39
  // VolumeRoot is where /volumeN lives; empty means "/". Tests set it.
39
40
  VolumeRoot string
41
+ // SharedFolderRequired is the access each Shared Folder key needs. Keep it
42
+ // equal to install.sharedFolder.permission in dsm-package.yaml: ro is
43
+ // sharedfolder.Read, rw is sharedfolder.Write.
44
+ SharedFolderRequired map[string]sharedfolder.Access
45
+ // StorageAdapter, when set, hands a Shared Folder's path to the App's own
46
+ // storage settings (a Local storage, a library root): the first time the
47
+ // folder is ready, and again whenever its path changes. Make it idempotent.
48
+ StorageAdapter func(ctx context.Context, key, path string) error
40
49
  // Portal serves portal.status; PortalExecutor adds the executor's entry
41
50
  // below DSM 7.2. PortalConfig overrides DSM's saved AppPortal.json path in
42
51
  // tests.
@@ -49,6 +58,7 @@ func Routes(cfg Config) []api.Route {
49
58
  routes := cfg.delegateRoutes()
50
59
  routes = append(routes, []api.Route{
51
60
  api.Handle("app.status", cfg.status),
61
+ api.Handle("sharedfolder.status", cfg.sharedFolderStatus),
52
62
  api.Handle("greeting.get", cfg.getGreeting),
53
63
  api.Handle("greeting.set", cfg.setGreeting),
54
64
  }...)
@@ -59,7 +69,7 @@ func Routes(cfg Config) []api.Route {
59
69
  }
60
70
 
61
71
  func (cfg Config) status(_ context.Context, caller api.Caller, _ json.RawMessage) (any, error) {
62
- return cfg.withSharedFolders(map[string]any{
72
+ return map[string]any{
63
73
  "package": cfg.PackageID,
64
74
  "version": cfg.Version,
65
75
  "user": caller.User(),
@@ -68,7 +78,7 @@ func (cfg Config) status(_ context.Context, caller api.Caller, _ json.RawMessage
68
78
  "go_version": runtime.Version(),
69
79
  "platform": runtime.GOOS + "/" + runtime.GOARCH,
70
80
  "time": time.Now().UTC().Format(time.RFC3339),
71
- }), nil
81
+ }, nil
72
82
  }
73
83
 
74
84
  type greeting struct {
@@ -107,9 +107,10 @@ var (
107
107
  var sampleOutcome = map[string]string{}
108
108
 
109
109
  var sampleParams = map[string]string{
110
- "app.status": `{}`,
111
- "greeting.get": `{}`,
112
- "greeting.set": `{"text":"Hi"}`,
110
+ "app.status": `{}`,
111
+ "sharedfolder.status": `{}`,
112
+ "greeting.get": `{}`,
113
+ "greeting.set": `{"text":"Hi"}`,
113
114
  }
114
115
 
115
116
  func TestEveryRouteHasSampleParamsAndNoneIsPublic(t *testing.T) {
@@ -1,144 +1,54 @@
1
1
  package app
2
2
 
3
3
  import (
4
+ "context"
4
5
  "encoding/json"
5
- "errors"
6
- "fmt"
7
- "io/fs"
8
- "os"
9
- "path/filepath"
10
- "sort"
11
- "strings"
12
- "syscall"
13
- )
14
-
15
- // The install wizard stores the chosen Shared Folder's *name* in
16
- // <DataDir>/config.json (see dsm-package.yaml install.sharedFolder). The path
17
- // is resolved on every call, so a Shared Folder moved to another volume keeps
18
- // working. config.json is written by the package's postinst/postupgrade, not
19
- // by the app.
20
6
 
21
- var (
22
- ErrSharedFolderNotFound = errors.New("shared folder not found on any volume")
23
- ErrSharedFolderAmbiguous = errors.New("shared folder name matches more than one volume")
7
+ "github.com/synopackageland/syno-pkg-land-sdk/api"
8
+ "github.com/synopackageland/syno-pkg-land-sdk/sharedfolder"
24
9
  )
25
10
 
26
- // SharedFolder says where one configured Shared Folder is right now and what
27
- // the package account may do there.
28
- type SharedFolder struct {
29
- Key string `json:"key"`
30
- Name string `json:"name"`
31
- Path string `json:"path,omitempty"`
32
- Exists bool `json:"exists"`
33
- Readable bool `json:"readable"`
34
- Writable bool `json:"writable"`
35
- Error string `json:"error,omitempty"`
36
- }
37
-
38
- func validShareName(name string) bool {
39
- return name != "" && !strings.ContainsAny(name, "/\\\x00") && !strings.Contains(name, "..") &&
40
- !strings.HasPrefix(name, "@") && !strings.HasPrefix(name, ".")
41
- }
42
-
43
- // ReadSharedFolders returns the Shared Folder names the installer stored, by
44
- // key. A package without config.json has none.
45
- func ReadSharedFolders(dataDir string) (map[string]string, error) {
46
- data, err := os.ReadFile(filepath.Join(dataDir, "config.json"))
47
- if errors.Is(err, fs.ErrNotExist) {
48
- return nil, nil
49
- }
50
- if err != nil {
51
- return nil, err
52
- }
53
- var cfg struct {
54
- Schema int `json:"schema"`
55
- SharedFolders map[string]string `json:"shared_folders"`
56
- }
57
- if err := json.Unmarshal(data, &cfg); err != nil {
58
- return nil, fmt.Errorf("config.json: %w", err)
59
- }
60
- if cfg.Schema != 1 {
61
- return nil, fmt.Errorf("config.json: unsupported schema %d", cfg.Schema)
62
- }
63
- for key, name := range cfg.SharedFolders {
64
- if !validShareName(name) {
65
- return nil, fmt.Errorf("config.json: invalid shared folder name for %q", key)
66
- }
67
- }
68
- return cfg.SharedFolders, nil
69
- }
70
-
71
- // ResolveSharedFolder finds /volumeN/<name> under root ("/" on a NAS).
72
- func ResolveSharedFolder(root, name string) (string, error) {
73
- if !validShareName(name) {
74
- return "", fmt.Errorf("invalid shared folder name %q", name)
75
- }
76
- volumes, err := filepath.Glob(filepath.Join(root, "volume[0-9]*"))
77
- if err != nil {
78
- return "", err
79
- }
80
- var found []string
81
- for _, volume := range volumes {
82
- path := filepath.Join(volume, name)
83
- if info, err := os.Stat(path); err == nil && info.IsDir() {
84
- found = append(found, path)
85
- }
86
- }
87
- switch len(found) {
88
- case 0:
89
- return "", ErrSharedFolderNotFound
90
- case 1:
91
- return found[0], nil
92
- }
93
- return "", ErrSharedFolderAmbiguous
11
+ // The install wizard stores the chosen Shared Folder's *name* in
12
+ // <DataDir>/config.json (see dsm-package.yaml install.sharedFolder). DSM does
13
+ // not give the package account access to a folder chosen at install time: an
14
+ // administrator grants it in Control Panel (or with one click in the settings
15
+ // page). The SDK's sharedfolder package measures what the account may do, so
16
+ // this route, the settings page and any daemon read one state.
17
+
18
+ // sharedFolders is the SDK's view of this package's Shared Folders.
19
+ func (cfg Config) sharedFolders() sharedfolder.Config {
20
+ return sharedfolder.Config{DataDir: cfg.DataDir, Root: cfg.VolumeRoot, Required: cfg.SharedFolderRequired}
94
21
  }
95
22
 
96
- const (
97
- accessRead = 0x4 // R_OK
98
- accessWrite = 0x2 // W_OK
99
- accessExec = 0x1 // X_OK
100
- )
101
-
102
- // SharedFolders reports every configured Shared Folder, sorted by key. The
103
- // error is a corrupt config.json.
104
- func (cfg Config) SharedFolders() ([]SharedFolder, error) {
105
- names, err := ReadSharedFolders(cfg.DataDir)
23
+ // sharedFolderStatus answers the settings page: each folder, what the package
24
+ // account has and what the App declared it needs, and whether an
25
+ // administrator still has to grant. Administrators only, as the default
26
+ // permissions say; list the method in internal/api/permissions.json to let
27
+ // every DSM user read it.
28
+ func (cfg Config) sharedFolderStatus(ctx context.Context, _ api.Caller, _ json.RawMessage) (any, error) {
29
+ status, err := cfg.sharedFolders().Status()
106
30
  if err != nil {
107
31
  return nil, err
108
32
  }
109
- root := cfg.VolumeRoot
110
- if root == "" {
111
- root = "/"
112
- }
113
- keys := make([]string, 0, len(names))
114
- for key := range names {
115
- keys = append(keys, key)
116
- }
117
- sort.Strings(keys)
118
- folders := make([]SharedFolder, 0, len(keys))
119
- for _, key := range keys {
120
- folder := SharedFolder{Key: key, Name: names[key]}
121
- path, err := ResolveSharedFolder(root, folder.Name)
122
- if err != nil {
123
- folder.Error = err.Error()
124
- } else {
125
- folder.Path, folder.Exists = path, true
126
- folder.Readable = syscall.Access(path, accessRead|accessExec) == nil
127
- folder.Writable = syscall.Access(path, accessWrite|accessExec) == nil
33
+ // The App's own storage follows a changed path (another folder chosen,
34
+ // the share moved to another volume) here, the first time anything reads it.
35
+ if cfg.StorageAdapter != nil {
36
+ for _, folder := range status.Folders {
37
+ if folder.State == sharedfolder.Ready {
38
+ if _, err := cfg.sharedFolders().Sync(folder.Key, func(path string) error {
39
+ return cfg.StorageAdapter(ctx, folder.Key, path)
40
+ }); err != nil {
41
+ return nil, sharedfolder.APIError(err)
42
+ }
43
+ }
128
44
  }
129
- folders = append(folders, folder)
130
45
  }
131
- return folders, nil
46
+ return status, nil
132
47
  }
133
48
 
134
- // withSharedFolders adds the configured Shared Folders to an app.status
135
- // answer; a package that configures none is answered exactly as before.
136
- func (cfg Config) withSharedFolders(status map[string]any) map[string]any {
137
- folders, err := cfg.SharedFolders()
138
- if err != nil {
139
- status["shared_folders_error"] = err.Error()
140
- } else if len(folders) > 0 {
141
- status["shared_folders"] = folders
142
- }
143
- return status
49
+ // SharedFolderPath is what a daemon calls before it touches the folder: the
50
+ // path once the account has the declared access, otherwise an error naming
51
+ // what to grant, never a failed file read.
52
+ func (cfg Config) SharedFolderPath(key string) (string, error) {
53
+ return cfg.sharedFolders().Require(key)
144
54
  }