@axium/storage 0.24.3 → 0.25.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/api.d.ts +2 -1
- package/dist/client/api.js +13 -5
- package/dist/client/sync.js +1 -1
- package/dist/common.d.ts +3 -3
- package/dist/common.js +1 -1
- package/dist/server/batch.js +1 -1
- package/dist/server/item.d.ts +5 -2
- package/dist/server/item.js +16 -7
- package/dist/server/raw.js +18 -10
- package/lib/Add.svelte +36 -15
- package/lib/List.svelte +1 -1
- package/locales/en.json +2 -1
- package/package.json +1 -1
package/dist/client/api.d.ts
CHANGED
|
@@ -9,10 +9,11 @@ export interface CreateItemInit {
|
|
|
9
9
|
name: string;
|
|
10
10
|
size: number;
|
|
11
11
|
type: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
12
13
|
}
|
|
13
14
|
export declare function createItem(stream: ReadableStream<Uint8Array<ArrayBuffer>>, init: CreateItemInit): Promise<StorageItemMetadata>;
|
|
14
15
|
export declare function createItemFromFile(file: File, init: Partial<CreateItemInit>): Promise<StorageItemMetadata>;
|
|
15
|
-
export declare function updateItem(fileId: string, newSize: number | bigint, stream: ReadableStream<Uint8Array<ArrayBuffer>>, onProgress?: ProgressHandler): Promise<StorageItemMetadata>;
|
|
16
|
+
export declare function updateItem(fileId: string, newSize: number | bigint, stream: ReadableStream<Uint8Array<ArrayBuffer>>, onProgress?: ProgressHandler, signal?: AbortSignal): Promise<StorageItemMetadata>;
|
|
16
17
|
export declare function getItemMetadata(fileId: string, options?: GetItemOptions): Promise<StorageItemMetadata>;
|
|
17
18
|
/** Gets the metadata for all items in a directory. */
|
|
18
19
|
export declare function getDirectoryMetadata(parentId: string): Promise<StorageItemMetadata[]>;
|
package/dist/client/api.js
CHANGED
|
@@ -39,14 +39,21 @@ async function handleResponse(response) {
|
|
|
39
39
|
throw prettifyError(e);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
async function _upload(upload, stream, itemSize, onProgress) {
|
|
42
|
+
async function _upload(upload, stream, itemSize, onProgress, signal) {
|
|
43
43
|
if (upload.status == 'created')
|
|
44
44
|
return upload.item;
|
|
45
|
+
signal?.addEventListener('abort', () => {
|
|
46
|
+
void fetch(rawStorage('upload'), {
|
|
47
|
+
method: 'DELETE',
|
|
48
|
+
headers: { 'x-upload': upload.token },
|
|
49
|
+
});
|
|
50
|
+
});
|
|
45
51
|
const targetChunkSize = upload.max_transfer_size * 1_000_000;
|
|
46
52
|
let response;
|
|
47
53
|
const reader = stream.getReader();
|
|
48
54
|
let buffer = new Uint8Array(0);
|
|
49
55
|
for (let offset = 0; offset < itemSize; offset += targetChunkSize) {
|
|
56
|
+
signal?.throwIfAborted();
|
|
50
57
|
const chunkSize = Math.min(targetChunkSize, itemSize - offset);
|
|
51
58
|
let bytesReadForChunk = 0;
|
|
52
59
|
const headers = {
|
|
@@ -108,10 +115,11 @@ async function _upload(upload, stream, itemSize, onProgress) {
|
|
|
108
115
|
body = chunkData.subarray(0, bytesReadForChunk);
|
|
109
116
|
onProgress?.(offset + bytesReadForChunk, itemSize);
|
|
110
117
|
}
|
|
111
|
-
response = await fetch(rawStorage('
|
|
118
|
+
response = await fetch(rawStorage('upload'), {
|
|
112
119
|
method: 'POST',
|
|
113
120
|
headers,
|
|
114
121
|
body,
|
|
122
|
+
signal,
|
|
115
123
|
...init,
|
|
116
124
|
}).catch(handleFetchFailed);
|
|
117
125
|
if (!response.ok)
|
|
@@ -132,14 +140,14 @@ export async function createItem(stream, init) {
|
|
|
132
140
|
if (!init.name)
|
|
133
141
|
throw 'item name is required';
|
|
134
142
|
const upload = await fetchAPI('PUT', 'storage', { ...init, hash: null });
|
|
135
|
-
return await _upload(upload, stream, init.size, init.onProgress);
|
|
143
|
+
return await _upload(upload, stream, init.size, init.onProgress, init.signal);
|
|
136
144
|
}
|
|
137
145
|
export async function createItemFromFile(file, init) {
|
|
138
146
|
return await createItem(file.stream(), { ...pick(file, 'name', 'size', 'type'), ...init });
|
|
139
147
|
}
|
|
140
|
-
export async function updateItem(fileId, newSize, stream, onProgress) {
|
|
148
|
+
export async function updateItem(fileId, newSize, stream, onProgress, signal) {
|
|
141
149
|
const upload = await fetchAPI('POST', 'storage/item/:id', newSize, fileId);
|
|
142
|
-
return await _upload(upload, stream, Number(newSize), onProgress);
|
|
150
|
+
return await _upload(upload, stream, Number(newSize), onProgress, signal);
|
|
143
151
|
}
|
|
144
152
|
export async function getItemMetadata(fileId, options = {}) {
|
|
145
153
|
return await fetchAPI('GET', 'storage/item/:id', options, fileId);
|
package/dist/client/sync.js
CHANGED
|
@@ -76,7 +76,7 @@ export function computeDelta(sync) {
|
|
|
76
76
|
}));
|
|
77
77
|
const delta = {
|
|
78
78
|
_items: items,
|
|
79
|
-
items:
|
|
79
|
+
items: items.values().toArray(),
|
|
80
80
|
synced: Array.from(synced.difference(modified)).map(p => items.get(p)),
|
|
81
81
|
modified: Array.from(modified).map(p => items.get(p)),
|
|
82
82
|
local_only: Array.from(new Set(files.keys()).difference(items)).map(p => files.get(p)),
|
package/dist/common.d.ts
CHANGED
|
@@ -244,7 +244,7 @@ export declare const UploadInitResult: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
244
244
|
}, z.core.$strip>;
|
|
245
245
|
max_transfer_size: z.ZodInt;
|
|
246
246
|
status: z.ZodLiteral<"accepted">;
|
|
247
|
-
token: z.
|
|
247
|
+
token: z.ZodBase64URL;
|
|
248
248
|
}, z.core.$strip>, z.ZodObject<{
|
|
249
249
|
status: z.ZodLiteral<"created">;
|
|
250
250
|
item: z.ZodObject<{
|
|
@@ -510,7 +510,7 @@ declare const StorageAPI: {
|
|
|
510
510
|
}, z.core.$strip>;
|
|
511
511
|
max_transfer_size: z.ZodInt;
|
|
512
512
|
status: z.ZodLiteral<"accepted">;
|
|
513
|
-
token: z.
|
|
513
|
+
token: z.ZodBase64URL;
|
|
514
514
|
}, z.core.$strip>, z.ZodObject<{
|
|
515
515
|
status: z.ZodLiteral<"created">;
|
|
516
516
|
item: z.ZodObject<{
|
|
@@ -743,7 +743,7 @@ declare const StorageAPI: {
|
|
|
743
743
|
}, z.core.$strip>;
|
|
744
744
|
max_transfer_size: z.ZodInt;
|
|
745
745
|
status: z.ZodLiteral<"accepted">;
|
|
746
|
-
token: z.
|
|
746
|
+
token: z.ZodBase64URL;
|
|
747
747
|
}, z.core.$strip>, z.ZodObject<{
|
|
748
748
|
status: z.ZodLiteral<"created">;
|
|
749
749
|
item: z.ZodObject<{
|
package/dist/common.js
CHANGED
|
@@ -144,7 +144,7 @@ export const UploadInitResult = z.discriminatedUnion('status', [
|
|
|
144
144
|
StoragePublicConfig.safeExtend({
|
|
145
145
|
status: z.literal('accepted'),
|
|
146
146
|
/** Used for chunked uploads */
|
|
147
|
-
token: z.
|
|
147
|
+
token: z.base64url(),
|
|
148
148
|
}),
|
|
149
149
|
z.object({
|
|
150
150
|
status: z.literal('created'),
|
package/dist/server/batch.js
CHANGED
package/dist/server/item.d.ts
CHANGED
|
@@ -28,8 +28,11 @@ export interface UploadInfo {
|
|
|
28
28
|
init: StorageItemInit;
|
|
29
29
|
/** If set we are updating an existing item. Explicit null used to avoid bugs */
|
|
30
30
|
itemId: string | null;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Remove the upload from pending and clean up resources
|
|
33
|
+
* @param isSuccess whether the upload was successful. If not, abort-style behavior will be used instead.
|
|
34
|
+
*/
|
|
35
|
+
remove(isSuccess?: boolean): void;
|
|
33
36
|
}
|
|
34
37
|
export declare function startUpload(init: StorageItemInit, session: Session, itemId: string | null): string;
|
|
35
38
|
export declare function requireUpload(request: Request): Promise<UploadInfo>;
|
package/dist/server/item.js
CHANGED
|
@@ -4,7 +4,7 @@ import { authRequestForItem, authSessionForItem, requireSession } from '@axium/s
|
|
|
4
4
|
import { database } from '@axium/server/database';
|
|
5
5
|
import { error, withError } from '@axium/server/requests';
|
|
6
6
|
import { createHash, randomBytes } from 'node:crypto';
|
|
7
|
-
import { createWriteStream, linkSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { createWriteStream, linkSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { Writable } from 'node:stream';
|
|
10
10
|
import * as z from 'zod';
|
|
@@ -138,20 +138,29 @@ export async function finishItemUpdate(itemId, size, hash, writeContent) {
|
|
|
138
138
|
const inProgress = new Map();
|
|
139
139
|
export function startUpload(init, session, itemId) {
|
|
140
140
|
const { temp_dir, upload_timeout } = getConfig('@axium/storage');
|
|
141
|
-
const token = randomBytes(32);
|
|
141
|
+
const token = randomBytes(32), tokenB64 = token.toBase64({ alphabet: 'base64url', omitPadding: true });
|
|
142
142
|
mkdirSync(temp_dir, { recursive: true });
|
|
143
143
|
const file = join(temp_dir, token.toHex());
|
|
144
144
|
let removed = false;
|
|
145
|
-
function remove() {
|
|
145
|
+
function remove(isSuccess = false) {
|
|
146
146
|
if (removed)
|
|
147
147
|
return;
|
|
148
148
|
removed = true;
|
|
149
|
-
inProgress.delete(
|
|
150
|
-
|
|
149
|
+
inProgress.delete(tokenB64);
|
|
150
|
+
if (isSuccess)
|
|
151
|
+
void stream.close();
|
|
152
|
+
else
|
|
153
|
+
void stream.abort();
|
|
151
154
|
hash.end();
|
|
155
|
+
try {
|
|
156
|
+
unlinkSync(file);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// probably renamed
|
|
160
|
+
}
|
|
152
161
|
}
|
|
153
162
|
const hash = createHash('BLAKE2b512'), stream = Writable.toWeb(createWriteStream(file));
|
|
154
|
-
inProgress.set(
|
|
163
|
+
inProgress.set(tokenB64, {
|
|
155
164
|
hash,
|
|
156
165
|
file,
|
|
157
166
|
stream,
|
|
@@ -165,7 +174,7 @@ export function startUpload(init, session, itemId) {
|
|
|
165
174
|
setTimeout(() => {
|
|
166
175
|
remove();
|
|
167
176
|
}, upload_timeout * 60_000);
|
|
168
|
-
return
|
|
177
|
+
return tokenB64;
|
|
169
178
|
}
|
|
170
179
|
export async function requireUpload(request) {
|
|
171
180
|
const token = request.headers.get('x-upload');
|
package/dist/server/raw.js
CHANGED
|
@@ -4,7 +4,7 @@ import { authRequestForItem, requireSession } from '@axium/server/auth';
|
|
|
4
4
|
import { error, withError } from '@axium/server/requests';
|
|
5
5
|
import { addRoute } from '@axium/server/routes';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import { copyFileSync, renameSync,
|
|
7
|
+
import { copyFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path/posix';
|
|
9
9
|
import * as z from 'zod';
|
|
10
10
|
import { streamRead } from '../node.js';
|
|
@@ -44,7 +44,7 @@ addRoute({
|
|
|
44
44
|
},
|
|
45
45
|
});
|
|
46
46
|
addRoute({
|
|
47
|
-
path: '/raw/storage/
|
|
47
|
+
path: '/raw/storage/upload',
|
|
48
48
|
async POST(request) {
|
|
49
49
|
if (!getConfig('@axium/storage').enabled)
|
|
50
50
|
error(503, 'User storage is disabled');
|
|
@@ -70,6 +70,9 @@ addRoute({
|
|
|
70
70
|
/* @todo Figure out if we need to handle stream cancellation differently.
|
|
71
71
|
Right now an error with this chunk cancels the streams but may not cleanly fail the upload */
|
|
72
72
|
await request.body.pipeThrough(counter).pipeTo(upload.stream, { preventClose: true });
|
|
73
|
+
if (request.signal.aborted) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
73
76
|
if (actualSize != size) {
|
|
74
77
|
upload.remove();
|
|
75
78
|
await audit('storage_size_mismatch', upload.userId, { item: null });
|
|
@@ -82,7 +85,6 @@ addRoute({
|
|
|
82
85
|
upload.init.hash ??= hash.toHex();
|
|
83
86
|
if (hash.toHex() != upload.init.hash)
|
|
84
87
|
error(409, 'Hash mismatch');
|
|
85
|
-
upload.remove();
|
|
86
88
|
function writeContent(path) {
|
|
87
89
|
try {
|
|
88
90
|
renameSync(upload.file, path);
|
|
@@ -93,16 +95,22 @@ addRoute({
|
|
|
93
95
|
copyFileSync(upload.file, path);
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
|
-
const item = upload.itemId
|
|
97
|
-
? await finishItemUpdate(upload.itemId, upload.init.size, hash, writeContent)
|
|
98
|
-
: await createNewItem(upload.init, upload.userId, writeContent);
|
|
99
98
|
try {
|
|
100
|
-
|
|
99
|
+
const item = upload.itemId
|
|
100
|
+
? await finishItemUpdate(upload.itemId, upload.init.size, hash, writeContent)
|
|
101
|
+
: await createNewItem(upload.init, upload.userId, writeContent);
|
|
102
|
+
return item;
|
|
101
103
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
finally {
|
|
105
|
+
upload.remove(true);
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
},
|
|
108
|
+
async DELETE(request) {
|
|
109
|
+
if (!getConfig('@axium/storage').enabled)
|
|
110
|
+
error(503, 'User storage is disabled');
|
|
111
|
+
const upload = await requireUpload(request);
|
|
112
|
+
upload.remove();
|
|
113
|
+
return new Response(null, { status: 204 });
|
|
106
114
|
},
|
|
107
115
|
});
|
|
108
116
|
function parseRange(itemSize, range) {
|
package/lib/Add.svelte
CHANGED
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
import { createItemFromFile } from '@axium/storage/client';
|
|
5
5
|
import type { StorageItemMetadata } from '@axium/storage/common';
|
|
6
6
|
import { text } from '@axium/client';
|
|
7
|
+
import { toast } from '@axium/client/toast';
|
|
7
8
|
|
|
8
9
|
const { parentId, onAdd }: { parentId?: string; onAdd?(item: StorageItemMetadata): void } = $props();
|
|
9
10
|
|
|
10
|
-
let uploadDialog = $state<HTMLDialogElement>()
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
let uploadDialog = $state<HTMLDialogElement>()!,
|
|
12
|
+
uploadProgress = $state<[number, number][]>([]),
|
|
13
|
+
uploading = $state(false),
|
|
14
|
+
controller = $state(new AbortController()),
|
|
15
|
+
files = $state<FileList>()!;
|
|
13
16
|
|
|
14
|
-
let createDialog = $state<HTMLDialogElement>()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
let createDialog = $state<HTMLDialogElement>()!,
|
|
18
|
+
createType = $state<string>(),
|
|
19
|
+
createIncludesContent = $state(false);
|
|
17
20
|
</script>
|
|
18
21
|
|
|
19
22
|
{#snippet _item(type: string, text: string, includeContent: boolean = false)}
|
|
@@ -37,23 +40,41 @@
|
|
|
37
40
|
|
|
38
41
|
<span class="menu-item" onclick={() => uploadDialog.showModal()}><Icon i="upload" />{text('storage.Add.upload')}</span>
|
|
39
42
|
{@render _item('inode/directory', text('storage.Add.new_folder'))}
|
|
40
|
-
{@render _item('text/plain', text('storage.Add.plain_text'))}
|
|
43
|
+
{@render _item('text/plain', text('storage.Add.plain_text'), true)}
|
|
41
44
|
</Popover>
|
|
42
45
|
|
|
43
46
|
<FormDialog
|
|
44
47
|
bind:dialog={uploadDialog}
|
|
45
48
|
submitText={text('storage.Add.upload')}
|
|
46
|
-
cancel={() =>
|
|
49
|
+
cancel={() => {
|
|
50
|
+
files = new DataTransfer().files;
|
|
51
|
+
if (!uploading) return;
|
|
52
|
+
uploading = false;
|
|
53
|
+
uploadProgress = [];
|
|
54
|
+
controller.abort();
|
|
55
|
+
toast('info', text('storage.Add.cancelled'));
|
|
56
|
+
controller = new AbortController();
|
|
57
|
+
}}
|
|
47
58
|
submit={async () => {
|
|
59
|
+
uploading = true;
|
|
48
60
|
for (const [i, file] of Array.from(files!).entries()) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
try {
|
|
62
|
+
const item = await createItemFromFile(file, {
|
|
63
|
+
parentId,
|
|
64
|
+
onProgress(uploaded, total) {
|
|
65
|
+
uploadProgress[i] = [uploaded, total];
|
|
66
|
+
},
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
});
|
|
69
|
+
onAdd?.(item);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (e && e instanceof DOMException && e.name == 'AbortError') return;
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
56
74
|
}
|
|
75
|
+
uploading = false;
|
|
76
|
+
uploadProgress = [];
|
|
77
|
+
files = new DataTransfer().files;
|
|
57
78
|
}}
|
|
58
79
|
>
|
|
59
80
|
<Upload bind:files bind:progress={uploadProgress} multiple />
|
package/lib/List.svelte
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
const [a, b] = sort.descending ? [_b, _a] : [_a, _b];
|
|
52
52
|
// @ts-expect-error 2362 — `Date`s have a `valueOf` and can be treated like numbers
|
|
53
|
-
return sort.by == 'name' ? a.name.localeCompare(b.name) : a[sort.by] - b[sort.by];
|
|
53
|
+
return sort.by == 'name' ? a.name.localeCompare(b.name) : Number(a[sort.by] - b[sort.by]);
|
|
54
54
|
})
|
|
55
55
|
);
|
|
56
56
|
|
package/locales/en.json
CHANGED