@axium/storage 0.7.9 → 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.
Files changed (44) hide show
  1. package/dist/{client.d.ts → client/api.d.ts} +1 -1
  2. package/dist/{client.js → client/api.js} +12 -4
  3. package/dist/client/cli.d.ts +1 -0
  4. package/dist/client/cli.js +124 -0
  5. package/dist/client/config.d.ts +15 -0
  6. package/dist/client/config.js +21 -0
  7. package/dist/client/hooks.d.ts +1 -0
  8. package/dist/client/hooks.js +6 -0
  9. package/dist/client/index.d.ts +1 -0
  10. package/dist/client/index.js +1 -0
  11. package/dist/client/paths.d.ts +2 -0
  12. package/dist/client/paths.js +16 -0
  13. package/dist/client/sync.d.ts +55 -0
  14. package/dist/client/sync.js +205 -0
  15. package/dist/common.d.ts +60 -0
  16. package/dist/common.js +20 -0
  17. package/dist/polyfills.d.ts +1 -1
  18. package/dist/polyfills.js +3 -3
  19. package/dist/server/api.d.ts +1 -0
  20. package/dist/server/api.js +187 -0
  21. package/dist/server/batch.d.ts +1 -0
  22. package/dist/server/batch.js +147 -0
  23. package/dist/server/cli.d.ts +1 -0
  24. package/dist/server/cli.js +14 -0
  25. package/dist/{server.d.ts → server/config.d.ts} +12 -52
  26. package/dist/server/config.js +48 -0
  27. package/dist/server/db.d.ts +47 -0
  28. package/dist/server/db.js +77 -0
  29. package/dist/server/hooks.d.ts +9 -0
  30. package/dist/{plugin.js → server/hooks.js} +12 -14
  31. package/dist/server/index.d.ts +5 -0
  32. package/dist/server/index.js +5 -0
  33. package/dist/server/raw.d.ts +1 -0
  34. package/dist/server/raw.js +163 -0
  35. package/lib/List.svelte +2 -1
  36. package/lib/Usage.svelte +4 -2
  37. package/package.json +27 -16
  38. package/routes/files/[id]/+page.svelte +9 -5
  39. package/routes/files/shared/+page.svelte +2 -2
  40. package/routes/files/trash/+page.svelte +2 -2
  41. package/routes/files/usage/+page.svelte +5 -6
  42. package/dist/plugin.d.ts +0 -67
  43. package/dist/server.js +0 -412
  44. package/styles/list.css +0 -50
@@ -0,0 +1,187 @@
1
+ import { Permission } from '@axium/core';
2
+ import { checkAuthForItem, checkAuthForUser } from '@axium/server/auth';
3
+ import { config } from '@axium/server/config';
4
+ import { database } from '@axium/server/database';
5
+ import { error, parseBody, withError } from '@axium/server/requests';
6
+ import { addRoute } from '@axium/server/routes';
7
+ import { pick } from 'utilium';
8
+ import * as z from 'zod';
9
+ import { batchFormatVersion, StorageItemUpdate, syncProtocolVersion } from '../common.js';
10
+ import '../polyfills.js';
11
+ import { getLimits } from './config.js';
12
+ import { currentUsage, deleteRecursive, getRecursive, parseItem } from './db.js';
13
+ addRoute({
14
+ path: '/api/storage',
15
+ OPTIONS() {
16
+ return {
17
+ ...pick(config.storage, 'batch', 'chunk', 'max_chunks', 'max_transfer_size'),
18
+ syncProtocolVersion,
19
+ batchFormatVersion,
20
+ };
21
+ },
22
+ });
23
+ addRoute({
24
+ path: '/api/storage/item/:id',
25
+ params: { id: z.uuid() },
26
+ async GET(request, params) {
27
+ if (!config.storage.enabled)
28
+ error(503, 'User storage is disabled');
29
+ const itemId = params.id;
30
+ const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
31
+ return parseItem(item);
32
+ },
33
+ async PATCH(request, params) {
34
+ if (!config.storage.enabled)
35
+ error(503, 'User storage is disabled');
36
+ const itemId = params.id;
37
+ const body = await parseBody(request, StorageItemUpdate);
38
+ await checkAuthForItem(request, 'storage', itemId, Permission.Manage);
39
+ const values = {};
40
+ if ('publicPermission' in body)
41
+ values.publicPermission = body.publicPermission;
42
+ if ('trash' in body)
43
+ values.trashedAt = body.trash ? new Date() : null;
44
+ if ('owner' in body)
45
+ values.userId = body.owner;
46
+ if ('name' in body)
47
+ values.name = body.name;
48
+ if (!Object.keys(values).length)
49
+ error(400, 'No valid fields to update');
50
+ return parseItem(await database
51
+ .updateTable('storage')
52
+ .where('id', '=', itemId)
53
+ .set(values)
54
+ .returningAll()
55
+ .executeTakeFirstOrThrow()
56
+ .catch(withError('Could not update item')));
57
+ },
58
+ async DELETE(request, params) {
59
+ if (!config.storage.enabled)
60
+ error(503, 'User storage is disabled');
61
+ const itemId = params.id;
62
+ const auth = await checkAuthForItem(request, 'storage', itemId, Permission.Manage);
63
+ const item = parseItem(auth.item);
64
+ await deleteRecursive(item.type != 'inode/directory', itemId);
65
+ return item;
66
+ },
67
+ });
68
+ addRoute({
69
+ path: '/api/storage/directory/:id',
70
+ params: { id: z.uuid() },
71
+ async GET(request, params) {
72
+ if (!config.storage.enabled)
73
+ error(503, 'User storage is disabled');
74
+ const itemId = params.id;
75
+ const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
76
+ if (item.type != 'inode/directory')
77
+ error(409, 'Item is not a directory');
78
+ const items = await database
79
+ .selectFrom('storage')
80
+ .where('parentId', '=', itemId)
81
+ .where('trashedAt', 'is', null)
82
+ .selectAll()
83
+ .execute();
84
+ return items.map(parseItem);
85
+ },
86
+ });
87
+ addRoute({
88
+ path: '/api/storage/directory/:id/recursive',
89
+ params: { id: z.uuid() },
90
+ async GET(request, params) {
91
+ if (!config.storage.enabled)
92
+ error(503, 'User storage is disabled');
93
+ const itemId = params.id;
94
+ const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
95
+ if (item.type != 'inode/directory')
96
+ error(409, 'Item is not a directory');
97
+ const items = await Array.fromAsync(getRecursive(itemId)).catch(withError('Could not get some directory items'));
98
+ return items;
99
+ },
100
+ });
101
+ addRoute({
102
+ path: '/api/users/:id/storage',
103
+ params: { id: z.uuid() },
104
+ async OPTIONS(request, params) {
105
+ if (!config.storage.enabled)
106
+ error(503, 'User storage is disabled');
107
+ const userId = params.id;
108
+ await checkAuthForUser(request, userId);
109
+ const [usage, limits] = await Promise.all([currentUsage(userId), getLimits(userId)]).catch(withError('Could not fetch data'));
110
+ return { usage, limits };
111
+ },
112
+ async GET(request, params) {
113
+ if (!config.storage.enabled)
114
+ error(503, 'User storage is disabled');
115
+ const userId = params.id;
116
+ await checkAuthForUser(request, userId);
117
+ const [items, usage, limits] = await Promise.all([
118
+ database.selectFrom('storage').where('userId', '=', userId).where('trashedAt', 'is', null).selectAll().execute(),
119
+ currentUsage(userId),
120
+ getLimits(userId),
121
+ ]).catch(withError('Could not fetch data'));
122
+ return { usage, limits, items: items.map(parseItem) };
123
+ },
124
+ });
125
+ addRoute({
126
+ path: '/api/users/:id/storage/root',
127
+ params: { id: z.uuid() },
128
+ async GET(request, params) {
129
+ if (!config.storage.enabled)
130
+ error(503, 'User storage is disabled');
131
+ const userId = params.id;
132
+ await checkAuthForUser(request, userId);
133
+ const items = await database
134
+ .selectFrom('storage')
135
+ .where('userId', '=', userId)
136
+ .where('trashedAt', 'is', null)
137
+ .where('parentId', 'is', null)
138
+ .selectAll()
139
+ .execute()
140
+ .catch(withError('Could not get storage items'));
141
+ return items.map(parseItem);
142
+ },
143
+ });
144
+ function existsInACL(column, userId) {
145
+ return (eb) => eb.exists(eb
146
+ .selectFrom('acl.storage')
147
+ .whereRef('itemId', '=', `item.${column}`)
148
+ .where('userId', '=', userId)
149
+ .where('permission', '!=', Permission.None));
150
+ }
151
+ addRoute({
152
+ path: '/api/users/:id/storage/shared',
153
+ params: { id: z.uuid() },
154
+ async GET(request, params) {
155
+ if (!config.storage.enabled)
156
+ error(503, 'User storage is disabled');
157
+ const userId = params.id;
158
+ await checkAuthForUser(request, userId);
159
+ const items = await database
160
+ .selectFrom('storage as item')
161
+ .selectAll('item')
162
+ .where('trashedAt', 'is', null)
163
+ .where(existsInACL('id', userId))
164
+ .where(eb => eb.not(existsInACL('parentId', userId)))
165
+ .execute()
166
+ .catch(withError('Could not get storage items'));
167
+ return items.map(parseItem);
168
+ },
169
+ });
170
+ addRoute({
171
+ path: '/api/users/:id/storage/trash',
172
+ params: { id: z.uuid() },
173
+ async GET(request, params) {
174
+ if (!config.storage.enabled)
175
+ error(503, 'User storage is disabled');
176
+ const userId = params.id;
177
+ await checkAuthForUser(request, userId);
178
+ const items = await database
179
+ .selectFrom('storage')
180
+ .where('userId', '=', userId)
181
+ .where('trashedAt', 'is not', null)
182
+ .selectAll()
183
+ .execute()
184
+ .catch(withError('Could not get trash'));
185
+ return items.map(parseItem);
186
+ },
187
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,147 @@
1
+ import { Permission } from '@axium/core';
2
+ import * as acl from '@axium/server/acl';
3
+ import { audit } from '@axium/server/audit';
4
+ import { getSessionAndUser } from '@axium/server/auth';
5
+ import config from '@axium/server/config';
6
+ import { database } from '@axium/server/database';
7
+ import { getToken, withError } from '@axium/server/requests';
8
+ import { addRoute } from '@axium/server/routes';
9
+ import { error } from '@sveltejs/kit';
10
+ import { createHash } from 'node:crypto';
11
+ import { unlinkSync, writeFileSync } from 'node:fs';
12
+ import { join } from 'node:path/posix';
13
+ import * as z from 'zod';
14
+ import { StorageBatchUpdate } from '../common.js';
15
+ import { getLimits } from './config.js';
16
+ import { currentUsage, getRecursiveIds, parseItem } from './db.js';
17
+ addRoute({
18
+ path: '/api/storage/batch',
19
+ async POST(req) {
20
+ if (!config.storage.enabled)
21
+ error(503, 'User storage is disabled');
22
+ if (!config.storage.batch.enabled)
23
+ error(503, 'Batch updates are disabled');
24
+ const token = getToken(req);
25
+ if (!token)
26
+ error(401, 'Missing session token');
27
+ const { userId, user } = await getSessionAndUser(token).catch(withError('Invalid session token', 401));
28
+ if (user.isSuspended)
29
+ error(403, 'User is suspended');
30
+ const [usage, limits] = await Promise.all([currentUsage(userId), getLimits(userId)]).catch(withError('Could not fetch usage and/or limits'));
31
+ const batchHeaderSize = Number(req.headers.get('x-batch-header-size'));
32
+ if (!Number.isSafeInteger(batchHeaderSize) || batchHeaderSize < 2)
33
+ error(400, 'Invalid or missing header, X-Batch-Header-Size');
34
+ const size = Number(req.headers.get('content-length'));
35
+ if (Number.isNaN(size))
36
+ error(411, 'Missing or invalid content length header');
37
+ const raw = await req.bytes();
38
+ if (raw.byteLength - batchHeaderSize > size) {
39
+ await audit('storage_size_mismatch', userId, { item: null });
40
+ error(400, 'Content length does not match size header');
41
+ }
42
+ if (req.headers.get('content-type') != 'x-axium/storage-batch')
43
+ error(415, 'Invalid content type, expected x-axium/storage-batch');
44
+ let header;
45
+ try {
46
+ const text = new TextDecoder().decode(raw.subarray(0, batchHeaderSize));
47
+ header = StorageBatchUpdate.parse(JSON.parse(text));
48
+ }
49
+ catch (e) {
50
+ error(400, e instanceof z.core.$ZodError ? z.prettifyError(e) : 'invalid batch header');
51
+ }
52
+ const deletedIds = new Set(header.deleted);
53
+ const changedIds = new Set(Object.keys(header.content));
54
+ for (const id of [...Object.keys(header.metadata), ...changedIds]) {
55
+ if (deletedIds.has(id))
56
+ error(400, 'Item cannot be updated and deleted in the same batch: ' + id);
57
+ }
58
+ // checkAuthForItem but optimized to not re-fetch the session and also only run one DB query
59
+ const items = await database
60
+ .selectFrom('storage')
61
+ .selectAll()
62
+ .where('id', 'in', [...deletedIds, ...Object.keys(header.metadata), ...changedIds])
63
+ .select(acl.from('storage', { onlyId: userId }))
64
+ .$castTo()
65
+ .execute()
66
+ .catch(withError('Item(s) not found', 404));
67
+ for (const item of items) {
68
+ const permission = !changedIds.has(item.id) ? Permission.Manage : Permission.Edit;
69
+ if (changedIds.has(item.id)) {
70
+ // Extra checks for content changes
71
+ if (item.immutable)
72
+ error(409, 'Item is immutable and cannot be modified: ' + item.id);
73
+ if (item.type == 'inode/directory')
74
+ error(409, 'Directories do not have content');
75
+ if (item.trashedAt)
76
+ error(410, 'Trashed items can not be changed');
77
+ if (limits.item_size && size > limits.item_size * 1_000_000)
78
+ error(413, 'Item size exceeds maximum size: ' + item.id);
79
+ }
80
+ if (item.publicPermission >= permission)
81
+ continue;
82
+ if (userId == item.userId)
83
+ continue;
84
+ if (!item.acl || !item.acl.length)
85
+ error(403, 'Missing permission for item: ' + item.id);
86
+ const [control] = item.acl;
87
+ if (control.userId !== userId) {
88
+ await audit('acl_id_mismatch', userId, { item: item.id });
89
+ error(500, 'Access control entry does not match expected user ID');
90
+ }
91
+ if (control.permission >= permission)
92
+ continue;
93
+ error(403, 'Missing permission for item: ' + item.id);
94
+ }
95
+ if (limits.user_size && (usage.bytes + size - items.reduce((sum, item) => sum + item.size, 0)) / 1_000_000 >= limits.user_size)
96
+ error(413, 'Not enough space');
97
+ const tx = await database.startTransaction().execute();
98
+ const results = new Map();
99
+ try {
100
+ for (const [itemId, { offset, size }] of Object.entries(header.content)) {
101
+ const content = raw.subarray(offset, offset + size);
102
+ const hash = createHash('BLAKE2b512').update(content).digest();
103
+ const result = await tx
104
+ .updateTable('storage')
105
+ .where('id', '=', itemId)
106
+ .set({ size, modifiedAt: new Date(), hash })
107
+ .returningAll()
108
+ .executeTakeFirstOrThrow();
109
+ writeFileSync(join(config.storage.data, result.id), content);
110
+ await tx.commit().execute();
111
+ results.set(itemId, parseItem(result));
112
+ }
113
+ const toDelete = await Array.fromAsync(getRecursiveIds(...header.deleted)).catch(withError('Could not get items to delete', 500));
114
+ const deleted = await tx.deleteFrom('storage').where('id', 'in', header.deleted).returningAll().execute();
115
+ for (const id of toDelete)
116
+ unlinkSync(join(config.storage.data, id));
117
+ for (const item of deleted)
118
+ results.set(item.id, parseItem(item));
119
+ for (const [itemId, update] of Object.entries(header.metadata)) {
120
+ const values = {};
121
+ if ('publicPermission' in update)
122
+ values.publicPermission = update.publicPermission;
123
+ if ('trash' in update)
124
+ values.trashedAt = update.trash ? new Date() : null;
125
+ if ('owner' in update)
126
+ values.userId = update.owner;
127
+ if ('name' in update)
128
+ values.name = update.name;
129
+ if (!Object.keys(values).length)
130
+ error(400, 'No valid fields to update: ' + itemId);
131
+ const updated = await tx
132
+ .updateTable('storage')
133
+ .where('id', '=', itemId)
134
+ .set(values)
135
+ .returningAll()
136
+ .executeTakeFirstOrThrow()
137
+ .catch(withError('Could not update item: ' + itemId));
138
+ results.set(itemId, parseItem(updated));
139
+ }
140
+ }
141
+ catch (error) {
142
+ await tx.rollback().execute();
143
+ throw withError('Could not update item', 500)(error);
144
+ }
145
+ return Array.from(results.values());
146
+ },
147
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { formatBytes } from '@axium/core/format';
2
+ import { count, database } from '@axium/server/database';
3
+ import { program } from 'commander';
4
+ const cli = program.command('files').helpGroup('Plugins:').description('CLI integration for @axium/storage');
5
+ cli.command('usage')
6
+ .description('Show storage usage information')
7
+ .action(async () => {
8
+ const { storage: items } = await count('storage');
9
+ const { size } = await database
10
+ .selectFrom('storage')
11
+ .select(eb => eb.fn.sum('size').as('size'))
12
+ .executeTakeFirstOrThrow();
13
+ console.log(`${items} items totaling ${formatBytes(Number(size))}`);
14
+ });
@@ -1,47 +1,10 @@
1
- import { type Schema } from '@axium/server/database';
2
- import type { Generated, Selectable } from 'kysely';
3
- import type { StorageItemMetadata, StorageLimits, StorageUsage } from './common.js';
4
- import './polyfills.js';
5
- declare module '@axium/server/database' {
6
- interface Schema {
7
- storage: {
8
- createdAt: Generated<Date>;
9
- hash: Uint8Array | null;
10
- id: Generated<string>;
11
- immutable: Generated<boolean>;
12
- modifiedAt: Generated<Date>;
13
- name: string;
14
- parentId: string | null;
15
- size: number;
16
- trashedAt: Date | null;
17
- type: string;
18
- userId: string;
19
- publicPermission: Generated<number>;
20
- metadata: Generated<Record<string, unknown>>;
21
- };
22
- }
23
- interface ExpectedSchema {
24
- storage: ColumnTypes<Schema['storage']>;
25
- }
26
- }
27
- /**
28
- * @internal A storage item selected from the database.
29
- */
30
- interface SelectedItem extends Selectable<Schema['storage']> {
31
- }
1
+ import type { StorageLimits, StoragePublicConfig } from '../common.js';
2
+ import '../polyfills.js';
32
3
  declare module '@axium/server/config' {
33
4
  interface Config {
34
- storage: {
35
- /** Whether the storage API endpoints are enabled */
36
- enabled: boolean;
5
+ storage: StoragePublicConfig & {
37
6
  /** Whether the files app is enabled. Requires `enabled` */
38
7
  app_enabled: boolean;
39
- /** Path to data directory */
40
- data: string;
41
- /** How many days files are kept in the trash */
42
- trash_duration: number;
43
- /** Default limits */
44
- limits: StorageLimits;
45
8
  /** Content Addressable Storage (CAS) configuration */
46
9
  cas: {
47
10
  /** Whether to use CAS */
@@ -51,9 +14,18 @@ declare module '@axium/server/config' {
51
14
  /** Mime types to exclude when determining if CAS should be used */
52
15
  exclude: string[];
53
16
  };
17
+ /** Path to data directory */
18
+ data: string;
19
+ /** Whether the storage API endpoints are enabled */
20
+ enabled: boolean;
21
+ /** Default limits */
22
+ limits: StorageLimits;
23
+ /** How many days files are kept in the trash */
24
+ trash_duration: number;
54
25
  };
55
26
  }
56
27
  }
28
+ export declare const defaultCASMime: RegExp[];
57
29
  declare module '@axium/server/audit' {
58
30
  interface $EventTypes {
59
31
  storage_type_mismatch: {
@@ -67,21 +39,9 @@ declare module '@axium/server/audit' {
67
39
  };
68
40
  }
69
41
  }
70
- export interface StorageItem extends StorageItemMetadata {
71
- data: Uint8Array<ArrayBufferLike>;
72
- }
73
- export declare function parseItem<T extends SelectedItem>(item: T): Omit<T, keyof Schema['storage']> & StorageItemMetadata;
74
- /**
75
- * Returns the current usage of the storage for a user in bytes.
76
- */
77
- export declare function currentUsage(userId: string): Promise<StorageUsage>;
78
- export declare function get(itemId: string): Promise<StorageItemMetadata>;
79
42
  export type ExternalLimitHandler = (userId?: string) => StorageLimits | Promise<StorageLimits>;
80
43
  /**
81
44
  * Define the handler to get limits for a user externally.
82
45
  */
83
46
  export declare function useLimits(handler: ExternalLimitHandler): void;
84
47
  export declare function getLimits(userId?: string): Promise<StorageLimits>;
85
- export declare function getRecursive(id: string): AsyncGenerator<string>;
86
- export declare function deleteRecursive(itemId: string, deleteSelf: boolean): Promise<void>;
87
- export {};
@@ -0,0 +1,48 @@
1
+ import { Severity } from '@axium/core';
2
+ import { addEvent } from '@axium/server/audit';
3
+ import { addConfigDefaults, config } from '@axium/server/config';
4
+ import '../polyfills.js';
5
+ export const defaultCASMime = [/video\/.*/, /audio\/.*/];
6
+ addConfigDefaults({
7
+ storage: {
8
+ app_enabled: true,
9
+ batch: {
10
+ enabled: false,
11
+ max_items: 100,
12
+ max_item_size: 100,
13
+ },
14
+ cas: {
15
+ enabled: true,
16
+ include: [],
17
+ exclude: [],
18
+ },
19
+ chunk: false,
20
+ data: '/srv/axium/storage',
21
+ enabled: true,
22
+ limits: {
23
+ user_size: 1000,
24
+ item_size: 100,
25
+ user_items: 10_000,
26
+ },
27
+ max_chunks: 10,
28
+ max_transfer_size: 100,
29
+ trash_duration: 30,
30
+ },
31
+ });
32
+ addEvent({ source: '@axium/storage', name: 'storage_type_mismatch', severity: Severity.Warning, tags: ['mimetype'] });
33
+ addEvent({ source: '@axium/storage', name: 'storage_size_mismatch', severity: Severity.Warning, tags: [] });
34
+ let _getLimits = null;
35
+ /**
36
+ * Define the handler to get limits for a user externally.
37
+ */
38
+ export function useLimits(handler) {
39
+ _getLimits = handler;
40
+ }
41
+ export async function getLimits(userId) {
42
+ try {
43
+ return await _getLimits(userId);
44
+ }
45
+ catch {
46
+ return config.storage.limits;
47
+ }
48
+ }
@@ -0,0 +1,47 @@
1
+ import { type Schema } from '@axium/server/database';
2
+ import type { Generated, Selectable } from 'kysely';
3
+ import type { StorageItemMetadata, StorageUsage } from '../common.js';
4
+ import '../polyfills.js';
5
+ declare module '@axium/server/database' {
6
+ interface Schema {
7
+ storage: {
8
+ createdAt: Generated<Date>;
9
+ hash: Uint8Array | null;
10
+ id: Generated<string>;
11
+ immutable: Generated<boolean>;
12
+ modifiedAt: Generated<Date>;
13
+ name: string;
14
+ parentId: string | null;
15
+ size: number;
16
+ trashedAt: Date | null;
17
+ type: string;
18
+ userId: string;
19
+ publicPermission: Generated<number>;
20
+ metadata: Generated<Record<string, unknown>>;
21
+ };
22
+ }
23
+ interface ExpectedSchema {
24
+ storage: ColumnTypes<Schema['storage']>;
25
+ }
26
+ }
27
+ /**
28
+ * @internal A storage item selected from the database.
29
+ */
30
+ export interface SelectedItem extends Selectable<Schema['storage']> {
31
+ }
32
+ export interface StorageItem extends StorageItemMetadata {
33
+ data: Uint8Array<ArrayBufferLike>;
34
+ }
35
+ export declare function parseItem<T extends SelectedItem>(item: T): Omit<T, keyof Schema['storage']> & StorageItemMetadata;
36
+ /**
37
+ * Returns the current usage of the storage for a user in bytes.
38
+ */
39
+ export declare function currentUsage(userId: string): Promise<StorageUsage>;
40
+ export declare function get(itemId: string): Promise<StorageItemMetadata>;
41
+ export declare function getRecursive(this: {
42
+ path: string;
43
+ } | void, ...ids: string[]): AsyncGenerator<StorageItemMetadata & {
44
+ path: string;
45
+ }>;
46
+ export declare function getRecursiveIds(...ids: string[]): AsyncGenerator<string>;
47
+ export declare function deleteRecursive(deleteSelf: boolean, ...itemId: string[]): Promise<void>;
@@ -0,0 +1,77 @@
1
+ import { config } from '@axium/server/config';
2
+ import { database, expectedTypes } from '@axium/server/database';
3
+ import { withError } from '@axium/server/requests';
4
+ import { unlinkSync } from 'node:fs';
5
+ import { join } from 'node:path/posix';
6
+ import '../polyfills.js';
7
+ expectedTypes.storage = {
8
+ createdAt: { type: 'timestamptz', required: true, hasDefault: true },
9
+ hash: { type: 'bytea' },
10
+ id: { type: 'uuid', required: true, hasDefault: true },
11
+ immutable: { type: 'bool', required: true },
12
+ modifiedAt: { type: 'timestamptz', required: true, hasDefault: true },
13
+ name: { type: 'text' },
14
+ parentId: { type: 'uuid' },
15
+ size: { type: 'int4', required: true },
16
+ trashedAt: { type: 'timestamptz' },
17
+ type: { type: 'text', required: true },
18
+ userId: { type: 'uuid', required: true },
19
+ publicPermission: { type: 'int4', required: true, hasDefault: true },
20
+ metadata: { type: 'jsonb', required: true, hasDefault: true },
21
+ };
22
+ export function parseItem(item) {
23
+ return {
24
+ ...item,
25
+ dataURL: `/raw/storage/${item.id}`,
26
+ hash: item.hash?.toHex?.(),
27
+ };
28
+ }
29
+ /**
30
+ * Returns the current usage of the storage for a user in bytes.
31
+ */
32
+ export async function currentUsage(userId) {
33
+ const result = await database
34
+ .selectFrom('storage')
35
+ .where('userId', '=', userId)
36
+ .select(eb => eb.fn.countAll().as('items'))
37
+ .select(eb => eb.fn.sum('size').as('bytes'))
38
+ .executeTakeFirstOrThrow();
39
+ result.bytes = Number(result.bytes || 0);
40
+ result.items = Number(result.items);
41
+ return result;
42
+ }
43
+ export async function get(itemId) {
44
+ const result = await database
45
+ .selectFrom('storage')
46
+ .where('id', '=', itemId)
47
+ .selectAll()
48
+ .$narrowType()
49
+ .executeTakeFirstOrThrow();
50
+ return parseItem(result);
51
+ }
52
+ export async function* getRecursive(...ids) {
53
+ const items = await database.selectFrom('storage').where('parentId', 'in', ids).selectAll().execute();
54
+ for (const item of items) {
55
+ const path = this?.path ? this.path + '/' + item.name : item.name;
56
+ yield Object.assign(parseItem(item), { path });
57
+ if (item.type == 'inode/directory')
58
+ yield* getRecursive.call({ path }, item.id);
59
+ }
60
+ }
61
+ export async function* getRecursiveIds(...ids) {
62
+ const items = await database.selectFrom('storage').where('parentId', 'in', ids).select(['id', 'type']).execute();
63
+ for (const item of items) {
64
+ if (item.type != 'inode/directory')
65
+ yield item.id;
66
+ else
67
+ yield* getRecursiveIds(item.id);
68
+ }
69
+ }
70
+ export async function deleteRecursive(deleteSelf, ...itemId) {
71
+ const toDelete = await Array.fromAsync(getRecursiveIds(...itemId)).catch(withError('Could not get items to delete'));
72
+ if (deleteSelf)
73
+ toDelete.push(...itemId);
74
+ await database.deleteFrom('storage').where('id', '=', itemId).returningAll().execute().catch(withError('Could not delete item'));
75
+ for (const id of toDelete)
76
+ unlinkSync(join(config.storage.data, id));
77
+ }
@@ -0,0 +1,9 @@
1
+ import type { InitOptions, OpOptions } from '@axium/server/database';
2
+ import '../common.js';
3
+ import './index.js';
4
+ export declare function statusText(): Promise<string>;
5
+ export declare function init(): void;
6
+ export declare function db_init(opt: InitOptions): Promise<void>;
7
+ export declare function db_wipe(opt: OpOptions): Promise<void>;
8
+ export declare function remove(opt: OpOptions): Promise<void>;
9
+ export declare function clean(opt: OpOptions): Promise<void>;