@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.
- package/ai/skills/stacks-analytics/SKILL.md +104 -26
- package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
- package/ai/skills/stacks-dashboard/SKILL.md +60 -0
- package/ai/skills/stacks-orm/SKILL.md +1 -1
- package/ai/skills/stacks-storage/SKILL.md +58 -4
- package/ai/skills/stacks-technical-diagrams/SKILL.md +1 -1
- package/app/Actions/Dashboard/Content/FileDuplicateAction.ts +25 -0
- package/app/Actions/Dashboard/Content/FileFavoriteAction.ts +25 -0
- package/app/Actions/Dashboard/Content/FileRenameAction.ts +25 -0
- package/app/Actions/Dashboard/Content/FileReprocessAction.ts +31 -0
- package/app/Actions/Dashboard/Content/FileTagsAction.ts +27 -0
- package/app/Actions/Dashboard/Content/FileVisibilityAction.ts +25 -0
- package/app/Actions/Dashboard/Content/file-manager.test.ts +227 -9
- package/app/Actions/Dashboard/Content/file-manager.ts +544 -11
- package/app/Actions/Dashboard/Content/file-metadata-store.ts +432 -0
- package/app/Actions/Dashboard/Content/file-metadata.test.ts +357 -0
- package/app/Actions/Dashboard/Content/file-metadata.ts +550 -0
- package/app/Actions/Dashboard/Content/file-pipeline.test.ts +344 -0
- package/app/Jobs/OptimizeStorageImageJob.ts +74 -0
- package/app/Jobs/TagStorageMediaJob.ts +122 -0
- package/app/Jobs/TranscodeStorageVideoJob.ts +88 -0
- package/app/Models/StorageItem.ts +123 -0
- package/app/Models/StorageItemTask.ts +134 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/routes/dashboard-api.ts +13 -0
- package/vcs/github/workflows/buddy-bot.yml +109 -0
- package/vcs/github/workflows/ci.yml +3 -3
- package/vcs/github/workflows/release.yml +1 -1
- package/resources/assets/fonts/Monaco.ttf +0 -0
- 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
|
+
})
|