@axium/storage 0.15.1 → 0.16.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 +17 -1
- package/dist/client/api.js +83 -24
- package/dist/common.d.ts +116 -6
- package/dist/common.js +23 -4
- package/dist/polyfills.js +6 -1
- package/dist/server/api.js +27 -9
- package/dist/server/item.d.ts +26 -0
- package/dist/server/item.js +136 -0
- package/dist/server/raw.js +56 -71
- package/lib/Add.svelte +10 -4
- package/package.json +6 -5
package/dist/client/api.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { StorageItemUpdate, UserStorage, UserStorageInfo } from '../common.js';
|
|
2
|
+
import { StorageItemMetadata } from '../common.js';
|
|
3
|
+
import '../polyfills.js';
|
|
2
4
|
export interface UploadOptions {
|
|
3
5
|
parentId?: string;
|
|
4
6
|
name?: string;
|
|
7
|
+
onProgress?(this: void, uploaded: number, total: number): void;
|
|
8
|
+
}
|
|
9
|
+
declare global {
|
|
10
|
+
interface NetworkInformation {
|
|
11
|
+
readonly downlink: number;
|
|
12
|
+
readonly downlinkMax?: number;
|
|
13
|
+
readonly effectiveType: 'slow-2g' | '2g' | '3g' | '4g';
|
|
14
|
+
readonly rtt: number;
|
|
15
|
+
readonly saveData: boolean;
|
|
16
|
+
readonly type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown';
|
|
17
|
+
}
|
|
18
|
+
interface Navigator {
|
|
19
|
+
connection?: NetworkInformation;
|
|
20
|
+
}
|
|
5
21
|
}
|
|
6
22
|
export declare function uploadItem(file: Blob | File, opt?: UploadOptions): Promise<StorageItemMetadata>;
|
|
7
23
|
export declare function updateItem(fileId: string, data: Blob): Promise<StorageItemMetadata>;
|
package/dist/client/api.js
CHANGED
|
@@ -1,13 +1,91 @@
|
|
|
1
1
|
import { fetchAPI, prefix, token } from '@axium/client/requests';
|
|
2
|
-
import {
|
|
2
|
+
import { blake2b } from 'blakejs';
|
|
3
3
|
import { prettifyError } from 'zod';
|
|
4
|
-
|
|
4
|
+
import { StorageItemMetadata } from '../common.js';
|
|
5
|
+
import '../polyfills.js';
|
|
6
|
+
function rawStorage(suffix) {
|
|
7
|
+
const raw = '/raw/storage' + (suffix ? '/' + suffix : '');
|
|
8
|
+
if (prefix[0] == '/')
|
|
9
|
+
return raw;
|
|
10
|
+
const url = new URL(prefix);
|
|
11
|
+
url.pathname = raw;
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
const conTypeToSpeed = {
|
|
15
|
+
'slow-2g': 1,
|
|
16
|
+
'2g': 4,
|
|
17
|
+
'3g': 16,
|
|
18
|
+
'4g': 64,
|
|
19
|
+
};
|
|
20
|
+
async function handleError(response) {
|
|
21
|
+
if (response.headers.get('Content-Type')?.trim() != 'application/json')
|
|
22
|
+
throw await response.text();
|
|
23
|
+
const json = await response.json();
|
|
24
|
+
throw json.message;
|
|
25
|
+
}
|
|
26
|
+
export async function uploadItem(file, opt = {}) {
|
|
27
|
+
if (file instanceof File)
|
|
28
|
+
opt.name ||= file.name;
|
|
29
|
+
if (!opt.name)
|
|
30
|
+
throw 'item name is required';
|
|
31
|
+
const content = await file.bytes();
|
|
32
|
+
/**
|
|
33
|
+
* For big files, it takes a *really* long time to compute the hash, so we just don't do it ahead of time and leave it up to the server.
|
|
34
|
+
*/
|
|
35
|
+
const hash = content.length < 10_000_000 ? blake2b(content).toHex() : null;
|
|
36
|
+
const upload = await fetchAPI('PUT', 'storage', {
|
|
37
|
+
parentId: opt.parentId,
|
|
38
|
+
name: opt.name,
|
|
39
|
+
type: file.type,
|
|
40
|
+
size: file.size,
|
|
41
|
+
hash,
|
|
42
|
+
});
|
|
43
|
+
if (upload.status == 'created')
|
|
44
|
+
return upload.item;
|
|
45
|
+
let chunkSize = upload.max_transfer_size * 1_000_000;
|
|
46
|
+
if (globalThis.navigator?.connection) {
|
|
47
|
+
chunkSize = Math.min(upload.max_transfer_size, conTypeToSpeed[globalThis.navigator.connection.effectiveType]) * 1_000_000;
|
|
48
|
+
}
|
|
49
|
+
let response;
|
|
50
|
+
for (let offset = 0; offset < content.length; offset += chunkSize) {
|
|
51
|
+
const size = Math.min(chunkSize, content.length - offset);
|
|
52
|
+
response = await fetch(rawStorage('chunk'), {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'x-upload': upload.token,
|
|
56
|
+
'x-offset': offset.toString(),
|
|
57
|
+
'content-length': size.toString(),
|
|
58
|
+
'content-type': 'application/octet-stream',
|
|
59
|
+
},
|
|
60
|
+
body: content.slice(offset, offset + size),
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
await handleError(response);
|
|
64
|
+
opt.onProgress?.(offset + size, content.length);
|
|
65
|
+
if (offset + size != content.length && response.status != 204)
|
|
66
|
+
console.warn('Unexpected end of upload before last chunk');
|
|
67
|
+
}
|
|
68
|
+
if (!response)
|
|
69
|
+
throw new Error('BUG: No response');
|
|
70
|
+
if (!response.headers.get('Content-Type')?.includes('application/json')) {
|
|
71
|
+
throw new Error(`Unexpected response type: ${response.headers.get('Content-Type')}`);
|
|
72
|
+
}
|
|
73
|
+
const json = await response.json().catch(() => ({ message: 'Unknown server error (invalid JSON response)' }));
|
|
74
|
+
if (!response.ok)
|
|
75
|
+
await handleError(response);
|
|
76
|
+
try {
|
|
77
|
+
return StorageItemMetadata.parse(json);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
throw prettifyError(e);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function updateItem(fileId, data) {
|
|
5
84
|
const init = {
|
|
6
|
-
method,
|
|
85
|
+
method: 'POST',
|
|
7
86
|
headers: {
|
|
8
87
|
'Content-Type': data.type,
|
|
9
88
|
'Content-Length': data.size.toString(),
|
|
10
|
-
...extraHeaders,
|
|
11
89
|
},
|
|
12
90
|
body: data,
|
|
13
91
|
};
|
|
@@ -15,7 +93,7 @@ async function _upload(method, url, data, extraHeaders = {}) {
|
|
|
15
93
|
init.headers['X-Name'] = data.name;
|
|
16
94
|
if (token)
|
|
17
95
|
init.headers.Authorization = 'Bearer ' + token;
|
|
18
|
-
const response = await fetch(
|
|
96
|
+
const response = await fetch(rawStorage(fileId), init);
|
|
19
97
|
if (!response.headers.get('Content-Type')?.includes('application/json')) {
|
|
20
98
|
throw new Error(`Unexpected response type: ${response.headers.get('Content-Type')}`);
|
|
21
99
|
}
|
|
@@ -29,25 +107,6 @@ async function _upload(method, url, data, extraHeaders = {}) {
|
|
|
29
107
|
throw prettifyError(e);
|
|
30
108
|
}
|
|
31
109
|
}
|
|
32
|
-
function rawStorage(fileId) {
|
|
33
|
-
const raw = '/raw/storage' + (fileId ? '/' + fileId : '');
|
|
34
|
-
if (prefix[0] == '/')
|
|
35
|
-
return raw;
|
|
36
|
-
const url = new URL(prefix);
|
|
37
|
-
url.pathname = raw;
|
|
38
|
-
return url;
|
|
39
|
-
}
|
|
40
|
-
export async function uploadItem(file, opt = {}) {
|
|
41
|
-
const headers = {};
|
|
42
|
-
if (opt.parentId)
|
|
43
|
-
headers['x-parent'] = opt.parentId;
|
|
44
|
-
if (opt.name)
|
|
45
|
-
headers['x-name'] = opt.name;
|
|
46
|
-
return await _upload('PUT', rawStorage(), file, headers);
|
|
47
|
-
}
|
|
48
|
-
export async function updateItem(fileId, data) {
|
|
49
|
-
return await _upload('POST', rawStorage(fileId), data);
|
|
50
|
-
}
|
|
51
110
|
export async function getItemMetadata(fileId) {
|
|
52
111
|
return await fetchAPI('GET', 'storage/item/:id', undefined, fileId);
|
|
53
112
|
}
|
package/dist/common.d.ts
CHANGED
|
@@ -160,9 +160,7 @@ export declare const StoragePublicConfig: z.ZodObject<{
|
|
|
160
160
|
max_items: z.ZodInt;
|
|
161
161
|
max_item_size: z.ZodInt;
|
|
162
162
|
}, z.core.$strip>;
|
|
163
|
-
chunk: z.ZodBoolean;
|
|
164
163
|
max_transfer_size: z.ZodInt;
|
|
165
|
-
max_chunks: z.ZodInt;
|
|
166
164
|
}, z.core.$strip>;
|
|
167
165
|
export interface StoragePublicConfig extends z.infer<typeof StoragePublicConfig> {
|
|
168
166
|
}
|
|
@@ -172,9 +170,7 @@ export declare const StorageConfig: z.ZodObject<{
|
|
|
172
170
|
max_items: z.ZodInt;
|
|
173
171
|
max_item_size: z.ZodInt;
|
|
174
172
|
}, z.core.$strip>;
|
|
175
|
-
chunk: z.ZodBoolean;
|
|
176
173
|
max_transfer_size: z.ZodInt;
|
|
177
|
-
max_chunks: z.ZodInt;
|
|
178
174
|
cas: z.ZodObject<{
|
|
179
175
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
180
176
|
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -188,12 +184,73 @@ export declare const StorageConfig: z.ZodObject<{
|
|
|
188
184
|
user_size: z.ZodInt;
|
|
189
185
|
}, z.core.$strip>;
|
|
190
186
|
trash_duration: z.ZodNumber;
|
|
187
|
+
temp_dir: z.ZodString;
|
|
188
|
+
upload_timeout: z.ZodNumber;
|
|
191
189
|
}, z.core.$strip>;
|
|
192
190
|
declare module '@axium/core/plugins' {
|
|
193
191
|
interface $PluginConfigs {
|
|
194
192
|
'@axium/storage': z.infer<typeof StorageConfig>;
|
|
195
193
|
}
|
|
196
194
|
}
|
|
195
|
+
export declare const StorageItemInit: z.ZodObject<{
|
|
196
|
+
name: z.ZodString;
|
|
197
|
+
size: z.ZodInt;
|
|
198
|
+
type: z.ZodString;
|
|
199
|
+
parentId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
200
|
+
hash: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
201
|
+
}, z.core.$strip>;
|
|
202
|
+
export interface StorageItemInit extends z.infer<typeof StorageItemInit> {
|
|
203
|
+
}
|
|
204
|
+
export declare const UploadInitResult: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
205
|
+
batch: z.ZodObject<{
|
|
206
|
+
enabled: z.ZodBoolean;
|
|
207
|
+
max_items: z.ZodInt;
|
|
208
|
+
max_item_size: z.ZodInt;
|
|
209
|
+
}, z.core.$strip>;
|
|
210
|
+
max_transfer_size: z.ZodInt;
|
|
211
|
+
status: z.ZodLiteral<"accepted">;
|
|
212
|
+
token: z.ZodBase64;
|
|
213
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
214
|
+
status: z.ZodLiteral<"created">;
|
|
215
|
+
item: z.ZodObject<{
|
|
216
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
217
|
+
dataURL: z.ZodString;
|
|
218
|
+
hash: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
219
|
+
id: z.ZodUUID;
|
|
220
|
+
immutable: z.ZodBoolean;
|
|
221
|
+
modifiedAt: z.ZodCoercedDate<unknown>;
|
|
222
|
+
name: z.ZodString;
|
|
223
|
+
userId: z.ZodUUID;
|
|
224
|
+
parentId: z.ZodNullable<z.ZodUUID>;
|
|
225
|
+
size: z.ZodInt;
|
|
226
|
+
trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
|
|
227
|
+
type: z.ZodString;
|
|
228
|
+
metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
229
|
+
acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
230
|
+
itemId: z.ZodUUID;
|
|
231
|
+
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
232
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
233
|
+
tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
234
|
+
user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
235
|
+
id: z.ZodUUID;
|
|
236
|
+
name: z.ZodString;
|
|
237
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
238
|
+
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
239
|
+
image: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
|
|
240
|
+
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
241
|
+
debug: z.ZodDefault<z.ZodBoolean>;
|
|
242
|
+
}, z.core.$strip>>>;
|
|
243
|
+
roles: z.ZodArray<z.ZodString>;
|
|
244
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
245
|
+
registeredAt: z.ZodCoercedDate<unknown>;
|
|
246
|
+
isAdmin: z.ZodOptional<z.ZodBoolean>;
|
|
247
|
+
isSuspended: z.ZodOptional<z.ZodBoolean>;
|
|
248
|
+
}, z.core.$strip>>>;
|
|
249
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
250
|
+
}, z.core.$loose>>>;
|
|
251
|
+
}, z.core.$strip>;
|
|
252
|
+
}, z.core.$strip>], "status">;
|
|
253
|
+
export type UploadInitResult = z.infer<typeof UploadInitResult>;
|
|
197
254
|
declare const StorageAPI: {
|
|
198
255
|
readonly 'users/:id/storage': {
|
|
199
256
|
readonly OPTIONS: z.ZodObject<{
|
|
@@ -380,12 +437,65 @@ declare const StorageAPI: {
|
|
|
380
437
|
max_items: z.ZodInt;
|
|
381
438
|
max_item_size: z.ZodInt;
|
|
382
439
|
}, z.core.$strip>;
|
|
383
|
-
chunk: z.ZodBoolean;
|
|
384
440
|
max_transfer_size: z.ZodInt;
|
|
385
|
-
max_chunks: z.ZodInt;
|
|
386
441
|
syncProtocolVersion: z.ZodInt32;
|
|
387
442
|
batchFormatVersion: z.ZodInt32;
|
|
388
443
|
}, z.core.$strip>;
|
|
444
|
+
readonly PUT: readonly [z.ZodObject<{
|
|
445
|
+
name: z.ZodString;
|
|
446
|
+
size: z.ZodInt;
|
|
447
|
+
type: z.ZodString;
|
|
448
|
+
parentId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
449
|
+
hash: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
450
|
+
}, z.core.$strip>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
451
|
+
batch: z.ZodObject<{
|
|
452
|
+
enabled: z.ZodBoolean;
|
|
453
|
+
max_items: z.ZodInt;
|
|
454
|
+
max_item_size: z.ZodInt;
|
|
455
|
+
}, z.core.$strip>;
|
|
456
|
+
max_transfer_size: z.ZodInt;
|
|
457
|
+
status: z.ZodLiteral<"accepted">;
|
|
458
|
+
token: z.ZodBase64;
|
|
459
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
460
|
+
status: z.ZodLiteral<"created">;
|
|
461
|
+
item: z.ZodObject<{
|
|
462
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
463
|
+
dataURL: z.ZodString;
|
|
464
|
+
hash: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
465
|
+
id: z.ZodUUID;
|
|
466
|
+
immutable: z.ZodBoolean;
|
|
467
|
+
modifiedAt: z.ZodCoercedDate<unknown>;
|
|
468
|
+
name: z.ZodString;
|
|
469
|
+
userId: z.ZodUUID;
|
|
470
|
+
parentId: z.ZodNullable<z.ZodUUID>;
|
|
471
|
+
size: z.ZodInt;
|
|
472
|
+
trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
|
|
473
|
+
type: z.ZodString;
|
|
474
|
+
metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
475
|
+
acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
476
|
+
itemId: z.ZodUUID;
|
|
477
|
+
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
478
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
479
|
+
tag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
480
|
+
user: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
481
|
+
id: z.ZodUUID;
|
|
482
|
+
name: z.ZodString;
|
|
483
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
484
|
+
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
485
|
+
image: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
|
|
486
|
+
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
487
|
+
debug: z.ZodDefault<z.ZodBoolean>;
|
|
488
|
+
}, z.core.$strip>>>;
|
|
489
|
+
roles: z.ZodArray<z.ZodString>;
|
|
490
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
491
|
+
registeredAt: z.ZodCoercedDate<unknown>;
|
|
492
|
+
isAdmin: z.ZodOptional<z.ZodBoolean>;
|
|
493
|
+
isSuspended: z.ZodOptional<z.ZodBoolean>;
|
|
494
|
+
}, z.core.$strip>>>;
|
|
495
|
+
createdAt: z.ZodCoercedDate<unknown>;
|
|
496
|
+
}, z.core.$loose>>>;
|
|
497
|
+
}, z.core.$strip>;
|
|
498
|
+
}, z.core.$strip>], "status">];
|
|
389
499
|
};
|
|
390
500
|
readonly 'storage/batch': {
|
|
391
501
|
readonly POST: readonly [z.ZodArray<z.ZodObject<{
|
package/dist/common.js
CHANGED
|
@@ -79,12 +79,8 @@ export const StoragePublicConfig = z.object({
|
|
|
79
79
|
/** Maximum size in KiB per item */
|
|
80
80
|
max_item_size: z.int().positive(),
|
|
81
81
|
}),
|
|
82
|
-
/** Whether to split files larger than `max_transfer_size` into multiple chunks */
|
|
83
|
-
chunk: z.boolean(),
|
|
84
82
|
/** Maximum size in MiB per transfer/request */
|
|
85
83
|
max_transfer_size: z.int().positive(),
|
|
86
|
-
/** Maximum number of chunks */
|
|
87
|
-
max_chunks: z.int().positive(),
|
|
88
84
|
});
|
|
89
85
|
export const StorageConfig = StoragePublicConfig.safeExtend({
|
|
90
86
|
/** Content Addressable Storage (CAS) configuration */
|
|
@@ -106,8 +102,30 @@ export const StorageConfig = StoragePublicConfig.safeExtend({
|
|
|
106
102
|
limits: StorageLimits,
|
|
107
103
|
/** How many days files are kept in the trash */
|
|
108
104
|
trash_duration: z.number(),
|
|
105
|
+
/** Where to put in-progress chunked uploads */
|
|
106
|
+
temp_dir: z.string(),
|
|
107
|
+
/** How many minutes before an in-progress upload times out */
|
|
108
|
+
upload_timeout: z.number(),
|
|
109
109
|
});
|
|
110
110
|
setServerConfig('@axium/storage', StorageConfig);
|
|
111
|
+
export const StorageItemInit = z.object({
|
|
112
|
+
name: z.string(),
|
|
113
|
+
size: z.int().nonnegative(),
|
|
114
|
+
type: z.string(),
|
|
115
|
+
parentId: z.uuid().nullish(),
|
|
116
|
+
hash: z.hex().nullish(),
|
|
117
|
+
});
|
|
118
|
+
export const UploadInitResult = z.discriminatedUnion('status', [
|
|
119
|
+
StoragePublicConfig.safeExtend({
|
|
120
|
+
status: z.literal('accepted'),
|
|
121
|
+
/** Used for chunked uploads */
|
|
122
|
+
token: z.base64(),
|
|
123
|
+
}),
|
|
124
|
+
z.object({
|
|
125
|
+
status: z.literal('created'),
|
|
126
|
+
item: StorageItemMetadata,
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
111
129
|
const StorageAPI = {
|
|
112
130
|
'users/:id/storage': {
|
|
113
131
|
OPTIONS: UserStorageInfo,
|
|
@@ -127,6 +145,7 @@ const StorageAPI = {
|
|
|
127
145
|
syncProtocolVersion: z.int32().nonnegative(),
|
|
128
146
|
batchFormatVersion: z.int32().nonnegative(),
|
|
129
147
|
}),
|
|
148
|
+
PUT: [StorageItemInit, UploadInitResult],
|
|
130
149
|
},
|
|
131
150
|
'storage/batch': {
|
|
132
151
|
POST: [StorageBatchUpdate.array(), StorageItemMetadata.array()],
|
package/dist/polyfills.js
CHANGED
|
@@ -7,12 +7,17 @@ https://github.com/microsoft/TypeScript/issues/61695
|
|
|
7
7
|
|
|
8
8
|
@todo Remove when TypeScript 5.9 is released
|
|
9
9
|
*/
|
|
10
|
-
import { debug } from '@axium/core/
|
|
10
|
+
import { debug } from '@axium/core/io';
|
|
11
11
|
Uint8Array.prototype.toHex ??=
|
|
12
12
|
(debug('Using a polyfill of Uint8Array.prototype.toHex'),
|
|
13
13
|
function toHex() {
|
|
14
14
|
return [...this].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
15
15
|
});
|
|
16
|
+
Uint8Array.prototype.toBase64 ??=
|
|
17
|
+
(debug('Using a polyfill of Uint8Array.prototype.toBase64'),
|
|
18
|
+
function toBase64() {
|
|
19
|
+
return btoa(String.fromCharCode(...this));
|
|
20
|
+
});
|
|
16
21
|
Uint8Array.fromHex ??=
|
|
17
22
|
(debug('Using a polyfill of Uint8Array.fromHex'),
|
|
18
23
|
function fromHex(hex) {
|
package/dist/server/api.js
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
import { getConfig } from '@axium/core';
|
|
2
|
-
import {
|
|
2
|
+
import { authRequestForItem, checkAuthForUser, requireSession } from '@axium/server/auth';
|
|
3
3
|
import { database } from '@axium/server/database';
|
|
4
|
-
import { error, parseBody, withError } from '@axium/server/requests';
|
|
4
|
+
import { error, json, parseBody, withError } from '@axium/server/requests';
|
|
5
5
|
import { addRoute } from '@axium/server/routes';
|
|
6
6
|
import { pick } from 'utilium';
|
|
7
7
|
import * as z from 'zod';
|
|
8
|
-
import { batchFormatVersion, StorageItemUpdate, syncProtocolVersion } from '../common.js';
|
|
8
|
+
import { batchFormatVersion, StorageItemInit, StorageItemUpdate, syncProtocolVersion } from '../common.js';
|
|
9
9
|
import '../polyfills.js';
|
|
10
10
|
import { getLimits } from './config.js';
|
|
11
11
|
import { deleteRecursive, getRecursive, getUserStats, parseItem } from './db.js';
|
|
12
12
|
import { from as aclFrom } from '@axium/server/acl';
|
|
13
|
+
import { checkNewItem, createNewItem, startUpload } from './item.js';
|
|
13
14
|
addRoute({
|
|
14
15
|
path: '/api/storage',
|
|
15
16
|
OPTIONS() {
|
|
16
17
|
return {
|
|
17
|
-
...pick(getConfig('@axium/storage'), 'batch', '
|
|
18
|
+
...pick(getConfig('@axium/storage'), 'batch', 'max_transfer_size'),
|
|
18
19
|
syncProtocolVersion,
|
|
19
20
|
batchFormatVersion,
|
|
20
21
|
};
|
|
21
22
|
},
|
|
23
|
+
async PUT(request) {
|
|
24
|
+
const { enabled, ...rest } = getConfig('@axium/storage');
|
|
25
|
+
if (!enabled)
|
|
26
|
+
error(503, 'User storage is disabled');
|
|
27
|
+
const session = await requireSession(request);
|
|
28
|
+
const init = await parseBody(request, StorageItemInit);
|
|
29
|
+
const { existing } = await checkNewItem(init, session);
|
|
30
|
+
if (existing || init.type == 'inode/directory') {
|
|
31
|
+
const item = await createNewItem(init, session.userId);
|
|
32
|
+
return json({ status: 'created', item }, { status: 201 });
|
|
33
|
+
}
|
|
34
|
+
return json({
|
|
35
|
+
...pick(rest, 'batch', 'max_transfer_size'),
|
|
36
|
+
status: 'accepted',
|
|
37
|
+
token: startUpload(init, session),
|
|
38
|
+
}, { status: 202 });
|
|
39
|
+
},
|
|
22
40
|
});
|
|
23
41
|
addRoute({
|
|
24
42
|
path: '/api/storage/item/:id',
|
|
@@ -26,14 +44,14 @@ addRoute({
|
|
|
26
44
|
async GET(request, { id: itemId }) {
|
|
27
45
|
if (!getConfig('@axium/storage').enabled)
|
|
28
46
|
error(503, 'User storage is disabled');
|
|
29
|
-
const { item } = await
|
|
47
|
+
const { item } = await authRequestForItem(request, 'storage', itemId, { read: true });
|
|
30
48
|
return parseItem(item);
|
|
31
49
|
},
|
|
32
50
|
async PATCH(request, { id: itemId }) {
|
|
33
51
|
if (!getConfig('@axium/storage').enabled)
|
|
34
52
|
error(503, 'User storage is disabled');
|
|
35
53
|
const body = await parseBody(request, StorageItemUpdate);
|
|
36
|
-
await
|
|
54
|
+
await authRequestForItem(request, 'storage', itemId, { manage: true });
|
|
37
55
|
const values = {};
|
|
38
56
|
if ('trash' in body)
|
|
39
57
|
values.trashedAt = body.trash ? new Date() : null;
|
|
@@ -54,7 +72,7 @@ addRoute({
|
|
|
54
72
|
async DELETE(request, { id: itemId }) {
|
|
55
73
|
if (!getConfig('@axium/storage').enabled)
|
|
56
74
|
error(503, 'User storage is disabled');
|
|
57
|
-
const auth = await
|
|
75
|
+
const auth = await authRequestForItem(request, 'storage', itemId, { manage: true });
|
|
58
76
|
const item = parseItem(auth.item);
|
|
59
77
|
await deleteRecursive(item.type != 'inode/directory', itemId);
|
|
60
78
|
return item;
|
|
@@ -66,7 +84,7 @@ addRoute({
|
|
|
66
84
|
async GET(request, { id: itemId }) {
|
|
67
85
|
if (!getConfig('@axium/storage').enabled)
|
|
68
86
|
error(503, 'User storage is disabled');
|
|
69
|
-
const { item } = await
|
|
87
|
+
const { item } = await authRequestForItem(request, 'storage', itemId, { read: true });
|
|
70
88
|
if (item.type != 'inode/directory')
|
|
71
89
|
error(409, 'Item is not a directory');
|
|
72
90
|
const items = await database
|
|
@@ -85,7 +103,7 @@ addRoute({
|
|
|
85
103
|
async GET(request, { id: itemId }) {
|
|
86
104
|
if (!getConfig('@axium/storage').enabled)
|
|
87
105
|
error(503, 'User storage is disabled');
|
|
88
|
-
const { item } = await
|
|
106
|
+
const { item } = await authRequestForItem(request, 'storage', itemId, { read: true });
|
|
89
107
|
if (item.type != 'inode/directory')
|
|
90
108
|
error(409, 'Item is not a directory');
|
|
91
109
|
const items = await Array.fromAsync(getRecursive(itemId)).catch(withError('Could not get some directory items'));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Session } from '@axium/core';
|
|
2
|
+
import { type SessionAndUser } from '@axium/server/auth';
|
|
3
|
+
import { type Hash } from 'node:crypto';
|
|
4
|
+
import type { StorageItemInit, StorageItemMetadata } from '../common.js';
|
|
5
|
+
import '../polyfills.js';
|
|
6
|
+
export interface NewItemResult {
|
|
7
|
+
existing?: {
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
needsHashing?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function useCAS(type: string): boolean;
|
|
13
|
+
export declare function checkNewItem(init: StorageItemInit, session: SessionAndUser): Promise<NewItemResult>;
|
|
14
|
+
export declare function createNewItem(init: StorageItemInit, userId: string, writeContent?: (path: string) => void): Promise<StorageItemMetadata>;
|
|
15
|
+
export interface UploadInfo {
|
|
16
|
+
file: string;
|
|
17
|
+
fd: number;
|
|
18
|
+
hash: Hash;
|
|
19
|
+
uploadedBytes: number;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
init: StorageItemInit;
|
|
23
|
+
remove(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function startUpload(init: StorageItemInit, session: Session): string;
|
|
26
|
+
export declare function requireUpload(request: Request): Promise<UploadInfo>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getConfig } from '@axium/core';
|
|
2
|
+
import { authSessionForItem, requireSession } from '@axium/server/auth';
|
|
3
|
+
import { database } from '@axium/server/database';
|
|
4
|
+
import { error, withError } from '@axium/server/requests';
|
|
5
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
6
|
+
import { closeSync, linkSync, mkdirSync, openSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
import '../polyfills.js';
|
|
10
|
+
import { defaultCASMime, getLimits } from './config.js';
|
|
11
|
+
import { getUserStats, parseItem } from './db.js';
|
|
12
|
+
export function useCAS(type) {
|
|
13
|
+
const { cas } = getConfig('@axium/storage');
|
|
14
|
+
return !!(cas.enabled &&
|
|
15
|
+
type != 'inode/directory' &&
|
|
16
|
+
(defaultCASMime.some(pattern => pattern.test(type)) || cas.include?.some(mime => type.match(mime))));
|
|
17
|
+
}
|
|
18
|
+
export async function checkNewItem(init, session) {
|
|
19
|
+
const { userId } = session;
|
|
20
|
+
const { size, name, type, hash } = init;
|
|
21
|
+
const [usage, limits] = await Promise.all([getUserStats(userId), getLimits(userId)]).catch(withError('Could not fetch usage and/or limits'));
|
|
22
|
+
if (!name)
|
|
23
|
+
error(400, 'Missing name');
|
|
24
|
+
if (name.length > 255)
|
|
25
|
+
error(400, 'Name is too long');
|
|
26
|
+
const parentId = init.parentId
|
|
27
|
+
? await z
|
|
28
|
+
.uuid()
|
|
29
|
+
.parseAsync(init.parentId)
|
|
30
|
+
.catch(() => error(400, 'Invalid parent ID'))
|
|
31
|
+
: null;
|
|
32
|
+
if (parentId)
|
|
33
|
+
await authSessionForItem('storage', parentId, { write: true }, session);
|
|
34
|
+
if (Number.isNaN(size))
|
|
35
|
+
error(411, 'Missing or invalid content length');
|
|
36
|
+
if (limits.user_items && usage.itemCount >= limits.user_items)
|
|
37
|
+
error(409, 'Too many items');
|
|
38
|
+
if (limits.user_size && (usage.usedBytes + size) / 1_000_000 >= limits.user_size)
|
|
39
|
+
error(413, 'Not enough space');
|
|
40
|
+
if (limits.item_size && size > limits.item_size * 1_000_000)
|
|
41
|
+
error(413, 'File size exceeds maximum size');
|
|
42
|
+
const isDirectory = type == 'inode/directory';
|
|
43
|
+
if (isDirectory && size > 0)
|
|
44
|
+
error(400, 'Directories can not have content');
|
|
45
|
+
if (!useCAS(type))
|
|
46
|
+
return {};
|
|
47
|
+
if (!hash)
|
|
48
|
+
return { needsHashing: true };
|
|
49
|
+
const existing = await database
|
|
50
|
+
.selectFrom('storage')
|
|
51
|
+
.select('id')
|
|
52
|
+
.where(eb => eb.and({ hash: Uint8Array.fromHex(hash), immutable: true }))
|
|
53
|
+
.limit(1)
|
|
54
|
+
.executeTakeFirst();
|
|
55
|
+
return { existing };
|
|
56
|
+
}
|
|
57
|
+
export async function createNewItem(init, userId, writeContent) {
|
|
58
|
+
const tx = await database.startTransaction().execute();
|
|
59
|
+
const { data: dataDir } = getConfig('@axium/storage');
|
|
60
|
+
const immutable = useCAS(init.type);
|
|
61
|
+
try {
|
|
62
|
+
const hash = typeof init.hash == 'string' ? Uint8Array.fromHex(init.hash) : null;
|
|
63
|
+
const existing = immutable
|
|
64
|
+
? await database
|
|
65
|
+
.selectFrom('storage')
|
|
66
|
+
.select('id')
|
|
67
|
+
.where(eb => eb.and({ hash, immutable: true }))
|
|
68
|
+
.limit(1)
|
|
69
|
+
.executeTakeFirst()
|
|
70
|
+
: null;
|
|
71
|
+
const item = parseItem(await tx
|
|
72
|
+
.insertInto('storage')
|
|
73
|
+
.values({
|
|
74
|
+
...init,
|
|
75
|
+
userId,
|
|
76
|
+
immutable,
|
|
77
|
+
hash,
|
|
78
|
+
})
|
|
79
|
+
.returningAll()
|
|
80
|
+
.executeTakeFirstOrThrow());
|
|
81
|
+
const path = join(dataDir, item.id);
|
|
82
|
+
if (existing)
|
|
83
|
+
linkSync(join(dataDir, existing.id), path);
|
|
84
|
+
else if (init.type != 'inode/directory') {
|
|
85
|
+
if (!writeContent)
|
|
86
|
+
error(501, 'Missing writeContent (this is a bug!)');
|
|
87
|
+
writeContent(path);
|
|
88
|
+
}
|
|
89
|
+
await tx.commit().execute();
|
|
90
|
+
return item;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
await tx.rollback().execute();
|
|
94
|
+
throw withError('Could not create item', 500)(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const inProgress = new Map();
|
|
98
|
+
export function startUpload(init, session) {
|
|
99
|
+
const { temp_dir, upload_timeout } = getConfig('@axium/storage');
|
|
100
|
+
const token = randomBytes(32);
|
|
101
|
+
mkdirSync(temp_dir, { recursive: true });
|
|
102
|
+
const file = join(temp_dir, token.toHex());
|
|
103
|
+
const fd = openSync(file, 'a');
|
|
104
|
+
function remove() {
|
|
105
|
+
inProgress.delete(token.toBase64());
|
|
106
|
+
closeSync(fd);
|
|
107
|
+
}
|
|
108
|
+
inProgress.set(token.toBase64(), {
|
|
109
|
+
hash: createHash('BLAKE2b512'),
|
|
110
|
+
file,
|
|
111
|
+
fd,
|
|
112
|
+
uploadedBytes: 0,
|
|
113
|
+
sessionId: session.id,
|
|
114
|
+
userId: session.userId,
|
|
115
|
+
init,
|
|
116
|
+
remove,
|
|
117
|
+
});
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
remove();
|
|
120
|
+
}, upload_timeout * 60_000);
|
|
121
|
+
return token.toBase64();
|
|
122
|
+
}
|
|
123
|
+
export async function requireUpload(request) {
|
|
124
|
+
const token = request.headers.get('x-upload');
|
|
125
|
+
if (!token)
|
|
126
|
+
error(401, 'Missing upload token');
|
|
127
|
+
const upload = inProgress.get(token);
|
|
128
|
+
if (!upload)
|
|
129
|
+
error(400, 'Invalid upload token');
|
|
130
|
+
const session = await requireSession(request);
|
|
131
|
+
if (session.id != upload.sessionId)
|
|
132
|
+
error(403, 'Upload does not belong to the current session');
|
|
133
|
+
if (session.userId != upload.userId)
|
|
134
|
+
error(403, 'Upload does not belong to the current user');
|
|
135
|
+
return upload;
|
|
136
|
+
}
|
package/dist/server/raw.js
CHANGED
|
@@ -52,95 +52,80 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
52
52
|
});
|
|
53
53
|
import { getConfig } from '@axium/core';
|
|
54
54
|
import { audit } from '@axium/server/audit';
|
|
55
|
-
import {
|
|
55
|
+
import { authRequestForItem, requireSession } from '@axium/server/auth';
|
|
56
56
|
import { database } from '@axium/server/database';
|
|
57
57
|
import { error, withError } from '@axium/server/requests';
|
|
58
58
|
import { addRoute } from '@axium/server/routes';
|
|
59
59
|
import { createHash } from 'node:crypto';
|
|
60
|
-
import { closeSync,
|
|
60
|
+
import { closeSync, openSync, readFileSync, readSync, renameSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
61
61
|
import { join } from 'node:path/posix';
|
|
62
62
|
import * as z from 'zod';
|
|
63
63
|
import '../polyfills.js';
|
|
64
|
-
import {
|
|
64
|
+
import { getLimits } from './config.js';
|
|
65
65
|
import { getUserStats, parseItem } from './db.js';
|
|
66
|
+
import { checkNewItem, createNewItem, requireUpload } from './item.js';
|
|
66
67
|
addRoute({
|
|
67
68
|
path: '/raw/storage',
|
|
68
69
|
async PUT(request) {
|
|
69
70
|
if (!getConfig('@axium/storage').enabled)
|
|
70
71
|
error(503, 'User storage is disabled');
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const name = request.headers.get('x-name');
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
error(400, 'Name is too long');
|
|
78
|
-
const maybeParentId = request.headers.get('x-parent');
|
|
79
|
-
const parentId = maybeParentId
|
|
80
|
-
? await z
|
|
81
|
-
.uuid()
|
|
82
|
-
.parseAsync(maybeParentId)
|
|
83
|
-
.catch(() => error(400, 'Invalid parent ID'))
|
|
84
|
-
: null;
|
|
85
|
-
if (parentId)
|
|
86
|
-
await checkAuthForItem(request, 'storage', parentId, { write: true });
|
|
87
|
-
const size = Number(request.headers.get('content-length'));
|
|
88
|
-
if (Number.isNaN(size))
|
|
89
|
-
error(411, 'Missing or invalid content length header');
|
|
90
|
-
if (limits.user_items && usage.itemCount >= limits.user_items)
|
|
91
|
-
error(409, 'Too many items');
|
|
92
|
-
if (limits.user_size && (usage.usedBytes + size) / 1_000_000 >= limits.user_size)
|
|
93
|
-
error(413, 'Not enough space');
|
|
94
|
-
if (limits.item_size && size > limits.item_size * 1_000_000)
|
|
95
|
-
error(413, 'File size exceeds maximum size');
|
|
72
|
+
const session = await requireSession(request);
|
|
73
|
+
const { userId } = session;
|
|
74
|
+
const name = request.headers.get('x-name'); // checked in `checkNewItem`
|
|
75
|
+
const parentId = request.headers.get('x-parent');
|
|
76
|
+
const size = Number(request.headers.get('x-size'));
|
|
77
|
+
const type = request.headers.get('content-type') || 'application/octet-stream';
|
|
96
78
|
const content = await request.bytes();
|
|
97
79
|
if (content.byteLength > size) {
|
|
98
80
|
await audit('storage_size_mismatch', userId, { item: null });
|
|
99
81
|
error(400, 'Content length does not match size header');
|
|
100
82
|
}
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
83
|
+
const hash = type == 'inode/directory' ? null : createHash('BLAKE2b512').update(content).digest();
|
|
84
|
+
const init = { name, size, type, parentId, hash: hash?.toHex() };
|
|
85
|
+
await checkNewItem(init, session);
|
|
86
|
+
return await createNewItem(init, userId, path => writeFileSync(path, content));
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
addRoute({
|
|
90
|
+
path: '/raw/storage/chunk',
|
|
91
|
+
async POST(request) {
|
|
92
|
+
if (!getConfig('@axium/storage').enabled)
|
|
93
|
+
error(503, 'User storage is disabled');
|
|
94
|
+
const upload = await requireUpload(request);
|
|
95
|
+
const size = Number(request.headers.get('content-length'));
|
|
96
|
+
if (Number.isNaN(size))
|
|
97
|
+
error(411, 'Missing or invalid content length');
|
|
98
|
+
if (upload.uploadedBytes + size > upload.init.size)
|
|
99
|
+
error(413, 'Upload exceeds allowed size');
|
|
100
|
+
const content = await request.bytes();
|
|
101
|
+
if (content.byteLength != size) {
|
|
102
|
+
await audit('storage_size_mismatch', upload.userId, { item: null });
|
|
103
|
+
error(400, `Content length mismatch: expected ${size}, got ${content.byteLength}`);
|
|
104
|
+
}
|
|
105
|
+
const offset = Number(request.headers.get('x-offset'));
|
|
106
|
+
if (offset != upload.uploadedBytes)
|
|
107
|
+
error(400, `Expected offset ${upload.uploadedBytes} but got ${offset}`);
|
|
108
|
+
writeSync(upload.fd, content); // opened with 'a', this appends
|
|
109
|
+
upload.hash.update(content);
|
|
110
|
+
upload.uploadedBytes += size;
|
|
111
|
+
if (upload.uploadedBytes != upload.init.size)
|
|
112
|
+
return new Response(null, { status: 204 });
|
|
113
|
+
const hash = upload.hash.digest();
|
|
114
|
+
upload.init.hash ??= hash.toHex();
|
|
115
|
+
if (hash.toHex() != upload.init.hash)
|
|
116
|
+
error(409, 'Hash mismatch');
|
|
117
|
+
upload.remove();
|
|
118
|
+
return await createNewItem(upload.init, upload.userId, path => {
|
|
119
|
+
try {
|
|
120
|
+
renameSync(upload.file, path);
|
|
122
121
|
}
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.limit(1)
|
|
129
|
-
.executeTakeFirst();
|
|
130
|
-
if (!existing) {
|
|
131
|
-
if (!isDirectory)
|
|
132
|
-
writeFileSync(path, content);
|
|
133
|
-
await tx.commit().execute();
|
|
134
|
-
return item;
|
|
122
|
+
catch (e) {
|
|
123
|
+
if (e.code != 'EXDEV')
|
|
124
|
+
throw e;
|
|
125
|
+
writeFileSync(path, readFileSync(upload.file));
|
|
126
|
+
unlinkSync(upload.file);
|
|
135
127
|
}
|
|
136
|
-
|
|
137
|
-
await tx.commit().execute();
|
|
138
|
-
return item;
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
await tx.rollback().execute();
|
|
142
|
-
throw withError('Could not create item', 500)(error);
|
|
143
|
-
}
|
|
128
|
+
});
|
|
144
129
|
},
|
|
145
130
|
});
|
|
146
131
|
addRoute({
|
|
@@ -151,7 +136,7 @@ addRoute({
|
|
|
151
136
|
try {
|
|
152
137
|
if (!getConfig('@axium/storage').enabled)
|
|
153
138
|
error(503, 'User storage is disabled');
|
|
154
|
-
const { item } = await
|
|
139
|
+
const { item } = await authRequestForItem(request, 'storage', itemId, { read: true });
|
|
155
140
|
if (item.trashedAt)
|
|
156
141
|
error(410, 'Trashed items can not be downloaded');
|
|
157
142
|
const path = join(getConfig('@axium/storage').data, item.id);
|
|
@@ -198,7 +183,7 @@ addRoute({
|
|
|
198
183
|
async POST(request, { id: itemId }) {
|
|
199
184
|
if (!getConfig('@axium/storage').enabled)
|
|
200
185
|
error(503, 'User storage is disabled');
|
|
201
|
-
const { item, session } = await
|
|
186
|
+
const { item, session } = await authRequestForItem(request, 'storage', itemId, { write: true });
|
|
202
187
|
if (item.immutable)
|
|
203
188
|
error(405, 'Item is immutable');
|
|
204
189
|
if (item.type == 'inode/directory')
|
package/lib/Add.svelte
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
const { parentId, onAdd }: { parentId?: string; onAdd?(item: StorageItemMetadata): void } = $props();
|
|
8
8
|
|
|
9
9
|
let uploadDialog = $state<HTMLDialogElement>()!;
|
|
10
|
-
let
|
|
10
|
+
let uploadProgress = $state<[number, number][]>([]);
|
|
11
|
+
let input = $state<HTMLInputElement>()!;
|
|
11
12
|
|
|
12
13
|
let createDialog = $state<HTMLDialogElement>()!;
|
|
13
14
|
let createType = $state<string>();
|
|
@@ -43,13 +44,18 @@
|
|
|
43
44
|
bind:dialog={uploadDialog}
|
|
44
45
|
submitText="Upload"
|
|
45
46
|
submit={async () => {
|
|
46
|
-
for (const file of input
|
|
47
|
-
const item = await uploadItem(file, {
|
|
47
|
+
for (const [i, file] of Array.from(input.files!).entries()) {
|
|
48
|
+
const item = await uploadItem(file, {
|
|
49
|
+
parentId,
|
|
50
|
+
onProgress(uploaded, total) {
|
|
51
|
+
uploadProgress[i] = [uploaded, total];
|
|
52
|
+
},
|
|
53
|
+
});
|
|
48
54
|
onAdd?.(item);
|
|
49
55
|
}
|
|
50
56
|
}}
|
|
51
57
|
>
|
|
52
|
-
<Upload bind:input multiple />
|
|
58
|
+
<Upload bind:input bind:progress={uploadProgress} multiple />
|
|
53
59
|
</FormDialog>
|
|
54
60
|
|
|
55
61
|
<FormDialog
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
5
|
"description": "User file storage for Axium",
|
|
6
6
|
"funding": {
|
|
@@ -41,11 +41,12 @@
|
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@axium/client": ">=0.13.0",
|
|
43
43
|
"@axium/core": ">=0.19.0",
|
|
44
|
-
"@axium/server": ">=0.
|
|
44
|
+
"@axium/server": ">=0.35.0",
|
|
45
45
|
"@sveltejs/kit": "^2.27.3",
|
|
46
46
|
"utilium": "^2.3.8"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"blakejs": "^1.2.1",
|
|
49
50
|
"zod": "^4.0.5"
|
|
50
51
|
},
|
|
51
52
|
"axium": {
|
|
@@ -80,7 +81,6 @@
|
|
|
80
81
|
"include": [],
|
|
81
82
|
"exclude": []
|
|
82
83
|
},
|
|
83
|
-
"chunk": false,
|
|
84
84
|
"data": "/srv/axium/storage",
|
|
85
85
|
"enabled": true,
|
|
86
86
|
"limits": {
|
|
@@ -88,9 +88,10 @@
|
|
|
88
88
|
"item_size": 100,
|
|
89
89
|
"user_items": 10000
|
|
90
90
|
},
|
|
91
|
-
"max_chunks": 10,
|
|
92
91
|
"max_transfer_size": 100,
|
|
93
|
-
"trash_duration": 30
|
|
92
|
+
"trash_duration": 30,
|
|
93
|
+
"temp_dir": "/tmp/axium",
|
|
94
|
+
"upload_timeout": 15
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
}
|