@axium/storage 0.23.2 → 0.23.4

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.
@@ -1,11 +1,11 @@
1
- import { fetchAPI, prefix, token } from '@axium/client/requests';
1
+ import { fetchAPI, origin, prefix, token } from '@axium/client/requests';
2
2
  import { pick } from 'utilium';
3
3
  import { prettifyError } from 'zod';
4
4
  import { StorageItemMetadata } from '../common.js';
5
5
  import '../polyfills.js';
6
6
  import { warnOnce } from 'ioium';
7
7
  function rawStorage(suffix) {
8
- const raw = '/raw/storage' + (suffix ? '/' + suffix : '');
8
+ const raw = origin + '/raw/storage' + (suffix ? '/' + suffix : '');
9
9
  if (prefix[0] == '/')
10
10
  return raw;
11
11
  const url = new URL(prefix);
@@ -50,16 +50,15 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
+ import { formatBytes } from '@axium/core';
53
54
  import { Command } from 'commander';
54
55
  import * as io from 'ioium/node';
55
56
  import mime from 'mime';
56
57
  import * as fs from 'node:fs';
57
58
  import { basename } from 'node:path';
58
- import { Readable } from 'node:stream';
59
- import { colorItem, formatItems } from '../../node.js';
59
+ import { colorItem, formatItems, streamRead } from '../../node.js';
60
60
  import * as api from '../api.js';
61
61
  import { getDirectory, resolveItem, resolvePathWithParent, syncCache, writeCache } from '../local.js';
62
- import { formatBytes } from '@axium/core';
63
62
  export const ls = new Command('ls')
64
63
  .alias('list')
65
64
  .description('List the contents of a folder')
@@ -132,10 +131,9 @@ export const upload = new Command('upload')
132
131
  }
133
132
  else if (existingTarget && !opts.force)
134
133
  throw 'File exists at remote path, use --force to overwrite it';
135
- const stream = Readable.toWeb(fs.createReadStream(local));
136
134
  const type = mime.getType(local) || 'application/octet-stream';
137
135
  const _ = __addDisposableResource(env_1, io.start('Uploading ' + name), false);
138
- const item = await api.createItem(stream, {
136
+ const item = await api.createItem(streamRead(local), {
139
137
  parentId: parent?.id,
140
138
  name,
141
139
  size: stats.size,
@@ -6,3 +6,4 @@ export declare function copyShortURL(item: StorageItemMetadata): Promise<void>;
6
6
  export declare function formatItemName(item?: {
7
7
  name?: string | null;
8
8
  } | null): string;
9
+ export declare function _downloadItem(item?: StorageItemMetadata): void;
@@ -1,6 +1,6 @@
1
1
  import { copy } from '@axium/client/gui';
2
2
  import { encodeUUID } from 'utilium';
3
- import { text } from '@axium/client';
3
+ import { origin, text } from '@axium/client';
4
4
  export function copyShortURL(item) {
5
5
  const { href } = new URL('/f/' + encodeUUID(item.id).toBase64({ alphabet: 'base64url', omitPadding: true }), location.origin);
6
6
  return copy('text/plain', href);
@@ -13,3 +13,8 @@ export function formatItemName(item) {
13
13
  return text('storage.generic.no_name_in_dialog');
14
14
  return item.name.length > 23 ? item.name.slice(0, 20) + '...' : item.name;
15
15
  }
16
+ export function _downloadItem(item) {
17
+ if (!item)
18
+ throw text('storage.generic.no_item');
19
+ open(new URL(item.type != 'inode/directory' ? item.dataURL : '/raw/storage/directory-zip/' + item.id, origin), '_blank');
20
+ }
@@ -5,11 +5,11 @@ import mime from 'mime';
5
5
  import { createHash } from 'node:crypto';
6
6
  import * as fs from 'node:fs';
7
7
  import { basename, dirname, join, matchesGlob, relative } from 'node:path';
8
- import { Readable, Writable } from 'node:stream';
9
8
  import { pick } from 'utilium';
10
9
  import * as z from 'zod';
10
+ import { streamRead, streamWrite } from '../node.js';
11
11
  import '../polyfills.js';
12
- import { deleteItem, downloadItemStream, updateItem, createItem, createDirectory } from './api.js';
12
+ import { createDirectory, createItem, deleteItem, downloadItemStream, updateItem } from './api.js';
13
13
  /**
14
14
  * A Sync is a storage item that has been selected for synchronization by the user.
15
15
  * Importantly, it is *only* the "top-level" item.
@@ -141,8 +141,7 @@ export async function doSync(sync, opt) {
141
141
  else {
142
142
  const type = mime.getType(dirent._path) || 'application/octet-stream';
143
143
  const { size } = fs.statSync(join(sync.local_path, dirent._path));
144
- // cast because Node.js' internals use different types from lib.dom.d.ts
145
- const stream = Readable.toWeb(fs.createReadStream(join(sync.local_path, dirent._path)));
144
+ const stream = streamRead(join(sync.local_path, dirent._path));
146
145
  const file = await createItem(stream, { ...uploadOpts, type, size });
147
146
  _items.set(dirent._path, Object.assign(pick(file, 'id', 'modifiedAt', 'hash'), { path: dirent._path }));
148
147
  }
@@ -168,23 +167,19 @@ export async function doSync(sync, opt) {
168
167
  fs.mkdirSync(fullPath, { recursive: true });
169
168
  }
170
169
  else {
171
- const writeStream = fs.createWriteStream(fullPath);
172
- const stream = await downloadItemStream(item.id);
173
- await stream.pipeTo(Writable.toWeb(writeStream));
170
+ await streamWrite(fullPath, await downloadItemStream(item.id));
174
171
  }
175
172
  });
176
173
  if (!opt.verbose && delta.modified.length)
177
174
  io.start('Syncing modified items');
178
175
  await applyAction(opt, delta.modified, item => (opt.verbose ? 'Updating ' : '') + item.path, async (item) => {
179
176
  if (item.modifiedAt.getTime() > fs.statSync(join(sync.local_path, item.path)).mtime.getTime()) {
180
- const writeStream = fs.createWriteStream(join(sync.local_path, item.path));
181
- const stream = await downloadItemStream(item.id);
182
- await stream.pipeTo(Writable.toWeb(writeStream));
177
+ await streamWrite(join(sync.local_path, item.path), await downloadItemStream(item.id));
183
178
  return 'server.';
184
179
  }
185
180
  else {
186
181
  const { size } = fs.statSync(join(sync.local_path, item.path));
187
- const stream = Readable.toWeb(fs.createReadStream(join(sync.local_path, item.path)));
182
+ const stream = streamRead(join(sync.local_path, item.path));
188
183
  const updated = await updateItem(item.id, size, stream);
189
184
  _items.set(item.path, Object.assign(pick(updated, 'id', 'modifiedAt', 'hash'), { path: item.path }));
190
185
  return 'local.';
package/dist/node.d.ts CHANGED
@@ -10,4 +10,6 @@ interface FormatItemsConfig {
10
10
  humanReadable: boolean;
11
11
  }
12
12
  export declare function formatItems({ items, users, humanReadable }: FormatItemsConfig): Generator<string>;
13
+ export declare function streamRead(path: string, start?: number, end?: number): ReadableStream<Uint8Array<ArrayBuffer>>;
14
+ export declare function streamWrite(path: string, stream: ReadableStream): Promise<void>;
13
15
  export {};
package/dist/node.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { formatBytes } from '@axium/core/format';
2
2
  import { styleText } from 'node:util';
3
+ import { Readable, Writable } from 'node:stream';
4
+ import { createReadStream, createWriteStream } from 'node:fs';
3
5
  const executables = ['application/x-pie-executable', 'application/x-sharedlib', 'application/vnd.microsoft.portable-executable'];
4
6
  const archives = [
5
7
  'application/gzip',
@@ -48,3 +50,12 @@ export function* formatItems({ items, users, humanReadable }) {
48
50
  yield `${type}${ownerPerm}${ownerPerm}. ${owner.padEnd(nameWidth)} ${item.__size.padStart(sizeWidth)} ${formatDate(item.modifiedAt)} ${colorItem(item)}`;
49
51
  }
50
52
  }
53
+ // 2 MiB
54
+ const highWaterMark = 1024 * 1024 * 2;
55
+ export function streamRead(path, start, end) {
56
+ // cast because Node.js' internals use different types from lib.dom.d.ts
57
+ return Readable.toWeb(createReadStream(path, { start, end, highWaterMark }));
58
+ }
59
+ export async function streamWrite(path, stream) {
60
+ await stream.pipeTo(Writable.toWeb(createWriteStream(path, { highWaterMark })));
61
+ }
@@ -4,10 +4,10 @@ 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, createReadStream, renameSync, unlinkSync, writeFileSync } from 'node:fs';
7
+ import { copyFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
8
8
  import { join } from 'node:path/posix';
9
- import { Readable } from 'node:stream';
10
9
  import * as z from 'zod';
10
+ import { streamRead } from '../node.js';
11
11
  import '../polyfills.js';
12
12
  import { getLimits } from './config.js';
13
13
  import { getUserStats } from './db.js';
@@ -139,7 +139,7 @@ addRoute({
139
139
  headers: { 'Content-Range': `bytes */${item.size}` },
140
140
  });
141
141
  }
142
- const content = Readable.toWeb(createReadStream(path, { start, end }));
142
+ const content = streamRead(path, start, end);
143
143
  return new Response(content, {
144
144
  status: BigInt(length) == item.size ? 200 : 206,
145
145
  headers: {
package/lib/List.svelte CHANGED
@@ -9,7 +9,7 @@
9
9
  import { formatBytes } from '@axium/core/format';
10
10
  import { forMime as iconForMime } from '@axium/core/icons';
11
11
  import { getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
12
- import { copyShortURL, formatItemName } from '@axium/storage/client/frontend';
12
+ import { _downloadItem, copyShortURL, formatItemName } from '@axium/storage/client/frontend';
13
13
  import { StorageItemSorting, type StorageItemMetadata } from '@axium/storage/common';
14
14
  import { errorText } from 'ioium';
15
15
  import Preview from './Preview.svelte';
@@ -106,11 +106,7 @@
106
106
  text: text('storage.List.share'),
107
107
  action: () => ((activeId = item.id), dialogs['share:' + item.id].showModal()),
108
108
  },
109
- {
110
- i: 'download',
111
- text: text('storage.generic.download'),
112
- action: () => ((activeId = item.id), dialogs.download.showModal()),
113
- },
109
+ { i: 'download', text: text('storage.generic.download'), action: () => _downloadItem(item) },
114
110
  { i: 'link-horizontal', text: text('storage.List.copy_link'), action: () => ((activeId = item.id), copyShortURL(item)) },
115
111
  { i: 'trash', text: text('storage.generic.trash'), action: trash },
116
112
  user?.preferences?.debug && {
@@ -135,7 +131,9 @@
135
131
  >
136
132
  {@render action('rename', 'pencil', item.id)}
137
133
  {@render action('share:' + item.id, 'user-group', item.id)}
138
- {@render action('download', 'download', item.id)}
134
+ <span class="icon-text action" onclick={() => _downloadItem(item)}>
135
+ <Icon i="download" --size="14px" />
136
+ </span>
139
137
  <span class="icon-text action" onclick={trash}>
140
138
  <Icon i="trash" --size="14px" />
141
139
  </span>
@@ -167,16 +165,6 @@
167
165
  <input name="name" type="text" required value={activeItem?.name} />
168
166
  </div>
169
167
  </FormDialog>
170
- <FormDialog
171
- bind:dialog={dialogs.download}
172
- submitText={text('storage.generic.download')}
173
- submit={async () => {
174
- if (!activeId || !activeItem) throw text('storage.generic.no_item');
175
- open(activeItem.type != 'inode/directory' ? activeItem.dataURL : '/raw/storage/directory-zip/' + activeId, '_blank');
176
- }}
177
- >
178
- <p>{text('storage.generic.download_confirm_named', { name: activeItemName })}</p>
179
- </FormDialog>
180
168
 
181
169
  <style>
182
170
  .item-actions {
@@ -5,7 +5,7 @@
5
5
  import type { AccessControllable } from '@axium/core';
6
6
  import { downloadItem, downloadItemStream, getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
7
7
  import { openers, previews } from '@axium/storage/client/3rd-party';
8
- import { copyShortURL } from '@axium/storage/client/frontend';
8
+ import { _downloadItem, copyShortURL } from '@axium/storage/client/frontend';
9
9
  import type { StorageItemMetadata } from '@axium/storage/common';
10
10
  import '@axium/storage/polyfills';
11
11
  import './Preview.css';
@@ -63,7 +63,9 @@
63
63
  <Icon i="user-group" />
64
64
  </span>
65
65
  {/if}
66
- {@render action('download', 'download')}
66
+ <span class="icon-text preview-action" onclick={() => _downloadItem(item)}>
67
+ <Icon i="download" />
68
+ </span>
67
69
  <span class="icon-text preview-action" onclick={() => copyShortURL(item)}>
68
70
  <Icon i="link-horizontal" />
69
71
  </span>
@@ -102,17 +104,9 @@
102
104
  {#if item.type.startsWith('image/')}
103
105
  <img src={item.dataURL} alt={item.name} />
104
106
  {:else if item.type.startsWith('audio/')}
105
- {#await downloadItemStream(item.id)}
106
- {@render loading()}
107
- {:then stream}
108
- <Audio src={item.dataURL} {...item} metadataSource={stream} cover />
109
- {/await}
107
+ <Audio src={item.dataURL} {...item} metadataSource={downloadItemStream(item.id)} cover />
110
108
  {:else if item.type.startsWith('video/')}
111
- {#await downloadItemStream(item.id)}
112
- {@render loading()}
113
- {:then stream}
114
- <Video src={item.dataURL} {...item} metadataSource={stream} />
115
- {/await}
109
+ <Video src={item.dataURL} {...item} />
116
110
  {:else if item.type == 'application/pdf'}
117
111
  <object data={item.dataURL} type="application/pdf" width="100%" height="100%">
118
112
  <embed src={item.dataURL} type="application/pdf" width="100%" height="100%" />
@@ -152,16 +146,3 @@
152
146
  <input name="name" type="text" required value={item.name} />
153
147
  </div>
154
148
  </FormDialog>
155
- <FormDialog
156
- bind:dialog={dialogs.download}
157
- submitText={text('storage.generic.download')}
158
- submit={async () => {
159
- if (item!.type == 'inode/directory') {
160
- /** @todo ZIP support */
161
- const children = await getDirectoryMetadata(item.id);
162
- for (const child of children) open(child.dataURL, '_blank');
163
- } else open(item!.dataURL, '_blank');
164
- }}
165
- >
166
- <p>{text('storage.generic.download_confirm')}</p>
167
- </FormDialog>
@@ -5,7 +5,7 @@
5
5
  import { copy } from '@axium/client/gui';
6
6
  import * as icon from '@axium/core/icons';
7
7
  import { deleteItem, updateItemMetadata } from '@axium/storage/client';
8
- import { formatItemName } from '@axium/storage/client/frontend';
8
+ import { _downloadItem, formatItemName } from '@axium/storage/client/frontend';
9
9
  import type { StorageItemMetadata } from '@axium/storage/common';
10
10
  import { getDirectory, selection, toggle, toggleRange } from '@axium/storage/sidebar';
11
11
  import SidebarItem from './SidebarItem.svelte';
@@ -100,7 +100,11 @@
100
100
  {@render action('rename', 'pen', text('storage.generic.rename'))}
101
101
  {@render action('delete', 'trash', text('storage.SidebarItem.delete'))}
102
102
  {#if item.type == 'cas_item'}
103
- {@render action('download', 'download', text('storage.generic.download'))}
103
+ {@render action('download', 'download')}
104
+ <div onclick={() => _downloadItem(item)} class="action icon-text">
105
+ <Icon i="download" --size="14px" />
106
+ {text('storage.generic.download')}
107
+ </div>
104
108
  {/if}
105
109
  {#if page.data.session?.user.preferences.debug}
106
110
  <div class="action icon-text" onclick={() => copy('text/plain', item.id)}>
@@ -135,18 +139,6 @@
135
139
  >
136
140
  <p>{text('storage.generic.delete_confirm_named', { name: itemName })}</p>
137
141
  </FormDialog>
138
- <FormDialog
139
- bind:dialog={dialogs.download}
140
- submitText={text('storage.generic.download')}
141
- submit={async () => {
142
- open(item.dataURL, '_blank');
143
- }}
144
- >
145
- <p>
146
- {text('storage.SidebarItem.download_disclaimer')} <br />
147
- {text('storage.generic.download_confirm_named', { name: itemName })}
148
- </p>
149
- </FormDialog>
150
142
 
151
143
  <style>
152
144
  .StorageSidebarItem {
package/locales/en.json CHANGED
@@ -57,8 +57,6 @@
57
57
  "generic": {
58
58
  "copy_id": "Copy ID",
59
59
  "download": "Download",
60
- "download_confirm": "Are you sure you want to download this?",
61
- "download_confirm_named": "Are you sure you want to download {name}?",
62
60
  "name": "Name",
63
61
  "no_name_in_dialog": "this",
64
62
  "no_item": "No item is selected",
@@ -93,8 +91,7 @@
93
91
  "no_files": "No files yet"
94
92
  },
95
93
  "SidebarItem": {
96
- "delete": "Delete",
97
- "download_disclaimer": "We are not responsible for the contents of this file."
94
+ "delete": "Delete"
98
95
  },
99
96
  "Usage": {
100
97
  "error": "Couldn't load your uploads.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "User file storage for Axium",
6
6
  "funding": {
@@ -2,8 +2,8 @@
2
2
  import { text } from '@axium/client';
3
3
  import { AccessControlDialog, FormDialog, Icon } from '@axium/client/components';
4
4
  import { toastStatus } from '@axium/client/toast';
5
- import { getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
6
- import { copyShortURL } from '@axium/storage/client/frontend';
5
+ import { updateItemMetadata } from '@axium/storage/client';
6
+ import { _downloadItem, copyShortURL } from '@axium/storage/client/frontend';
7
7
  import { Add, List, Preview } from '@axium/storage/components';
8
8
  import type { PageProps } from './$types';
9
9
 
@@ -58,7 +58,7 @@
58
58
  {@render action('folder-arrow-up', text('page.files.back'), () => (location.href = parentHref))}
59
59
  {@render action('pencil', text('page.files.rename'), () => dialogs.rename.showModal())}
60
60
  {@render action('user-group', text('page.files.share'), () => shareDialog.showModal())}
61
- {@render action('download', text('page.files.download'), () => dialogs.download.showModal())}
61
+ {@render action('download', text('page.files.download'), () => _downloadItem(item))}
62
62
  {@render action('link-horizontal', text('page.files.copy_link'), () => copyShortURL(item))}
63
63
  {@render action('trash', text('page.files.trash'), () =>
64
64
  toastStatus(
@@ -88,13 +88,6 @@
88
88
  <input name="name" type="text" required value={item.name} />
89
89
  </div>
90
90
  </FormDialog>
91
- <FormDialog
92
- bind:dialog={dialogs.download}
93
- submitText={text('page.files.download')}
94
- submit={async () => open(item.type != 'inode/directory' ? item.dataURL : '/raw/storage/directory-zip/' + item.id, '_blank')}
95
- >
96
- <p>{text('storage.generic.download_confirm_named', { name: item.name })}</p>
97
- </FormDialog>
98
91
  {/if}
99
92
 
100
93
  <style>