@stacksjs/defaults 0.74.32 → 0.74.34

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 (31) hide show
  1. package/ai/skills/stacks-analytics/SKILL.md +104 -26
  2. package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
  3. package/ai/skills/stacks-dashboard/SKILL.md +60 -0
  4. package/ai/skills/stacks-orm/SKILL.md +1 -1
  5. package/ai/skills/stacks-storage/SKILL.md +58 -4
  6. package/ai/skills/stacks-technical-diagrams/SKILL.md +1 -1
  7. package/app/Actions/Dashboard/Content/FileDuplicateAction.ts +25 -0
  8. package/app/Actions/Dashboard/Content/FileFavoriteAction.ts +25 -0
  9. package/app/Actions/Dashboard/Content/FileRenameAction.ts +25 -0
  10. package/app/Actions/Dashboard/Content/FileReprocessAction.ts +31 -0
  11. package/app/Actions/Dashboard/Content/FileTagsAction.ts +27 -0
  12. package/app/Actions/Dashboard/Content/FileVisibilityAction.ts +25 -0
  13. package/app/Actions/Dashboard/Content/file-manager.test.ts +227 -9
  14. package/app/Actions/Dashboard/Content/file-manager.ts +544 -11
  15. package/app/Actions/Dashboard/Content/file-metadata-store.ts +432 -0
  16. package/app/Actions/Dashboard/Content/file-metadata.test.ts +357 -0
  17. package/app/Actions/Dashboard/Content/file-metadata.ts +550 -0
  18. package/app/Actions/Dashboard/Content/file-pipeline.test.ts +344 -0
  19. package/app/Jobs/OptimizeStorageImageJob.ts +74 -0
  20. package/app/Jobs/TagStorageMediaJob.ts +122 -0
  21. package/app/Jobs/TranscodeStorageVideoJob.ts +88 -0
  22. package/app/Models/StorageItem.ts +123 -0
  23. package/app/Models/StorageItemTask.ts +134 -0
  24. package/ide/vscode/package.json +1 -1
  25. package/package.json +2 -2
  26. package/routes/dashboard-api.ts +13 -0
  27. package/vcs/github/workflows/buddy-bot.yml +109 -0
  28. package/vcs/github/workflows/ci.yml +3 -3
  29. package/vcs/github/workflows/release.yml +1 -1
  30. package/resources/assets/fonts/Monaco.ttf +0 -0
  31. package/vcs/github/renovate.json +0 -5
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-analytics
3
- description: Use when adding analytics to a Stacks application - configuring Fathom or self-hosted analytics, generating tracking scripts, privacy-friendly analytics setup, or the analytics configuration. Covers @stacksjs/analytics and config/analytics.ts.
3
+ description: Use when adding analytics to a Stacks application - configuring Fathom, Plausible, Google Analytics or self-hosted analytics, generating tracking scripts, privacy-friendly analytics setup, or the analytics configuration. Covers @stacksjs/analytics and config/analytics.ts.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -8,24 +8,84 @@ allowed-tools: Read Edit Write Bash Grep Glob
8
8
 
9
9
  # Stacks Analytics
10
10
 
11
- Privacy-friendly analytics with Fathom and self-hosted driver support.
11
+ Privacy-friendly analytics with four drivers: Fathom, Plausible, Google Analytics
12
+ and self-hosted.
12
13
 
13
14
  ## Key Paths
14
15
  - Core package: `storage/framework/core/analytics/src/`
16
+ - Drivers: `storage/framework/core/analytics/src/drivers/`
17
+ - Registry (dispatch on the configured driver): `storage/framework/core/analytics/src/registry.ts`
15
18
  - Configuration: `config/analytics.ts`
16
19
 
20
+ ## Driver registry
21
+
22
+ Read `config/analytics.ts` and render the driver it names. This is the entry
23
+ point; reach for an individual driver only when you need one specific script
24
+ regardless of config.
25
+
26
+ ```typescript
27
+ import { generateAnalyticsScript, getAnalyticsHead } from '@stacksjs/analytics'
28
+ import { analytics } from '@stacksjs/config'
29
+
30
+ // HTML you can drop into a layout <head>
31
+ const script = generateAnalyticsScript(analytics)
32
+
33
+ // Or the [tag, attributes] pairs a docs `head` array takes
34
+ const head = getAnalyticsHead(analytics)
35
+ ```
36
+
37
+ The registry is loud on purpose. A `driver` value with no implementation throws,
38
+ and so does a driver that is selected but missing its config (the message names
39
+ the exact key to set). Only an app with no `driver` at all gets an empty result.
40
+
41
+ Nothing injects the script for you - a framework that silently posted pageviews
42
+ to a third party would be the wrong default. Render it yourself in the layout
43
+ that should carry it.
44
+
17
45
  ## Drivers
18
46
 
19
- ### Fathom Analytics
20
- Privacy-focused, GDPR-compliant analytics. Requires a Fathom account.
47
+ ### Fathom
48
+ Hosted, cookie-free, GDPR-compliant. Requires a Fathom account.
49
+ `scriptUrl` points the tag at your own origin so a content blocker does not drop it.
21
50
 
22
- ### Self-Hosted Analytics
23
- Generate tracking scripts for self-hosted analytics:
51
+ ```typescript
52
+ import { generateFathomScript } from '@stacksjs/analytics'
53
+
54
+ generateFathomScript({
55
+ siteId: 'ABCDEFGH',
56
+ honorDnt: true, // -> data-honor-dnt="true"
57
+ spa: true, // -> data-spa="auto"
58
+ scriptUrl: undefined, // defaults to Fathom's CDN
59
+ })
60
+ ```
61
+
62
+ ### Plausible
63
+ Cookie-free, hosted or self-hosted. `hashMode` and `trackLocalhost` pick the
64
+ script variant (`script.hash.js`, `script.local.js`); `scriptUrl` overrides the
65
+ computed URL entirely, which is how you point at a self-hosted install.
24
66
 
25
67
  ```typescript
26
- import { generateSelfHostedScript, getSelfHostedAnalyticsHead, generateInlineScript } from '@stacksjs/analytics'
68
+ import { generatePlausibleScript } from '@stacksjs/analytics'
69
+
70
+ generatePlausibleScript({ domain: 'example.com', hashMode: true })
71
+ ```
72
+
73
+ ### Google Analytics
74
+ GA4 via gtag.js. Emits the loader plus the inline `js` / `config` bootstrap;
75
+ `debug: true` sets `debug_mode` so the property shows up in DebugView.
76
+
77
+ ```typescript
78
+ import { generateGoogleAnalyticsScript } from '@stacksjs/analytics'
79
+
80
+ generateGoogleAnalyticsScript({ trackingId: 'G-XXXXXXXXXX', debug: false })
81
+ ```
82
+
83
+ ### Self-Hosted
84
+ A small first-party tracker that POSTs to your own endpoint.
85
+
86
+ ```typescript
87
+ import { generateSelfHostedScript, getSelfHostedAnalyticsHead } from '@stacksjs/analytics'
27
88
 
28
- // Generate tracking script tag
29
89
  const script = generateSelfHostedScript({
30
90
  siteId: 'ABCDEF',
31
91
  apiEndpoint: 'https://analytics.myapp.com/api/event',
@@ -34,19 +94,34 @@ const script = generateSelfHostedScript({
34
94
  trackOutboundLinks: true // track external link clicks
35
95
  })
36
96
 
37
- // Generate head configuration for STX
38
97
  const headConfig = getSelfHostedAnalyticsHead({
39
98
  siteId: 'ABCDEF',
40
99
  apiEndpoint: 'https://analytics.myapp.com/api/event'
41
100
  })
42
-
43
- // Generate inline script (no external JS file)
44
- const inline = generateInlineScript(config)
45
101
  ```
46
102
 
47
- ## SelfHostedConfig Interface
103
+ ## Driver config interfaces
48
104
 
49
105
  ```typescript
106
+ interface FathomConfig {
107
+ siteId: string // Fathom site ID
108
+ scriptUrl?: string // serve the script first-party
109
+ honorDnt?: boolean // respect Do Not Track
110
+ spa?: boolean // re-record pageviews on client routing
111
+ }
112
+
113
+ interface PlausibleConfig {
114
+ domain: string // the site's domain
115
+ scriptUrl?: string // self-hosted Plausible or a proxy
116
+ trackLocalhost?: boolean // keep localhost pageviews
117
+ hashMode?: boolean // count hash routes
118
+ }
119
+
120
+ interface GoogleAnalyticsConfig {
121
+ trackingId: string // GA4 measurement ID
122
+ debug?: boolean // debug_mode -> DebugView
123
+ }
124
+
50
125
  interface SelfHostedConfig {
51
126
  siteId: string // unique site identifier
52
127
  apiEndpoint: string // analytics API URL
@@ -60,18 +135,22 @@ interface SelfHostedConfig {
60
135
 
61
136
  ```typescript
62
137
  {
63
- driver: 'fathom', // 'fathom' | 'google-analytics'
138
+ driver: 'fathom', // 'fathom' | 'plausible' | 'google-analytics' | 'self-hosted'
64
139
  drivers: {
65
- googleAnalytics: {
66
- trackingId: '' // GA tracking ID
67
- },
68
- fathom: {
69
- siteId: '' // Fathom site ID
70
- }
140
+ googleAnalytics: { trackingId: '' },
141
+ fathom: { siteId: '' },
142
+ plausible: { domain: '' },
143
+ selfHosted: { siteId: '', apiEndpoint: '' },
71
144
  }
72
145
  }
73
146
  ```
74
147
 
148
+ ## First-party pageview capture
149
+
150
+ Separate from the drivers, and no script at all: `capturePageviews: true` has the
151
+ stx servers record page GETs into `analytics_events`, which is what feeds the
152
+ native `/analytics/pages`, `/referrers` and `/devices` dashboards.
153
+
75
154
  ## Dashboard Integration
76
155
 
77
156
  Analytics dashboard at `/dashboard/analytics` displays:
@@ -81,11 +160,10 @@ Analytics dashboard at `/dashboard/analytics` displays:
81
160
  - Engagement metrics
82
161
 
83
162
  ## Gotchas
84
- - Default driver is `fathom` — privacy-focused by default
85
- - Self-hosted analytics require your own analytics endpoint
163
+ - Selecting a driver does not inject anything - call `generateAnalyticsScript()` in your layout
164
+ - A misconfigured driver throws rather than emitting nothing; the message names the config key
86
165
  - `honorDnt: true` respects browser Do Not Track settings
87
- - `escapeAttr()` is used internally to prevent XSS in generated scripts
88
- - Google Analytics tracking ID goes in config, not env (unless you override)
89
- - Analytics scripts are generated server-side and injected into page head
90
- - Fathom is a paid service — self-hosted is free but requires infrastructure
166
+ - Attribute values are escaped, and inline script values are JS-escaped, to prevent XSS
167
+ - Fathom is a paid service - self-hosted is free but requires infrastructure
91
168
  - `trackOutboundLinks` adds click handlers to external `<a>` tags
169
+ - `capturePageviews` is server-side and independent of `driver`
@@ -66,7 +66,7 @@ globalThis.toggleDark = toggleDark
66
66
  - **Custom Functions**: From `resources/functions/` (counter, dark mode, GPX, geo utilities)
67
67
 
68
68
  ### Server Auto-Imports (100+)
69
- - **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (100 models)
69
+ - **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (102 models)
70
70
  - **Request Models**: UserRequest, PostRequest, OrderRequest, etc.
71
71
  - **Actions**: Action types and helpers
72
72
  - **Schema**: validation schema builder
@@ -61,6 +61,66 @@ dashboard data Actions.
61
61
  - `/content/comments` - comment moderation
62
62
  - `/content/files`, `/content/blog`, `/content/seo` - files, blog operations, and SEO
63
63
 
64
+ ### The file manager's two layers (stacksjs/stacks#2577)
65
+
66
+ Worth knowing before adding anything to it, because the split is not obvious
67
+ from the endpoints:
68
+
69
+ - **Storage operations** map to a `StorageAdapter` method and go straight to the
70
+ disk: list, upload, create folder, rename, visibility, duplicate, delete.
71
+ - **Metadata** - favourites and tags - has nowhere to live on a disk (extended
72
+ attributes do not survive a copy; S3 object metadata is set at write time, so
73
+ starring a 2 GB video would rewrite 2 GB). It lives in `storage_items`, keyed
74
+ by `(disk, path)`, written by `PUT /files/favorite` and `PUT /files/tags`.
75
+
76
+ **The disk is authoritative and the table is advisory.** The listing comes from
77
+ the disk and rows are joined onto it, so a path with no row is a file with
78
+ nothing recorded - which is most files. Renames and deletes made THROUGH the
79
+ dashboard reconcile eagerly (a folder is a prefix update, because moving a
80
+ folder moves everything under it); a completed listing sweeps rows for paths it
81
+ did not see, which is free because the walk already enumerated them. A TRUNCATED
82
+ listing sweeps nothing - it has not proved a path is absent.
83
+
84
+ A file renamed outside the dashboard loses its metadata, and that is by design:
85
+ a rename and a copy-then-delete are the same two events to a bucket listing, so
86
+ reconciling would be guessing.
87
+
88
+ ### The media pipeline (stacksjs/stacks#2578)
89
+
90
+ None of the three things an upload might need can happen inside the request: a
91
+ transcode is minutes, a vision call is a round trip to a third party. So an
92
+ upload dispatches and the dashboard shows state.
93
+
94
+ - `storage_item_tasks`, one row per `(disk, path, kind)`, kind being
95
+ `optimize` (images, via `ts-images`), `transcode` (video, via `ts-videos`) or
96
+ `tag` (a vision model). They succeed and fail independently, which is why this
97
+ is not a column on `storage_items` - a video whose transcode finished and
98
+ whose tagging failed is a normal state.
99
+ - `dispatchDashboardFileTasks` decides from the CONTENT TYPE what a file needs.
100
+ Most uploads are documents and get nothing. A transcode waits for a video
101
+ profile, because the ladder is derived from the source dimensions.
102
+ - A dispatch failure is RECORDED, not thrown: a queue that is down leaves a
103
+ visible failure rather than an upload that fails or a file that is silently
104
+ never processed.
105
+ - `runTask` owns the queued -> running -> done/failed transitions so the three
106
+ jobs cannot disagree about them. It rethrows after recording, because the row
107
+ and the queue answer different questions - the queue decides whether to retry,
108
+ the row is what somebody looking at the file sees.
109
+ - `POST /files/reprocess` re-runs everything, or the kinds you name.
110
+
111
+ Derivatives are written back to the same disk under `.variants/<path>/`. The
112
+ leading dot keeps them out of the listing, which skips hidden components - a
113
+ folder of thirty derivatives beside every photo makes the browser useless.
114
+
115
+ **There is no ffmpeg.** #2578 asked whether video was in scope given the
116
+ external binary, its licensing and its provisioning; `@stacksjs/video` is built
117
+ on `ts-videos`, which encodes itself, so that question was already answered.
118
+
119
+ Tags go through `taggables` + `taggable_models` with `taggable_type =
120
+ 'storage_items'` - the trait the CMS already uses. Do NOT declare a
121
+ `belongsToMany` to the `Tag` model for this: `taggable_models.tag_id` resolves
122
+ against `taggables`, which is a different table from `tags`.
123
+
64
124
  ### Data Management
65
125
  - `/data/dashboard` - data overview
66
126
  - `/data/users` - user management
@@ -11,7 +11,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
11
11
  ## Key Paths
12
12
  - Core ORM package: `storage/framework/core/orm/src/`
13
13
  - ORM implementation: `storage/framework/orm/`
14
- - Model definitions: `storage/framework/defaults/app/Models/` (100 models)
14
+ - Model definitions: `storage/framework/defaults/app/Models/` (102 models)
15
15
  - Application models: `app/Models/`
16
16
  - Default model templates: `storage/framework/defaults/app/Models/`
17
17
  - ORM type globals: `storage/framework/types/orm-globals.d.ts`
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
8
8
 
9
9
  # Stacks Storage
10
10
 
11
- File system abstraction with a Laravel-style Storage facade, local/S3 adapters, file upload handling, and low-level file utilities.
11
+ File system abstraction with a Laravel-style Storage facade, local/S3/Azure adapters, file upload handling, and low-level file utilities.
12
12
 
13
13
  ## Key Paths
14
14
  - Core package: `storage/framework/core/storage/src/`
@@ -18,6 +18,8 @@ File system abstraction with a Laravel-style Storage facade, local/S3 adapters,
18
18
  - Filesystem config types: `storage/framework/core/storage/src/types/filesystem.ts`
19
19
  - Local adapter: `storage/framework/core/storage/src/adapters/local.ts`
20
20
  - S3 adapter: `storage/framework/core/storage/src/adapters/s3.ts`
21
+ - Azure adapter: `storage/framework/core/storage/src/adapters/azure.ts`
22
+ - Azure request signing: `storage/framework/core/storage/src/azure-signing.ts`
21
23
  - Memory adapter: `storage/framework/core/storage/src/adapters/memory.ts`
22
24
  - Bun adapter: `storage/framework/core/storage/src/adapters/bun.ts`
23
25
  - File utilities: `storage/framework/core/storage/src/files.ts`
@@ -45,7 +47,7 @@ import { createLocalStorage, LocalStorageAdapter } from '@stacksjs/storage'
45
47
  import { createS3Storage, S3StorageAdapter } from '@stacksjs/storage'
46
48
 
47
49
  // Config helpers
48
- import { localDisk, s3Disk, configFromEnv } from '@stacksjs/storage'
50
+ import { localDisk, s3Disk, azureDisk, r2Disk, gcsDisk, filebaseDisk, backblazeDisk, hetznerDisk, configFromEnv } from '@stacksjs/storage'
49
51
 
50
52
  // Types
51
53
  import type { StorageAdapter, FileContents, StatEntry, DirectoryEntry, DirectoryListing } from '@stacksjs/storage'
@@ -233,6 +235,46 @@ const s3 = new S3StorageAdapter(client, { bucket: 'my-bucket', region: 'us-east-
233
235
  - `list()` supports pagination via continuation tokens; `deep: true` uses `listAllObjects()`
234
236
  - `fileExists()` uses `headObject()` and catches 404/NoSuchKey/NotFound errors
235
237
 
238
+ Every S3-COMPATIBLE provider is an `s3` disk with a different endpoint, and has
239
+ a preset that fills it in: `r2Disk(bucket, accountId)`, `gcsDisk(bucket)`,
240
+ `filebaseDisk(bucket)`, `backblazeDisk(bucket, region)`,
241
+ `hetznerDisk(bucket, location)`. R2 additionally needs `url` set, because its
242
+ API host never serves public objects.
243
+
244
+ ### Azure Adapter (`AzureBlobStorageAdapter`)
245
+
246
+ ```typescript
247
+ import { azureDisk, AzureBlobStorageAdapter } from '@stacksjs/storage'
248
+
249
+ const azure = new AzureBlobStorageAdapter({
250
+ account: 'mystorageaccount',
251
+ accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
252
+ container: 'uploads',
253
+ prefix: 'tenant-7',
254
+ })
255
+ ```
256
+
257
+ Its own driver rather than an `s3` disk with an endpoint, because Azure is the
258
+ one provider with no S3-compatible API.
259
+
260
+ - Speaks the blob REST API over `fetch`; no SDK, and no lazy-client dance
261
+ - Shared Key authorization for server-side calls, service SAS for signed URLs,
262
+ both in `azure-signing.ts` and tested there directly
263
+ - `changeVisibility()` THROWS -- Azure has no per-blob ACL. Public access is a
264
+ container property, so `visibility()` reports the container's level, and a
265
+ per-object grant is `signedUrl()`
266
+ - `putStream()` stages blocks (Put Block) and commits one Put Block List, so the
267
+ write is atomic; block ids are zero-padded to a fixed width because Azure
268
+ rejects unequal-length ids
269
+ - `getStream()` is genuinely incremental, unlike the S3 adapter's buffered one
270
+ - `createDirectory()` is a no-op and `deleteDirectory()` deletes by prefix, same
271
+ as S3
272
+ - `endpoint` defaults to `https://<account>.blob.core.windows.net`; point it at
273
+ `http://127.0.0.1:10000/devstoreaccount1` for Azurite
274
+ - A `sasToken` disk can read and write but cannot sign URLs -- minting a SAS
275
+ needs the account key, and re-serving the configured token would hand out its
276
+ full grant
277
+
236
278
  ## File Uploads (UploadedFile)
237
279
 
238
280
  ```typescript
@@ -490,8 +532,8 @@ interface ChecksumOptions {
490
532
  }
491
533
 
492
534
  // Filesystem config types
493
- type FilesystemDriver = 'local' | 's3'
494
- type DiskConfig = LocalDiskConfig | S3DiskConfig
535
+ type FilesystemDriver = 'local' | 's3' | 'azure'
536
+ type DiskConfig = LocalDiskConfig | S3DiskConfig | AzureDiskConfig
495
537
 
496
538
  interface LocalDiskConfig {
497
539
  driver: 'local'
@@ -512,6 +554,18 @@ interface S3DiskConfig {
512
554
  visibility?: 'public' | 'private'
513
555
  }
514
556
 
557
+ interface AzureDiskConfig {
558
+ driver: 'azure'
559
+ account: string
560
+ container: string
561
+ accountKey?: string
562
+ sasToken?: string
563
+ prefix?: string
564
+ url?: string
565
+ endpoint?: string
566
+ visibility?: 'public' | 'private'
567
+ }
568
+
515
569
  interface FilesystemConfig {
516
570
  default: string
517
571
  disks: Record<string, DiskConfig>
@@ -30,7 +30,7 @@ Run `bun --config=storage/framework/defaults/ai/skills/stacks-technical-diagrams
30
30
  1. Inspect the implementation before drawing. Treat `app/` as overrides and `storage/framework/defaults/app/` as fallbacks. Follow registrations in `app/Routes.ts`, route files, actions, jobs, listeners, middleware, models, config, resources, and framework entry points that matter to the requested view.
31
31
  2. Read the relevant Stacks domain skill before mapping an unfamiliar subsystem. Common companions include `stacks-router`, `stacks-actions`, `stacks-models`, `stacks-database`, `stacks-jobs`, `stacks-events`, `stacks-realtime`, and `stacks-cloud`.
32
32
  3. Draw one question per diagram. For a runtime overview, prefer browser or client -> stx/router -> action or service -> model/query -> database, then add only the external systems and trust boundaries needed for that story.
33
- 4. Write output to the user's requested path. When no path is given, use `docs/diagrams/<descriptive-name>.html` and keep the source JSON beside it.
33
+ 4. Write output to the user's requested path. When no path is given, use `docs/public/diagrams/<descriptive-name>/index.html` and keep the source JSON beside it. That path matters: BunPress only renders `docs/**/*.md` and copies `docs/public/**`, so a diagram written anywhere else under `docs/` never reaches the built site. The directory-plus-`index.html` shape is what the deployed docs host serves at the clean URL `/docs/diagrams/<descriptive-name>` - a bare `<name>.html` is redirected to an extensionless path that does not exist. Link to it from a docs page as `/diagrams/<descriptive-name>` (BunPress adds the `/docs` base).
34
34
 
35
35
  If Bun cannot run, fall back to architecture mode: copy `assets/template.html`, hand-place SVG using the design system below, and run the self-review checklist before delivering.
36
36
 
@@ -0,0 +1,25 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { response } from '@stacksjs/router'
4
+ import { DashboardFileError, duplicateDashboardFile } from './file-manager'
5
+
6
+ export default new Action({
7
+ name: 'FileDuplicateAction',
8
+ description: 'Copies a file or directory beside the original on a configured storage disk.',
9
+ method: 'POST',
10
+ async handle(request: RequestInstance) {
11
+ try {
12
+ const duplicated = await duplicateDashboardFile({
13
+ disk: String(request.get('disk', 'public')),
14
+ path: request.get('path'),
15
+ name: request.get('name'),
16
+ })
17
+ return response.json(duplicated)
18
+ }
19
+ catch (error) {
20
+ if (error instanceof DashboardFileError)
21
+ return response.json({ message: error.message, fields: error.fields }, error.status)
22
+ throw error
23
+ }
24
+ },
25
+ })
@@ -0,0 +1,25 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { response } from '@stacksjs/router'
4
+ import { DashboardFileError, setDashboardFileFavorite } from './file-manager'
5
+
6
+ export default new Action({
7
+ name: 'FileFavoriteAction',
8
+ description: 'Stars or unstars a file or directory on a configured storage disk.',
9
+ method: 'PUT',
10
+ async handle(request: RequestInstance) {
11
+ try {
12
+ const result = await setDashboardFileFavorite({
13
+ disk: String(request.get('disk', 'public')),
14
+ path: request.get('path'),
15
+ favorite: request.get('favorite'),
16
+ })
17
+ return response.json(result)
18
+ }
19
+ catch (error) {
20
+ if (error instanceof DashboardFileError)
21
+ return response.json({ message: error.message, fields: error.fields }, error.status)
22
+ throw error
23
+ }
24
+ },
25
+ })
@@ -0,0 +1,25 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { response } from '@stacksjs/router'
4
+ import { DashboardFileError, renameDashboardFile } from './file-manager'
5
+
6
+ export default new Action({
7
+ name: 'FileRenameAction',
8
+ description: 'Renames a file or directory in place on a configured storage disk.',
9
+ method: 'PATCH',
10
+ async handle(request: RequestInstance) {
11
+ try {
12
+ const renamed = await renameDashboardFile({
13
+ disk: String(request.get('disk', 'public')),
14
+ path: request.get('path'),
15
+ name: request.get('name'),
16
+ })
17
+ return response.json(renamed)
18
+ }
19
+ catch (error) {
20
+ if (error instanceof DashboardFileError)
21
+ return response.json({ message: error.message, fields: error.fields }, error.status)
22
+ throw error
23
+ }
24
+ },
25
+ })
@@ -0,0 +1,31 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { response } from '@stacksjs/router'
4
+ import { DashboardFileError, reprocessDashboardFile } from './file-manager'
5
+
6
+ export default new Action({
7
+ name: 'FileReprocessAction',
8
+ description: 'Re-runs the background processing for a file: image variants, video renditions, AI tags.',
9
+ method: 'POST',
10
+ async handle(request: RequestInstance) {
11
+ try {
12
+ const result = await reprocessDashboardFile({
13
+ disk: String(request.get('disk', 'public')),
14
+ path: request.get('path'),
15
+ // Omit to run every kind the file's content type calls for; name kinds
16
+ // to re-run just one, which is what a failed transcode beside a
17
+ // successful tagging wants.
18
+ kinds: request.get('kinds'),
19
+ // A transcode derives its ladder from the source dimensions, so it only
20
+ // runs when the caller supplies them.
21
+ videoProfile: request.get('profile'),
22
+ })
23
+ return response.json(result)
24
+ }
25
+ catch (error) {
26
+ if (error instanceof DashboardFileError)
27
+ return response.json({ message: error.message, fields: error.fields }, error.status)
28
+ throw error
29
+ }
30
+ },
31
+ })
@@ -0,0 +1,27 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { response } from '@stacksjs/router'
4
+ import { DashboardFileError, setDashboardFileTags } from './file-manager'
5
+
6
+ export default new Action({
7
+ name: 'FileTagsAction',
8
+ description: 'Replaces the tags on a file or directory on a configured storage disk.',
9
+ method: 'PUT',
10
+ async handle(request: RequestInstance) {
11
+ try {
12
+ const result = await setDashboardFileTags({
13
+ disk: String(request.get('disk', 'public')),
14
+ path: request.get('path'),
15
+ // The whole set, not a delta: a UI that can add a tag can also remove
16
+ // one, and there is no separate signal for a removal.
17
+ tags: request.get('tags'),
18
+ })
19
+ return response.json(result)
20
+ }
21
+ catch (error) {
22
+ if (error instanceof DashboardFileError)
23
+ return response.json({ message: error.message, fields: error.fields }, error.status)
24
+ throw error
25
+ }
26
+ },
27
+ })
@@ -0,0 +1,25 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { response } from '@stacksjs/router'
4
+ import { DashboardFileError, setDashboardFileVisibility } from './file-manager'
5
+
6
+ export default new Action({
7
+ name: 'FileVisibilityAction',
8
+ description: 'Sets a file or directory public or private on a configured storage disk.',
9
+ method: 'PUT',
10
+ async handle(request: RequestInstance) {
11
+ try {
12
+ const updated = await setDashboardFileVisibility({
13
+ disk: String(request.get('disk', 'public')),
14
+ path: request.get('path'),
15
+ visibility: request.get('visibility'),
16
+ })
17
+ return response.json(updated)
18
+ }
19
+ catch (error) {
20
+ if (error instanceof DashboardFileError)
21
+ return response.json({ message: error.message, fields: error.fields }, error.status)
22
+ throw error
23
+ }
24
+ },
25
+ })