@axium/storage 0.9.0 → 0.11.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.
- package/db.json +68 -0
- package/dist/client/local.d.ts +0 -13
- package/dist/common.d.ts +0 -39
- package/dist/common.js +0 -3
- package/dist/node.js +1 -3
- package/dist/server/api.js +22 -35
- package/dist/server/batch.js +6 -23
- package/dist/server/cli.js +84 -2
- package/dist/server/db.d.ts +1 -5
- package/dist/server/db.js +1 -16
- package/dist/server/hooks.d.ts +1 -5
- package/dist/server/hooks.js +2 -41
- package/dist/server/raw.js +8 -14
- package/lib/List.svelte +4 -3
- package/package.json +8 -6
- package/routes/files/usage/+page.svelte +2 -9
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
|
+
}
|
package/dist/client/local.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -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 {
|
|
11
|
+
import { deleteRecursive, getRecursive, getUserStats, parseItem } from './db.js';
|
|
13
12
|
addRoute({
|
|
14
13
|
path: '/api/storage',
|
|
15
14
|
OPTIONS() {
|
|
@@ -23,22 +22,18 @@ addRoute({
|
|
|
23
22
|
addRoute({
|
|
24
23
|
path: '/api/storage/item/:id',
|
|
25
24
|
params: { id: z.uuid() },
|
|
26
|
-
async GET(request,
|
|
25
|
+
async GET(request, { id: itemId }) {
|
|
27
26
|
if (!config.storage.enabled)
|
|
28
27
|
error(503, 'User storage is disabled');
|
|
29
|
-
const
|
|
30
|
-
const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
|
|
28
|
+
const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
|
|
31
29
|
return parseItem(item);
|
|
32
30
|
},
|
|
33
|
-
async PATCH(request,
|
|
31
|
+
async PATCH(request, { id: itemId }) {
|
|
34
32
|
if (!config.storage.enabled)
|
|
35
33
|
error(503, 'User storage is disabled');
|
|
36
|
-
const itemId = params.id;
|
|
37
34
|
const body = await parseBody(request, StorageItemUpdate);
|
|
38
|
-
await checkAuthForItem(request, 'storage', itemId,
|
|
35
|
+
await checkAuthForItem(request, 'storage', itemId, { manage: true });
|
|
39
36
|
const values = {};
|
|
40
|
-
if ('publicPermission' in body)
|
|
41
|
-
values.publicPermission = body.publicPermission;
|
|
42
37
|
if ('trash' in body)
|
|
43
38
|
values.trashedAt = body.trash ? new Date() : null;
|
|
44
39
|
if ('owner' in body)
|
|
@@ -55,11 +50,10 @@ addRoute({
|
|
|
55
50
|
.executeTakeFirstOrThrow()
|
|
56
51
|
.catch(withError('Could not update item')));
|
|
57
52
|
},
|
|
58
|
-
async DELETE(request,
|
|
53
|
+
async DELETE(request, { id: itemId }) {
|
|
59
54
|
if (!config.storage.enabled)
|
|
60
55
|
error(503, 'User storage is disabled');
|
|
61
|
-
const
|
|
62
|
-
const auth = await checkAuthForItem(request, 'storage', itemId, Permission.Manage);
|
|
56
|
+
const auth = await checkAuthForItem(request, 'storage', itemId, { manage: true });
|
|
63
57
|
const item = parseItem(auth.item);
|
|
64
58
|
await deleteRecursive(item.type != 'inode/directory', itemId);
|
|
65
59
|
return item;
|
|
@@ -68,11 +62,10 @@ addRoute({
|
|
|
68
62
|
addRoute({
|
|
69
63
|
path: '/api/storage/directory/:id',
|
|
70
64
|
params: { id: z.uuid() },
|
|
71
|
-
async GET(request,
|
|
65
|
+
async GET(request, { id: itemId }) {
|
|
72
66
|
if (!config.storage.enabled)
|
|
73
67
|
error(503, 'User storage is disabled');
|
|
74
|
-
const
|
|
75
|
-
const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
|
|
68
|
+
const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
|
|
76
69
|
if (item.type != 'inode/directory')
|
|
77
70
|
error(409, 'Item is not a directory');
|
|
78
71
|
const items = await database
|
|
@@ -87,11 +80,10 @@ addRoute({
|
|
|
87
80
|
addRoute({
|
|
88
81
|
path: '/api/storage/directory/:id/recursive',
|
|
89
82
|
params: { id: z.uuid() },
|
|
90
|
-
async GET(request,
|
|
83
|
+
async GET(request, { id: itemId }) {
|
|
91
84
|
if (!config.storage.enabled)
|
|
92
85
|
error(503, 'User storage is disabled');
|
|
93
|
-
const
|
|
94
|
-
const { item } = await checkAuthForItem(request, 'storage', itemId, Permission.Read);
|
|
86
|
+
const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
|
|
95
87
|
if (item.type != 'inode/directory')
|
|
96
88
|
error(409, 'Item is not a directory');
|
|
97
89
|
const items = await Array.fromAsync(getRecursive(itemId)).catch(withError('Could not get some directory items'));
|
|
@@ -101,18 +93,16 @@ addRoute({
|
|
|
101
93
|
addRoute({
|
|
102
94
|
path: '/api/users/:id/storage',
|
|
103
95
|
params: { id: z.uuid() },
|
|
104
|
-
async OPTIONS(request,
|
|
96
|
+
async OPTIONS(request, { id: userId }) {
|
|
105
97
|
if (!config.storage.enabled)
|
|
106
98
|
error(503, 'User storage is disabled');
|
|
107
|
-
const userId = params.id;
|
|
108
99
|
await checkAuthForUser(request, userId);
|
|
109
100
|
const [stats, limits] = await Promise.all([getUserStats(userId), getLimits(userId)]).catch(withError('Could not fetch data'));
|
|
110
101
|
return Object.assign(stats, { limits });
|
|
111
102
|
},
|
|
112
|
-
async GET(request,
|
|
103
|
+
async GET(request, { id: userId }) {
|
|
113
104
|
if (!config.storage.enabled)
|
|
114
105
|
error(503, 'User storage is disabled');
|
|
115
|
-
const userId = params.id;
|
|
116
106
|
await checkAuthForUser(request, userId);
|
|
117
107
|
const [items, stats, limits] = await Promise.all([
|
|
118
108
|
database.selectFrom('storage').where('userId', '=', userId).where('trashedAt', 'is', null).selectAll().execute(),
|
|
@@ -125,10 +115,9 @@ addRoute({
|
|
|
125
115
|
addRoute({
|
|
126
116
|
path: '/api/users/:id/storage/root',
|
|
127
117
|
params: { id: z.uuid() },
|
|
128
|
-
async GET(request,
|
|
118
|
+
async GET(request, { id: userId }) {
|
|
129
119
|
if (!config.storage.enabled)
|
|
130
120
|
error(503, 'User storage is disabled');
|
|
131
|
-
const userId = params.id;
|
|
132
121
|
await checkAuthForUser(request, userId);
|
|
133
122
|
const items = await database
|
|
134
123
|
.selectFrom('storage')
|
|
@@ -141,27 +130,26 @@ addRoute({
|
|
|
141
130
|
return items.map(parseItem);
|
|
142
131
|
},
|
|
143
132
|
});
|
|
144
|
-
function existsInACL(column,
|
|
133
|
+
function existsInACL(column, user) {
|
|
145
134
|
return (eb) => eb.exists(eb
|
|
146
135
|
.selectFrom('acl.storage')
|
|
147
136
|
.whereRef('itemId', '=', `item.${column}`)
|
|
148
|
-
.where('userId', '=',
|
|
149
|
-
.where('
|
|
137
|
+
.where('userId', '=', user.id)
|
|
138
|
+
.where(eb => eb.or([eb('userId', '=', user.id), eb('role', 'in', user.roles), eb('tag', 'in', user.tags)])));
|
|
150
139
|
}
|
|
151
140
|
addRoute({
|
|
152
141
|
path: '/api/users/:id/storage/shared',
|
|
153
142
|
params: { id: z.uuid() },
|
|
154
|
-
async GET(request,
|
|
143
|
+
async GET(request, { id: userId }) {
|
|
155
144
|
if (!config.storage.enabled)
|
|
156
145
|
error(503, 'User storage is disabled');
|
|
157
|
-
const
|
|
158
|
-
await checkAuthForUser(request, userId);
|
|
146
|
+
const { user } = await checkAuthForUser(request, userId);
|
|
159
147
|
const items = await database
|
|
160
148
|
.selectFrom('storage as item')
|
|
161
149
|
.selectAll('item')
|
|
162
150
|
.where('trashedAt', 'is', null)
|
|
163
|
-
.where(existsInACL('id',
|
|
164
|
-
.where(eb => eb.not(existsInACL('parentId',
|
|
151
|
+
.where(existsInACL('id', user))
|
|
152
|
+
.where(eb => eb.not(existsInACL('parentId', user)))
|
|
165
153
|
.execute()
|
|
166
154
|
.catch(withError('Could not get storage items'));
|
|
167
155
|
return items.map(parseItem);
|
|
@@ -170,10 +158,9 @@ addRoute({
|
|
|
170
158
|
addRoute({
|
|
171
159
|
path: '/api/users/:id/storage/trash',
|
|
172
160
|
params: { id: z.uuid() },
|
|
173
|
-
async GET(request,
|
|
161
|
+
async GET(request, { id: userId }) {
|
|
174
162
|
if (!config.storage.enabled)
|
|
175
163
|
error(503, 'User storage is disabled');
|
|
176
|
-
const userId = params.id;
|
|
177
164
|
await checkAuthForUser(request, userId);
|
|
178
165
|
const items = await database
|
|
179
166
|
.selectFrom('storage')
|
package/dist/server/batch.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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', {
|
|
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
|
-
|
|
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)
|
package/dist/server/cli.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { formatBytes } from '@axium/core
|
|
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
|
+
});
|
package/dist/server/db.d.ts
CHANGED
|
@@ -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
|
|
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,
|
package/dist/server/hooks.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type {
|
|
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 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
5
|
export declare function clean(opt: OpOptions): Promise<void>;
|
package/dist/server/hooks.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
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,
|
|
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';
|
|
8
|
+
mkdirSync(config.storage.data, { recursive: true });
|
|
10
9
|
export async function statusText() {
|
|
11
10
|
const { storage: items } = await count('storage');
|
|
12
11
|
const { size } = await database
|
|
@@ -15,44 +14,6 @@ export async function statusText() {
|
|
|
15
14
|
.executeTakeFirstOrThrow();
|
|
16
15
|
return `${items} items totaling ${formatBytes(Number(size))}`;
|
|
17
16
|
}
|
|
18
|
-
export function init() {
|
|
19
|
-
mkdirSync(config.storage.data, { recursive: true });
|
|
20
|
-
}
|
|
21
|
-
export async function db_init(opt) {
|
|
22
|
-
start('Creating table storage');
|
|
23
|
-
await database.schema
|
|
24
|
-
.createTable('storage')
|
|
25
|
-
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
26
|
-
.addColumn('userId', 'uuid', col => col.notNull().references('users.id').onDelete('cascade'))
|
|
27
|
-
.addColumn('parentId', 'uuid', col => col.references('storage.id').onDelete('cascade').defaultTo(null))
|
|
28
|
-
.addColumn('createdAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
|
|
29
|
-
.addColumn('modifiedAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
|
|
30
|
-
.addColumn('size', 'integer', col => col.notNull())
|
|
31
|
-
.addColumn('trashedAt', 'timestamptz', col => col.defaultTo(null))
|
|
32
|
-
.addColumn('hash', 'bytea', col => col)
|
|
33
|
-
.addColumn('name', 'text', col => col.defaultTo(null))
|
|
34
|
-
.addColumn('type', 'text', col => col.notNull())
|
|
35
|
-
.addColumn('immutable', 'boolean', col => col.notNull())
|
|
36
|
-
.addColumn('publicPermission', 'integer', col => col.notNull().defaultTo(0))
|
|
37
|
-
.addColumn('metadata', 'jsonb', col => col.notNull().defaultTo('{}'))
|
|
38
|
-
.execute()
|
|
39
|
-
.then(done)
|
|
40
|
-
.catch(warnExists);
|
|
41
|
-
await createIndex('storage', 'userId');
|
|
42
|
-
await createIndex('storage', 'parentId');
|
|
43
|
-
await acl.createTable('storage');
|
|
44
|
-
}
|
|
45
|
-
export async function db_wipe(opt) {
|
|
46
|
-
start('Removing data from user storage');
|
|
47
|
-
await database.deleteFrom('storage').execute().then(done);
|
|
48
|
-
await acl.wipeTable('storage');
|
|
49
|
-
}
|
|
50
|
-
export async function remove(opt) {
|
|
51
|
-
start('Dropping table storage');
|
|
52
|
-
await database.schema.dropTable('storage').execute();
|
|
53
|
-
await acl.dropTable('storage');
|
|
54
|
-
done();
|
|
55
|
-
}
|
|
56
17
|
export async function clean(opt) {
|
|
57
18
|
start('Removing expired trash items');
|
|
58
19
|
const nDaysAgo = new Date(Date.now() - 86400000 * config.storage.trash_duration);
|
package/dist/server/raw.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Permission } from '@axium/core';
|
|
2
1
|
import { audit } from '@axium/server/audit';
|
|
3
|
-
import { checkAuthForItem,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
97
|
+
async GET(request, { id: itemId }) {
|
|
102
98
|
if (!config.storage.enabled)
|
|
103
99
|
error(503, 'User storage is disabled');
|
|
104
|
-
const
|
|
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,
|
|
111
|
+
async POST(request, { id: itemId }) {
|
|
117
112
|
if (!config.storage.enabled)
|
|
118
113
|
error(503, 'User storage is disabled');
|
|
119
|
-
const
|
|
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/lib/List.svelte
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import { FormDialog, Icon } from '@axium/client/components';
|
|
2
|
+
import { AccessControlDialog, FormDialog, Icon } from '@axium/client/components';
|
|
4
3
|
import '@axium/client/styles/list';
|
|
5
4
|
import { formatBytes } from '@axium/core/format';
|
|
6
5
|
import { forMime as iconForMime } from '@axium/core/icons';
|
|
@@ -62,6 +61,7 @@
|
|
|
62
61
|
<span>{item.modifiedAt.toLocaleString()}</span>
|
|
63
62
|
<span>{item.type == 'inode/directory' ? '—' : formatBytes(item.size)}</span>
|
|
64
63
|
{@render action('rename', 'pencil', i)}
|
|
64
|
+
{@render action('share', 'user-group', i)}
|
|
65
65
|
{@render action('download', 'download', i)}
|
|
66
66
|
{@render action('trash', 'trash', i)}
|
|
67
67
|
</div>
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
<input name="name" type="text" required value={activeItem?.name} />
|
|
85
85
|
</div>
|
|
86
86
|
</FormDialog>
|
|
87
|
+
<AccessControlDialog bind:dialog={dialogs.share} bind:item={activeItem} itemType="storage" editable={true} />
|
|
87
88
|
<FormDialog
|
|
88
89
|
bind:dialog={dialogs.trash}
|
|
89
90
|
submitText="Trash"
|
|
@@ -114,6 +115,6 @@
|
|
|
114
115
|
|
|
115
116
|
<style>
|
|
116
117
|
.list-item {
|
|
117
|
-
grid-template-columns: 1em 4fr 15em 5em repeat(
|
|
118
|
+
grid-template-columns: 1em 4fr 15em 5em repeat(4, 1em);
|
|
118
119
|
}
|
|
119
120
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.
|
|
42
|
-
"@axium/core": ">=0.
|
|
43
|
-
"@axium/server": ">=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 {
|
|
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>
|