@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 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
- // @todo: add this to the audit log
201
- if (content.byteLength > size)
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
- // @todo: add this to the audit log
279
- if (type != item.type)
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
- // @todo: add this to the audit log
291
- if (content.byteLength > size)
292
- error(400, 'Content length does not match size header');
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
@@ -7,7 +7,7 @@
7
7
  import '../styles/list.css';
8
8
 
9
9
  let {
10
- items = $bindable([]),
10
+ items = $bindable(),
11
11
  appMode,
12
12
  emptyText = 'Folder is empty.',
13
13
  }: { appMode?: boolean; items: StorageItemMetadata[]; emptyText?: string } = $props();
package/lib/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { default as StorageAdd } from './Add.svelte';
1
2
  export { default as StorageList } from './List.svelte';
2
3
  export { default as StorageSidebar } from './Sidebar.svelte';
3
4
  export { default as StorageSidebarItem } from './SidebarItem.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.6.1",
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.20.2",
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 }: LayoutProps = $props();
6
+ let { children, data } = $props();
8
7
  </script>
9
8
 
10
9
  <div class="app">
@@ -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 }: PageProps = $props();
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={data.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}