@axium/storage 0.19.1 → 0.20.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/db.json CHANGED
@@ -79,6 +79,26 @@
79
79
  }
80
80
  }
81
81
  }
82
+ },
83
+ {
84
+ "delta": true,
85
+ "alter_tables": {
86
+ "storage": {
87
+ "add_constraints": {
88
+ "unique_name_parentId": { "type": "unique", "on": ["name", "parentId"] }
89
+ }
90
+ }
91
+ }
92
+ },
93
+ {
94
+ "delta": true,
95
+ "alter_tables": {
96
+ "storage": {
97
+ "alter_columns": {
98
+ "size": { "type": "bigint" }
99
+ }
100
+ }
101
+ }
82
102
  }
83
103
  ],
84
104
  "wipe": ["storage", "acl.storage"],
@@ -26,7 +26,7 @@ cli.command('usage')
26
26
  .action(async () => {
27
27
  const { limits, itemCount, usedBytes } = await api.getUserStats(session().userId);
28
28
  console.log(`Items: ${itemCount} ${limits.user_items ? ' / ' + limits.user_items : ''}`);
29
- console.log(`Space: ${formatBytes(usedBytes)} ${limits.user_size ? ' / ' + formatBytes(limits.user_size * 1_000_000) : ''}`);
29
+ console.log(`Space: ${formatBytes(usedBytes)} ${limits.user_size ? ' / ' + formatBytes(limits.user_size * 1000000n) : ''}`);
30
30
  });
31
31
  cli.command('ls')
32
32
  .alias('list')
@@ -14,7 +14,7 @@ declare const StorageCache: z.ZodObject<{
14
14
  name: z.ZodString;
15
15
  userId: z.ZodUUID;
16
16
  parentId: z.ZodNullable<z.ZodUUID>;
17
- size: z.ZodInt;
17
+ size: z.ZodCoercedBigInt<unknown>;
18
18
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
19
19
  type: z.ZodString;
20
20
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
package/dist/common.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const StorageItemMetadata: z.ZodObject<{
23
23
  name: z.ZodString;
24
24
  userId: z.ZodUUID;
25
25
  parentId: z.ZodNullable<z.ZodUUID>;
26
- size: z.ZodInt;
26
+ size: z.ZodCoercedBigInt<unknown>;
27
27
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
28
28
  type: z.ZodString;
29
29
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -61,16 +61,18 @@ export declare const StorageItemSorting: z.ZodObject<{
61
61
  descending: z.ZodOptional<z.ZodBoolean>;
62
62
  by: z.ZodLiteral<"name" | "createdAt" | "modifiedAt" | "size">;
63
63
  }, z.core.$strip>;
64
+ export interface StorageItemSorting extends z.infer<typeof StorageItemSorting> {
65
+ }
64
66
  export declare const syncProtocolVersion = 0;
65
67
  export declare const StorageLimits: z.ZodObject<{
66
68
  item_size: z.ZodInt;
67
69
  user_items: z.ZodInt;
68
- user_size: z.ZodInt;
70
+ user_size: z.ZodCoercedBigInt<unknown>;
69
71
  }, z.core.$strip>;
70
72
  export interface StorageLimits extends z.infer<typeof StorageLimits> {
71
73
  }
72
74
  export declare const StorageStats: z.ZodObject<{
73
- usedBytes: z.ZodInt;
75
+ usedBytes: z.ZodCoercedBigInt<unknown>;
74
76
  itemCount: z.ZodInt;
75
77
  lastModified: z.ZodCoercedDate<unknown>;
76
78
  lastTrashed: z.ZodNullable<z.ZodCoercedDate<unknown>>;
@@ -81,9 +83,9 @@ export declare const UserStorageInfo: z.ZodObject<{
81
83
  limits: z.ZodObject<{
82
84
  item_size: z.ZodInt;
83
85
  user_items: z.ZodInt;
84
- user_size: z.ZodInt;
86
+ user_size: z.ZodCoercedBigInt<unknown>;
85
87
  }, z.core.$strip>;
86
- usedBytes: z.ZodInt;
88
+ usedBytes: z.ZodCoercedBigInt<unknown>;
87
89
  itemCount: z.ZodInt;
88
90
  lastModified: z.ZodCoercedDate<unknown>;
89
91
  lastTrashed: z.ZodNullable<z.ZodCoercedDate<unknown>>;
@@ -101,7 +103,7 @@ export declare const UserStorage: z.ZodObject<{
101
103
  name: z.ZodString;
102
104
  userId: z.ZodUUID;
103
105
  parentId: z.ZodNullable<z.ZodUUID>;
104
- size: z.ZodInt;
106
+ size: z.ZodCoercedBigInt<unknown>;
105
107
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
106
108
  type: z.ZodString;
107
109
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -135,9 +137,9 @@ export declare const UserStorage: z.ZodObject<{
135
137
  limits: z.ZodObject<{
136
138
  item_size: z.ZodInt;
137
139
  user_items: z.ZodInt;
138
- user_size: z.ZodInt;
140
+ user_size: z.ZodCoercedBigInt<unknown>;
139
141
  }, z.core.$strip>;
140
- usedBytes: z.ZodInt;
142
+ usedBytes: z.ZodCoercedBigInt<unknown>;
141
143
  itemCount: z.ZodInt;
142
144
  lastModified: z.ZodCoercedDate<unknown>;
143
145
  lastTrashed: z.ZodNullable<z.ZodCoercedDate<unknown>>;
@@ -206,7 +208,7 @@ export declare const StorageConfig: z.ZodObject<{
206
208
  limits: z.ZodObject<{
207
209
  item_size: z.ZodInt;
208
210
  user_items: z.ZodInt;
209
- user_size: z.ZodInt;
211
+ user_size: z.ZodCoercedBigInt<unknown>;
210
212
  }, z.core.$strip>;
211
213
  trash_duration: z.ZodNumber;
212
214
  temp_dir: z.ZodString;
@@ -219,7 +221,7 @@ declare module '@axium/core/plugins' {
219
221
  }
220
222
  export declare const StorageItemInit: z.ZodObject<{
221
223
  name: z.ZodString;
222
- size: z.ZodInt;
224
+ size: z.ZodCoercedBigInt<unknown>;
223
225
  type: z.ZodString;
224
226
  parentId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
225
227
  hash: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
@@ -247,7 +249,7 @@ export declare const UploadInitResult: z.ZodDiscriminatedUnion<[z.ZodObject<{
247
249
  name: z.ZodString;
248
250
  userId: z.ZodUUID;
249
251
  parentId: z.ZodNullable<z.ZodUUID>;
250
- size: z.ZodInt;
252
+ size: z.ZodCoercedBigInt<unknown>;
251
253
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
252
254
  type: z.ZodString;
253
255
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -286,9 +288,9 @@ declare const StorageAPI: {
286
288
  limits: z.ZodObject<{
287
289
  item_size: z.ZodInt;
288
290
  user_items: z.ZodInt;
289
- user_size: z.ZodInt;
291
+ user_size: z.ZodCoercedBigInt<unknown>;
290
292
  }, z.core.$strip>;
291
- usedBytes: z.ZodInt;
293
+ usedBytes: z.ZodCoercedBigInt<unknown>;
292
294
  itemCount: z.ZodInt;
293
295
  lastModified: z.ZodCoercedDate<unknown>;
294
296
  lastTrashed: z.ZodNullable<z.ZodCoercedDate<unknown>>;
@@ -309,7 +311,7 @@ declare const StorageAPI: {
309
311
  name: z.ZodString;
310
312
  userId: z.ZodUUID;
311
313
  parentId: z.ZodNullable<z.ZodUUID>;
312
- size: z.ZodInt;
314
+ size: z.ZodCoercedBigInt<unknown>;
313
315
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
314
316
  type: z.ZodString;
315
317
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -343,9 +345,9 @@ declare const StorageAPI: {
343
345
  limits: z.ZodObject<{
344
346
  item_size: z.ZodInt;
345
347
  user_items: z.ZodInt;
346
- user_size: z.ZodInt;
348
+ user_size: z.ZodCoercedBigInt<unknown>;
347
349
  }, z.core.$strip>;
348
- usedBytes: z.ZodInt;
350
+ usedBytes: z.ZodCoercedBigInt<unknown>;
349
351
  itemCount: z.ZodInt;
350
352
  lastModified: z.ZodCoercedDate<unknown>;
351
353
  lastTrashed: z.ZodNullable<z.ZodCoercedDate<unknown>>;
@@ -362,7 +364,7 @@ declare const StorageAPI: {
362
364
  name: z.ZodString;
363
365
  userId: z.ZodUUID;
364
366
  parentId: z.ZodNullable<z.ZodUUID>;
365
- size: z.ZodInt;
367
+ size: z.ZodCoercedBigInt<unknown>;
366
368
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
367
369
  type: z.ZodString;
368
370
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -405,7 +407,7 @@ declare const StorageAPI: {
405
407
  name: z.ZodString;
406
408
  userId: z.ZodUUID;
407
409
  parentId: z.ZodNullable<z.ZodUUID>;
408
- size: z.ZodInt;
410
+ size: z.ZodCoercedBigInt<unknown>;
409
411
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
410
412
  type: z.ZodString;
411
413
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -448,7 +450,7 @@ declare const StorageAPI: {
448
450
  name: z.ZodString;
449
451
  userId: z.ZodUUID;
450
452
  parentId: z.ZodNullable<z.ZodUUID>;
451
- size: z.ZodInt;
453
+ size: z.ZodCoercedBigInt<unknown>;
452
454
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
453
455
  type: z.ZodString;
454
456
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -493,7 +495,7 @@ declare const StorageAPI: {
493
495
  }, z.core.$strip>;
494
496
  readonly PUT: readonly [z.ZodObject<{
495
497
  name: z.ZodString;
496
- size: z.ZodInt;
498
+ size: z.ZodCoercedBigInt<unknown>;
497
499
  type: z.ZodString;
498
500
  parentId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
499
501
  hash: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
@@ -518,7 +520,7 @@ declare const StorageAPI: {
518
520
  name: z.ZodString;
519
521
  userId: z.ZodUUID;
520
522
  parentId: z.ZodNullable<z.ZodUUID>;
521
- size: z.ZodInt;
523
+ size: z.ZodCoercedBigInt<unknown>;
522
524
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
523
525
  type: z.ZodString;
524
526
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -573,7 +575,7 @@ declare const StorageAPI: {
573
575
  name: z.ZodString;
574
576
  userId: z.ZodUUID;
575
577
  parentId: z.ZodNullable<z.ZodUUID>;
576
- size: z.ZodInt;
578
+ size: z.ZodCoercedBigInt<unknown>;
577
579
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
578
580
  type: z.ZodString;
579
581
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -618,7 +620,7 @@ declare const StorageAPI: {
618
620
  name: z.ZodString;
619
621
  userId: z.ZodUUID;
620
622
  parentId: z.ZodNullable<z.ZodUUID>;
621
- size: z.ZodInt;
623
+ size: z.ZodCoercedBigInt<unknown>;
622
624
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
623
625
  type: z.ZodString;
624
626
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -659,7 +661,7 @@ declare const StorageAPI: {
659
661
  name: z.ZodString;
660
662
  userId: z.ZodUUID;
661
663
  parentId: z.ZodNullable<z.ZodUUID>;
662
- size: z.ZodInt;
664
+ size: z.ZodCoercedBigInt<unknown>;
663
665
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
664
666
  type: z.ZodString;
665
667
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -704,7 +706,7 @@ declare const StorageAPI: {
704
706
  name: z.ZodString;
705
707
  userId: z.ZodUUID;
706
708
  parentId: z.ZodNullable<z.ZodUUID>;
707
- size: z.ZodInt;
709
+ size: z.ZodCoercedBigInt<unknown>;
708
710
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
709
711
  type: z.ZodString;
710
712
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -747,7 +749,7 @@ declare const StorageAPI: {
747
749
  name: z.ZodString;
748
750
  userId: z.ZodUUID;
749
751
  parentId: z.ZodNullable<z.ZodUUID>;
750
- size: z.ZodInt;
752
+ size: z.ZodCoercedBigInt<unknown>;
751
753
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
752
754
  type: z.ZodString;
753
755
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -790,7 +792,7 @@ declare const StorageAPI: {
790
792
  name: z.ZodString;
791
793
  userId: z.ZodUUID;
792
794
  parentId: z.ZodNullable<z.ZodUUID>;
793
- size: z.ZodInt;
795
+ size: z.ZodCoercedBigInt<unknown>;
794
796
  trashedAt: z.ZodNullable<z.ZodCoercedDate<unknown>>;
795
797
  type: z.ZodString;
796
798
  metadata: z.ZodRecord<z.ZodString, z.ZodUnknown>;
package/dist/common.js CHANGED
@@ -26,7 +26,7 @@ export const StorageItemMetadata = z.object({
26
26
  name: z.string(),
27
27
  userId: z.uuid(),
28
28
  parentId: z.uuid().nullable(),
29
- size: z.int().nonnegative(),
29
+ size: z.coerce.bigint().nonnegative(),
30
30
  trashedAt: z.coerce.date().nullable(),
31
31
  type: z.string(),
32
32
  metadata: z.record(z.string(), z.unknown()),
@@ -44,10 +44,10 @@ export const StorageLimits = z.object({
44
44
  /** Maximum number of items per user */
45
45
  user_items: z.int().nonnegative(),
46
46
  /** The maximum storage size per user in MB */
47
- user_size: z.int().nonnegative(),
47
+ user_size: z.coerce.bigint().nonnegative(),
48
48
  });
49
49
  export const StorageStats = z.object({
50
- usedBytes: z.int().nonnegative(),
50
+ usedBytes: z.coerce.bigint().nonnegative(),
51
51
  itemCount: z.int().nonnegative(),
52
52
  lastModified: z.coerce.date(),
53
53
  lastTrashed: z.coerce.date().nullable(),
@@ -126,7 +126,7 @@ export const StorageConfig = StoragePublicConfig.safeExtend({
126
126
  setServerConfig('@axium/storage', StorageConfig);
127
127
  export const StorageItemInit = z.object({
128
128
  name: z.string(),
129
- size: z.int().nonnegative(),
129
+ size: z.coerce.bigint().nonnegative(),
130
130
  type: z.string(),
131
131
  parentId: z.uuid().nullish(),
132
132
  hash: z.hex().nullish(),
@@ -25,8 +25,8 @@ addRoute({
25
25
  const batchHeaderSize = Number(req.headers.get('x-batch-header-size'));
26
26
  if (!Number.isSafeInteger(batchHeaderSize) || batchHeaderSize < 2)
27
27
  error(400, 'Invalid or missing header, X-Batch-Header-Size');
28
- const size = Number(req.headers.get('content-length'));
29
- if (Number.isNaN(size))
28
+ const size = BigInt(req.headers.get('content-length') || '-1');
29
+ if (size < 0n)
30
30
  error(411, 'Missing or invalid content length header');
31
31
  const raw = await req.bytes();
32
32
  if (raw.byteLength - batchHeaderSize > size) {
@@ -77,7 +77,8 @@ addRoute({
77
77
  acl.check(item.acl, changedIds.has(item.id) ? { write: true } : { manage: true });
78
78
  error(403, 'Missing permission for item: ' + item.id);
79
79
  }
80
- if (limits.user_size && (usage.usedBytes + size - items.reduce((sum, item) => sum + item.size, 0)) / 1_000_000 >= limits.user_size)
80
+ if (limits.user_size &&
81
+ (usage.usedBytes + size - items.reduce((sum, item) => sum + item.size, 0n)) / 1000000n >= limits.user_size)
81
82
  error(413, 'Not enough space');
82
83
  const tx = await database.startTransaction().execute();
83
84
  const results = new Map();
@@ -88,7 +89,7 @@ addRoute({
88
89
  const result = await tx
89
90
  .updateTable('storage')
90
91
  .where('id', '=', itemId)
91
- .set({ size, modifiedAt: new Date(), hash })
92
+ .set({ size: BigInt(size), modifiedAt: new Date(), hash })
92
93
  .returningAll()
93
94
  .executeTakeFirstOrThrow();
94
95
  writeFileSync(join(getConfig('@axium/storage').data, result.id), content);
@@ -15,7 +15,7 @@ cli.command('usage')
15
15
  .selectFrom('storage')
16
16
  .select(eb => eb.fn.sum('size').as('size'))
17
17
  .executeTakeFirstOrThrow();
18
- console.log(`${items} items totaling ${formatBytes(Number(size))}`);
18
+ console.log(`${items} items totaling ${formatBytes(BigInt(size))}`);
19
19
  });
20
20
  const _byteSize = (msg) => (v) => parseByteSize(v) ?? io.exit(msg);
21
21
  cli.command('query')
@@ -48,7 +48,7 @@ cli.command('query')
48
48
  }
49
49
  let validMinSize = false;
50
50
  if (opt.minSize !== undefined) {
51
- if (opt.minSize == 0)
51
+ if (opt.minSize == 0n)
52
52
  io.warn('Minimum size of 0 has no effect, ignoring.');
53
53
  else {
54
54
  query = query.where('size', '>=', opt.minSize);
@@ -50,6 +50,6 @@ export async function getLimits(userId) {
50
50
  catch {
51
51
  limits = structuredClone(getConfig('@axium/storage').limits);
52
52
  }
53
- limits.user_size ||= Number(await _unlimitedLimit());
53
+ limits.user_size ||= await _unlimitedLimit();
54
54
  return limits;
55
55
  }
package/dist/server/db.js CHANGED
@@ -25,9 +25,11 @@ export async function getUserStats(userId) {
25
25
  eb.fn.max('trashedAt').as('lastTrashed'),
26
26
  ])
27
27
  .executeTakeFirstOrThrow();
28
- result.usedBytes = Number(result.usedBytes || 0);
29
- result.itemCount = Number(result.itemCount);
30
- return result;
28
+ return {
29
+ ...result,
30
+ usedBytes: BigInt(result.usedBytes || 0n),
31
+ itemCount: Number(result.itemCount),
32
+ };
31
33
  }
32
34
  export async function get(itemId) {
33
35
  const result = await database
@@ -12,7 +12,7 @@ export function load() {
12
12
  export async function statusText() {
13
13
  const { storage: items } = await count('storage');
14
14
  const size = await getTotalUse();
15
- return `${items} items totaling ${formatBytes(Number(size))}`;
15
+ return `${items} items totaling ${formatBytes(size)}`;
16
16
  }
17
17
  export async function clean(opt) {
18
18
  start('Removing expired trash items');
@@ -16,7 +16,7 @@ export interface UploadInfo {
16
16
  file: string;
17
17
  fd: number;
18
18
  hash: Hash;
19
- uploadedBytes: number;
19
+ uploadedBytes: bigint;
20
20
  sessionId: string;
21
21
  userId: string;
22
22
  init: StorageItemInit;
@@ -31,11 +31,11 @@ export async function checkNewItem(init, session) {
31
31
  : null;
32
32
  if (parentId)
33
33
  await authSessionForItem('storage', parentId, { write: true }, session);
34
- if (Number.isNaN(size))
34
+ if (BigInt(size) < 0n)
35
35
  error(411, 'Missing or invalid content length');
36
36
  if (limits.user_items && usage.itemCount >= limits.user_items)
37
37
  error(409, 'Too many items');
38
- if (limits.user_size && (usage.usedBytes + size) / 1_000_000 >= limits.user_size)
38
+ if (limits.user_size && (usage.usedBytes + size) / 1000000n >= limits.user_size)
39
39
  error(413, 'Not enough space');
40
40
  if (limits.item_size && size > limits.item_size * 1_000_000)
41
41
  error(413, 'File size exceeds maximum size');
@@ -77,7 +77,14 @@ export async function createNewItem(init, userId, writeContent) {
77
77
  hash,
78
78
  })
79
79
  .returningAll()
80
- .executeTakeFirstOrThrow());
80
+ .executeTakeFirstOrThrow()
81
+ .catch(e => {
82
+ if (!(e instanceof Error))
83
+ throw e;
84
+ if (e.message.includes('unique_name_parentId') && e.message.includes('duplicate'))
85
+ error(409, 'A file with that name already exists in this folder.');
86
+ throw e;
87
+ }));
81
88
  const path = join(dataDir, item.id);
82
89
  if (existing)
83
90
  linkSync(join(dataDir, existing.id), path);
@@ -113,7 +120,7 @@ export function startUpload(init, session) {
113
120
  hash: createHash('BLAKE2b512'),
114
121
  file,
115
122
  fd,
116
- uploadedBytes: 0,
123
+ uploadedBytes: 0n,
117
124
  sessionId: session.id,
118
125
  userId: session.userId,
119
126
  init,
@@ -64,6 +64,15 @@ import '../polyfills.js';
64
64
  import { getLimits } from './config.js';
65
65
  import { getUserStats, parseItem } from './db.js';
66
66
  import { checkNewItem, createNewItem, requireUpload } from './item.js';
67
+ function contentDispositionFor(name) {
68
+ const fallback = name
69
+ .replace(/[\r\n]/g, '')
70
+ .replace(/[^\x20-\x7E]/g, '_')
71
+ .trim()
72
+ .replace(/[\\"]/g, '\\$&') || 'download';
73
+ const encoded = encodeURIComponent(name.replace(/[\r\n]/g, '')).replace(/['()*]/g, char => '%' + char.charCodeAt(0).toString(16).toUpperCase());
74
+ return `attachment; filename="${fallback}"; filename*=UTF-8''${encoded}`;
75
+ }
67
76
  addRoute({
68
77
  path: '/raw/storage',
69
78
  async PUT(request) {
@@ -73,7 +82,7 @@ addRoute({
73
82
  const { userId } = session;
74
83
  const name = request.headers.get('x-name'); // checked in `checkNewItem`
75
84
  const parentId = request.headers.get('x-parent');
76
- const size = Number(request.headers.get('x-size'));
85
+ const size = BigInt(request.headers.get('x-size') || -1);
77
86
  const type = request.headers.get('content-type') || 'application/octet-stream';
78
87
  const content = await request.bytes();
79
88
  if (content.byteLength > size) {
@@ -92,22 +101,22 @@ addRoute({
92
101
  if (!getConfig('@axium/storage').enabled)
93
102
  error(503, 'User storage is disabled');
94
103
  const upload = await requireUpload(request);
95
- const size = Number(request.headers.get('content-length'));
96
- if (Number.isNaN(size))
104
+ const size = BigInt(request.headers.get('content-length') || -1);
105
+ if (size < 0n)
97
106
  error(411, 'Missing or invalid content length');
98
107
  if (upload.uploadedBytes + size > upload.init.size)
99
108
  error(413, 'Upload exceeds allowed size');
100
109
  const content = await request.bytes();
101
- if (content.byteLength != size) {
110
+ if (content.byteLength != Number(size)) {
102
111
  await audit('storage_size_mismatch', upload.userId, { item: null });
103
112
  error(400, `Content length mismatch: expected ${size}, got ${content.byteLength}`);
104
113
  }
105
- const offset = Number(request.headers.get('x-offset'));
114
+ const offset = BigInt(request.headers.get('x-offset') || -1);
106
115
  if (offset != upload.uploadedBytes)
107
116
  error(400, `Expected offset ${upload.uploadedBytes} but got ${offset}`);
108
117
  writeSync(upload.fd, content); // opened with 'a', this appends
109
118
  upload.hash.update(content);
110
- upload.uploadedBytes += size;
119
+ upload.uploadedBytes += BigInt(size);
111
120
  if (upload.uploadedBytes != upload.init.size)
112
121
  return new Response(null, { status: 204 });
113
122
  const hash = upload.hash.digest();
@@ -149,14 +158,14 @@ addRoute({
149
158
  const range = request.headers.get('range');
150
159
  const fd = openSync(path, 'r');
151
160
  const _ = __addDisposableResource(env_1, { [Symbol.dispose]: () => closeSync(fd) }, false);
152
- let start = 0, end = item.size - 1, length = item.size;
161
+ let start = 0, end = Number(item.size - 1n), length = Number(item.size);
153
162
  if (range) {
154
- const [_start, _end = item.size - 1] = range
163
+ const [_start, _end = end] = range
155
164
  .replace(/bytes=/, '')
156
165
  .split('-')
157
166
  .map(val => (val && Number.isSafeInteger(parseInt(val)) ? parseInt(val) : undefined));
158
- start = typeof _start == 'number' ? _start : item.size - _end;
159
- end = typeof _start == 'number' ? _end : item.size - 1;
167
+ start = typeof _start == 'number' ? _start : Number(item.size) - _end;
168
+ end = typeof _start == 'number' ? _end : end;
160
169
  length = end - start + 1;
161
170
  }
162
171
  if (start >= item.size || end >= item.size || start > end || start < 0) {
@@ -168,13 +177,13 @@ addRoute({
168
177
  const content = new Uint8Array(length);
169
178
  readSync(fd, content, 0, length, start);
170
179
  return new Response(content, {
171
- status: length == item.size ? 200 : 206,
180
+ status: BigInt(length) == item.size ? 200 : 206,
172
181
  headers: {
173
182
  'Content-Range': `bytes ${start}-${end}/${item.size}`,
174
183
  'Accept-Ranges': 'bytes',
175
184
  'Content-Length': String(length),
176
185
  'Content-Type': item.type,
177
- 'Content-Disposition': `attachment; filename="${item.name}"`,
186
+ 'Content-Disposition': contentDispositionFor(item.name),
178
187
  },
179
188
  });
180
189
  }
@@ -205,7 +214,7 @@ addRoute({
205
214
  if (Number.isNaN(size))
206
215
  error(411, 'Missing or invalid content length header');
207
216
  const [usage, limits] = await Promise.all([getUserStats(item.userId), getLimits(item.userId)]).catch(withError('Could not fetch usage and/or limits'));
208
- if (limits.user_size && (usage.usedBytes + size - item.size) / 1_000_000 >= limits.user_size)
217
+ if (limits.user_size && (usage.usedBytes + BigInt(size) - item.size) / 1000000n >= limits.user_size)
209
218
  error(413, 'Not enough space');
210
219
  if (limits.item_size && size > limits.item_size * 1_000_000)
211
220
  error(413, 'File size exceeds maximum size');
@@ -220,7 +229,7 @@ addRoute({
220
229
  const result = await tx
221
230
  .updateTable('storage')
222
231
  .where('id', '=', itemId)
223
- .set({ size, modifiedAt: new Date(), hash })
232
+ .set({ size: BigInt(size), modifiedAt: new Date(), hash })
224
233
  .returningAll()
225
234
  .executeTakeFirstOrThrow();
226
235
  writeFileSync(join(getConfig('@axium/storage').data, result.id), content);
package/lib/Add.svelte CHANGED
@@ -38,7 +38,6 @@
38
38
  <span class="menu-item" onclick={() => uploadDialog.showModal()}><Icon i="upload" />{text('storage.Add.upload')}</span>
39
39
  {@render _item('inode/directory', text('storage.Add.new_folder'))}
40
40
  {@render _item('text/plain', text('storage.Add.plain_text'))}
41
- {@render _item('text/x-uri', text('storage.Add.url'), true)}
42
41
  </Popover>
43
42
 
44
43
  <FormDialog
package/lib/List.svelte CHANGED
@@ -7,9 +7,10 @@
7
7
  import type { AccessControllable, UserPublic } from '@axium/core';
8
8
  import { formatBytes } from '@axium/core/format';
9
9
  import { forMime as iconForMime } from '@axium/core/icons';
10
+ import { errorText } from '@axium/core/io';
10
11
  import { getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
11
12
  import { copyShortURL, formatItemName } from '@axium/storage/client/frontend';
12
- import type { StorageItemMetadata } from '@axium/storage/common';
13
+ import { StorageItemSorting, type StorageItemMetadata } from '@axium/storage/common';
13
14
  import Preview from './Preview.svelte';
14
15
 
15
16
  let {
@@ -23,6 +24,31 @@
23
24
  const activeItem = $derived(items[activeIndex]);
24
25
  const activeItemName = $derived(formatItemName(activeItem));
25
26
  const dialogs = $state<Record<string, HTMLDialogElement>>({});
27
+
28
+ const search = new URLSearchParams(location.search);
29
+ let sort = $state<StorageItemSorting | null>();
30
+ try {
31
+ sort = StorageItemSorting.parse({
32
+ by: search.get('sortBy'),
33
+ descending: search.has('descending'),
34
+ });
35
+ } catch (e) {
36
+ console.log('Ignoring invalid sorting parameters', errorText(e));
37
+ }
38
+
39
+ const sortedItems = $derived(
40
+ items
41
+ .map((item, i) => [item, i] as const)
42
+ .toSorted(
43
+ sort
44
+ ? ([_a], [_b]) => {
45
+ const [a, b] = sort?.descending ? [_b, _a] : [_a, _b];
46
+ // @ts-expect-error 2362 — `Date`s have a `valueOf` and can be treated like numbers
47
+ return sort.by == 'name' ? a.name.localeCompare(b.name) : a[sort.by] - b[sort.by];
48
+ }
49
+ : undefined
50
+ )
51
+ );
26
52
  </script>
27
53
 
28
54
  {#snippet action(name: string, icon: string, i: number, preview: boolean = false)}
@@ -40,11 +66,19 @@
40
66
  <div class="list">
41
67
  <div class="list-item list-header">
42
68
  <span></span>
43
- <span>{text('storage.generic.name')}</span>
44
- <span>{text('storage.List.last_modified')}</span>
45
- <span>{text('storage.List.size')}</span>
69
+ {#each [['name', 'storage.generic.name'], ['modifiedAt', 'storage.List.last_modified'], ['size', 'storage.List.size']] as const as [key, translation]}
70
+ <span
71
+ class="header-column"
72
+ onclick={() => (sort = sort?.descending === false ? null : { by: key, descending: !sort?.descending })}
73
+ >
74
+ {#if sort?.by == key}
75
+ <Icon i="sort-{sort.descending ? 'down' : 'up'}" />
76
+ {/if}
77
+ <span>{text(translation)}</span>
78
+ </span>
79
+ {/each}
46
80
  </div>
47
- {#each items as item, i (item.id)}
81
+ {#each sortedItems as [item, i] (item.id)}
48
82
  <div
49
83
  class="list-item"
50
84
  onclick={async () => {
@@ -167,6 +201,15 @@
167
201
  grid-template-columns: 1em 4fr 15em 5em repeat(4, 1em);
168
202
  }
169
203
 
204
+ .header-column {
205
+ display: inline-flex;
206
+ align-items: center;
207
+
208
+ &:hover {
209
+ cursor: pointer;
210
+ }
211
+ }
212
+
170
213
  @media (width < 700px) {
171
214
  .item-actions {
172
215
  display: none;
@@ -70,11 +70,11 @@
70
70
  </div>
71
71
  <div class="preview-content">
72
72
  {#if item.type.startsWith('image/')}
73
- <img src={item.dataURL} alt={item.name} width="100%" />
73
+ <img src={item.dataURL} alt={item.name} />
74
74
  {:else if item.type.startsWith('audio/')}
75
75
  <audio src={item.dataURL} controls></audio>
76
76
  {:else if item.type.startsWith('video/')}
77
- <video src={item.dataURL} controls width="100%">
77
+ <video src={item.dataURL} controls>
78
78
  <track kind="captions" />
79
79
  </video>
80
80
  {:else if item.type == 'application/pdf'}
@@ -195,7 +195,12 @@
195
195
 
196
196
  .preview-content {
197
197
  position: absolute;
198
- inset: 3em 10em 0;
198
+ inset: 3em 10em 1em;
199
+ display: flex;
200
+ text-align: center;
201
+ align-items: center;
202
+ justify-content: center;
203
+ flex-direction: column;
199
204
 
200
205
  .full-fill {
201
206
  position: absolute;
@@ -211,6 +216,12 @@
211
216
  background-color: var(--bg-menu);
212
217
  font-family: monospace;
213
218
  }
219
+
220
+ img,
221
+ video {
222
+ max-width: 100%;
223
+ max-height: 100%;
224
+ }
214
225
  }
215
226
 
216
227
  .no-preview {
package/lib/Usage.svelte CHANGED
@@ -15,11 +15,11 @@
15
15
  <p>
16
16
  <a href="/files/usage">
17
17
  <NumberBar
18
- max={info.limits.user_size && info.limits.user_size * 1_000_000}
18
+ max={info.limits.user_size && info.limits.user_size * 1_000_000n}
19
19
  value={info.usedBytes}
20
20
  text="{formatBytes(info.usedBytes)} {!info.limits.user_size
21
21
  ? ''
22
- : '/ ' + formatBytes(info.limits.user_size * 1_000_000)}"
22
+ : '/ ' + formatBytes(info.limits.user_size * 1_000_000n)}"
23
23
  />
24
24
  </a>
25
25
  </p>
package/locales/en.json CHANGED
@@ -70,8 +70,7 @@
70
70
  "new_folder": "New Folder",
71
71
  "plain_text": "Plain Text",
72
72
  "text": "Add",
73
- "upload": "Upload",
74
- "url": "URL"
73
+ "upload": "Upload"
75
74
  },
76
75
  "List": {
77
76
  "copy_link": "Copy Link",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "User file storage for Axium",
6
6
  "funding": {
@@ -40,8 +40,8 @@
40
40
  "build": "tsc"
41
41
  },
42
42
  "peerDependencies": {
43
- "@axium/client": ">=0.19.0",
44
- "@axium/core": ">=0.19.0",
43
+ "@axium/client": ">=0.19.1",
44
+ "@axium/core": ">=0.22.0",
45
45
  "@axium/server": ">=0.39.0",
46
46
  "@sveltejs/kit": "^2.27.3",
47
47
  "utilium": "^2.6.3"
@@ -12,7 +12,7 @@
12
12
 
13
13
  let barText = $derived(
14
14
  limits.user_size
15
- ? text('page.files.usage.bar_text', { used: formatBytes(usedBytes), total: formatBytes(limits.user_size * 1_000_000) })
15
+ ? text('page.files.usage.bar_text', { used: formatBytes(usedBytes), total: formatBytes(limits.user_size * 1_000_000n) })
16
16
  : text('page.files.usage.bar_text_unlimited', { used: formatBytes(usedBytes) })
17
17
  );
18
18
  </script>
@@ -23,6 +23,6 @@
23
23
 
24
24
  <h2>{text('page.files.usage.heading')}</h2>
25
25
 
26
- <p><NumberBar max={limits.user_size * 1_000_000} value={usedBytes} text={barText} /></p>
26
+ <p><NumberBar max={limits.user_size * 1_000_000n} value={usedBytes} text={barText} /></p>
27
27
 
28
28
  <List bind:items emptyText={text('page.files.usage.empty')} user={data.session?.user} />