@axium/storage 0.24.0 → 0.24.2

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.
@@ -5,9 +5,9 @@ import { StorageItemMetadata } from '../common.js';
5
5
  import '../polyfills.js';
6
6
  import { warnOnce } from 'ioium';
7
7
  function rawStorage(suffix) {
8
- const raw = origin + '/raw/storage' + (suffix ? '/' + suffix : '');
8
+ const raw = '/raw/storage' + (suffix ? '/' + suffix : '');
9
9
  if (prefix[0] == '/')
10
- return raw;
10
+ return origin + raw;
11
11
  const url = new URL(prefix);
12
12
  url.pathname = raw;
13
13
  return url;
@@ -55,7 +55,9 @@ import { Command } from 'commander';
55
55
  import * as io from 'ioium/node';
56
56
  import mime from 'mime';
57
57
  import * as fs from 'node:fs';
58
- import { basename } from 'node:path';
58
+ import { basename, join, parse } from 'node:path';
59
+ import { createInterface } from 'node:readline/promises';
60
+ import { stringbool } from 'zod';
59
61
  import { colorItem, formatItems, streamRead } from '../../node.js';
60
62
  import * as api from '../api.js';
61
63
  import { getDirectory, resolveItem, resolvePathWithParent, syncCache, writeCache } from '../local.js';
@@ -102,6 +104,32 @@ export const remove = new Command('remove')
102
104
  writeCache();
103
105
  }
104
106
  });
107
+ async function doUpload(local, name, size, parentId, text = name) {
108
+ const env_1 = { stack: [], error: void 0, hasError: false };
109
+ try {
110
+ const type = mime.getType(local) || 'application/octet-stream';
111
+ const _ = __addDisposableResource(env_1, io.start('Uploading ' + text), false);
112
+ const item = await api.createItem(streamRead(local), {
113
+ parentId,
114
+ name,
115
+ size,
116
+ type,
117
+ onProgress(uploaded, total) {
118
+ io.progress(uploaded, total, Math.round((uploaded / total) * 100) + '%', `${formatBytes(BigInt(uploaded))}/${formatBytes(BigInt(total))}`);
119
+ },
120
+ });
121
+ const { items } = await syncCache();
122
+ items.push(item);
123
+ return item;
124
+ }
125
+ catch (e_1) {
126
+ env_1.error = e_1;
127
+ env_1.hasError = true;
128
+ }
129
+ finally {
130
+ __disposeResources(env_1);
131
+ }
132
+ }
105
133
  export const upload = new Command('upload')
106
134
  .description('Upload a file or folder')
107
135
  .argument('<local>', 'local file or folder path to upload')
@@ -110,48 +138,76 @@ export const upload = new Command('upload')
110
138
  .option('-r, --recursive', 'operate recursively on local directories')
111
139
  .option('-T, --no-target-directory', 'always treat the remote path as a file')
112
140
  .action(async (local, remotePath, opts) => {
113
- const env_1 = { stack: [], error: void 0, hasError: false };
141
+ const env_2 = { stack: [], error: void 0, hasError: false };
114
142
  try {
115
143
  const stats = fs.statSync(local);
116
144
  const existingTarget = await resolveItem(remotePath);
117
145
  let { parent, name } = await resolvePathWithParent(remotePath);
118
- if (stats.isDirectory()) {
119
- if (!opts.recursive)
120
- throw '--recursive/-r not specified but the local path is a directory';
121
- else
122
- throw 'Uploading directories is not support yet';
146
+ if (!stats.isDirectory()) {
147
+ if (existingTarget?.type == 'inode/directory') {
148
+ if (opts.targetDirectory) {
149
+ parent = existingTarget;
150
+ name = basename(local);
151
+ }
152
+ else
153
+ throw 'Directory exists at remote path: ' + existingTarget.name;
154
+ }
155
+ else if (existingTarget && !opts.force)
156
+ throw 'File exists at remote path, use --force to overwrite it';
157
+ await doUpload(local, name, stats.size, parent?.id);
158
+ writeCache();
159
+ return;
160
+ }
161
+ if (!opts.recursive)
162
+ throw '--recursive/-r not specified but the local path is a directory';
163
+ if (existingTarget)
164
+ throw 'Folder exists at remote path. Merging is not supported yet.';
165
+ const rl = __addDisposableResource(env_2, createInterface({
166
+ input: process.stdin,
167
+ output: process.stdout,
168
+ }), false);
169
+ const toUpload = [];
170
+ let sum = 0n;
171
+ // Sort to make sure directories come first, e.g. `example` and `example/duck`
172
+ for (const path of fs.readdirSync(local, { recursive: true, encoding: 'utf8' }).sort((a, b) => a.localeCompare(b))) {
173
+ const full = join(local, path);
174
+ const stats = fs.statSync(full, { bigint: true });
175
+ toUpload.push({ path, stats, full });
176
+ sum += stats.size;
123
177
  }
124
- if (existingTarget?.type == 'inode/directory') {
125
- if (opts.targetDirectory) {
126
- parent = existingTarget;
127
- name = basename(local);
178
+ const { data, error } = stringbool()
179
+ .default(false)
180
+ .safeParse(await rl.question(`Upload ${toUpload.length} files totaling ${formatBytes(sum)}? [y/N]: `).catch(() => io.exit('Aborted.')));
181
+ if (error || !data)
182
+ io.exit('Aborted.');
183
+ const { id } = await io.track('Creating directory', api.createDirectory(name, parent?.id));
184
+ const dirs = new Map();
185
+ for (const { path, stats, full } of toUpload) {
186
+ const { dir, base } = parse(path);
187
+ let parentId;
188
+ if (dir) {
189
+ const md = dirs.get(dir);
190
+ if (!md)
191
+ throw `Could not get metadata for the directory '${dir}'.`;
192
+ parentId = md.id;
128
193
  }
129
194
  else
130
- throw 'Directory exists at remote path: ' + existingTarget.name;
195
+ parentId = id;
196
+ if (stats.isDirectory()) {
197
+ await io.track('Creating directory: ' + path, api.createDirectory(base, parentId));
198
+ }
199
+ else {
200
+ await doUpload(full, base, Number(stats.size), parentId, path);
201
+ }
131
202
  }
132
- else if (existingTarget && !opts.force)
133
- throw 'File exists at remote path, use --force to overwrite it';
134
- const type = mime.getType(local) || 'application/octet-stream';
135
- const _ = __addDisposableResource(env_1, io.start('Uploading ' + name), false);
136
- const item = await api.createItem(streamRead(local), {
137
- parentId: parent?.id,
138
- name,
139
- size: stats.size,
140
- type,
141
- onProgress(uploaded, total) {
142
- io.progress(uploaded, total, Math.round((uploaded / total) * 100) + '%', `${formatBytes(BigInt(uploaded))}/${formatBytes(BigInt(total))}`);
143
- },
144
- });
145
- const { items } = await syncCache();
146
- items.push(item);
147
203
  writeCache();
148
204
  }
149
- catch (e_1) {
150
- env_1.error = e_1;
151
- env_1.hasError = true;
205
+ catch (e_2) {
206
+ env_2.error = e_2;
207
+ env_2.hasError = true;
152
208
  }
153
209
  finally {
154
- __disposeResources(env_1);
210
+ __disposeResources(env_2);
155
211
  }
156
212
  });
157
213
  export const download = new Command('download')
@@ -159,7 +215,7 @@ export const download = new Command('download')
159
215
  .argument('<remote>', 'remote file path to download')
160
216
  .argument('[local]', 'local path to save the file to')
161
217
  .action(async (remotePath, localPath) => {
162
- const env_2 = { stack: [], error: void 0, hasError: false };
218
+ const env_3 = { stack: [], error: void 0, hasError: false };
163
219
  try {
164
220
  const item = await resolveItem(remotePath);
165
221
  if (!item)
@@ -167,7 +223,7 @@ export const download = new Command('download')
167
223
  if (item.type == 'inode/directory')
168
224
  throw "Can't download directories yet.";
169
225
  localPath ||= item.name;
170
- const _ = __addDisposableResource(env_2, io.start('Downloading to ' + localPath), false);
226
+ const _ = __addDisposableResource(env_3, io.start('Downloading to ' + localPath), false);
171
227
  const stream = await api.downloadItemStream(item.id);
172
228
  const size = Number(item.size);
173
229
  io.progress(0, size);
@@ -180,11 +236,11 @@ export const download = new Command('download')
180
236
  }
181
237
  fileStream.end();
182
238
  }
183
- catch (e_2) {
184
- env_2.error = e_2;
185
- env_2.hasError = true;
239
+ catch (e_3) {
240
+ env_3.error = e_3;
241
+ env_3.hasError = true;
186
242
  }
187
243
  finally {
188
- __disposeResources(env_2);
244
+ __disposeResources(env_3);
189
245
  }
190
246
  });
@@ -22,7 +22,6 @@ export interface UploadInfo {
22
22
  file: string;
23
23
  stream: WritableStream;
24
24
  hash: Hash;
25
- hashStream: WritableStream;
26
25
  uploadedBytes: bigint;
27
26
  sessionId: string;
28
27
  userId: string;
@@ -153,7 +153,6 @@ export function startUpload(init, session, itemId) {
153
153
  const hash = createHash('BLAKE2b512'), stream = Writable.toWeb(createWriteStream(file));
154
154
  inProgress.set(token.toBase64(), {
155
155
  hash,
156
- hashStream: Writable.toWeb(hash),
157
156
  file,
158
157
  stream,
159
158
  uploadedBytes: 0n,
@@ -63,16 +63,13 @@ addRoute({
63
63
  const counter = new TransformStream({
64
64
  transform(chunk, controller) {
65
65
  actualSize += BigInt(chunk.length);
66
+ upload.hash.update(chunk);
66
67
  controller.enqueue(chunk);
67
68
  },
68
69
  });
69
- const [forFile, forHash] = request.body.pipeThrough(counter).tee();
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
- await Promise.all([
73
- forFile.pipeTo(upload.stream, { preventClose: true }),
74
- forHash.pipeTo(upload.hashStream, { preventClose: true }),
75
- ]);
72
+ await request.body.pipeThrough(counter).pipeTo(upload.stream, { preventClose: true });
76
73
  if (actualSize != size) {
77
74
  upload.remove();
78
75
  await audit('storage_size_mismatch', upload.userId, { item: null });
package/lib/List.svelte CHANGED
@@ -39,7 +39,7 @@
39
39
  if (!sort) {
40
40
  const dirDiff = +(_b.type == 'inode/directory') - +(_a.type == 'inode/directory');
41
41
  if (sort_folders_first && dirDiff) return dirDiff;
42
- return _b.name.localeCompare(_a.name);
42
+ return _a.name.localeCompare(_b.name);
43
43
  }
44
44
 
45
45
  const [a, b] = sort.descending ? [_b, _a] : [_a, _b];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "User file storage for Axium",
6
6
  "funding": {