@lacrous/omnicloud 0.2.32 → 0.2.33

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/index.d.cts CHANGED
@@ -429,6 +429,12 @@ declare class FilesApi {
429
429
  * fetch elsewhere.
430
430
  */
431
431
  upload(input: UploadInput, options?: UploadOptions): Promise<FileDTO$1>;
432
+ private canStreamFromDisk;
433
+ /**
434
+ * Node: streams a file from disk as a multipart body. The file is re-opened on
435
+ * every attempt, so a retry sends the same bytes without holding them in memory.
436
+ */
437
+ private uploadPathOnce;
432
438
  private uploadOnce;
433
439
  private uploadViaXhr;
434
440
  /** Uploads a new version of an existing file. */
package/dist/index.d.ts CHANGED
@@ -429,6 +429,12 @@ declare class FilesApi {
429
429
  * fetch elsewhere.
430
430
  */
431
431
  upload(input: UploadInput, options?: UploadOptions): Promise<FileDTO$1>;
432
+ private canStreamFromDisk;
433
+ /**
434
+ * Node: streams a file from disk as a multipart body. The file is re-opened on
435
+ * every attempt, so a retry sends the same bytes without holding them in memory.
436
+ */
437
+ private uploadPathOnce;
432
438
  private uploadOnce;
433
439
  private uploadViaXhr;
434
440
  /** Uploads a new version of an existing file. */
package/dist/index.js CHANGED
@@ -14,6 +14,51 @@ var OmniCloudError = class extends Error {
14
14
  details;
15
15
  };
16
16
  var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 300 };
17
+ function randomId() {
18
+ return globalThis.crypto.randomUUID().replace(/-/g, "");
19
+ }
20
+ function multipartHead(boundary, fields, fileName) {
21
+ const parts = fields.map(
22
+ ([name, value]) => `--${boundary}\r
23
+ Content-Disposition: form-data; name="${name}"\r
24
+ \r
25
+ ${value}\r
26
+ `
27
+ );
28
+ const safeName = fileName.replace(/["\\\r\n]/g, "_");
29
+ parts.push(
30
+ `--${boundary}\r
31
+ Content-Disposition: form-data; name="file"; filename="${safeName}"\r
32
+ Content-Type: application/octet-stream\r
33
+ \r
34
+ `
35
+ );
36
+ return parts.join("");
37
+ }
38
+ function nodeMultipartStream(head, file, tail) {
39
+ const encoder = new TextEncoder();
40
+ const iterator = file[Symbol.asyncIterator]();
41
+ let sentHead = false;
42
+ return new ReadableStream({
43
+ async pull(controller) {
44
+ if (!sentHead) {
45
+ sentHead = true;
46
+ controller.enqueue(encoder.encode(head));
47
+ return;
48
+ }
49
+ const { done, value } = await iterator.next();
50
+ if (done) {
51
+ controller.enqueue(encoder.encode(tail));
52
+ controller.close();
53
+ return;
54
+ }
55
+ controller.enqueue(typeof value === "string" ? encoder.encode(value) : new Uint8Array(value));
56
+ },
57
+ async cancel(reason) {
58
+ await iterator.return?.(reason);
59
+ }
60
+ });
61
+ }
17
62
  function sleep(ms, signal) {
18
63
  return new Promise((resolve, reject) => {
19
64
  const timer = setTimeout(resolve, ms);
@@ -305,10 +350,13 @@ var FilesApi = class {
305
350
  async upload(input, options = {}) {
306
351
  const attempts = Math.max(1, (options.retry ?? 2) + 1);
307
352
  let lastError;
308
- const blob = await toBlob(input);
353
+ const source = input.path !== void 0 && this.canStreamFromDisk() ? { kind: "path", path: input.path } : { kind: "blob", blob: await toBlob(input) };
309
354
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
310
355
  try {
311
- return await this.uploadOnce(blob, input.name, options);
356
+ if (source.kind === "path") {
357
+ return await this.uploadPathOnce(source.path, input.name, options);
358
+ }
359
+ return await this.uploadOnce(source.blob, input.name, options);
312
360
  } catch (error) {
313
361
  lastError = error;
314
362
  if (options.signal?.aborted) throw error;
@@ -318,6 +366,46 @@ var FilesApi = class {
318
366
  }
319
367
  throw lastError;
320
368
  }
369
+ canStreamFromDisk() {
370
+ return typeof XMLHttpRequest !== "function" && typeof ReadableStream === "function";
371
+ }
372
+ /**
373
+ * Node: streams a file from disk as a multipart body. The file is re-opened on
374
+ * every attempt, so a retry sends the same bytes without holding them in memory.
375
+ */
376
+ async uploadPathOnce(path, name, options) {
377
+ const { createReadStream, statSync } = await import("fs");
378
+ const size = statSync(path).size;
379
+ const boundary = `omnicloud-${randomId()}`;
380
+ const fields = [];
381
+ if (options.folderId) fields.push(["folderId", options.folderId]);
382
+ const head = multipartHead(boundary, fields, name);
383
+ const tail = `\r
384
+ --${boundary}--\r
385
+ `;
386
+ const body = nodeMultipartStream(head, createReadStream(path), tail);
387
+ const response = await this.client.fetchImpl(`${this.client.baseUrl}/api/files`, {
388
+ method: "POST",
389
+ headers: {
390
+ ...this.client.defaultHeaders,
391
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
392
+ "Content-Length": String(Buffer.byteLength(head) + size + Buffer.byteLength(tail))
393
+ },
394
+ body,
395
+ credentials: "include",
396
+ duplex: "half"
397
+ });
398
+ const text = await response.text();
399
+ if (!response.ok) {
400
+ const error = safeJsonParse(text)?.error;
401
+ throw new OmniCloudError(
402
+ response.status,
403
+ error?.code ?? "UPLOAD_FAILED",
404
+ error?.message ?? `Upload failed (${response.status})`
405
+ );
406
+ }
407
+ return JSON.parse(text).file;
408
+ }
321
409
  async uploadOnce(blob, name, options) {
322
410
  const form = new FormData();
323
411
  form.append("file", blob, name);