@axium/storage 0.10.0 → 0.11.1

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/db.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "../server/schemas/db.json",
3
+ "format": 0,
4
+ "versions": [
5
+ {
6
+ "delta": false,
7
+ "tables": {
8
+ "storage": {
9
+ "columns": {
10
+ "id": { "type": "uuid", "required": true, "primary": true, "default": "gen_random_uuid()" },
11
+ "createdAt": { "type": "timestamptz", "required": true, "default": "now()" },
12
+ "hash": { "type": "bytea" },
13
+ "immutable": { "type": "bool", "required": true },
14
+ "modifiedAt": { "type": "timestamptz", "required": true, "default": "now()" },
15
+ "name": { "type": "text" },
16
+ "parentId": { "type": "uuid", "references": "storage.id", "onDelete": "cascade" },
17
+ "size": { "type": "integer", "required": true },
18
+ "trashedAt": { "type": "timestamptz" },
19
+ "type": { "type": "text", "required": true },
20
+ "userId": { "type": "uuid", "required": true, "references": "users.id", "onDelete": "cascade" },
21
+ "publicPermission": { "type": "integer", "required": true, "default": 0 },
22
+ "metadata": { "type": "jsonb", "required": true, "default": "{}" }
23
+ }
24
+ },
25
+ "acl.storage": {
26
+ "columns": {
27
+ "userId": { "type": "uuid", "required": true, "primary": true, "references": "users.id", "onDelete": "cascade" },
28
+ "itemId": { "type": "uuid", "required": true, "primary": true, "references": "storage.id", "onDelete": "cascade" },
29
+ "createdAt": { "type": "timestamptz", "required": true, "default": "now()" },
30
+ "permission": { "type": "integer", "required": true, "check": "permission >= 0 AND permission <= 5" }
31
+ }
32
+ }
33
+ },
34
+ "indexes": ["storage:userId", "storage:parentId", "acl.storage:userId", "acl.storage:itemId"]
35
+ },
36
+ {
37
+ "delta": true,
38
+ "alter_tables": {
39
+ "storage": {
40
+ "drop_columns": ["publicPermission"]
41
+ },
42
+ "acl.storage": {
43
+ "drop_constraints": ["PK_acl_storage"],
44
+ "drop_columns": ["permission"],
45
+ "add_columns": {
46
+ "role": { "type": "text" },
47
+ "tag": { "type": "text" },
48
+ "read": { "type": "boolean", "required": true, "default": false },
49
+ "download": { "type": "boolean", "required": true, "default": false },
50
+ "write": { "type": "boolean", "required": true, "default": false },
51
+ "comment": { "type": "boolean", "required": true, "default": false },
52
+ "manage": { "type": "boolean", "required": true, "default": false }
53
+ },
54
+ "alter_columns": {
55
+ "userId": { "ops": ["drop_required"] }
56
+ },
57
+ "add_constraints": {
58
+ "unique_storage": { "type": "unique", "on": ["itemId", "userId", "role", "tag"], "nulls_not_distinct": true }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ ],
64
+ "wipe": ["storage", "acl.storage"],
65
+ "acl_tables": {
66
+ "storage": "acl.storage"
67
+ }
68
+ }
@@ -14,19 +14,6 @@ declare const StorageCache: z.ZodObject<{
14
14
  name: z.ZodString;
15
15
  userId: z.ZodUUID;
16
16
  parentId: z.ZodNullable<z.ZodUUID>;
17
- publicPermission: z.ZodEnum<{
18
- readonly None: 0;
19
- readonly Read: 1;
20
- readonly Comment: 2;
21
- readonly Edit: 3;
22
- readonly Manage: 5;
23
- }> & {
24
- readonly None: 0;
25
- readonly Read: 1;
26
- readonly Comment: 2;
27
- readonly Edit: 3;
28
- readonly Manage: 5;
29
- };
30
17
  size: z.ZodInt;
31
18
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
32
19
  type: z.ZodString;
package/dist/common.d.ts CHANGED
@@ -83,19 +83,6 @@ export declare const StorageItemUpdate: z.ZodObject<{
83
83
  name: z.ZodOptional<z.ZodString>;
84
84
  owner: z.ZodOptional<z.ZodUUID>;
85
85
  trash: z.ZodOptional<z.ZodBoolean>;
86
- publicPermission: z.ZodOptional<z.ZodEnum<{
87
- readonly None: 0;
88
- readonly Read: 1;
89
- readonly Comment: 2;
90
- readonly Edit: 3;
91
- readonly Manage: 5;
92
- }> & {
93
- readonly None: 0;
94
- readonly Read: 1;
95
- readonly Comment: 2;
96
- readonly Edit: 3;
97
- readonly Manage: 5;
98
- }>;
99
86
  }, z.core.$strip>;
100
87
  export type StorageItemUpdate = z.infer<typeof StorageItemUpdate>;
101
88
  export declare const StorageItemMetadata: z.ZodObject<{
@@ -108,19 +95,6 @@ export declare const StorageItemMetadata: z.ZodObject<{
108
95
  name: z.ZodString;
109
96
  userId: z.ZodUUID;
110
97
  parentId: z.ZodNullable<z.ZodUUID>;
111
- publicPermission: z.ZodEnum<{
112
- readonly None: 0;
113
- readonly Read: 1;
114
- readonly Comment: 2;
115
- readonly Edit: 3;
116
- readonly Manage: 5;
117
- }> & {
118
- readonly None: 0;
119
- readonly Read: 1;
120
- readonly Comment: 2;
121
- readonly Edit: 3;
122
- readonly Manage: 5;
123
- };
124
98
  size: z.ZodInt;
125
99
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
126
100
  type: z.ZodString;
@@ -148,19 +122,6 @@ export declare const StorageBatchUpdate: z.ZodObject<{
148
122
  name: z.ZodOptional<z.ZodString>;
149
123
  owner: z.ZodOptional<z.ZodUUID>;
150
124
  trash: z.ZodOptional<z.ZodBoolean>;
151
- publicPermission: z.ZodOptional<z.ZodEnum<{
152
- readonly None: 0;
153
- readonly Read: 1;
154
- readonly Comment: 2;
155
- readonly Edit: 3;
156
- readonly Manage: 5;
157
- }> & {
158
- readonly None: 0;
159
- readonly Read: 1;
160
- readonly Comment: 2;
161
- readonly Edit: 3;
162
- readonly Manage: 5;
163
- }>;
164
125
  }, z.core.$strip>>;
165
126
  content: z.ZodRecord<z.ZodUUID, z.ZodObject<{
166
127
  offset: z.ZodInt;
package/dist/common.js CHANGED
@@ -1,4 +1,3 @@
1
- import { Permission } from '@axium/core/access';
2
1
  import * as z from 'zod';
3
2
  export const syncProtocolVersion = 0;
4
3
  /**
@@ -9,7 +8,6 @@ export const StorageItemUpdate = z
9
8
  name: z.string(),
10
9
  owner: z.uuid(),
11
10
  trash: z.boolean(),
12
- publicPermission: Permission,
13
11
  })
14
12
  .partial();
15
13
  export const StorageItemMetadata = z.object({
@@ -23,7 +21,6 @@ export const StorageItemMetadata = z.object({
23
21
  name: z.string(),
24
22
  userId: z.uuid(),
25
23
  parentId: z.uuid().nullable(),
26
- publicPermission: Permission,
27
24
  size: z.int().nonnegative(),
28
25
  trashedAt: z.coerce.date().nullable(),
29
26
  type: z.string(),
package/dist/node.js CHANGED
@@ -25,7 +25,6 @@ export function colorItem(item) {
25
25
  return styleText('red', name);
26
26
  return name;
27
27
  }
28
- const publicPermString = ['---', 'r--', 'r-x', 'rwx', 'rwx', 'rwx'];
29
28
  const __formatter = new Intl.DateTimeFormat('en-US', {
30
29
  year: 'numeric',
31
30
  month: 'short',
@@ -46,7 +45,6 @@ export function* formatItems({ items, users, humanReadable }) {
46
45
  const owner = users[item.userId].name;
47
46
  const type = item.type == 'inode/directory' ? 'd' : '-';
48
47
  const ownerPerm = `r${item.immutable ? '-' : 'w'}x`;
49
- const publicPerm = publicPermString[item.publicPermission];
50
- yield `${type}${ownerPerm}${ownerPerm}${publicPerm}. ${owner.padEnd(nameWidth)} ${item.__size.padStart(sizeWidth)} ${formatDate(item.modifiedAt)} ${colorItem(item)}`;
48
+ yield `${type}${ownerPerm}${ownerPerm}. ${owner.padEnd(nameWidth)} ${item.__size.padStart(sizeWidth)} ${formatDate(item.modifiedAt)} ${colorItem(item)}`;
51
49
  }
52
50
  }
@@ -1,4 +1,3 @@
1
- import { Permission } from '@axium/core';
2
1
  import { checkAuthForItem, checkAuthForUser } from '@axium/server/auth';
3
2
  import { config } from '@axium/server/config';
4
3
  import { database } from '@axium/server/database';
@@ -9,7 +8,7 @@ import * as z from 'zod';
9
8
  import { batchFormatVersion, StorageItemUpdate, syncProtocolVersion } from '../common.js';
10
9
  import '../polyfills.js';
11
10
  import { getLimits } from './config.js';
12
- import { getUserStats, deleteRecursive, getRecursive, parseItem } from './db.js';
11
+ import { deleteRecursive, getRecursive, getUserStats, parseItem } from './db.js';
13
12
  addRoute({
14
13
  path: '/api/storage',
15
14
  OPTIONS() {
@@ -26,17 +25,15 @@ addRoute({
26
25
  async GET(request, { id: itemId }) {
27
26
  if (!config.storage.enabled)
28
27
  error(503, 'User storage is disabled');
29
- const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
28
+ const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
30
29
  return parseItem(item);
31
30
  },
32
31
  async PATCH(request, { id: itemId }) {
33
32
  if (!config.storage.enabled)
34
33
  error(503, 'User storage is disabled');
35
34
  const body = await parseBody(request, StorageItemUpdate);
36
- await checkAuthForItem(request, 'storage', itemId, Permission.Manage);
35
+ await checkAuthForItem(request, 'storage', itemId, { manage: true });
37
36
  const values = {};
38
- if ('publicPermission' in body)
39
- values.publicPermission = body.publicPermission;
40
37
  if ('trash' in body)
41
38
  values.trashedAt = body.trash ? new Date() : null;
42
39
  if ('owner' in body)
@@ -56,7 +53,7 @@ addRoute({
56
53
  async DELETE(request, { id: itemId }) {
57
54
  if (!config.storage.enabled)
58
55
  error(503, 'User storage is disabled');
59
- const auth = await checkAuthForItem(request, 'storage', itemId, Permission.Manage);
56
+ const auth = await checkAuthForItem(request, 'storage', itemId, { manage: true });
60
57
  const item = parseItem(auth.item);
61
58
  await deleteRecursive(item.type != 'inode/directory', itemId);
62
59
  return item;
@@ -68,7 +65,7 @@ addRoute({
68
65
  async GET(request, { id: itemId }) {
69
66
  if (!config.storage.enabled)
70
67
  error(503, 'User storage is disabled');
71
- const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
68
+ const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
72
69
  if (item.type != 'inode/directory')
73
70
  error(409, 'Item is not a directory');
74
71
  const items = await database
@@ -86,7 +83,7 @@ addRoute({
86
83
  async GET(request, { id: itemId }) {
87
84
  if (!config.storage.enabled)
88
85
  error(503, 'User storage is disabled');
89
- const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
86
+ const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
90
87
  if (item.type != 'inode/directory')
91
88
  error(409, 'Item is not a directory');
92
89
  const items = await Array.fromAsync(getRecursive(itemId)).catch(withError('Could not get some directory items'));
@@ -133,12 +130,19 @@ addRoute({
133
130
  return items.map(parseItem);
134
131
  },
135
132
  });
136
- function existsInACL(column, userId) {
133
+ function existsInACL(column, user) {
137
134
  return (eb) => eb.exists(eb
138
135
  .selectFrom('acl.storage')
139
136
  .whereRef('itemId', '=', `item.${column}`)
140
- .where('userId', '=', userId)
141
- .where('permission', '!=', Permission.None));
137
+ .where('userId', '=', user.id)
138
+ .where(eb => {
139
+ const ors = [eb('userId', '=', user.id)];
140
+ if (user.roles.length)
141
+ ors.push(eb('role', 'in', user.roles));
142
+ if (user.tags.length)
143
+ ors.push(eb('tag', 'in', user.tags));
144
+ return eb.or(ors);
145
+ }));
142
146
  }
143
147
  addRoute({
144
148
  path: '/api/users/:id/storage/shared',
@@ -146,13 +150,13 @@ addRoute({
146
150
  async GET(request, { id: userId }) {
147
151
  if (!config.storage.enabled)
148
152
  error(503, 'User storage is disabled');
149
- await checkAuthForUser(request, userId);
153
+ const { user } = await checkAuthForUser(request, userId);
150
154
  const items = await database
151
155
  .selectFrom('storage as item')
152
156
  .selectAll('item')
153
157
  .where('trashedAt', 'is', null)
154
- .where(existsInACL('id', userId))
155
- .where(eb => eb.not(existsInACL('parentId', userId)))
158
+ .where(existsInACL('id', user))
159
+ .where(eb => eb.not(existsInACL('parentId', user)))
156
160
  .execute()
157
161
  .catch(withError('Could not get storage items'));
158
162
  return items.map(parseItem);
@@ -1,10 +1,9 @@
1
- import { Permission } from '@axium/core';
2
1
  import * as acl from '@axium/server/acl';
3
2
  import { audit } from '@axium/server/audit';
4
- import { getSessionAndUser } from '@axium/server/auth';
3
+ import { requireSession } from '@axium/server/auth';
5
4
  import config from '@axium/server/config';
6
5
  import { database } from '@axium/server/database';
7
- import { getToken, withError } from '@axium/server/requests';
6
+ import { withError } from '@axium/server/requests';
8
7
  import { addRoute } from '@axium/server/routes';
9
8
  import { error } from '@sveltejs/kit';
10
9
  import { createHash } from 'node:crypto';
@@ -13,7 +12,7 @@ import { join } from 'node:path/posix';
13
12
  import * as z from 'zod';
14
13
  import { StorageBatchUpdate } from '../common.js';
15
14
  import { getLimits } from './config.js';
16
- import { getUserStats, getRecursiveIds, parseItem } from './db.js';
15
+ import { getRecursiveIds, getUserStats, parseItem } from './db.js';
17
16
  addRoute({
18
17
  path: '/api/storage/batch',
19
18
  async POST(req) {
@@ -21,12 +20,7 @@ addRoute({
21
20
  error(503, 'User storage is disabled');
22
21
  if (!config.storage.batch.enabled)
23
22
  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');
23
+ const { userId, user } = await requireSession(req);
30
24
  const [usage, limits] = await Promise.all([getUserStats(userId), getLimits(userId)]).catch(withError('Could not fetch usage and/or limits'));
31
25
  const batchHeaderSize = Number(req.headers.get('x-batch-header-size'));
32
26
  if (!Number.isSafeInteger(batchHeaderSize) || batchHeaderSize < 2)
@@ -60,12 +54,11 @@ addRoute({
60
54
  .selectFrom('storage')
61
55
  .selectAll()
62
56
  .where('id', 'in', [...deletedIds, ...Object.keys(header.metadata), ...changedIds])
63
- .select(acl.from('storage', { onlyId: userId }))
57
+ .select(acl.from('storage', { user }))
64
58
  .$castTo()
65
59
  .execute()
66
60
  .catch(withError('Item(s) not found', 404));
67
61
  for (const item of items) {
68
- const permission = !changedIds.has(item.id) ? Permission.Manage : Permission.Edit;
69
62
  if (changedIds.has(item.id)) {
70
63
  // Extra checks for content changes
71
64
  if (item.immutable)
@@ -77,19 +70,11 @@ addRoute({
77
70
  if (limits.item_size && size > limits.item_size * 1_000_000)
78
71
  error(413, 'Item size exceeds maximum size: ' + item.id);
79
72
  }
80
- if (item.publicPermission >= permission)
81
- continue;
82
73
  if (userId == item.userId)
83
74
  continue;
84
75
  if (!item.acl || !item.acl.length)
85
76
  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;
77
+ acl.check(item.acl, changedIds.has(item.id) ? { write: true } : { manage: true });
93
78
  error(403, 'Missing permission for item: ' + item.id);
94
79
  }
95
80
  if (limits.user_size && (usage.usedBytes + size - items.reduce((sum, item) => sum + item.size, 0)) / 1_000_000 >= limits.user_size)
@@ -118,8 +103,6 @@ addRoute({
118
103
  results.set(item.id, parseItem(item));
119
104
  for (const [itemId, update] of Object.entries(header.metadata)) {
120
105
  const values = {};
121
- if ('publicPermission' in update)
122
- values.publicPermission = update.publicPermission;
123
106
  if ('trash' in update)
124
107
  values.trashedAt = update.trash ? new Date() : null;
125
108
  if ('owner' in update)
@@ -1,6 +1,11 @@
1
- import { formatBytes } from '@axium/core/format';
1
+ import { formatBytes, parseByteSize } from '@axium/core';
2
+ import { io } from '@axium/core/node';
3
+ import { lookupUser } from '@axium/server/cli';
2
4
  import { count, database } from '@axium/server/database';
3
- import { program } from 'commander';
5
+ import { Option, program } from 'commander';
6
+ import { styleText } from 'node:util';
7
+ import * as z from 'zod';
8
+ import { parseItem } from './db.js';
4
9
  const cli = program.command('files').helpGroup('Plugins:').description('CLI integration for @axium/storage');
5
10
  cli.command('usage')
6
11
  .description('Show storage usage information')
@@ -12,3 +17,80 @@ cli.command('usage')
12
17
  .executeTakeFirstOrThrow();
13
18
  console.log(`${items} items totaling ${formatBytes(Number(size))}`);
14
19
  });
20
+ const _byteSize = (msg) => (v) => parseByteSize(v) ?? io.exit(msg);
21
+ cli.command('query')
22
+ .alias('q')
23
+ .alias('find')
24
+ .description('Find storage items')
25
+ .option('-n, --name <name>', 'Filter by name')
26
+ .option('-t, --type <type>', 'Filter by MIME type')
27
+ .addOption(new Option('-u, --user <user>', 'Filter by user UUID or email').argParser(lookupUser))
28
+ .option('-m, --min-size <size>', 'Filter by minimum size', _byteSize('Invalid minimum size.'))
29
+ .option('-M, --max-size <size>', 'Filter by maximum size', _byteSize('Invalid maximum size.'))
30
+ .addOption(new Option('--size', 'Filter by exact size').conflicts(['minSize', 'maxSize']).argParser(_byteSize('Invalid size.')))
31
+ .option('-l, --limit <n>', 'Limit the number of results', (v) => z.coerce.number().int().min(1).max(1000).parse(v), 100)
32
+ .option('-j, --json', 'Output results as JSON', false)
33
+ .addOption(new Option('-f, --format <format>', 'How to format output lines').conflicts('json').default('{id} {type} {size} {userId} {name}'))
34
+ .action(async (opt) => {
35
+ let query = database
36
+ .selectFrom('storage')
37
+ .selectAll()
38
+ .limit(opt.limit + 1);
39
+ if (opt.name)
40
+ query = query.where('name', 'like', `%${opt.name}%`);
41
+ if (opt.type) {
42
+ const hasWildcard = opt.type.endsWith('/*') || opt.type.startsWith('*/');
43
+ query = query.where('type', hasWildcard ? 'like' : '=', !hasWildcard ? opt.type : opt.type.replace('*', '%'));
44
+ }
45
+ if (opt.user) {
46
+ const user = await opt.user;
47
+ query = query.where('userId', '=', user.id);
48
+ }
49
+ let validMinSize = false;
50
+ if (opt.minSize !== undefined) {
51
+ if (opt.minSize == 0)
52
+ io.warn('Minimum size of 0 has no effect, ignoring.');
53
+ else {
54
+ query = query.where('size', '>=', opt.minSize);
55
+ validMinSize = true;
56
+ }
57
+ }
58
+ if (opt.maxSize) {
59
+ if (validMinSize && opt.maxSize < opt.minSize)
60
+ io.exit('Maximum size cannot be smaller than minimum size.');
61
+ query = query.where('size', '<=', opt.maxSize);
62
+ }
63
+ if (opt.size !== undefined) {
64
+ query = query.where('size', '=', opt.size);
65
+ }
66
+ const rawItems = await query.execute().catch(io.handleError);
67
+ const items = rawItems.map(parseItem);
68
+ if (!items.length) {
69
+ console.log(styleText(['italic', 'dim'], 'No storage items match the provided filters.'));
70
+ process.exit(2);
71
+ }
72
+ if (items.length > opt.limit) {
73
+ items.pop();
74
+ io.warn('Showing first', opt.limit, 'results, others have been omitted.');
75
+ }
76
+ if (opt.json) {
77
+ console.log(JSON.stringify(items, null, 4));
78
+ return;
79
+ }
80
+ let maxTypeLength = 0;
81
+ for (const item of items) {
82
+ maxTypeLength = Math.max(maxTypeLength, item.type.length);
83
+ }
84
+ for (const item of items) {
85
+ const replacements = Object.assign(Object.create(null), {
86
+ id: item.id,
87
+ type: item.type.padStart(maxTypeLength),
88
+ userId: item.userId,
89
+ size: styleText('blueBright', item.type == 'inode/directory' ? ' - ' : formatBytes(item.size).padStart(9)),
90
+ name: styleText('yellow', JSON.stringify(item.name)),
91
+ modified: new Date(item.modifiedAt).toISOString(),
92
+ });
93
+ const text = opt.format.replaceAll(/(?<!\\)\{(\w+)\}/g, (_, key) => (key in replacements ? replacements[key] : `{${key}}`));
94
+ console.log(text);
95
+ }
96
+ });
@@ -2,7 +2,6 @@ import { type Schema } from '@axium/server/database';
2
2
  import type { Generated, Selectable } from 'kysely';
3
3
  import type { StorageItemMetadata, StorageStats } from '../common.js';
4
4
  import '../polyfills.js';
5
- import type { Permission } from '@axium/core';
6
5
  declare module '@axium/server/database' {
7
6
  interface Schema {
8
7
  storage: {
@@ -17,12 +16,9 @@ declare module '@axium/server/database' {
17
16
  trashedAt: Date | null;
18
17
  type: string;
19
18
  userId: string;
20
- publicPermission: Generated<Permission>;
21
19
  metadata: Generated<Record<string, unknown>>;
22
20
  };
23
- }
24
- interface ExpectedSchema {
25
- storage: ColumnTypes<Schema['storage']>;
21
+ 'acl.storage': DBAccessControl & DBBool<'read' | 'write' | 'manage' | 'download' | 'comment'>;
26
22
  }
27
23
  }
28
24
  /**
package/dist/server/db.js CHANGED
@@ -1,24 +1,9 @@
1
1
  import { config } from '@axium/server/config';
2
- import { database, expectedTypes } from '@axium/server/database';
2
+ import { database } from '@axium/server/database';
3
3
  import { withError } from '@axium/server/requests';
4
4
  import { unlinkSync } from 'node:fs';
5
5
  import { join } from 'node:path/posix';
6
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
7
  export function parseItem(item) {
23
8
  return {
24
9
  ...item,
@@ -1,8 +1,5 @@
1
- import type { InitOptions, OpOptions } from '@axium/server/database';
1
+ import type { OpOptions } from '@axium/server/database';
2
2
  import '../common.js';
3
3
  import './index.js';
4
4
  export declare function statusText(): Promise<string>;
5
- export declare function db_init(opt: InitOptions): Promise<void>;
6
- export declare function db_wipe(opt: OpOptions): Promise<void>;
7
- export declare function remove(opt: OpOptions): Promise<void>;
8
5
  export declare function clean(opt: OpOptions): Promise<void>;
@@ -1,9 +1,7 @@
1
1
  import { formatBytes } from '@axium/core/format';
2
2
  import { done, start } from '@axium/core/node/io';
3
- import * as acl from '@axium/server/acl';
4
3
  import config from '@axium/server/config';
5
- import { count, createIndex, database, warnExists } from '@axium/server/database';
6
- import { sql } from 'kysely';
4
+ import { count, database } from '@axium/server/database';
7
5
  import { mkdirSync } from 'node:fs';
8
6
  import '../common.js';
9
7
  import './index.js';
@@ -16,41 +14,6 @@ export async function statusText() {
16
14
  .executeTakeFirstOrThrow();
17
15
  return `${items} items totaling ${formatBytes(Number(size))}`;
18
16
  }
19
- export async function db_init(opt) {
20
- start('Creating table storage');
21
- await database.schema
22
- .createTable('storage')
23
- .addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
24
- .addColumn('userId', 'uuid', col => col.notNull().references('users.id').onDelete('cascade'))
25
- .addColumn('parentId', 'uuid', col => col.references('storage.id').onDelete('cascade').defaultTo(null))
26
- .addColumn('createdAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
27
- .addColumn('modifiedAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
28
- .addColumn('size', 'integer', col => col.notNull())
29
- .addColumn('trashedAt', 'timestamptz', col => col.defaultTo(null))
30
- .addColumn('hash', 'bytea', col => col)
31
- .addColumn('name', 'text', col => col.defaultTo(null))
32
- .addColumn('type', 'text', col => col.notNull())
33
- .addColumn('immutable', 'boolean', col => col.notNull())
34
- .addColumn('publicPermission', 'integer', col => col.notNull().defaultTo(0))
35
- .addColumn('metadata', 'jsonb', col => col.notNull().defaultTo('{}'))
36
- .execute()
37
- .then(done)
38
- .catch(warnExists);
39
- await createIndex('storage', 'userId');
40
- await createIndex('storage', 'parentId');
41
- await acl.createTable('storage');
42
- }
43
- export async function db_wipe(opt) {
44
- start('Removing data from user storage');
45
- await database.deleteFrom('storage').execute().then(done);
46
- await acl.wipeTable('storage');
47
- }
48
- export async function remove(opt) {
49
- start('Dropping table storage');
50
- await database.schema.dropTable('storage').execute();
51
- await acl.dropTable('storage');
52
- done();
53
- }
54
17
  export async function clean(opt) {
55
18
  start('Removing expired trash items');
56
19
  const nDaysAgo = new Date(Date.now() - 86400000 * config.storage.trash_duration);
@@ -1,9 +1,8 @@
1
- import { Permission } from '@axium/core';
2
1
  import { audit } from '@axium/server/audit';
3
- import { checkAuthForItem, getSessionAndUser } from '@axium/server/auth';
2
+ import { checkAuthForItem, requireSession } from '@axium/server/auth';
4
3
  import { config } from '@axium/server/config';
5
4
  import { database } from '@axium/server/database';
6
- import { error, getToken, withError } from '@axium/server/requests';
5
+ import { error, withError } from '@axium/server/requests';
7
6
  import { addRoute } from '@axium/server/routes';
8
7
  import { createHash } from 'node:crypto';
9
8
  import { linkSync, readFileSync, writeFileSync } from 'node:fs';
@@ -17,10 +16,7 @@ addRoute({
17
16
  async PUT(request) {
18
17
  if (!config.storage.enabled)
19
18
  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));
19
+ const { userId } = await requireSession(request);
24
20
  const [usage, limits] = await Promise.all([getUserStats(userId), getLimits(userId)]).catch(withError('Could not fetch usage and/or limits'));
25
21
  const name = request.headers.get('x-name');
26
22
  if (!name)
@@ -35,7 +31,7 @@ addRoute({
35
31
  .catch(() => error(400, 'Invalid parent ID'))
36
32
  : null;
37
33
  if (parentId)
38
- await checkAuthForItem(request, 'storage', parentId, Permission.Edit);
34
+ await checkAuthForItem(request, 'storage', parentId, { write: true });
39
35
  const size = Number(request.headers.get('content-length'));
40
36
  if (Number.isNaN(size))
41
37
  error(411, 'Missing or invalid content length header');
@@ -98,11 +94,10 @@ addRoute({
98
94
  addRoute({
99
95
  path: '/raw/storage/:id',
100
96
  params: { id: z.uuid() },
101
- async GET(request, params) {
97
+ async GET(request, { id: itemId }) {
102
98
  if (!config.storage.enabled)
103
99
  error(503, 'User storage is disabled');
104
- const itemId = params.id;
105
- const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
100
+ const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
106
101
  if (item.trashedAt)
107
102
  error(410, 'Trashed items can not be downloaded');
108
103
  const content = new Uint8Array(readFileSync(join(config.storage.data, item.id)));
@@ -113,11 +108,10 @@ addRoute({
113
108
  },
114
109
  });
115
110
  },
116
- async POST(request, params) {
111
+ async POST(request, { id: itemId }) {
117
112
  if (!config.storage.enabled)
118
113
  error(503, 'User storage is disabled');
119
- const itemId = params.id;
120
- const { item, session } = await checkAuthForItem(request, 'storage', itemId, Permission.Edit);
114
+ const { item, session } = await checkAuthForItem(request, 'storage', itemId, { write: true });
121
115
  if (item.immutable)
122
116
  error(405, 'Item is immutable');
123
117
  if (item.type == 'inode/directory')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "User file storage for Axium",
6
6
  "funding": {
@@ -32,15 +32,16 @@
32
32
  "dist",
33
33
  "lib",
34
34
  "build",
35
- "routes"
35
+ "routes",
36
+ "db.json"
36
37
  ],
37
38
  "scripts": {
38
39
  "build": "tsc"
39
40
  },
40
41
  "peerDependencies": {
41
- "@axium/client": ">=0.7.0",
42
- "@axium/core": ">=0.11.0",
43
- "@axium/server": ">=0.27.0",
42
+ "@axium/client": ">=0.9.0",
43
+ "@axium/core": ">=0.12.0",
44
+ "@axium/server": ">=0.28.0",
44
45
  "@sveltejs/kit": "^2.27.3",
45
46
  "utilium": "^2.3.8"
46
47
  },
@@ -52,7 +53,8 @@
52
53
  "http_handler": "./build/handler.js",
53
54
  "hooks": "./dist/server/hooks.js",
54
55
  "routes": "./routes",
55
- "cli": "./dist/server/cli.js"
56
+ "cli": "./dist/server/cli.js",
57
+ "db": "./db.json"
56
58
  },
57
59
  "client": {
58
60
  "cli": "./dist/client/cli.js",
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
- import { Icon, NumberBar } from '@axium/client/components';
2
+ import { NumberBar } from '@axium/client/components';
3
+ import '@axium/client/styles/list';
3
4
  import { formatBytes } from '@axium/core/format';
4
5
  import { StorageList } from '@axium/storage/components';
5
- import '@axium/client/styles/list';
6
6
 
7
7
  const { data } = $props();
8
8
  const { limits } = data.info;
@@ -10,7 +10,6 @@
10
10
  let items = $state(data.info.items.filter(i => i.type != 'inode/directory').sort((a, b) => Math.sign(b.size - a.size)));
11
11
  const usedBytes = $state(data.info.usedBytes);
12
12
 
13
- let dialogs = $state<Record<string, HTMLDialogElement>>({});
14
13
  let barText = $derived(`Using ${formatBytes(usedBytes)} ${limits.user_size ? 'of ' + formatBytes(limits.user_size * 1_000_000) : ''}`);
15
14
  </script>
16
15
 
@@ -18,12 +17,6 @@
18
17
  <title>Your Storage Usage</title>
19
18
  </svelte:head>
20
19
 
21
- {#snippet action(name: string, i: string = 'pen')}
22
- <span class="action" onclick={() => dialogs[name].showModal()}>
23
- <Icon {i} --size="16px" />
24
- </span>
25
- {/snippet}
26
-
27
20
  <h2>Storage Usage</h2>
28
21
 
29
22
  <p><NumberBar max={limits.user_size * 1_000_000} value={usedBytes} text={barText} /></p>