@axium/storage 0.14.2 → 0.15.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/README.md +2 -15
- package/dist/client/api.d.ts +1 -1
- package/dist/client/api.js +8 -1
- package/dist/client/previews.d.ts +4 -0
- package/dist/client/previews.js +1 -0
- package/dist/server/raw.js +100 -13
- package/lib/Add.svelte +3 -3
- package/lib/List.svelte +112 -9
- package/lib/Usage.svelte +2 -2
- package/package.json +1 -1
- package/routes/files/+page.svelte +1 -1
- package/routes/files/[id]/+page.svelte +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
1
|
# Axium Storage
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
Update the configuration to include the files data directory:
|
|
8
|
-
|
|
9
|
-
```json
|
|
10
|
-
{
|
|
11
|
-
"storage": {
|
|
12
|
-
"data": "/path/to/storage/data"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Also, make sure to run `axium plugin init @axium/storage` to add the necessary database tables for the plugin.
|
|
3
|
+
File storage and management for Axium.
|
|
4
|
+
This plugin provides a competitor to big tech's cloud storage.
|
package/dist/client/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { StorageItemMetadata, type StorageItemUpdate, type UserStorage, type UserStorageInfo } from '../common.js';
|
|
2
2
|
export interface UploadOptions {
|
|
3
3
|
parentId?: string;
|
|
4
4
|
name?: string;
|
package/dist/client/api.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { fetchAPI, prefix, token } from '@axium/client/requests';
|
|
2
|
+
import { StorageItemMetadata } from '../common.js';
|
|
3
|
+
import { prettifyError } from 'zod';
|
|
2
4
|
async function _upload(method, url, data, extraHeaders = {}) {
|
|
3
5
|
const init = {
|
|
4
6
|
method,
|
|
@@ -20,7 +22,12 @@ async function _upload(method, url, data, extraHeaders = {}) {
|
|
|
20
22
|
const json = await response.json().catch(() => ({ message: 'Unknown server error (invalid JSON response)' }));
|
|
21
23
|
if (!response.ok)
|
|
22
24
|
throw new Error(json.message);
|
|
23
|
-
|
|
25
|
+
try {
|
|
26
|
+
return StorageItemMetadata.parse(json);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
throw prettifyError(e);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
function rawStorage(fileId) {
|
|
26
33
|
const raw = '/raw/storage' + (fileId ? '/' + fileId : '');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default new Map();
|
package/dist/server/raw.js
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
import { getConfig } from '@axium/core';
|
|
2
54
|
import { audit } from '@axium/server/audit';
|
|
3
55
|
import { checkAuthForItem, requireSession } from '@axium/server/auth';
|
|
@@ -5,7 +57,7 @@ import { database } from '@axium/server/database';
|
|
|
5
57
|
import { error, withError } from '@axium/server/requests';
|
|
6
58
|
import { addRoute } from '@axium/server/routes';
|
|
7
59
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { linkSync,
|
|
60
|
+
import { closeSync, linkSync, openSync, readSync, writeFileSync } from 'node:fs';
|
|
9
61
|
import { join } from 'node:path/posix';
|
|
10
62
|
import * as z from 'zod';
|
|
11
63
|
import '../polyfills.js';
|
|
@@ -95,18 +147,53 @@ addRoute({
|
|
|
95
147
|
path: '/raw/storage/:id',
|
|
96
148
|
params: { id: z.uuid() },
|
|
97
149
|
async GET(request, { id: itemId }) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
|
|
150
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
151
|
+
try {
|
|
152
|
+
if (!getConfig('@axium/storage').enabled)
|
|
153
|
+
error(503, 'User storage is disabled');
|
|
154
|
+
const { item } = await checkAuthForItem(request, 'storage', itemId, { read: true });
|
|
155
|
+
if (item.trashedAt)
|
|
156
|
+
error(410, 'Trashed items can not be downloaded');
|
|
157
|
+
const path = join(getConfig('@axium/storage').data, item.id);
|
|
158
|
+
const range = request.headers.get('range');
|
|
159
|
+
const fd = openSync(path, 'r');
|
|
160
|
+
const _ = __addDisposableResource(env_1, { [Symbol.dispose]: () => closeSync(fd) }, false);
|
|
161
|
+
let start = 0, end = item.size - 1, length = item.size;
|
|
162
|
+
if (range) {
|
|
163
|
+
const [_start, _end = item.size - 1] = range
|
|
164
|
+
.replace(/bytes=/, '')
|
|
165
|
+
.split('-')
|
|
166
|
+
.map(val => (val && Number.isSafeInteger(parseInt(val)) ? parseInt(val) : undefined));
|
|
167
|
+
start = typeof _start == 'number' ? _start : item.size - _end;
|
|
168
|
+
end = typeof _start == 'number' ? _end : item.size - 1;
|
|
169
|
+
length = end - start + 1;
|
|
170
|
+
}
|
|
171
|
+
if (start >= item.size || end >= item.size || start > end || start < 0) {
|
|
172
|
+
return new Response(null, {
|
|
173
|
+
status: 416,
|
|
174
|
+
headers: { 'Content-Range': `bytes */${item.size}` },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const content = new Uint8Array(length);
|
|
178
|
+
readSync(fd, content, 0, length, start);
|
|
179
|
+
return new Response(content, {
|
|
180
|
+
status: length == item.size ? 200 : 206,
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Range': `bytes ${start}-${end}/${item.size}`,
|
|
183
|
+
'Accept-Ranges': 'bytes',
|
|
184
|
+
'Content-Length': String(length),
|
|
185
|
+
'Content-Type': item.type,
|
|
186
|
+
'Content-Disposition': `attachment; filename="${item.name}"`,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (e_1) {
|
|
191
|
+
env_1.error = e_1;
|
|
192
|
+
env_1.hasError = true;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
__disposeResources(env_1);
|
|
196
|
+
}
|
|
110
197
|
},
|
|
111
198
|
async POST(request, { id: itemId }) {
|
|
112
199
|
if (!getConfig('@axium/storage').enabled)
|
package/lib/Add.svelte
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { uploadItem } from '@axium/storage/client';
|
|
5
5
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
6
6
|
|
|
7
|
-
const { parentId,
|
|
7
|
+
const { parentId, onAdd }: { parentId?: string; onAdd?(item: StorageItemMetadata): void } = $props();
|
|
8
8
|
|
|
9
9
|
let uploadDialog = $state<HTMLDialogElement>()!;
|
|
10
10
|
let input = $state<HTMLInputElement>();
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
submit={async () => {
|
|
46
46
|
for (const file of input?.files!) {
|
|
47
47
|
const item = await uploadItem(file, { parentId });
|
|
48
|
-
|
|
48
|
+
onAdd?.(item);
|
|
49
49
|
}
|
|
50
50
|
}}
|
|
51
51
|
>
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
submit={async (data: { name: string; content?: string }) => {
|
|
59
59
|
const file = new File(createIncludesContent ? [data.content!] : [], data.name, { type: createType });
|
|
60
60
|
const item = await uploadItem(file, { parentId });
|
|
61
|
-
|
|
61
|
+
onAdd?.(item);
|
|
62
62
|
}}
|
|
63
63
|
>
|
|
64
64
|
<div>
|
package/lib/List.svelte
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import type { AccessControllable, UserPublic } from '@axium/core';
|
|
5
5
|
import { formatBytes } from '@axium/core/format';
|
|
6
6
|
import { forMime as iconForMime } from '@axium/core/icons';
|
|
7
|
-
import { getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
|
|
7
|
+
import { downloadItem, getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
|
|
8
|
+
import previews from '@axium/storage/client/previews';
|
|
8
9
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
9
10
|
|
|
10
11
|
let {
|
|
@@ -19,15 +20,15 @@
|
|
|
19
20
|
const dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
20
21
|
</script>
|
|
21
22
|
|
|
22
|
-
{#snippet action(name: string, icon: string, i: number)}
|
|
23
|
+
{#snippet action(name: string, icon: string, i: number, preview: boolean = false)}
|
|
23
24
|
<span
|
|
24
|
-
class=
|
|
25
|
+
class={[!preview && 'action']}
|
|
25
26
|
onclick={() => {
|
|
26
27
|
activeIndex = i;
|
|
27
28
|
dialogs[name].showModal();
|
|
28
29
|
}}
|
|
29
30
|
>
|
|
30
|
-
<Icon i={icon} --size=
|
|
31
|
+
<Icon i={icon} --size={preview ? '18px' : '14px'} />
|
|
31
32
|
</span>
|
|
32
33
|
{/snippet}
|
|
33
34
|
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
class="list-item"
|
|
52
53
|
onclick={async () => {
|
|
53
54
|
if (item.type != 'inode/directory') {
|
|
54
|
-
|
|
55
|
+
dialogs['preview:' + item.id].showModal();
|
|
55
56
|
} else if (appMode) location.href = '/files/' + item.id;
|
|
56
57
|
else items = await getDirectoryMetadata(item.id);
|
|
57
58
|
}}
|
|
@@ -83,6 +84,41 @@
|
|
|
83
84
|
/>
|
|
84
85
|
{@render action('download', 'download', i)}
|
|
85
86
|
{@render action('trash', 'trash', i)}
|
|
87
|
+
<dialog bind:this={dialogs['preview:' + item.id]} class="preview">
|
|
88
|
+
<div class="title">{item.name}</div>
|
|
89
|
+
<div class="actions">
|
|
90
|
+
{@render action('rename', 'pencil', i, true)}
|
|
91
|
+
{@render action('share' + item.id, 'user-group', i, true)}
|
|
92
|
+
{@render action('download', 'download', i, true)}
|
|
93
|
+
{@render action('trash', 'trash', i, true)}
|
|
94
|
+
<span onclick={() => dialogs['preview:' + item.id].close()}><Icon i="xmark" --size="20px" /></span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="content">
|
|
97
|
+
{#if item.type.startsWith('image/')}
|
|
98
|
+
<img src={item.dataURL} alt={item.name} width="100%" />
|
|
99
|
+
{:else if item.type.startsWith('audio/')}
|
|
100
|
+
<audio src={item.dataURL} controls></audio>
|
|
101
|
+
{:else if item.type.startsWith('video/')}
|
|
102
|
+
<video src={item.dataURL} controls width="100%">
|
|
103
|
+
<track kind="captions" />
|
|
104
|
+
</video>
|
|
105
|
+
{:else if item.type == 'application/pdf'}
|
|
106
|
+
<object data={item.dataURL} type="application/pdf" width="100%" height="100%">
|
|
107
|
+
<embed src={item.dataURL} type="application/pdf" width="100%" height="100%" />
|
|
108
|
+
<p>PDF not displayed? <a href={item.dataURL} download={item.name}>Download</a></p>
|
|
109
|
+
</object>
|
|
110
|
+
{:else if item.type.startsWith('text/')}
|
|
111
|
+
<pre class="preview-text">{#await downloadItem(item.id).then(b => b.text()) then content}{content}{/await}</pre>
|
|
112
|
+
{:else if previews.has(item.type)}
|
|
113
|
+
{@render previews.get(item.type)!(item)}
|
|
114
|
+
{:else}
|
|
115
|
+
<div class="no-preview">
|
|
116
|
+
<Icon i="eye-slash" />
|
|
117
|
+
<span>Preview not available</span>
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
121
|
+
</dialog>
|
|
86
122
|
</div>
|
|
87
123
|
</div>
|
|
88
124
|
{:else}
|
|
@@ -121,19 +157,86 @@
|
|
|
121
157
|
submitText="Download"
|
|
122
158
|
submit={async () => {
|
|
123
159
|
if (activeItem!.type == 'inode/directory') {
|
|
160
|
+
/** @todo ZIP support */
|
|
124
161
|
const children = await getDirectoryMetadata(activeItem!.id);
|
|
125
162
|
for (const child of children) open(child.dataURL, '_blank');
|
|
126
163
|
} else open(activeItem!.dataURL, '_blank');
|
|
127
164
|
}}
|
|
128
165
|
>
|
|
129
|
-
<p>
|
|
130
|
-
We are not responsible for the contents of this {activeItem?.type == 'inode/directory' ? 'folder' : 'file'}. <br />
|
|
131
|
-
Are you sure you want to download {@render _itemName()}?
|
|
132
|
-
</p>
|
|
166
|
+
<p>Are you sure you want to download {@render _itemName()}?</p>
|
|
133
167
|
</FormDialog>
|
|
134
168
|
|
|
135
169
|
<style>
|
|
136
170
|
.list-item {
|
|
137
171
|
grid-template-columns: 1em 4fr 15em 5em repeat(4, 1em);
|
|
138
172
|
}
|
|
173
|
+
|
|
174
|
+
dialog.preview {
|
|
175
|
+
inset: 0;
|
|
176
|
+
width: 100%;
|
|
177
|
+
height: 100%;
|
|
178
|
+
background-color: #0008;
|
|
179
|
+
border: none;
|
|
180
|
+
padding: 1em;
|
|
181
|
+
word-wrap: normal;
|
|
182
|
+
|
|
183
|
+
.title,
|
|
184
|
+
.actions {
|
|
185
|
+
align-items: center;
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: 1em;
|
|
189
|
+
position: absolute;
|
|
190
|
+
top: 1em;
|
|
191
|
+
padding: 1em;
|
|
192
|
+
height: 1em;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.title {
|
|
196
|
+
left: 1em;
|
|
197
|
+
overflow: hidden;
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
text-overflow: ellipsis;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.actions {
|
|
203
|
+
right: 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.content {
|
|
207
|
+
position: absolute;
|
|
208
|
+
inset: 3em 10em 0;
|
|
209
|
+
|
|
210
|
+
.preview-text {
|
|
211
|
+
position: absolute;
|
|
212
|
+
inset: 0;
|
|
213
|
+
width: 100%;
|
|
214
|
+
height: 100%;
|
|
215
|
+
white-space: pre-wrap;
|
|
216
|
+
overflow-y: scroll;
|
|
217
|
+
line-height: 1.6;
|
|
218
|
+
background-color: var(--bg-menu);
|
|
219
|
+
font-family: monospace;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.no-preview {
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: 1em;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@media (width < 700px) {
|
|
232
|
+
.actions {
|
|
233
|
+
top: 3em;
|
|
234
|
+
left: 1em;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.content {
|
|
238
|
+
inset: 5em 1em 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
139
242
|
</style>
|
package/lib/Usage.svelte
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
<NumberBar
|
|
17
17
|
max={info.limits.user_size && info.limits.user_size * 1_000_000}
|
|
18
18
|
value={info.usedBytes}
|
|
19
|
-
text="
|
|
19
|
+
text="{formatBytes(info.usedBytes)} {!info.limits.user_size
|
|
20
20
|
? ''
|
|
21
|
-
: '
|
|
21
|
+
: '/ ' + formatBytes(info.limits.user_size * 1_000_000)}"
|
|
22
22
|
/>
|
|
23
23
|
</a>
|
|
24
24
|
</p>
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<Icon i="folder-arrow-up" /> Back
|
|
36
36
|
</button>
|
|
37
37
|
<StorageList appMode bind:items user={data.session?.user} />
|
|
38
|
-
<StorageAdd parentId={item.id}
|
|
38
|
+
<StorageAdd parentId={item.id} onAdd={item => items.push(item)} />
|
|
39
39
|
{:else}
|
|
40
40
|
<p>No preview available.</p>
|
|
41
41
|
{/if}
|