@axium/storage 0.7.10 → 0.8.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.
@@ -0,0 +1,163 @@
1
+ import { Permission } from '@axium/core';
2
+ import { audit } from '@axium/server/audit';
3
+ import { checkAuthForItem, getSessionAndUser } from '@axium/server/auth';
4
+ import { config } from '@axium/server/config';
5
+ import { database } from '@axium/server/database';
6
+ import { error, getToken, withError } from '@axium/server/requests';
7
+ import { addRoute } from '@axium/server/routes';
8
+ import { createHash } from 'node:crypto';
9
+ import { linkSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path/posix';
11
+ import * as z from 'zod';
12
+ import '../polyfills.js';
13
+ import { defaultCASMime, getLimits } from './config.js';
14
+ import { currentUsage, parseItem } from './db.js';
15
+ addRoute({
16
+ path: '/raw/storage',
17
+ async PUT(request) {
18
+ if (!config.storage.enabled)
19
+ error(503, 'User storage is disabled');
20
+ const token = getToken(request);
21
+ if (!token)
22
+ error(401, 'Missing session token');
23
+ const { userId } = await getSessionAndUser(token).catch(withError('Invalid session token', 401));
24
+ const [usage, limits] = await Promise.all([currentUsage(userId), getLimits(userId)]).catch(withError('Could not fetch usage and/or limits'));
25
+ const name = request.headers.get('x-name');
26
+ if (!name)
27
+ error(400, 'Missing name header');
28
+ if (name.length > 255)
29
+ error(400, 'Name is too long');
30
+ const maybeParentId = request.headers.get('x-parent');
31
+ const parentId = maybeParentId
32
+ ? await z
33
+ .uuid()
34
+ .parseAsync(maybeParentId)
35
+ .catch(() => error(400, 'Invalid parent ID'))
36
+ : null;
37
+ if (parentId)
38
+ await checkAuthForItem(request, 'storage', parentId, Permission.Edit);
39
+ const size = Number(request.headers.get('content-length'));
40
+ if (Number.isNaN(size))
41
+ error(411, 'Missing or invalid content length header');
42
+ if (limits.user_items && usage.items >= limits.user_items)
43
+ error(409, 'Too many items');
44
+ if (limits.user_size && (usage.bytes + size) / 1_000_000 >= limits.user_size)
45
+ error(413, 'Not enough space');
46
+ if (limits.item_size && size > limits.item_size * 1_000_000)
47
+ error(413, 'File size exceeds maximum size');
48
+ const content = await request.bytes();
49
+ if (content.byteLength > size) {
50
+ await audit('storage_size_mismatch', userId, { item: null });
51
+ error(400, 'Content length does not match size header');
52
+ }
53
+ const type = request.headers.get('content-type') || 'application/octet-stream';
54
+ const isDirectory = type == 'inode/directory';
55
+ if (isDirectory && size > 0)
56
+ error(400, 'Directories can not have content');
57
+ const useCAS = config.storage.cas.enabled &&
58
+ !isDirectory &&
59
+ (defaultCASMime.some(pattern => pattern.test(type)) || config.storage.cas.include.some(mime => type.match(mime)));
60
+ const hash = isDirectory ? null : createHash('BLAKE2b512').update(content).digest();
61
+ const tx = await database.startTransaction().execute();
62
+ try {
63
+ const item = parseItem(await tx
64
+ .insertInto('storage')
65
+ .values({ userId, hash, name, size, type, immutable: useCAS, parentId })
66
+ .returningAll()
67
+ .executeTakeFirstOrThrow());
68
+ const path = join(config.storage.data, item.id);
69
+ if (!useCAS) {
70
+ if (!isDirectory)
71
+ writeFileSync(path, content);
72
+ await tx.commit().execute();
73
+ return item;
74
+ }
75
+ const existing = await tx
76
+ .selectFrom('storage')
77
+ .select('id')
78
+ .where('hash', '=', hash)
79
+ .where('id', '!=', item.id)
80
+ .limit(1)
81
+ .executeTakeFirst();
82
+ if (!existing) {
83
+ if (!isDirectory)
84
+ writeFileSync(path, content);
85
+ await tx.commit().execute();
86
+ return item;
87
+ }
88
+ linkSync(join(config.storage.data, existing.id), path);
89
+ await tx.commit().execute();
90
+ return item;
91
+ }
92
+ catch (error) {
93
+ await tx.rollback().execute();
94
+ throw withError('Could not create item', 500)(error);
95
+ }
96
+ },
97
+ });
98
+ addRoute({
99
+ path: '/raw/storage/:id',
100
+ params: { id: z.uuid() },
101
+ async GET(request, params) {
102
+ if (!config.storage.enabled)
103
+ error(503, 'User storage is disabled');
104
+ const itemId = params.id;
105
+ const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
106
+ if (item.trashedAt)
107
+ error(410, 'Trashed items can not be downloaded');
108
+ const content = new Uint8Array(readFileSync(join(config.storage.data, item.id)));
109
+ return new Response(content, {
110
+ headers: {
111
+ 'Content-Type': item.type,
112
+ 'Content-Disposition': `attachment; filename="${item.name}"`,
113
+ },
114
+ });
115
+ },
116
+ async POST(request, params) {
117
+ if (!config.storage.enabled)
118
+ error(503, 'User storage is disabled');
119
+ const itemId = params.id;
120
+ const { item, session } = await checkAuthForItem(request, 'storage', itemId, Permission.Edit);
121
+ if (item.immutable)
122
+ error(405, 'Item is immutable');
123
+ if (item.type == 'inode/directory')
124
+ error(409, 'Directories do not have content');
125
+ if (item.trashedAt)
126
+ error(410, 'Trashed items can not be changed');
127
+ const type = request.headers.get('content-type') || 'application/octet-stream';
128
+ if (type != item.type) {
129
+ await audit('storage_type_mismatch', session?.userId, { item: item.id });
130
+ error(400, 'Content type does not match existing item type');
131
+ }
132
+ const size = Number(request.headers.get('content-length'));
133
+ if (Number.isNaN(size))
134
+ error(411, 'Missing or invalid content length header');
135
+ const [usage, limits] = await Promise.all([currentUsage(item.userId), getLimits(item.userId)]).catch(withError('Could not fetch usage and/or limits'));
136
+ if (limits.user_size && (usage.bytes + size - item.size) / 1_000_000 >= limits.user_size)
137
+ error(413, 'Not enough space');
138
+ if (limits.item_size && size > limits.item_size * 1_000_000)
139
+ error(413, 'File size exceeds maximum size');
140
+ const content = await request.bytes();
141
+ if (content.byteLength > size) {
142
+ await audit('storage_size_mismatch', session?.userId, { item: item.id });
143
+ error(400, 'Actual content length does not match header');
144
+ }
145
+ const hash = createHash('BLAKE2b512').update(content).digest();
146
+ const tx = await database.startTransaction().execute();
147
+ try {
148
+ const result = await tx
149
+ .updateTable('storage')
150
+ .where('id', '=', itemId)
151
+ .set({ size, modifiedAt: new Date(), hash })
152
+ .returningAll()
153
+ .executeTakeFirstOrThrow();
154
+ writeFileSync(join(config.storage.data, result.id), content);
155
+ await tx.commit().execute();
156
+ return parseItem(result);
157
+ }
158
+ catch (error) {
159
+ await tx.rollback().execute();
160
+ throw withError('Could not update item', 500)(error);
161
+ }
162
+ },
163
+ });
package/lib/List.svelte CHANGED
@@ -53,7 +53,7 @@
53
53
  onclick={async () => {
54
54
  if (item.type != 'inode/directory') {
55
55
  // @todo get preview
56
- } else if (appMode) goto('/files/' + item.id);
56
+ } else if (appMode) location.href = '/files/' + item.id;
57
57
  else items = await getDirectoryMetadata(item.id);
58
58
  }}
59
59
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.7.10",
3
+ "version": "0.8.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "User file storage for Axium",
6
6
  "funding": {
@@ -21,6 +21,7 @@
21
21
  "types": "dist/index.d.ts",
22
22
  "exports": {
23
23
  ".": "./dist/index.js",
24
+ "./client": "./dist/client/index.js",
24
25
  "./*": "./dist/*.js",
25
26
  "./sidebar": "./lib/sidebar.svelte.js",
26
27
  "./components": "./lib/index.js",
@@ -37,20 +38,26 @@
37
38
  "build": "tsc"
38
39
  },
39
40
  "peerDependencies": {
40
- "@axium/client": ">=0.5.0",
41
- "@axium/core": ">=0.8.0",
42
- "@axium/server": ">=0.25.0",
41
+ "@axium/client": ">=0.6.0",
42
+ "@axium/core": ">=0.9.0",
43
+ "@axium/server": ">=0.26.0",
43
44
  "@sveltejs/kit": "^2.27.3",
44
45
  "utilium": "^2.3.8"
45
46
  },
46
47
  "dependencies": {
47
- "blakejs": "^1.2.1",
48
48
  "zod": "^4.0.5"
49
49
  },
50
50
  "axium": {
51
- "http_handler": "./build/handler.js",
52
- "hooks": "./dist/hooks.js",
53
- "routes": "./routes",
51
+ "server": {
52
+ "http_handler": "./build/handler.js",
53
+ "hooks": "./dist/server/hooks.js",
54
+ "routes": "./routes",
55
+ "cli": "./dist/server/cli.js"
56
+ },
57
+ "client": {
58
+ "cli": "./dist/client/cli.js",
59
+ "hooks": "./dist/client/hooks.js"
60
+ },
54
61
  "apps": [
55
62
  {
56
63
  "id": "files",
@@ -25,11 +25,15 @@
25
25
  <Icon i="trash-can-undo" /> Restore
26
26
  </button>
27
27
  {:else if item.type == 'inode/directory'}
28
- <a href="/files{item.parentId ? '/' + item.parentId : ''}">
29
- <button class="icon-text">
30
- <Icon i="folder-arrow-up" /> Back
31
- </button>
32
- </a>
28
+ <button
29
+ class="icon-text"
30
+ onclick={e => {
31
+ e.preventDefault();
32
+ location.href = '/files' + (item.parentId ? '/' + item.parentId : '');
33
+ }}
34
+ >
35
+ <Icon i="folder-arrow-up" /> Back
36
+ </button>
33
37
  <StorageList appMode bind:items />
34
38
  <StorageAdd parentId={item.id} onadd={item => items.push(item)} />
35
39
  {:else}
package/dist/server.js DELETED
@@ -1,412 +0,0 @@
1
- import { Permission, Severity } from '@axium/core';
2
- import { addEvent, audit } from '@axium/server/audit';
3
- import { checkAuthForItem, checkAuthForUser, getSessionAndUser } from '@axium/server/auth';
4
- import { addConfigDefaults, config } from '@axium/server/config';
5
- import { database, expectedTypes } from '@axium/server/database';
6
- import { dirs } from '@axium/server/io';
7
- import { error, getToken, parseBody, withError } from '@axium/server/requests';
8
- import { addRoute } from '@axium/server/routes';
9
- import { createHash } from 'node:crypto';
10
- import { linkSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
11
- import { join } from 'node:path/posix';
12
- import * as z from 'zod';
13
- import { StorageItemUpdate } from './common.js';
14
- import './polyfills.js';
15
- expectedTypes.storage = {
16
- createdAt: { type: 'timestamptz', required: true, hasDefault: true },
17
- hash: { type: 'bytea' },
18
- id: { type: 'uuid', required: true, hasDefault: true },
19
- immutable: { type: 'bool', required: true },
20
- modifiedAt: { type: 'timestamptz', required: true, hasDefault: true },
21
- name: { type: 'text' },
22
- parentId: { type: 'uuid' },
23
- size: { type: 'int4', required: true },
24
- trashedAt: { type: 'timestamptz' },
25
- type: { type: 'text', required: true },
26
- userId: { type: 'uuid', required: true },
27
- publicPermission: { type: 'int4', required: true, hasDefault: true },
28
- metadata: { type: 'jsonb', required: true, hasDefault: true },
29
- };
30
- const defaultCASMime = [/video\/.*/, /audio\/.*/];
31
- addConfigDefaults({
32
- storage: {
33
- enabled: true,
34
- app_enabled: true,
35
- data: dirs.at(-1) + '/storage',
36
- trash_duration: 30,
37
- limits: {
38
- user_size: 1000,
39
- item_size: 100,
40
- user_items: 10_000,
41
- },
42
- cas: {
43
- enabled: true,
44
- include: [],
45
- exclude: [],
46
- },
47
- },
48
- });
49
- addEvent({ source: '@axium/storage', name: 'storage_type_mismatch', severity: Severity.Warning, tags: ['mimetype'] });
50
- addEvent({ source: '@axium/storage', name: 'storage_size_mismatch', severity: Severity.Warning, tags: [] });
51
- export function parseItem(item) {
52
- return {
53
- ...item,
54
- dataURL: `/raw/storage/${item.id}`,
55
- hash: item.hash?.toHex?.(),
56
- };
57
- }
58
- /**
59
- * Returns the current usage of the storage for a user in bytes.
60
- */
61
- export async function currentUsage(userId) {
62
- const result = await database
63
- .selectFrom('storage')
64
- .where('userId', '=', userId)
65
- .select(eb => eb.fn.countAll().as('items'))
66
- .select(eb => eb.fn.sum('size').as('bytes'))
67
- .executeTakeFirstOrThrow();
68
- result.bytes = Number(result.bytes || 0);
69
- result.items = Number(result.items);
70
- return result;
71
- }
72
- export async function get(itemId) {
73
- const result = await database
74
- .selectFrom('storage')
75
- .where('id', '=', itemId)
76
- .selectAll()
77
- .$narrowType()
78
- .executeTakeFirstOrThrow();
79
- return parseItem(result);
80
- }
81
- let _getLimits = null;
82
- /**
83
- * Define the handler to get limits for a user externally.
84
- */
85
- export function useLimits(handler) {
86
- _getLimits = handler;
87
- }
88
- export async function getLimits(userId) {
89
- try {
90
- return await _getLimits(userId);
91
- }
92
- catch {
93
- return config.storage.limits;
94
- }
95
- }
96
- export async function* getRecursive(id) {
97
- const items = await database.selectFrom('storage').where('parentId', '=', id).selectAll().execute();
98
- for (const item of items) {
99
- if (item.type != 'inode/directory')
100
- yield item.id;
101
- else
102
- yield* getRecursive(item.id);
103
- }
104
- }
105
- export async function deleteRecursive(itemId, deleteSelf) {
106
- const toDelete = await Array.fromAsync(getRecursive(itemId)).catch(withError('Could not get items to delete'));
107
- if (deleteSelf)
108
- toDelete.push(itemId);
109
- await database.deleteFrom('storage').where('id', '=', itemId).returningAll().execute().catch(withError('Could not delete item'));
110
- for (const id of toDelete)
111
- unlinkSync(join(config.storage.data, id));
112
- }
113
- addRoute({
114
- path: '/api/storage/item/:id',
115
- params: { id: z.uuid() },
116
- async GET(request, params) {
117
- if (!config.storage.enabled)
118
- error(503, 'User storage is disabled');
119
- const itemId = params.id;
120
- const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
121
- return parseItem(item);
122
- },
123
- async PATCH(request, params) {
124
- if (!config.storage.enabled)
125
- error(503, 'User storage is disabled');
126
- const itemId = params.id;
127
- const body = await parseBody(request, StorageItemUpdate);
128
- await checkAuthForItem(request, 'storage', itemId, Permission.Manage);
129
- const values = {};
130
- if ('publicPermission' in body)
131
- values.publicPermission = body.publicPermission;
132
- if ('trash' in body)
133
- values.trashedAt = body.trash ? new Date() : null;
134
- if ('owner' in body)
135
- values.userId = body.owner;
136
- if ('name' in body)
137
- values.name = body.name;
138
- if (!Object.keys(values).length)
139
- error(400, 'No valid fields to update');
140
- return parseItem(await database
141
- .updateTable('storage')
142
- .where('id', '=', itemId)
143
- .set(values)
144
- .returningAll()
145
- .executeTakeFirstOrThrow()
146
- .catch(withError('Could not update item')));
147
- },
148
- async DELETE(request, params) {
149
- if (!config.storage.enabled)
150
- error(503, 'User storage is disabled');
151
- const itemId = params.id;
152
- const auth = await checkAuthForItem(request, 'storage', itemId, Permission.Manage);
153
- const item = parseItem(auth.item);
154
- await deleteRecursive(itemId, item.type != 'inode/directory');
155
- return item;
156
- },
157
- });
158
- addRoute({
159
- path: '/api/storage/directory/:id',
160
- params: { id: z.uuid() },
161
- async GET(request, params) {
162
- if (!config.storage.enabled)
163
- error(503, 'User storage is disabled');
164
- const itemId = params.id;
165
- const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
166
- if (item.type != 'inode/directory')
167
- error(409, 'Item is not a directory');
168
- const items = await database
169
- .selectFrom('storage')
170
- .where('parentId', '=', itemId)
171
- .where('trashedAt', 'is', null)
172
- .selectAll()
173
- .execute();
174
- return items.map(parseItem);
175
- },
176
- });
177
- addRoute({
178
- path: '/raw/storage',
179
- async PUT(request) {
180
- if (!config.storage.enabled)
181
- error(503, 'User storage is disabled');
182
- const token = getToken(request);
183
- if (!token)
184
- error(401, 'Missing session token');
185
- const { userId } = await getSessionAndUser(token).catch(withError('Invalid session token', 401));
186
- const [usage, limits] = await Promise.all([currentUsage(userId), getLimits(userId)]).catch(withError('Could not fetch usage and/or limits'));
187
- const name = request.headers.get('x-name');
188
- if (!name)
189
- error(400, 'Missing name header');
190
- if (name.length > 255)
191
- error(400, 'Name is too long');
192
- const maybeParentId = request.headers.get('x-parent');
193
- const parentId = maybeParentId
194
- ? await z
195
- .uuid()
196
- .parseAsync(maybeParentId)
197
- .catch(() => error(400, 'Invalid parent ID'))
198
- : null;
199
- if (parentId)
200
- await checkAuthForItem(request, 'storage', parentId, Permission.Edit);
201
- const size = Number(request.headers.get('content-length'));
202
- if (Number.isNaN(size))
203
- error(411, 'Missing or invalid content length header');
204
- if (limits.user_items && usage.items >= limits.user_items)
205
- error(409, 'Too many items');
206
- if (limits.user_size && (usage.bytes + size) / 1_000_000 >= limits.user_size)
207
- error(413, 'Not enough space');
208
- if (limits.item_size && size > limits.item_size * 1_000_000)
209
- error(413, 'File size exceeds maximum size');
210
- const content = await request.bytes();
211
- if (content.byteLength > size) {
212
- await audit('storage_size_mismatch', userId, { item: null });
213
- error(400, 'Content length does not match size header');
214
- }
215
- const type = request.headers.get('content-type') || 'application/octet-stream';
216
- const isDirectory = type == 'inode/directory';
217
- if (isDirectory && size > 0)
218
- error(400, 'Directories can not have content');
219
- const useCAS = config.storage.cas.enabled &&
220
- !isDirectory &&
221
- (defaultCASMime.some(pattern => pattern.test(type)) || config.storage.cas.include.some(mime => type.match(mime)));
222
- const hash = isDirectory ? null : createHash('BLAKE2b512').update(content).digest();
223
- const tx = await database.startTransaction().execute();
224
- try {
225
- const item = parseItem(await tx
226
- .insertInto('storage')
227
- .values({ userId, hash, name, size, type, immutable: useCAS, parentId })
228
- .returningAll()
229
- .executeTakeFirstOrThrow());
230
- const path = join(config.storage.data, item.id);
231
- if (!useCAS) {
232
- if (!isDirectory)
233
- writeFileSync(path, content);
234
- await tx.commit().execute();
235
- return item;
236
- }
237
- const existing = await tx
238
- .selectFrom('storage')
239
- .select('id')
240
- .where('hash', '=', hash)
241
- .where('id', '!=', item.id)
242
- .limit(1)
243
- .executeTakeFirst();
244
- if (!existing) {
245
- if (!isDirectory)
246
- writeFileSync(path, content);
247
- await tx.commit().execute();
248
- return item;
249
- }
250
- linkSync(join(config.storage.data, existing.id), path);
251
- await tx.commit().execute();
252
- return item;
253
- }
254
- catch (error) {
255
- await tx.rollback().execute();
256
- throw withError('Could not create item', 500)(error);
257
- }
258
- },
259
- });
260
- addRoute({
261
- path: '/raw/storage/:id',
262
- params: { id: z.uuid() },
263
- async GET(request, params) {
264
- if (!config.storage.enabled)
265
- error(503, 'User storage is disabled');
266
- const itemId = params.id;
267
- const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
268
- if (item.trashedAt)
269
- error(410, 'Trashed items can not be downloaded');
270
- const content = new Uint8Array(readFileSync(join(config.storage.data, item.id)));
271
- return new Response(content, {
272
- headers: {
273
- 'Content-Type': item.type,
274
- 'Content-Disposition': `attachment; filename="${item.name}"`,
275
- },
276
- });
277
- },
278
- async POST(request, params) {
279
- if (!config.storage.enabled)
280
- error(503, 'User storage is disabled');
281
- const itemId = params.id;
282
- const { item, session } = await checkAuthForItem(request, 'storage', itemId, Permission.Edit);
283
- if (item.immutable)
284
- error(405, 'Item is immutable');
285
- if (item.type == 'inode/directory')
286
- error(409, 'Directories do not have content');
287
- if (item.trashedAt)
288
- error(410, 'Trashed items can not be changed');
289
- const type = request.headers.get('content-type') || 'application/octet-stream';
290
- if (type != item.type) {
291
- await audit('storage_type_mismatch', session?.userId, { item: item.id });
292
- error(400, 'Content type does not match existing item type');
293
- }
294
- const size = Number(request.headers.get('content-length'));
295
- if (Number.isNaN(size))
296
- error(411, 'Missing or invalid content length header');
297
- const [usage, limits] = await Promise.all([currentUsage(item.userId), getLimits(item.userId)]).catch(withError('Could not fetch usage and/or limits'));
298
- if (limits.user_size && (usage.bytes + size - item.size) / 1_000_000 >= limits.user_size)
299
- error(413, 'Not enough space');
300
- if (limits.item_size && size > limits.item_size * 1_000_000)
301
- error(413, 'File size exceeds maximum size');
302
- const content = await request.bytes();
303
- if (content.byteLength > size) {
304
- await audit('storage_size_mismatch', session?.userId, { item: item.id });
305
- error(400, 'Actual content length does not match header');
306
- }
307
- const hash = createHash('BLAKE2b512').update(content).digest();
308
- const tx = await database.startTransaction().execute();
309
- try {
310
- const result = await tx
311
- .updateTable('storage')
312
- .where('id', '=', itemId)
313
- .set({ size, modifiedAt: new Date(), hash })
314
- .returningAll()
315
- .executeTakeFirstOrThrow();
316
- writeFileSync(join(config.storage.data, result.id), content);
317
- await tx.commit().execute();
318
- return parseItem(result);
319
- }
320
- catch (error) {
321
- await tx.rollback().execute();
322
- throw withError('Could not update item', 500)(error);
323
- }
324
- },
325
- });
326
- addRoute({
327
- path: '/api/users/:id/storage',
328
- params: { id: z.uuid() },
329
- async OPTIONS(request, params) {
330
- if (!config.storage.enabled)
331
- error(503, 'User storage is disabled');
332
- const userId = params.id;
333
- await checkAuthForUser(request, userId);
334
- const [usage, limits] = await Promise.all([currentUsage(userId), getLimits(userId)]).catch(withError('Could not fetch data'));
335
- return { usage, limits };
336
- },
337
- async GET(request, params) {
338
- if (!config.storage.enabled)
339
- error(503, 'User storage is disabled');
340
- const userId = params.id;
341
- await checkAuthForUser(request, userId);
342
- const [items, usage, limits] = await Promise.all([
343
- database.selectFrom('storage').where('userId', '=', userId).where('trashedAt', 'is', null).selectAll().execute(),
344
- currentUsage(userId),
345
- getLimits(userId),
346
- ]).catch(withError('Could not fetch data'));
347
- return { usage, limits, items: items.map(parseItem) };
348
- },
349
- });
350
- addRoute({
351
- path: '/api/users/:id/storage/root',
352
- params: { id: z.uuid() },
353
- async GET(request, params) {
354
- if (!config.storage.enabled)
355
- error(503, 'User storage is disabled');
356
- const userId = params.id;
357
- await checkAuthForUser(request, userId);
358
- const items = await database
359
- .selectFrom('storage')
360
- .where('userId', '=', userId)
361
- .where('trashedAt', 'is', null)
362
- .where('parentId', 'is', null)
363
- .selectAll()
364
- .execute()
365
- .catch(withError('Could not get storage items'));
366
- return items.map(parseItem);
367
- },
368
- });
369
- function existsInACL(column, userId) {
370
- return (eb) => eb.exists(eb
371
- .selectFrom('acl.storage')
372
- .whereRef('itemId', '=', `item.${column}`)
373
- .where('userId', '=', userId)
374
- .where('permission', '!=', Permission.None));
375
- }
376
- addRoute({
377
- path: '/api/users/:id/storage/shared',
378
- params: { id: z.uuid() },
379
- async GET(request, params) {
380
- if (!config.storage.enabled)
381
- error(503, 'User storage is disabled');
382
- const userId = params.id;
383
- await checkAuthForUser(request, userId);
384
- const items = await database
385
- .selectFrom('storage as item')
386
- .selectAll('item')
387
- .where('trashedAt', 'is', null)
388
- .where(existsInACL('id', userId))
389
- .where(eb => eb.not(existsInACL('parentId', userId)))
390
- .execute()
391
- .catch(withError('Could not get storage items'));
392
- return items.map(parseItem);
393
- },
394
- });
395
- addRoute({
396
- path: '/api/users/:id/storage/trash',
397
- params: { id: z.uuid() },
398
- async GET(request, params) {
399
- if (!config.storage.enabled)
400
- error(503, 'User storage is disabled');
401
- const userId = params.id;
402
- await checkAuthForUser(request, userId);
403
- const items = await database
404
- .selectFrom('storage')
405
- .where('userId', '=', userId)
406
- .where('trashedAt', 'is not', null)
407
- .selectAll()
408
- .execute()
409
- .catch(withError('Could not get trash'));
410
- return items.map(parseItem);
411
- },
412
- });