@axium/storage 0.6.1 → 0.6.3
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/dist/server.d.ts +13 -0
- package/dist/server.js +14 -9
- package/lib/Add.svelte +93 -0
- package/lib/List.svelte +1 -1
- package/lib/index.ts +1 -0
- package/package.json +2 -2
- package/routes/files/+layout.svelte +1 -2
- package/routes/files/+layout.ts +1 -4
- package/routes/files/+page.svelte +5 -4
- package/routes/files/[id]/+page.svelte +2 -1
package/dist/server.d.ts
CHANGED
|
@@ -54,6 +54,19 @@ declare module '@axium/server/config' {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
declare module '@axium/server/audit' {
|
|
58
|
+
interface $EventTypes {
|
|
59
|
+
storage_type_mismatch: {
|
|
60
|
+
/** The ID of the target item */
|
|
61
|
+
item: string;
|
|
62
|
+
};
|
|
63
|
+
/** Mismatch between the actual size of an upload and the size reported in the header */
|
|
64
|
+
storage_size_mismatch: {
|
|
65
|
+
/** ID of the target item, null for new uploads */
|
|
66
|
+
item: string | null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
57
70
|
export interface StorageItem extends StorageItemMetadata {
|
|
58
71
|
data: Uint8Array<ArrayBufferLike>;
|
|
59
72
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/unbound-method */
|
|
2
1
|
import { Permission } from '@axium/core/access';
|
|
2
|
+
import { addEvent, audit, Severity } from '@axium/server/audit';
|
|
3
3
|
import { checkAuthForItem, checkAuthForUser, getSessionAndUser } from '@axium/server/auth';
|
|
4
4
|
import { addConfigDefaults, config } from '@axium/server/config';
|
|
5
5
|
import { database, expectedTypes } from '@axium/server/database';
|
|
@@ -27,6 +27,8 @@ expectedTypes.storage = {
|
|
|
27
27
|
publicPermission: { type: 'int4', required: true, hasDefault: true },
|
|
28
28
|
metadata: { type: 'jsonb', required: true, hasDefault: true },
|
|
29
29
|
};
|
|
30
|
+
addEvent({ source: '@axium/storage', name: 'storage_type_mismatch', severity: Severity.Warning, tags: ['mimetype'] });
|
|
31
|
+
addEvent({ source: '@axium/storage', name: 'storage_size_mismatch', severity: Severity.Warning, tags: [] });
|
|
30
32
|
const defaultCASMime = [/video\/.*/, /audio\/.*/];
|
|
31
33
|
addConfigDefaults({
|
|
32
34
|
storage: {
|
|
@@ -197,9 +199,10 @@ addRoute({
|
|
|
197
199
|
if (size > limits.item_size * 1_000_000)
|
|
198
200
|
error(413, 'File size exceeds maximum size');
|
|
199
201
|
const content = await event.request.bytes();
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
if (content.byteLength > size) {
|
|
203
|
+
await audit('storage_size_mismatch', userId, { item: null });
|
|
202
204
|
error(400, 'Content length does not match size header');
|
|
205
|
+
}
|
|
203
206
|
const type = event.request.headers.get('content-type') || 'application/octet-stream';
|
|
204
207
|
const isDirectory = type == 'inode/directory';
|
|
205
208
|
if (isDirectory && size > 0)
|
|
@@ -267,7 +270,7 @@ addRoute({
|
|
|
267
270
|
if (!config.storage.enabled)
|
|
268
271
|
error(503, 'User storage is disabled');
|
|
269
272
|
const itemId = event.params.id;
|
|
270
|
-
const { item } = await checkAuthForItem(event, 'storage', itemId, Permission.Edit);
|
|
273
|
+
const { item, session } = await checkAuthForItem(event, 'storage', itemId, Permission.Edit);
|
|
271
274
|
if (item.immutable)
|
|
272
275
|
error(405, 'Item is immutable');
|
|
273
276
|
if (item.type == 'inode/directory')
|
|
@@ -275,9 +278,10 @@ addRoute({
|
|
|
275
278
|
if (item.trashedAt)
|
|
276
279
|
error(410, 'Trashed items can not be changed');
|
|
277
280
|
const type = event.request.headers.get('content-type') || 'application/octet-stream';
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
if (type != item.type) {
|
|
282
|
+
await audit('storage_type_mismatch', session?.userId, { item: item.id });
|
|
280
283
|
error(400, 'Content type does not match existing item type');
|
|
284
|
+
}
|
|
281
285
|
const size = Number(event.request.headers.get('content-length'));
|
|
282
286
|
if (Number.isNaN(size))
|
|
283
287
|
error(411, 'Missing or invalid content length header');
|
|
@@ -287,9 +291,10 @@ addRoute({
|
|
|
287
291
|
if (size > limits.item_size * 1_000_000)
|
|
288
292
|
error(413, 'File size exceeds maximum size');
|
|
289
293
|
const content = await event.request.bytes();
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
error(400, '
|
|
294
|
+
if (content.byteLength > size) {
|
|
295
|
+
await audit('storage_size_mismatch', session?.userId, { item: item.id });
|
|
296
|
+
error(400, 'Actual content length does not match header');
|
|
297
|
+
}
|
|
293
298
|
const hash = createHash('BLAKE2b512').update(content).digest();
|
|
294
299
|
const tx = await database.startTransaction().execute();
|
|
295
300
|
try {
|
package/lib/Add.svelte
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { forMime } from '@axium/core/icons';
|
|
3
|
+
import { FormDialog, Icon, Popover, Upload } from '@axium/server/components';
|
|
4
|
+
import { uploadItem } from '@axium/storage/client';
|
|
5
|
+
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
6
|
+
|
|
7
|
+
const { parentId, onadd }: { parentId?: string; onadd?(item: StorageItemMetadata): void } = $props();
|
|
8
|
+
|
|
9
|
+
let uploadDialog = $state<HTMLDialogElement>()!;
|
|
10
|
+
let input = $state<HTMLInputElement>();
|
|
11
|
+
|
|
12
|
+
let createDialog = $state<HTMLDialogElement>()!;
|
|
13
|
+
let createType = $state<string>();
|
|
14
|
+
let createIncludesContent = $state(false);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#snippet _item(type: string, text: string, includeContent: boolean = false)}
|
|
18
|
+
<span
|
|
19
|
+
class="icon-text add-menu-item"
|
|
20
|
+
onclick={() => {
|
|
21
|
+
createType = type;
|
|
22
|
+
createIncludesContent = includeContent;
|
|
23
|
+
createDialog.showModal();
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<Icon i={forMime(type)} />
|
|
27
|
+
{text}
|
|
28
|
+
</span>
|
|
29
|
+
{/snippet}
|
|
30
|
+
|
|
31
|
+
<Popover>
|
|
32
|
+
{#snippet toggle()}
|
|
33
|
+
<button class="icon-text"><Icon i="plus" />Add</button>
|
|
34
|
+
{/snippet}
|
|
35
|
+
|
|
36
|
+
<div class="add-menu">
|
|
37
|
+
<span class="icon-text add-menu-item" onclick={() => uploadDialog.showModal()}><Icon i="upload" />Upload</span>
|
|
38
|
+
{@render _item('inode/directory', 'New Folder')}
|
|
39
|
+
{@render _item('text/plain', 'Plain Text')}
|
|
40
|
+
{@render _item('text/x-uri', 'URL', true)}
|
|
41
|
+
</div>
|
|
42
|
+
</Popover>
|
|
43
|
+
|
|
44
|
+
<FormDialog
|
|
45
|
+
bind:dialog={uploadDialog}
|
|
46
|
+
submitText="Upload"
|
|
47
|
+
submit={async () => {
|
|
48
|
+
for (const file of input?.files!) {
|
|
49
|
+
const item = await uploadItem(file, { parentId });
|
|
50
|
+
onadd?.(item);
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<Upload bind:input multiple />
|
|
55
|
+
</FormDialog>
|
|
56
|
+
|
|
57
|
+
<FormDialog
|
|
58
|
+
bind:dialog={createDialog}
|
|
59
|
+
submitText="Create"
|
|
60
|
+
submit={async (data: { name: string; content?: string }) => {
|
|
61
|
+
const file = new File(createIncludesContent ? [data.content!] : [], data.name, { type: createType });
|
|
62
|
+
const item = await uploadItem(file, { parentId });
|
|
63
|
+
onadd?.(item);
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<div>
|
|
67
|
+
<label for="name">Name</label>
|
|
68
|
+
<input name="name" type="text" required />
|
|
69
|
+
</div>
|
|
70
|
+
{#if createIncludesContent}
|
|
71
|
+
<div>
|
|
72
|
+
<label for="content">Content</label>
|
|
73
|
+
<input name="content" type="text" size="40" required />
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
</FormDialog>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.add-menu {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.add-menu-item {
|
|
85
|
+
border-radius: 0.5em;
|
|
86
|
+
padding: 0.25em 0.25em;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.add-menu-item:hover {
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
background-color: #4455;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
package/lib/List.svelte
CHANGED
package/lib/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/storage",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
|
|
5
5
|
"description": "User file storage for Axium",
|
|
6
6
|
"funding": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@axium/client": ">=0.1.0",
|
|
42
42
|
"@axium/core": ">=0.5.0",
|
|
43
|
-
"@axium/server": ">=0.
|
|
43
|
+
"@axium/server": ">=0.21.0",
|
|
44
44
|
"@sveltejs/kit": "^2.27.3",
|
|
45
45
|
"utilium": "^2.3.8"
|
|
46
46
|
},
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { Icon } from '@axium/server/components';
|
|
3
3
|
import { StorageUsage } from '@axium/storage/components';
|
|
4
4
|
import { capitalize } from 'utilium';
|
|
5
|
-
import type { LayoutProps } from './$types';
|
|
6
5
|
|
|
7
|
-
let { children, data }
|
|
6
|
+
let { children, data } = $props();
|
|
8
7
|
</script>
|
|
9
8
|
|
|
10
9
|
<div class="app">
|
package/routes/files/+layout.ts
CHANGED
|
@@ -15,8 +15,5 @@ export async function load({ url, route }: LayoutLoadEvent) {
|
|
|
15
15
|
{ name: 'shared', href: '/files/shared', icon: 'user-group', active: route.id.endsWith('/files/shared') },
|
|
16
16
|
] satisfies { name: string; href: LayoutRouteId; icon: string; active: boolean }[];
|
|
17
17
|
|
|
18
|
-
return {
|
|
19
|
-
session: await getCurrentSession(),
|
|
20
|
-
tabs,
|
|
21
|
-
};
|
|
18
|
+
return { session, tabs };
|
|
22
19
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { StorageList } from '@axium/storage/components';
|
|
3
|
-
import type { PageProps } from './$types';
|
|
2
|
+
import { StorageAdd, StorageList } from '@axium/storage/components';
|
|
4
3
|
|
|
5
|
-
const { data }
|
|
4
|
+
const { data } = $props();
|
|
5
|
+
let items = $state(data.items!);
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<svelte:head>
|
|
9
9
|
<title>Files</title>
|
|
10
10
|
</svelte:head>
|
|
11
11
|
|
|
12
|
-
<StorageList appMode bind:items
|
|
12
|
+
<StorageList appMode bind:items />
|
|
13
|
+
<StorageAdd onadd={item => items.push(item)} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Icon } from '@axium/server/components';
|
|
3
|
-
import { StorageList } from '@axium/storage/components';
|
|
3
|
+
import { StorageAdd, StorageList } from '@axium/storage/components';
|
|
4
4
|
import type { PageProps } from './$types';
|
|
5
5
|
import { updateItemMetadata } from '@axium/storage/client';
|
|
6
6
|
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
</button>
|
|
27
27
|
{:else if item.type == 'inode/directory'}
|
|
28
28
|
<StorageList appMode bind:items />
|
|
29
|
+
<StorageAdd parentId={item.id} onadd={item => items.push(item)} />
|
|
29
30
|
{:else}
|
|
30
31
|
<p>No preview available.</p>
|
|
31
32
|
{/if}
|