@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
@@ -3,20 +3,43 @@ import { mkdir, mkdtemp, rm } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
  import { StorageManager } from '@stacksjs/storage'
6
+ import { createMemoryMetadataStore } from './file-metadata'
6
7
  import {
7
8
  createDashboardDirectory,
8
9
  deleteDashboardFile,
10
+ duplicateDashboardFile,
9
11
  getDashboardFileSnapshot,
10
12
  normalizeDashboardFileName,
11
13
  normalizeDashboardFileLimit,
12
14
  normalizeDashboardFilePath,
15
+ renameDashboardFile,
16
+ setDashboardFileVisibility,
13
17
  uploadDashboardFiles,
14
18
  } from './file-manager'
15
19
 
16
20
  let root = ''
17
21
  let manager: StorageManager
22
+ /**
23
+ * The metadata layer, in memory (stacksjs/stacks#2577).
24
+ *
25
+ * These tests build a real disk in a temp directory; the metadata store is the
26
+ * one collaborator that would otherwise pull a database in, so it is injected
27
+ * the same way the storage manager already is.
28
+ */
29
+ let store = createMemoryMetadataStore()
30
+ /**
31
+ * A dispatcher that records instead of queueing (stacksjs/stacks#2578). These
32
+ * tests are about the storage operations, not the pipeline, and a real queue
33
+ * would make them depend on a worker.
34
+ */
35
+ let dispatched: Array<{ job: string, payload: Record<string, unknown> }> = []
36
+ const dispatch = async (job: string, payload: Record<string, unknown>): Promise<void> => {
37
+ dispatched.push({ job, payload })
38
+ }
18
39
 
19
40
  beforeEach(async () => {
41
+ store = createMemoryMetadataStore()
42
+ dispatched = []
20
43
  root = await mkdtemp(join(tmpdir(), 'stacks-dashboard-files-'))
21
44
  await mkdir(join(root, 'public'), { recursive: true })
22
45
  manager = new StorageManager().init({
@@ -44,7 +67,7 @@ describe('dashboard file manager', () => {
44
67
  await disk.write('images/logo.png', new Uint8Array([137, 80, 78, 71]))
45
68
  await disk.write('.hidden/secret.txt', 'not listed')
46
69
 
47
- const snapshot = await getDashboardFileSnapshot({}, manager)
70
+ const snapshot = await getDashboardFileSnapshot({}, manager, store)
48
71
  const documents = snapshot.root.items?.find(item => item.path === 'documents')
49
72
  const readme = documents?.items?.find(item => item.path === 'documents/readme.txt')
50
73
 
@@ -71,7 +94,7 @@ describe('dashboard file manager', () => {
71
94
  await manager.disk('public').write('one.txt', '1')
72
95
  await manager.disk('public').write('two.txt', '2')
73
96
 
74
- const snapshot = await getDashboardFileSnapshot({ maxEntries: 1 }, manager)
97
+ const snapshot = await getDashboardFileSnapshot({ maxEntries: 1 }, manager, store)
75
98
 
76
99
  expect(snapshot.truncated).toBe(true)
77
100
  expect(snapshot.stats.files).toBe(1)
@@ -91,7 +114,7 @@ describe('dashboard file manager', () => {
91
114
  throw new Error('metadata unavailable')
92
115
  }
93
116
 
94
- await expect(getDashboardFileSnapshot({}, manager))
117
+ await expect(getDashboardFileSnapshot({}, manager, store))
95
118
  .rejects
96
119
  .toThrow('Metadata for storage file "unreadable.txt" could not be read')
97
120
  })
@@ -103,7 +126,7 @@ describe('dashboard file manager', () => {
103
126
  throw new Error('URL unavailable')
104
127
  }
105
128
 
106
- const snapshot = await getDashboardFileSnapshot({}, manager)
129
+ const snapshot = await getDashboardFileSnapshot({}, manager, store)
107
130
  expect(snapshot.stats.files).toBe(1)
108
131
  expect(snapshot.warnings).toEqual(['Public URL for "document.txt" could not be resolved.'])
109
132
  })
@@ -113,10 +136,10 @@ describe('dashboard file manager', () => {
113
136
  expect(await manager.disk('public').directoryExists('Product shots')).toBe(true)
114
137
 
115
138
  await manager.disk('public').write('Product shots/photo.jpg', 'photo')
116
- await deleteDashboardFile({ path: 'Product shots/photo.jpg' }, manager)
139
+ await deleteDashboardFile({ path: 'Product shots/photo.jpg' }, manager, store)
117
140
  expect(await manager.disk('public').fileExists('Product shots/photo.jpg')).toBe(false)
118
141
 
119
- await deleteDashboardFile({ path: 'Product shots' }, manager)
142
+ await deleteDashboardFile({ path: 'Product shots' }, manager, store)
120
143
  expect(await manager.disk('public').directoryExists('Product shots')).toBe(false)
121
144
  })
122
145
 
@@ -127,14 +150,14 @@ describe('dashboard file manager', () => {
127
150
  bytes: async () => new TextEncoder().encode(contents),
128
151
  })
129
152
 
130
- const uploaded = await uploadDashboardFiles({ path: 'documents', files: [file('first')] }, manager)
153
+ const uploaded = await uploadDashboardFiles({ path: 'documents', files: [file('first')] }, manager, store, dispatch)
131
154
  expect(uploaded[0]).toMatchObject({
132
155
  path: 'documents/release_notes.txt',
133
156
  url: '/storage/documents/release_notes.txt',
134
157
  size: 5,
135
158
  })
136
159
 
137
- await expect(uploadDashboardFiles({ path: 'documents', files: [file('second')] }, manager))
160
+ await expect(uploadDashboardFiles({ path: 'documents', files: [file('second')] }, manager, store, dispatch))
138
161
  .rejects
139
162
  .toThrow('File already exists: documents/release_notes.txt')
140
163
  expect(await manager.disk('public').readToString('documents/release_notes.txt')).toBe('first')
@@ -150,7 +173,7 @@ describe('dashboard file manager', () => {
150
173
 
151
174
  await expect(uploadDashboardFiles({
152
175
  files: [file('new.txt', 'new'), file('duplicate.txt', 'replacement')],
153
- }, manager)).rejects.toThrow('No files from this upload were kept')
176
+ }, manager, store, dispatch)).rejects.toThrow('No files from this upload were kept')
154
177
 
155
178
  expect(await manager.disk('public').fileExists('new.txt')).toBe(false)
156
179
  expect(await manager.disk('public').readToString('duplicate.txt')).toBe('existing')
@@ -163,3 +186,198 @@ describe('dashboard file manager', () => {
163
186
  expect(() => normalizeDashboardFileName('nested/name')).toThrow('separators')
164
187
  })
165
188
  })
189
+
190
+ describe('renameDashboardFile', () => {
191
+ test('renames a file and leaves nothing behind under the old name', async () => {
192
+ const disk = manager.disk('public')
193
+ await disk.write('documents/readme.txt', 'hello')
194
+
195
+ const result = await renameDashboardFile({ path: 'documents/readme.txt', name: 'guide.txt' }, manager, store)
196
+
197
+ expect(result).toEqual({ from: 'documents/readme.txt', to: 'documents/guide.txt', type: 'file', moved: 1 })
198
+ expect(await disk.fileExists('documents/guide.txt')).toBe(true)
199
+ expect(await disk.fileExists('documents/readme.txt')).toBe(false)
200
+ expect(await disk.readToString('documents/guide.txt')).toBe('hello')
201
+ })
202
+
203
+ test('renames at the top level, where there is no parent to keep', async () => {
204
+ const disk = manager.disk('public')
205
+ await disk.write('notes.txt', 'top level')
206
+
207
+ expect(await renameDashboardFile({ path: 'notes.txt', name: 'todo.txt' }, manager, store))
208
+ .toEqual({ from: 'notes.txt', to: 'todo.txt', type: 'file', moved: 1 })
209
+ expect(await disk.readToString('todo.txt')).toBe('top level')
210
+ })
211
+
212
+ /**
213
+ * The parent is kept deliberately. Renaming is changing the last segment;
214
+ * moving something elsewhere is a different gesture and would want its own
215
+ * endpoint, so a name is a name and never a path.
216
+ */
217
+ test('refuses a name that is really a path', async () => {
218
+ await manager.disk('public').write('a/b.txt', 'x')
219
+
220
+ await expect(renameDashboardFile({ path: 'a/b.txt', name: '../escaped.txt' }, manager, store))
221
+ .rejects.toMatchObject({ status: 422 })
222
+ await expect(renameDashboardFile({ path: 'a/b.txt', name: 'nested/deep.txt' }, manager, store))
223
+ .rejects.toMatchObject({ status: 422 })
224
+ })
225
+
226
+ test('renames a directory by moving what is inside it, at any depth', async () => {
227
+ const disk = manager.disk('public')
228
+ await disk.write('images/logo.png', 'a')
229
+ await disk.write('images/icons/favicon.png', 'b')
230
+
231
+ const result = await renameDashboardFile({ path: 'images', name: 'media' }, manager, store)
232
+
233
+ expect(result).toEqual({ from: 'images', to: 'media', type: 'directory', moved: 2 })
234
+ expect(await disk.readToString('media/logo.png')).toBe('a')
235
+ expect(await disk.readToString('media/icons/favicon.png')).toBe('b')
236
+ expect(await disk.fileExists('images/logo.png')).toBe(false)
237
+ expect(await disk.directoryExists('images')).toBe(false)
238
+ })
239
+
240
+ test('refuses to overwrite an existing name', async () => {
241
+ const disk = manager.disk('public')
242
+ await disk.write('documents/readme.txt', 'keep me')
243
+ await disk.write('documents/guide.txt', 'me too')
244
+
245
+ await expect(renameDashboardFile({ path: 'documents/readme.txt', name: 'guide.txt' }, manager, store))
246
+ .rejects.toMatchObject({ status: 409 })
247
+ // Neither side moved: a refused rename is not a partial one.
248
+ expect(await disk.readToString('documents/readme.txt')).toBe('keep me')
249
+ expect(await disk.readToString('documents/guide.txt')).toBe('me too')
250
+ })
251
+
252
+ test('says so rather than silently doing nothing when the name is unchanged', async () => {
253
+ await manager.disk('public').write('a.txt', 'x')
254
+
255
+ await expect(renameDashboardFile({ path: 'a.txt', name: 'a.txt' }, manager, store))
256
+ .rejects.toMatchObject({ status: 422 })
257
+ })
258
+
259
+ test('is a 404 when the item does not exist', async () => {
260
+ await expect(renameDashboardFile({ path: 'nope.txt', name: 'yes.txt' }, manager, store))
261
+ .rejects.toMatchObject({ status: 404 })
262
+ })
263
+ })
264
+
265
+ describe('setDashboardFileVisibility', () => {
266
+ test('flips a single file and reads back as the adapter sees it', async () => {
267
+ const disk = manager.disk('public')
268
+ await disk.write('documents/readme.txt', 'hello')
269
+
270
+ expect(await setDashboardFileVisibility({ path: 'documents/readme.txt', visibility: 'private' }, manager))
271
+ .toEqual({ path: 'documents/readme.txt', visibility: 'private', type: 'file', changed: 1 })
272
+ expect(await disk.visibility('documents/readme.txt')).toBe('private')
273
+
274
+ await setDashboardFileVisibility({ path: 'documents/readme.txt', visibility: 'public' }, manager)
275
+ expect(await disk.visibility('documents/readme.txt')).toBe('public')
276
+ })
277
+
278
+ /**
279
+ * A folder is applied file by file, not to the folder. Object storage has no
280
+ * directories to carry an ACL, and on a local disk a directory's mode gates
281
+ * listing rather than reading - the files are what gate access on both.
282
+ */
283
+ test('applies to every file beneath a folder, at any depth', async () => {
284
+ const disk = manager.disk('public')
285
+ await disk.write('images/logo.png', 'a')
286
+ await disk.write('images/icons/favicon.png', 'b')
287
+
288
+ expect(await setDashboardFileVisibility({ path: 'images', visibility: 'private' }, manager))
289
+ .toEqual({ path: 'images', visibility: 'private', type: 'directory', changed: 2 })
290
+ expect(await disk.visibility('images/logo.png')).toBe('private')
291
+ expect(await disk.visibility('images/icons/favicon.png')).toBe('private')
292
+ })
293
+
294
+ test('leaves files outside the folder alone', async () => {
295
+ const disk = manager.disk('public')
296
+ await disk.write('images/logo.png', 'a')
297
+ await disk.write('documents/readme.txt', 'b')
298
+
299
+ await setDashboardFileVisibility({ path: 'images', visibility: 'private' }, manager)
300
+
301
+ expect(await disk.visibility('documents/readme.txt')).toBe('public')
302
+ })
303
+
304
+ test('rejects anything that is not public or private', async () => {
305
+ await manager.disk('public').write('a.txt', 'x')
306
+
307
+ for (const visibility of ['world-readable', '', 'PUBLIC', true, undefined]) {
308
+ await expect(setDashboardFileVisibility({ path: 'a.txt', visibility }, manager))
309
+ .rejects.toMatchObject({ status: 422 })
310
+ }
311
+ })
312
+
313
+ test('is a 404 when the item does not exist', async () => {
314
+ await expect(setDashboardFileVisibility({ path: 'nope.txt', visibility: 'private' }, manager))
315
+ .rejects.toMatchObject({ status: 404 })
316
+ })
317
+ })
318
+
319
+ describe('duplicateDashboardFile', () => {
320
+ test('names the copy before the extension, not after it', async () => {
321
+ const disk = manager.disk('public')
322
+ await disk.write('documents/readme.txt', 'hello')
323
+
324
+ const result = await duplicateDashboardFile({ path: 'documents/readme.txt' }, manager, store)
325
+
326
+ // `readme.txt copy` is a file whose type the OS, the browser and this
327
+ // dashboard's own type grouping would all read as unknown.
328
+ expect(result).toEqual({ from: 'documents/readme.txt', to: 'documents/readme copy.txt', type: 'file', copied: 1 })
329
+ expect(await disk.readToString('documents/readme copy.txt')).toBe('hello')
330
+ expect(await disk.readToString('documents/readme.txt')).toBe('hello')
331
+ })
332
+
333
+ test('counts up rather than colliding when a copy already exists', async () => {
334
+ const disk = manager.disk('public')
335
+ await disk.write('a.txt', 'x')
336
+
337
+ expect((await duplicateDashboardFile({ path: 'a.txt' }, manager, store)).to).toBe('a copy.txt')
338
+ expect((await duplicateDashboardFile({ path: 'a.txt' }, manager, store)).to).toBe('a copy 2.txt')
339
+ expect((await duplicateDashboardFile({ path: 'a.txt' }, manager, store)).to).toBe('a copy 3.txt')
340
+ })
341
+
342
+ test('takes an explicit name when given one', async () => {
343
+ await manager.disk('public').write('a.txt', 'x')
344
+
345
+ expect((await duplicateDashboardFile({ path: 'a.txt', name: 'b.txt' }, manager, store)).to).toBe('b.txt')
346
+ await expect(duplicateDashboardFile({ path: 'a.txt', name: 'b.txt' }, manager, store))
347
+ .rejects.toMatchObject({ status: 409 })
348
+ await expect(duplicateDashboardFile({ path: 'a.txt', name: '../escaped.txt' }, manager, store))
349
+ .rejects.toMatchObject({ status: 422 })
350
+ })
351
+
352
+ test('copies a folder and everything under it, leaving the original whole', async () => {
353
+ const disk = manager.disk('public')
354
+ await disk.write('images/logo.png', 'a')
355
+ await disk.write('images/icons/favicon.png', 'b')
356
+
357
+ const result = await duplicateDashboardFile({ path: 'images' }, manager, store)
358
+
359
+ expect(result).toEqual({ from: 'images', to: 'images copy', type: 'directory', copied: 2 })
360
+ expect(await disk.readToString('images copy/logo.png')).toBe('a')
361
+ expect(await disk.readToString('images copy/icons/favicon.png')).toBe('b')
362
+ expect(await disk.readToString('images/logo.png')).toBe('a')
363
+ })
364
+
365
+ /**
366
+ * The copy lands beside the original under the same parent, so a deep listing
367
+ * taken while copying could see the files it is itself creating. Paths are
368
+ * collected first; this is what says so.
369
+ */
370
+ test('does not copy the copy it is making', async () => {
371
+ const disk = manager.disk('public')
372
+ await disk.write('media/one.txt', '1')
373
+ await disk.write('media/two.txt', '2')
374
+
375
+ expect((await duplicateDashboardFile({ path: 'media' }, manager, store)).copied).toBe(2)
376
+ expect(await disk.fileExists('media copy/media copy/one.txt')).toBe(false)
377
+ })
378
+
379
+ test('is a 404 when the item does not exist', async () => {
380
+ await expect(duplicateDashboardFile({ path: 'nope.txt' }, manager, store))
381
+ .rejects.toMatchObject({ status: 404 })
382
+ })
383
+ })