@axium/storage 0.4.3 → 0.5.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/dist/client.d.ts +9 -2
- package/dist/client.js +25 -1
- package/dist/common.d.ts +15 -7
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +6 -0
- package/dist/server.js +64 -1
- package/lib/{StorageList.svelte → List.svelte} +53 -41
- package/lib/{StorageSidebar.svelte → Sidebar.svelte} +2 -2
- package/lib/{StorageSidebarItem.svelte → SidebarItem.svelte} +2 -2
- package/lib/Usage.svelte +24 -0
- package/lib/index.ts +4 -3
- package/lib/tsconfig.json +1 -2
- package/package.json +5 -2
- package/routes/files/+layout.svelte +58 -0
- package/routes/files/+layout.ts +16 -0
- package/routes/files/+page.svelte +8 -0
- package/routes/files/+page.ts +12 -0
- package/routes/files/[id]/+page.svelte +26 -0
- package/routes/files/[id]/+page.ts +17 -0
- package/routes/files/shared/+page.svelte +68 -0
- package/routes/files/shared/+page.ts +12 -0
- package/routes/files/trash/+page.svelte +102 -0
- package/routes/files/trash/+page.ts +12 -0
- package/routes/tsconfig.json +11 -0
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageItemMetadata, StorageItemUpdate,
|
|
1
|
+
import type { StorageItemMetadata, StorageItemUpdate, UserStorage, UserStorageInfo } from './common.js';
|
|
2
2
|
export declare function parseItem(result: StorageItemMetadata): StorageItemMetadata;
|
|
3
3
|
export interface UploadOptions {
|
|
4
4
|
parentId?: string;
|
|
@@ -7,8 +7,15 @@ export interface UploadOptions {
|
|
|
7
7
|
export declare function uploadItem(file: Blob | File, opt?: UploadOptions): Promise<StorageItemMetadata>;
|
|
8
8
|
export declare function updateItem(fileId: string, data: Blob): Promise<StorageItemMetadata>;
|
|
9
9
|
export declare function getItemMetadata(fileId: string): Promise<StorageItemMetadata>;
|
|
10
|
+
/**
|
|
11
|
+
* Gets the metadata for all items in a directory.
|
|
12
|
+
*/
|
|
10
13
|
export declare function getDirectoryMetadata(parentId: string): Promise<StorageItemMetadata[]>;
|
|
11
14
|
export declare function downloadItem(fileId: string): Promise<Blob>;
|
|
12
15
|
export declare function updateItemMetadata(fileId: string, metadata: StorageItemUpdate): Promise<StorageItemMetadata>;
|
|
13
16
|
export declare function deleteItem(fileId: string): Promise<StorageItemMetadata>;
|
|
14
|
-
export declare function
|
|
17
|
+
export declare function getUserStorage(userId: string): Promise<UserStorage>;
|
|
18
|
+
export declare function getUserStorageInfo(userId: string): Promise<UserStorageInfo>;
|
|
19
|
+
export declare function getUserTrash(userId: string): Promise<StorageItemMetadata[]>;
|
|
20
|
+
export declare function itemsSharedWith(userId: string): Promise<StorageItemMetadata[]>;
|
|
21
|
+
export declare function getUserStorageRoot(userId: string): Promise<StorageItemMetadata[]>;
|
package/dist/client.js
CHANGED
|
@@ -45,6 +45,9 @@ export async function getItemMetadata(fileId) {
|
|
|
45
45
|
const result = await fetchAPI('GET', 'storage/item/:id', undefined, fileId);
|
|
46
46
|
return parseItem(result);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Gets the metadata for all items in a directory.
|
|
50
|
+
*/
|
|
48
51
|
export async function getDirectoryMetadata(parentId) {
|
|
49
52
|
const result = await fetchAPI('GET', 'storage/directory/:id', undefined, parentId);
|
|
50
53
|
for (const item of result)
|
|
@@ -67,9 +70,30 @@ export async function deleteItem(fileId) {
|
|
|
67
70
|
const result = await fetchAPI('DELETE', 'storage/item/:id', undefined, fileId);
|
|
68
71
|
return parseItem(result);
|
|
69
72
|
}
|
|
70
|
-
export async function
|
|
73
|
+
export async function getUserStorage(userId) {
|
|
71
74
|
const result = await fetchAPI('GET', 'users/:id/storage', undefined, userId);
|
|
72
75
|
for (const item of result.items)
|
|
73
76
|
parseItem(item);
|
|
74
77
|
return result;
|
|
75
78
|
}
|
|
79
|
+
export async function getUserStorageInfo(userId) {
|
|
80
|
+
return await fetchAPI('OPTIONS', 'users/:id/storage', undefined, userId);
|
|
81
|
+
}
|
|
82
|
+
export async function getUserTrash(userId) {
|
|
83
|
+
const result = await fetchAPI('GET', 'users/:id/storage/trash', undefined, userId);
|
|
84
|
+
for (const item of result)
|
|
85
|
+
parseItem(item);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
export async function itemsSharedWith(userId) {
|
|
89
|
+
const result = await fetchAPI('GET', 'users/:id/storage/shared', undefined, userId);
|
|
90
|
+
for (const item of result)
|
|
91
|
+
parseItem(item);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
export async function getUserStorageRoot(userId) {
|
|
95
|
+
const result = await fetchAPI('GET', 'users/:id/storage/root', undefined, userId);
|
|
96
|
+
for (const item of result)
|
|
97
|
+
parseItem(item);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
package/dist/common.d.ts
CHANGED
|
@@ -2,11 +2,17 @@ import * as z from 'zod';
|
|
|
2
2
|
declare module '@axium/core/api' {
|
|
3
3
|
interface _apiTypes {
|
|
4
4
|
'users/:id/storage': {
|
|
5
|
-
OPTIONS:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
GET:
|
|
5
|
+
OPTIONS: UserStorageInfo;
|
|
6
|
+
GET: UserStorage;
|
|
7
|
+
};
|
|
8
|
+
'users/:id/storage/root': {
|
|
9
|
+
GET: StorageItemMetadata[];
|
|
10
|
+
};
|
|
11
|
+
'users/:id/storage/trash': {
|
|
12
|
+
GET: StorageItemMetadata[];
|
|
13
|
+
};
|
|
14
|
+
'users/:id/storage/shared': {
|
|
15
|
+
GET: StorageItemMetadata[];
|
|
10
16
|
};
|
|
11
17
|
'storage/item/:id': {
|
|
12
18
|
GET: StorageItemMetadata;
|
|
@@ -30,11 +36,13 @@ export interface StorageUsage {
|
|
|
30
36
|
bytes: number;
|
|
31
37
|
items: number;
|
|
32
38
|
}
|
|
33
|
-
export interface
|
|
34
|
-
items: StorageItemMetadata[];
|
|
39
|
+
export interface UserStorageInfo {
|
|
35
40
|
limits: StorageLimits;
|
|
36
41
|
usage: StorageUsage;
|
|
37
42
|
}
|
|
43
|
+
export interface UserStorage extends UserStorageInfo {
|
|
44
|
+
items: StorageItemMetadata[];
|
|
45
|
+
}
|
|
38
46
|
/**
|
|
39
47
|
* An update to file metadata.
|
|
40
48
|
*/
|
package/dist/plugin.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ declare const _default: {
|
|
|
41
41
|
"./components": string;
|
|
42
42
|
"./components/*": string;
|
|
43
43
|
};
|
|
44
|
+
routes: string;
|
|
44
45
|
files: string[];
|
|
45
46
|
scripts: {
|
|
46
47
|
build: string;
|
|
@@ -49,6 +50,7 @@ declare const _default: {
|
|
|
49
50
|
"@axium/client": string;
|
|
50
51
|
"@axium/core": string;
|
|
51
52
|
"@axium/server": string;
|
|
53
|
+
"@sveltejs/kit": string;
|
|
52
54
|
utilium: string;
|
|
53
55
|
};
|
|
54
56
|
dependencies: {
|
package/dist/plugin.js
CHANGED
|
@@ -7,6 +7,7 @@ import { sql } from 'kysely';
|
|
|
7
7
|
import pkg from '../package.json' with { type: 'json' };
|
|
8
8
|
import './common.js';
|
|
9
9
|
import './server.js';
|
|
10
|
+
import { App } from '@axium/server/apps';
|
|
10
11
|
async function statusText() {
|
|
11
12
|
const { storage: items } = await count('storage');
|
|
12
13
|
const { size } = await database
|
|
@@ -60,6 +61,11 @@ async function clean(opt) {
|
|
|
60
61
|
.executeTakeFirstOrThrow()
|
|
61
62
|
.then(done);
|
|
62
63
|
}
|
|
64
|
+
new App({
|
|
65
|
+
id: 'files',
|
|
66
|
+
name: 'Files',
|
|
67
|
+
version: pkg.version,
|
|
68
|
+
});
|
|
63
69
|
export default {
|
|
64
70
|
...pkg,
|
|
65
71
|
statusText,
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
1
2
|
import { Permission } from '@axium/core/access';
|
|
2
3
|
import { checkAuthForItem, checkAuthForUser, getSessionAndUser } from '@axium/server/auth';
|
|
3
4
|
import { addConfigDefaults, config } from '@axium/server/config';
|
|
@@ -325,10 +326,72 @@ addRoute({
|
|
|
325
326
|
const userId = event.params.id;
|
|
326
327
|
await checkAuthForUser(event, userId);
|
|
327
328
|
const [items, usage, limits] = await Promise.all([
|
|
328
|
-
database.selectFrom('storage').where('userId', '=', userId).selectAll().execute(),
|
|
329
|
+
database.selectFrom('storage').where('userId', '=', userId).where('trashedAt', '==', null).selectAll().execute(),
|
|
329
330
|
currentUsage(userId),
|
|
330
331
|
getLimits(userId),
|
|
331
332
|
]).catch(withError('Could not fetch data'));
|
|
332
333
|
return { usage, limits, items: items.map(parseItem) };
|
|
333
334
|
},
|
|
334
335
|
});
|
|
336
|
+
addRoute({
|
|
337
|
+
path: '/api/users/:id/storage/root',
|
|
338
|
+
params: { id: z.uuid() },
|
|
339
|
+
async GET(event) {
|
|
340
|
+
if (!config.storage.enabled)
|
|
341
|
+
error(503, 'User storage is disabled');
|
|
342
|
+
const userId = event.params.id;
|
|
343
|
+
await checkAuthForUser(event, userId);
|
|
344
|
+
const items = await database
|
|
345
|
+
.selectFrom('storage')
|
|
346
|
+
.where('userId', '=', userId)
|
|
347
|
+
.where('trashedAt', '==', null)
|
|
348
|
+
.where('parentId', '==', null)
|
|
349
|
+
.selectAll()
|
|
350
|
+
.execute()
|
|
351
|
+
.catch(withError('Could not get storage items'));
|
|
352
|
+
return items.map(parseItem);
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
addRoute({
|
|
356
|
+
path: '/api/users/:id/storage/shared',
|
|
357
|
+
params: { id: z.uuid() },
|
|
358
|
+
async GET(event) {
|
|
359
|
+
if (!config.storage.enabled)
|
|
360
|
+
error(503, 'User storage is disabled');
|
|
361
|
+
const userId = event.params.id;
|
|
362
|
+
await checkAuthForUser(event, userId);
|
|
363
|
+
const items = await database
|
|
364
|
+
.selectFrom('storage')
|
|
365
|
+
.where('trashedAt', '==', null)
|
|
366
|
+
.where(({ and, not, exists, selectFrom }) => {
|
|
367
|
+
const existsInAcl = (column) => exists(selectFrom('acl.storage')
|
|
368
|
+
.whereRef('itemId', '=', `storage.${column}`)
|
|
369
|
+
.where('userId', '=', userId)
|
|
370
|
+
.where('permission', '!=', Permission.None));
|
|
371
|
+
// Exclude items that are in a directory shared with the user
|
|
372
|
+
return and([existsInAcl('id'), not(existsInAcl('parentId'))]);
|
|
373
|
+
})
|
|
374
|
+
.selectAll()
|
|
375
|
+
.execute()
|
|
376
|
+
.catch(withError('Could not get storage items'));
|
|
377
|
+
return items.map(parseItem);
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
addRoute({
|
|
381
|
+
path: '/api/users/:id/storage/trash',
|
|
382
|
+
params: { id: z.uuid() },
|
|
383
|
+
async GET(event) {
|
|
384
|
+
if (!config.storage.enabled)
|
|
385
|
+
error(503, 'User storage is disabled');
|
|
386
|
+
const userId = event.params.id;
|
|
387
|
+
await checkAuthForUser(event, userId);
|
|
388
|
+
const items = await database
|
|
389
|
+
.selectFrom('storage')
|
|
390
|
+
.where('userId', '=', userId)
|
|
391
|
+
.where('trashedAt', '!=', null)
|
|
392
|
+
.selectAll()
|
|
393
|
+
.execute()
|
|
394
|
+
.catch(withError('Could not get trash'));
|
|
395
|
+
return items.map(parseItem);
|
|
396
|
+
},
|
|
397
|
+
});
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { formatBytes } from '@axium/core/format';
|
|
3
3
|
import { forMime as iconForMime } from '@axium/core/icons';
|
|
4
4
|
import { FormDialog, Icon } from '@axium/server/components';
|
|
5
|
-
import {
|
|
5
|
+
import { getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
|
|
6
6
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
7
7
|
|
|
8
|
-
let {
|
|
8
|
+
let { items = $bindable([]), appMode }: { appMode?: boolean; items: StorageItemMetadata[] } = $props();
|
|
9
9
|
|
|
10
10
|
let activeIndex = $state<number>(-1);
|
|
11
11
|
let activeItem = $derived(items[activeIndex]);
|
|
@@ -13,51 +13,55 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
{#snippet action(name: string, icon: string, i: number)}
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
--size="14px"
|
|
16
|
+
<span
|
|
17
|
+
class="action"
|
|
19
18
|
onclick={(e: Event) => {
|
|
20
19
|
e.stopPropagation();
|
|
21
20
|
e.preventDefault();
|
|
22
21
|
activeIndex = i;
|
|
23
22
|
dialogs[name].showModal();
|
|
24
23
|
}}
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
>
|
|
25
|
+
<Icon i={icon} --size="14px" />
|
|
26
|
+
</span>
|
|
27
27
|
{/snippet}
|
|
28
28
|
|
|
29
29
|
{#snippet _itemName()}
|
|
30
|
-
{#if activeItem
|
|
30
|
+
{#if activeItem?.name}
|
|
31
31
|
<strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
|
|
32
32
|
{:else}
|
|
33
33
|
this
|
|
34
34
|
{/if}
|
|
35
35
|
{/snippet}
|
|
36
36
|
|
|
37
|
+
{#snippet _item(item: StorageItemMetadata, i: number)}
|
|
38
|
+
<div class="StorageListItem">
|
|
39
|
+
<dfn title={item.type}><Icon i={iconForMime(item.type)} /></dfn>
|
|
40
|
+
<span class="name">{item.name}</span>
|
|
41
|
+
<span>{item.modifiedAt.toLocaleString()}</span>
|
|
42
|
+
<span>{formatBytes(item.size)}</span>
|
|
43
|
+
{@render action('rename', 'pencil', i)}
|
|
44
|
+
{@render action('download', 'download', i)}
|
|
45
|
+
{@render action('trash', 'trash', i)}
|
|
46
|
+
</div>
|
|
47
|
+
{/snippet}
|
|
48
|
+
|
|
37
49
|
<div class="StorageList">
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{#
|
|
46
|
-
<
|
|
47
|
-
<dfn title={item.type}><Icon i={iconForMime(item.type)} /></dfn>
|
|
48
|
-
<span class="name">{item.name}</span>
|
|
49
|
-
<span>{item.modifiedAt.toLocaleString()}</span>
|
|
50
|
-
<span>{formatBytes(item.size)}</span>
|
|
51
|
-
{@render action('rename', 'edit', i)}
|
|
52
|
-
{@render action('download', 'download', i)}
|
|
53
|
-
{@render action('delete', 'delete', i)}
|
|
54
|
-
</div>
|
|
50
|
+
<div class="StorageListItem SL_header">
|
|
51
|
+
<span></span>
|
|
52
|
+
<span>Name</span>
|
|
53
|
+
<span>Last Modified</span>
|
|
54
|
+
<span>Size</span>
|
|
55
|
+
</div>
|
|
56
|
+
{#each items as item, i (item.id)}
|
|
57
|
+
{#if item.type == 'inode/directory' && appMode}
|
|
58
|
+
<a class="StorageListItem-container" href="/files/{item.id}">{@render _item(item, i)}</a>
|
|
55
59
|
{:else}
|
|
56
|
-
|
|
57
|
-
{/
|
|
58
|
-
{:
|
|
59
|
-
<i
|
|
60
|
-
{/
|
|
60
|
+
{@render _item(item, i)}
|
|
61
|
+
{/if}
|
|
62
|
+
{:else}
|
|
63
|
+
<i>Empty.</i>
|
|
64
|
+
{/each}
|
|
61
65
|
</div>
|
|
62
66
|
|
|
63
67
|
<FormDialog
|
|
@@ -74,25 +78,28 @@
|
|
|
74
78
|
</div>
|
|
75
79
|
</FormDialog>
|
|
76
80
|
<FormDialog
|
|
77
|
-
bind:dialog={dialogs.
|
|
78
|
-
submitText="
|
|
81
|
+
bind:dialog={dialogs.trash}
|
|
82
|
+
submitText="Trash"
|
|
79
83
|
submitDanger
|
|
80
84
|
submit={async () => {
|
|
81
|
-
await
|
|
85
|
+
await updateItemMetadata(activeItem.id, { trash: true });
|
|
82
86
|
if (activeIndex != -1) items.splice(activeIndex, 1);
|
|
83
87
|
}}
|
|
84
88
|
>
|
|
85
|
-
<p>Are you sure you want to
|
|
89
|
+
<p>Are you sure you want to trash {@render _itemName()}?</p>
|
|
86
90
|
</FormDialog>
|
|
87
91
|
<FormDialog
|
|
88
92
|
bind:dialog={dialogs.download}
|
|
89
93
|
submitText="Download"
|
|
90
94
|
submit={async () => {
|
|
91
|
-
|
|
95
|
+
if (activeItem.type == 'inode/directory') {
|
|
96
|
+
const children = await getDirectoryMetadata(activeItem.id);
|
|
97
|
+
for (const child of children) open(child.dataURL, '_blank');
|
|
98
|
+
} else open(activeItem.dataURL, '_blank');
|
|
92
99
|
}}
|
|
93
100
|
>
|
|
94
101
|
<p>
|
|
95
|
-
We are not responsible for the contents of this file. <br />
|
|
102
|
+
We are not responsible for the contents of this {activeItem.type == 'inode/directory' ? 'folder' : 'file'}. <br />
|
|
96
103
|
Are you sure you want to download {@render _itemName()}?
|
|
97
104
|
</p>
|
|
98
105
|
</FormDialog>
|
|
@@ -104,25 +111,30 @@
|
|
|
104
111
|
padding: 0.5em;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
.StorageListItem.
|
|
114
|
+
.StorageListItem.SL_header {
|
|
108
115
|
font-weight: bold;
|
|
109
116
|
border-bottom: 1px solid #bbc;
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
.StorageListItem-container {
|
|
120
|
+
text-decoration: none;
|
|
121
|
+
color: inherit;
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
.StorageListItem {
|
|
113
125
|
display: grid;
|
|
114
|
-
grid-template-columns: 1em 4fr 15em 5em repeat(
|
|
126
|
+
grid-template-columns: 1em 4fr 15em 5em repeat(3, 1em);
|
|
115
127
|
align-items: center;
|
|
116
128
|
gap: 0.5em;
|
|
129
|
+
padding: 0.5em 0;
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
.StorageListItem:not(:last-child) {
|
|
120
|
-
padding: 0.5em 0;
|
|
121
133
|
border-bottom: 1px solid #bbc;
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
.StorageListItem:not(.
|
|
125
|
-
background-color: #
|
|
136
|
+
.StorageListItem:not(.SL_header):hover {
|
|
137
|
+
background-color: #7777;
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
.action {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
3
|
-
import
|
|
3
|
+
import SidebarItem from './SidebarItem.svelte';
|
|
4
4
|
import { items as sb_items, getDirectory } from '@axium/storage/sidebar';
|
|
5
5
|
|
|
6
6
|
let { root }: { root: string | StorageItemMetadata[] } = $props();
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<i>Loading...</i>
|
|
19
19
|
{:then}
|
|
20
20
|
{#each items as _, i (_.id)}
|
|
21
|
-
<
|
|
21
|
+
<SidebarItem bind:item={items[i]} bind:items />
|
|
22
22
|
{:else}
|
|
23
23
|
<i>No files yet</i>
|
|
24
24
|
{/each}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { deleteItem, updateItemMetadata } from '@axium/storage/client';
|
|
5
5
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
6
6
|
import { debug, getDirectory, selection, toggle, toggleRange } from '@axium/storage/sidebar';
|
|
7
|
-
import
|
|
7
|
+
import SidebarItem from './SidebarItem.svelte';
|
|
8
8
|
|
|
9
9
|
let {
|
|
10
10
|
item = $bindable(),
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
<i>Loading...</i>
|
|
85
85
|
{:then}
|
|
86
86
|
{#each children as _, i (_.id)}
|
|
87
|
-
<
|
|
87
|
+
<SidebarItem bind:item={children[i]} bind:items={children} />
|
|
88
88
|
{/each}
|
|
89
89
|
{:catch error}
|
|
90
90
|
<i style:color="#c44">{error.message}</i>
|
package/lib/Usage.svelte
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatBytes } from '@axium/core/format';
|
|
3
|
+
import { NumberBar } from '@axium/server/components';
|
|
4
|
+
import { getUserStorageInfo } from '@axium/storage/client';
|
|
5
|
+
import type { UserStorageInfo } from '@axium/storage/common';
|
|
6
|
+
|
|
7
|
+
const { userId, info }: { userId: string; info?: UserStorageInfo } = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#await info || getUserStorageInfo(userId)}
|
|
11
|
+
<p>Loading...</p>
|
|
12
|
+
{:then info}
|
|
13
|
+
<p>
|
|
14
|
+
<NumberBar
|
|
15
|
+
max={info.limits.user_size * 1_000_000}
|
|
16
|
+
value={info.usage.bytes}
|
|
17
|
+
text="Using {formatBytes(info.usage.bytes)} of {formatBytes(info.limits.user_size * 1_000_000)}"
|
|
18
|
+
--fill="#345"
|
|
19
|
+
/>
|
|
20
|
+
</p>
|
|
21
|
+
{:catch error}
|
|
22
|
+
<p>Couldn't load your uploads.</p>
|
|
23
|
+
<p>{error.message}</p>
|
|
24
|
+
{/await}
|
package/lib/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { default as StorageList } from './
|
|
2
|
-
export { default as StorageSidebar } from './
|
|
3
|
-
export { default as StorageSidebarItem } from './
|
|
1
|
+
export { default as StorageList } from './List.svelte';
|
|
2
|
+
export { default as StorageSidebar } from './Sidebar.svelte';
|
|
3
|
+
export { default as StorageSidebarItem } from './SidebarItem.svelte';
|
|
4
|
+
export { default as StorageUsage } from './Usage.svelte';
|
package/lib/tsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
|
|
5
5
|
"description": "User file storage for Axium",
|
|
6
6
|
"funding": {
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
"./components": "./lib/index.js",
|
|
27
27
|
"./components/*": "./lib/*.svelte"
|
|
28
28
|
},
|
|
29
|
+
"routes": "routes",
|
|
29
30
|
"files": [
|
|
30
31
|
"dist",
|
|
31
|
-
"lib"
|
|
32
|
+
"lib",
|
|
33
|
+
"routes"
|
|
32
34
|
],
|
|
33
35
|
"scripts": {
|
|
34
36
|
"build": "tsc"
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
"@axium/client": ">=0.1.0",
|
|
38
40
|
"@axium/core": ">=0.5.0",
|
|
39
41
|
"@axium/server": ">=0.19.2",
|
|
42
|
+
"@sveltejs/kit": "^2.27.3",
|
|
40
43
|
"utilium": "^2.3.8"
|
|
41
44
|
},
|
|
42
45
|
"dependencies": {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Icon } from '@axium/server/components';
|
|
3
|
+
import { capitalize } from 'utilium';
|
|
4
|
+
import { StorageUsage } from '@axium/storage/components';
|
|
5
|
+
import type { Session } from '@axium/core';
|
|
6
|
+
import type { LayoutProps, LayoutRouteId } from './$types';
|
|
7
|
+
|
|
8
|
+
type SidebarTab = 'files' | 'trash' | 'shared' | 'usage';
|
|
9
|
+
|
|
10
|
+
let { children, data }: LayoutProps = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
{#snippet tab(text: string, i: string, href: LayoutRouteId, isDefault: boolean = false)}
|
|
14
|
+
<a class={['item', data.route.id == href || (data.route.id == '/files/[id]' && isDefault)]} {href}><Icon {i} />{capitalize(text)}</a>
|
|
15
|
+
{/snippet}
|
|
16
|
+
|
|
17
|
+
<div class="app">
|
|
18
|
+
<div class="sidebar">
|
|
19
|
+
{@render tab('Files', 'folders', '/files', true)}
|
|
20
|
+
{@render tab('Trash', 'trash', '/files/trash')}
|
|
21
|
+
{@render tab('Shared', 'user-group', '/files/shared')}
|
|
22
|
+
|
|
23
|
+
<div class="usage">
|
|
24
|
+
<StorageUsage userId={data.session.userId} />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="content">
|
|
29
|
+
{@render children()}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.sidebar {
|
|
35
|
+
width: 20em;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 0.5em;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.item {
|
|
42
|
+
padding: 0.5em;
|
|
43
|
+
border-radius: 0.25em;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.item:hover {
|
|
47
|
+
background-color: #446;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.item.active {
|
|
52
|
+
background-color: #447;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.usage {
|
|
56
|
+
align-self: flex-end;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getCurrentSession } from '@axium/client/user';
|
|
2
|
+
import { redirect } from '@sveltejs/kit';
|
|
3
|
+
import type { LayoutLoadEvent } from './$types';
|
|
4
|
+
|
|
5
|
+
export const ssr = false;
|
|
6
|
+
|
|
7
|
+
export async function load({ url, route }: LayoutLoadEvent) {
|
|
8
|
+
const session = await getCurrentSession().catch(() => null);
|
|
9
|
+
|
|
10
|
+
if (!session) redirect(307, '/login?after=' + url.pathname);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
session: await getCurrentSession(),
|
|
14
|
+
route,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getUserStorageRoot } from '@axium/storage/client';
|
|
2
|
+
import type { LoadEvent } from '@sveltejs/kit';
|
|
3
|
+
|
|
4
|
+
export const ssr = false;
|
|
5
|
+
|
|
6
|
+
export async function load({ parent }: LoadEvent) {
|
|
7
|
+
const { session } = await parent();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
items: await getUserStorageRoot(session.userId),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Icon } from '@axium/server/components';
|
|
3
|
+
import { StorageList } from '@axium/storage/components';
|
|
4
|
+
import type { PageProps } from './$types';
|
|
5
|
+
import { updateItemMetadata } from '@axium/storage/client';
|
|
6
|
+
|
|
7
|
+
const { data }: PageProps = $props();
|
|
8
|
+
|
|
9
|
+
const item = $state(data.item);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if item.trashedAt}
|
|
13
|
+
<p>This item is trashed</p>
|
|
14
|
+
<button
|
|
15
|
+
onclick={async e => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
await updateItemMetadata(item.id, { trash: false });
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<Icon i="trash-can-undo" /> Restore
|
|
21
|
+
</button>
|
|
22
|
+
{:else if item.type == 'inode/directory'}
|
|
23
|
+
<StorageList items={data.items!} />
|
|
24
|
+
{:else}
|
|
25
|
+
<p>No preview available.</p>
|
|
26
|
+
{/if}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PageLoadEvent } from './$types';
|
|
2
|
+
import { getDirectoryMetadata, getItemMetadata } from '@axium/storage/client';
|
|
3
|
+
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
4
|
+
|
|
5
|
+
export const ssr = false;
|
|
6
|
+
|
|
7
|
+
export async function load({ parent, params }: PageLoadEvent) {
|
|
8
|
+
const item = await getItemMetadata(params.id);
|
|
9
|
+
|
|
10
|
+
let items: StorageItemMetadata[] | undefined;
|
|
11
|
+
|
|
12
|
+
if (item.type == 'inode/directory') {
|
|
13
|
+
items = await getDirectoryMetadata(item.id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { item, items };
|
|
17
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatBytes } from '@axium/core/format';
|
|
3
|
+
import { forMime as iconForMime } from '@axium/core/icons';
|
|
4
|
+
import { Icon } from '@axium/server/components';
|
|
5
|
+
import type { PageProps } from './$types';
|
|
6
|
+
|
|
7
|
+
const { data }: PageProps = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="SharedItemList">
|
|
11
|
+
<div class="SharedItem list-header">
|
|
12
|
+
<span></span>
|
|
13
|
+
<span>Name</span>
|
|
14
|
+
<span>Last Modified</span>
|
|
15
|
+
<span>Size</span>
|
|
16
|
+
</div>
|
|
17
|
+
{#each data.items as item, i (item.id)}
|
|
18
|
+
<div class="SharedItem">
|
|
19
|
+
<dfn title={item.type}><Icon i={iconForMime(item.type)} /></dfn>
|
|
20
|
+
<span class="name">{item.name}</span>
|
|
21
|
+
<span>{item.modifiedAt.toLocaleString()}</span>
|
|
22
|
+
<span>{formatBytes(item.size)}</span>
|
|
23
|
+
</div>
|
|
24
|
+
{:else}
|
|
25
|
+
<i>Empty.</i>
|
|
26
|
+
{/each}
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.SharedItemList {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
padding: 0.5em;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.SharedItem.list-header {
|
|
37
|
+
font-weight: bold;
|
|
38
|
+
border-bottom: 1px solid #bbc;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.SharedItem {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: 1em 4fr 15em 5em;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 0.5em;
|
|
46
|
+
padding: 0.5em 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.SharedItem:not(:last-child) {
|
|
50
|
+
border-bottom: 1px solid #bbc;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.SharedItem:not(.list-header):hover {
|
|
54
|
+
background-color: #7777;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.action {
|
|
58
|
+
visibility: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.SharedItem:hover .action {
|
|
62
|
+
visibility: visible;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.action:hover {
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LoadEvent } from '@sveltejs/kit';
|
|
2
|
+
import { itemsSharedWith } from '@axium/storage/client';
|
|
3
|
+
|
|
4
|
+
export const ssr = false;
|
|
5
|
+
|
|
6
|
+
export async function load({ parent }: LoadEvent) {
|
|
7
|
+
const { session } = await parent();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
items: await itemsSharedWith(session.userId),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatBytes } from '@axium/core/format';
|
|
3
|
+
import { forMime as iconForMime } from '@axium/core/icons';
|
|
4
|
+
import { FormDialog, Icon } from '@axium/server/components';
|
|
5
|
+
import { deleteItem } from '@axium/storage/client';
|
|
6
|
+
import type { PageProps } from './$types';
|
|
7
|
+
|
|
8
|
+
const { data }: PageProps = $props();
|
|
9
|
+
let items = $state(data.items);
|
|
10
|
+
let dialog = $state<HTMLDialogElement>();
|
|
11
|
+
|
|
12
|
+
let activeIndex = $state<number>(-1);
|
|
13
|
+
const activeItem = $derived(items[activeIndex]);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="TrashList">
|
|
17
|
+
<div class="TrashItem list-header">
|
|
18
|
+
<span></span>
|
|
19
|
+
<span>Name</span>
|
|
20
|
+
<span>Last Modified</span>
|
|
21
|
+
<span>Size</span>
|
|
22
|
+
</div>
|
|
23
|
+
{#each items as item, i (item.id)}
|
|
24
|
+
<div class="TrashItem">
|
|
25
|
+
<dfn title={item.type}><Icon i={iconForMime(item.type)} /></dfn>
|
|
26
|
+
<span class="name">{item.name}</span>
|
|
27
|
+
<span>{item.modifiedAt.toLocaleString()}</span>
|
|
28
|
+
<span>{formatBytes(item.size)}</span>
|
|
29
|
+
<span
|
|
30
|
+
class="action"
|
|
31
|
+
onclick={(e: Event) => {
|
|
32
|
+
e.stopPropagation();
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
activeIndex = i;
|
|
35
|
+
dialog?.showModal();
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<Icon i="trash" --size="14px" --fill="#c44" />
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
{:else}
|
|
42
|
+
<i>Empty.</i>
|
|
43
|
+
{/each}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<FormDialog
|
|
47
|
+
bind:dialog
|
|
48
|
+
submitText="Delete"
|
|
49
|
+
submitDanger
|
|
50
|
+
submit={async () => {
|
|
51
|
+
await deleteItem(activeItem.id);
|
|
52
|
+
if (activeIndex != -1) items.splice(activeIndex, 1);
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<p>
|
|
56
|
+
Are you sure you want to permanently delete
|
|
57
|
+
{#if activeItem?.name}<strong>{activeItem.name.length > 23 ? activeItem.name.slice(0, 20) + '...' : activeItem.name}</strong>
|
|
58
|
+
{:else}this
|
|
59
|
+
{/if}?
|
|
60
|
+
</p>
|
|
61
|
+
</FormDialog>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.TrashList {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
padding: 0.5em;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.TrashItem.list-header {
|
|
71
|
+
font-weight: bold;
|
|
72
|
+
border-bottom: 1px solid #bbc;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.TrashItem {
|
|
76
|
+
display: grid;
|
|
77
|
+
grid-template-columns: 1em 4fr 15em 5em 1em;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 0.5em;
|
|
80
|
+
padding: 0.5em 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.TrashItem:not(:last-child) {
|
|
84
|
+
border-bottom: 1px solid #bbc;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.TrashItem:not(.list-header):hover {
|
|
88
|
+
background-color: #7777;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.action {
|
|
92
|
+
visibility: hidden;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.TrashItem:hover .action {
|
|
96
|
+
visibility: visible;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.action:hover {
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getUserTrash } from '@axium/storage/client';
|
|
2
|
+
import type { LoadEvent } from '@sveltejs/kit';
|
|
3
|
+
|
|
4
|
+
export const ssr = false;
|
|
5
|
+
|
|
6
|
+
export async function load({ parent }: LoadEvent) {
|
|
7
|
+
const { session } = await parent();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
items: await getUserTrash(session.userId),
|
|
11
|
+
};
|
|
12
|
+
}
|