disk 0.8.14 → 0.8.15

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.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  ArchilError: () => ArchilError,
36
36
  ArchilS3Error: () => ArchilS3Error,
37
37
  Disk: () => Disk,
38
+ DiskMultipart: () => DiskMultipart,
38
39
  Disks: () => Disks,
39
40
  Tokens: () => Tokens,
40
41
  USER_AGENT: () => USER_AGENT,
@@ -43,6 +44,7 @@ __export(index_exports, {
43
44
  createApiKey: () => createApiKey,
44
45
  createDisk: () => createDisk,
45
46
  deleteApiKey: () => deleteApiKey,
47
+ effectiveUploadPartSize: () => effectiveUploadPartSize,
46
48
  exec: () => exec,
47
49
  getDisk: () => getDisk,
48
50
  listApiKeys: () => listApiKeys,
@@ -152,7 +154,7 @@ function deriveS3BaseUrl(controlBaseUrl) {
152
154
  }
153
155
 
154
156
  // src/version.ts
155
- var VERSION = true ? "0.8.14" : "0.0.0-dev";
157
+ var VERSION = true ? "0.8.15" : "0.0.0-dev";
156
158
  var USER_AGENT = `archil-js/${VERSION}`;
157
159
 
158
160
  // src/client.ts
@@ -228,6 +230,8 @@ var Disk = class {
228
230
  _archilRegion;
229
231
  /** Base URL for the S3-compatible API (port 9000 ingress). Empty if unset. */
230
232
  _s3BaseUrl;
233
+ /** Lazily-constructed multipart namespace (see {@link multipart}). @internal */
234
+ _multipart;
231
235
  /** @internal */
232
236
  constructor(data, client, archilRegion, s3BaseUrl) {
233
237
  this.id = data.id;
@@ -430,18 +434,105 @@ var Disk = class {
430
434
  return await this.headObject(key) !== null;
431
435
  }
432
436
  /**
433
- * Write an object to the disk using the S3-compatible PutObject API. Faster
434
- * than exec for large files no container overhead, no command-length limits.
435
- * Returns the entity tag the server assigned.
437
+ * Write an object to the disk via the S3-compatible API. Handles any size:
438
+ * bodies at or below `multipartThreshold` (defaults to `partSize`, i.e.
439
+ * 16 MiB) go through a single PutObject request; larger bodies are uploaded as
440
+ * a multipart upload — split into `partSize` parts, uploaded with bounded
441
+ * `concurrency` (default 4), and assembled. A failed part aborts the upload so
442
+ * nothing is left half-staged. For manual control over the multipart
443
+ * lifecycle, use the {@link multipart} namespace.
436
444
  *
437
- * @param key Path on the disk (e.g. "reports/2026-01/data.json")
438
- * @param body Contents as a string, Uint8Array/Buffer, or ArrayBuffer
439
- * @param contentType MIME type (default: "application/octet-stream")
445
+ * Faster than exec for large files — no container overhead, no command-length
446
+ * limits. Returns the entity tag the server assigned (a multipart upload's tag
447
+ * is S3's `md5(concat(partMd5s))-N` form rather than a plain MD5).
448
+ *
449
+ * @param key Path on the disk (e.g. "reports/2026-01/data.json")
450
+ * @param body Contents as a string, Uint8Array/Buffer, or ArrayBuffer
451
+ * @param options Either a content-type string, or {@link PutObjectOptions}
452
+ * (`contentType`, `multipartThreshold`, `partSize`,
453
+ * `concurrency`). Content type defaults to
454
+ * "application/octet-stream".
455
+ */
456
+ async putObject(key, body, options) {
457
+ const opts = typeof options === "string" ? { contentType: options } : options ?? {};
458
+ const contentType = opts.contentType ?? "application/octet-stream";
459
+ const partSize = Math.max(opts.partSize ?? DEFAULT_PART_SIZE, MIN_PART_SIZE);
460
+ const threshold = opts.multipartThreshold ?? partSize;
461
+ const bytes = toBytes(body);
462
+ if (bytes.length <= threshold) {
463
+ const resp = await this._s3Request("PUT", key, { body, contentType });
464
+ if (!resp.ok) {
465
+ throw parseS3Error("PutObject", resp.status, resp.statusText, decodeText(resp.body));
466
+ }
467
+ return { etag: resp.headers.get("etag") ?? void 0 };
468
+ }
469
+ return this._putMultipart(
470
+ key,
471
+ bytes,
472
+ contentType,
473
+ partSize,
474
+ Math.max(1, opts.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY)
475
+ );
476
+ }
477
+ /**
478
+ * Upload a large body through the multipart lifecycle: split into `partSize`
479
+ * parts, upload them with bounded concurrency, then complete — aborting the
480
+ * upload if any part fails so nothing is left half-staged. @internal
440
481
  */
441
- async putObject(key, body, contentType = "application/octet-stream") {
442
- const resp = await this._s3Request("PUT", key, { body, contentType });
482
+ async _putMultipart(key, bytes, contentType, partSize, concurrency) {
483
+ const effectivePartSize = effectiveUploadPartSize(bytes.length, partSize);
484
+ const mp = this.multipart;
485
+ const upload = await mp.create(key, contentType);
486
+ try {
487
+ const partCount = Math.ceil(bytes.length / effectivePartSize);
488
+ const parts = new Array(partCount);
489
+ let next = 0;
490
+ const worker = async () => {
491
+ for (; ; ) {
492
+ const index = next++;
493
+ if (index >= partCount) return;
494
+ const start = index * effectivePartSize;
495
+ const slice = bytes.subarray(start, Math.min(start + effectivePartSize, bytes.length));
496
+ parts[index] = await mp.uploadPart(key, upload.uploadId, index + 1, slice);
497
+ }
498
+ };
499
+ await Promise.all(Array.from({ length: Math.min(concurrency, partCount) }, worker));
500
+ const done = await mp.complete(key, upload.uploadId, parts);
501
+ return { etag: done.etag };
502
+ } catch (err) {
503
+ await mp.abort(key, upload.uploadId).catch(() => {
504
+ });
505
+ throw err;
506
+ }
507
+ }
508
+ /**
509
+ * Append bytes to an object via the S3-compatible PutObject append extension
510
+ * (`?append=true`). If the object already exists the bytes are appended to it;
511
+ * if it doesn't, it is created. Returns the entity tag of the full object
512
+ * after the append.
513
+ *
514
+ * Each call may append at most 1 MiB — the server rejects a larger body with
515
+ * `EntityTooLarge`. To grow an object past that, append in chunks (or use
516
+ * {@link putObject} for a one-shot large write).
517
+ *
518
+ * Unlike most operations this is NOT auto-retried on a transient error:
519
+ * append isn't idempotent, so retrying a succeeded-but-unacknowledged append
520
+ * would duplicate the bytes. On a transient failure, re-append yourself only
521
+ * after confirming the object's size.
522
+ *
523
+ * @param key Path on the disk (e.g. "logs/app.log")
524
+ * @param body Bytes to append (string, Uint8Array/Buffer, or ArrayBuffer)
525
+ * @param contentType MIME type, applied only when the object is newly created.
526
+ */
527
+ async appendObject(key, body, contentType = "application/octet-stream") {
528
+ const resp = await this._s3Request("PUT", key, {
529
+ body,
530
+ contentType,
531
+ query: { append: "true" },
532
+ retry: false
533
+ });
443
534
  if (!resp.ok) {
444
- throw parseS3Error("PutObject", resp.status, resp.statusText, decodeText(resp.body));
535
+ throw parseS3Error("AppendObject", resp.status, resp.statusText, decodeText(resp.body));
445
536
  }
446
537
  return { etag: resp.headers.get("etag") ?? void 0 };
447
538
  }
@@ -534,6 +625,50 @@ var Disk = class {
534
625
  }
535
626
  return parseListObjectsResult(decodeText(resp.body));
536
627
  }
628
+ /**
629
+ * Delete up to many objects in a single S3-compatible DeleteObjects request.
630
+ * Unlike {@link deleteObject}, failures are reported per key rather than
631
+ * thrown: the result's `deleted` lists the keys that were removed and
632
+ * `errors` lists the ones that weren't (with the server's code/message). A
633
+ * key that didn't exist still counts as deleted, per S3 semantics.
634
+ *
635
+ * The server caps a single request at 1000 keys; this method transparently
636
+ * splits larger inputs into 1000-key batches and merges the results.
637
+ *
638
+ * @param keys Object keys to delete.
639
+ * @param opts `quiet` suppresses the per-key success list server-side.
640
+ */
641
+ async deleteObjects(keys, opts = {}) {
642
+ const deleted = [];
643
+ const errors = [];
644
+ for (let i = 0; i < keys.length; i += MAX_DELETE_OBJECTS_PER_REQUEST) {
645
+ const batch = keys.slice(i, i + MAX_DELETE_OBJECTS_PER_REQUEST);
646
+ const xml = buildDeleteObjectsXml(batch, opts.quiet ?? false);
647
+ const resp = await this._s3Request("POST", "", {
648
+ query: { delete: "" },
649
+ body: xml,
650
+ contentType: "application/xml"
651
+ });
652
+ if (!resp.ok) {
653
+ throw parseS3Error("DeleteObjects", resp.status, resp.statusText, decodeText(resp.body));
654
+ }
655
+ const parsed = parseDeleteObjectsResult(decodeText(resp.body));
656
+ deleted.push(...parsed.deleted);
657
+ errors.push(...parsed.errors);
658
+ }
659
+ return { deleted, errors };
660
+ }
661
+ /**
662
+ * The advanced, opt-in multipart-upload API. Drive the raw lifecycle
663
+ * yourself — `create` → `uploadPart` → `complete` (or `abort`), plus
664
+ * `listParts` / `listUploads`. Most callers don't need this: {@link putObject}
665
+ * runs the whole lifecycle automatically for large bodies. Reach for it only
666
+ * when you need manual control (e.g. uploading parts from separate processes),
667
+ * and note you then own part-size, memory, and concurrency management.
668
+ */
669
+ get multipart() {
670
+ return this._multipart ??= new DiskMultipart(this.id, this._s3Request.bind(this));
671
+ }
537
672
  /**
538
673
  * Send a single request to the disk's S3-compatible endpoint. This reuses the
539
674
  * control-plane client purely for its credential and transport — the same
@@ -558,33 +693,55 @@ var Disk = class {
558
693
  const trimmedKey = key.replace(/^\//, "");
559
694
  const encodedKey = trimmedKey.split("/").map(encodeURIComponent).join("/");
560
695
  const path = encodedKey ? `/${this.id}/${encodedKey}` : `/${this.id}`;
561
- const { error, response } = await call(path, {
696
+ const init = {
562
697
  baseUrl: this._s3BaseUrl,
563
698
  parseAs: "stream",
564
699
  ...opts.query ? { params: { query: opts.query } } : {},
565
700
  // fetch accepts string / Uint8Array / ArrayBuffer bodies directly; pass it
566
- // through unchanged (no Node Buffer in the data path).
701
+ // through unchanged (no Node Buffer in the data path). Bodies are fully
702
+ // buffered (never streams), so re-sending one on a retry is safe.
567
703
  ...opts.body !== void 0 ? { body: opts.body, bodySerializer: (b) => b } : {},
568
704
  ...opts.contentType ? { headers: { "Content-Type": opts.contentType } } : {}
569
- });
570
- if (!response.ok) {
571
- const message = typeof error === "string" ? error : error ? JSON.stringify(error) : "";
705
+ };
706
+ const hasBody = method === "GET" || method === "POST";
707
+ const maxRetries = opts.retry === false ? 0 : MAX_S3_RETRIES;
708
+ for (let attempt = 0; ; attempt++) {
709
+ let error;
710
+ let response;
711
+ try {
712
+ ({ error, response } = await call(path, init));
713
+ } catch (transportError) {
714
+ if (attempt < maxRetries) {
715
+ await sleep(s3RetryDelayMs(attempt));
716
+ continue;
717
+ }
718
+ throw transportError;
719
+ }
720
+ if (!response.ok) {
721
+ if (isTransientS3Status(response.status) && attempt < maxRetries) {
722
+ await response.body?.cancel().catch(() => {
723
+ });
724
+ await sleep(s3RetryDelayMs(attempt));
725
+ continue;
726
+ }
727
+ const message = typeof error === "string" ? error : error ? JSON.stringify(error) : "";
728
+ return {
729
+ ok: false,
730
+ status: response.status,
731
+ statusText: response.statusText,
732
+ headers: response.headers,
733
+ body: new TextEncoder().encode(message)
734
+ };
735
+ }
736
+ const bytes = hasBody ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array(0);
572
737
  return {
573
- ok: false,
738
+ ok: true,
574
739
  status: response.status,
575
740
  statusText: response.statusText,
576
741
  headers: response.headers,
577
- body: new TextEncoder().encode(message)
742
+ body: bytes
578
743
  };
579
744
  }
580
- const bytes = method === "GET" ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array(0);
581
- return {
582
- ok: true,
583
- status: response.status,
584
- statusText: response.statusText,
585
- headers: response.headers,
586
- body: bytes
587
- };
588
745
  }
589
746
  /**
590
747
  * Connect to this disk's data plane via the native ArchilClient.
@@ -615,6 +772,199 @@ var Disk = class {
615
772
  });
616
773
  }
617
774
  };
775
+ var DiskMultipart = class {
776
+ /** @internal */
777
+ constructor(diskId, s3Request) {
778
+ this.diskId = diskId;
779
+ this.s3Request = s3Request;
780
+ }
781
+ diskId;
782
+ s3Request;
783
+ /**
784
+ * Start a multipart upload (CreateMultipartUpload) and return its `uploadId`.
785
+ *
786
+ * @param key Path on the disk the finished object will live at.
787
+ * @param contentType MIME type to store the object with.
788
+ */
789
+ async create(key, contentType) {
790
+ const resp = await this.s3Request("POST", key, { query: { uploads: "" }, contentType });
791
+ if (!resp.ok) {
792
+ throw parseS3Error("CreateMultipartUpload", resp.status, resp.statusText, decodeText(resp.body));
793
+ }
794
+ const root = parseXml(decodeText(resp.body)).InitiateMultipartUploadResult ?? {};
795
+ const uploadId = optionalString(root.UploadId);
796
+ if (!uploadId) {
797
+ throw new ArchilS3Error({
798
+ operation: "CreateMultipartUpload",
799
+ statusCode: resp.status,
800
+ message: "response did not contain an UploadId",
801
+ raw: decodeText(resp.body)
802
+ });
803
+ }
804
+ return {
805
+ uploadId,
806
+ key: optionalString(root.Key) ?? key,
807
+ bucket: optionalString(root.Bucket) ?? this.diskId
808
+ };
809
+ }
810
+ /**
811
+ * Upload one part (UploadPart) and return its entity tag, which you must
812
+ * collect (with its part number) and pass to {@link complete}. Every part
813
+ * except the last must be at least 5 MiB.
814
+ *
815
+ * @param key The upload's object key.
816
+ * @param uploadId The id from {@link create}.
817
+ * @param partNumber 1-based part number (1..=10000).
818
+ * @param body Part contents as a string, Uint8Array/Buffer, or ArrayBuffer.
819
+ */
820
+ async uploadPart(key, uploadId, partNumber, body) {
821
+ const resp = await this.s3Request("PUT", key, { query: { uploadId, partNumber }, body });
822
+ if (!resp.ok) {
823
+ throw parseS3Error("UploadPart", resp.status, resp.statusText, decodeText(resp.body));
824
+ }
825
+ return { partNumber, etag: resp.headers.get("etag") ?? "" };
826
+ }
827
+ /**
828
+ * Finish a multipart upload (CompleteMultipartUpload), assembling the listed
829
+ * parts into one object. Parts are sorted by part number before submission
830
+ * (the server requires strictly-increasing order).
831
+ *
832
+ * Unlike the other operations this is NOT auto-retried on a transient error:
833
+ * the gateway isn't idempotent for completion, so a retry after a
834
+ * successful-but-unacknowledged complete would return a spurious NoSuchUpload.
835
+ * On a transient failure, re-drive completion yourself only after confirming
836
+ * the object isn't already present.
837
+ *
838
+ * @param key The upload's object key.
839
+ * @param uploadId The id from {@link create}.
840
+ * @param parts The `{ partNumber, etag }` pairs from {@link uploadPart}.
841
+ */
842
+ async complete(key, uploadId, parts) {
843
+ const sorted = [...parts].sort((a, b) => a.partNumber - b.partNumber);
844
+ const xml = buildCompleteMultipartUploadXml(sorted);
845
+ const resp = await this.s3Request("POST", key, {
846
+ query: { uploadId },
847
+ body: xml,
848
+ contentType: "application/xml",
849
+ retry: false
850
+ });
851
+ if (!resp.ok) {
852
+ throw parseS3Error("CompleteMultipartUpload", resp.status, resp.statusText, decodeText(resp.body));
853
+ }
854
+ const root = parseXml(decodeText(resp.body)).CompleteMultipartUploadResult ?? {};
855
+ return {
856
+ etag: optionalString(root.ETag),
857
+ location: optionalString(root.Location),
858
+ bucket: optionalString(root.Bucket),
859
+ key: optionalString(root.Key)
860
+ };
861
+ }
862
+ /**
863
+ * Abort a multipart upload (AbortMultipartUpload), discarding every staged
864
+ * part. Idempotent against an upload that's already gone (404 / NoSuchUpload
865
+ * resolves successfully).
866
+ *
867
+ * @param key The upload's object key.
868
+ * @param uploadId The id from {@link create}.
869
+ */
870
+ async abort(key, uploadId) {
871
+ const resp = await this.s3Request("DELETE", key, { query: { uploadId } });
872
+ if (!resp.ok && resp.status !== 404) {
873
+ throw parseS3Error("AbortMultipartUpload", resp.status, resp.statusText, decodeText(resp.body));
874
+ }
875
+ }
876
+ /**
877
+ * List the parts already uploaded for an in-progress upload (ListParts).
878
+ * Returns a single page; follow `nextPartNumberMarker` (when `isTruncated`)
879
+ * to page through the rest.
880
+ *
881
+ * @param key The upload's object key.
882
+ * @param uploadId The id from {@link create}.
883
+ * @param opts `maxParts` / `partNumberMarker` pagination controls.
884
+ */
885
+ async listParts(key, uploadId, opts = {}) {
886
+ const query = { uploadId };
887
+ if (opts.maxParts !== void 0) query["max-parts"] = opts.maxParts;
888
+ if (opts.partNumberMarker !== void 0) query["part-number-marker"] = opts.partNumberMarker;
889
+ const resp = await this.s3Request("GET", key, { query });
890
+ if (!resp.ok) {
891
+ throw parseS3Error("ListParts", resp.status, resp.statusText, decodeText(resp.body));
892
+ }
893
+ return parseListPartsResult(decodeText(resp.body));
894
+ }
895
+ /**
896
+ * List in-progress multipart uploads on the disk (ListMultipartUploads).
897
+ * Returns a single page; follow `nextKeyMarker` / `nextUploadIdMarker` (when
898
+ * `isTruncated`) for the rest.
899
+ *
900
+ * @param opts Prefix/delimiter filter and pagination markers.
901
+ */
902
+ async listUploads(opts = {}) {
903
+ const query = { uploads: "" };
904
+ if (opts.prefix !== void 0) query.prefix = opts.prefix;
905
+ if (opts.delimiter !== void 0) query.delimiter = opts.delimiter;
906
+ if (opts.keyMarker !== void 0) query["key-marker"] = opts.keyMarker;
907
+ if (opts.uploadIdMarker !== void 0) query["upload-id-marker"] = opts.uploadIdMarker;
908
+ if (opts.maxUploads !== void 0) query["max-uploads"] = opts.maxUploads;
909
+ const resp = await this.s3Request("GET", "", { query });
910
+ if (!resp.ok) {
911
+ throw parseS3Error("ListMultipartUploads", resp.status, resp.statusText, decodeText(resp.body));
912
+ }
913
+ return parseListMultipartUploadsResult(decodeText(resp.body));
914
+ }
915
+ };
916
+ var MAX_DELETE_OBJECTS_PER_REQUEST = 1e3;
917
+ var MAX_S3_RETRIES = 3;
918
+ var S3_RETRY_BASE_MS = 100;
919
+ var S3_RETRY_CAP_MS = 2e3;
920
+ function isTransientS3Status(status) {
921
+ return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
922
+ }
923
+ function s3RetryDelayMs(attempt) {
924
+ const ceiling = Math.min(S3_RETRY_CAP_MS, S3_RETRY_BASE_MS * 2 ** attempt);
925
+ return Math.random() * ceiling;
926
+ }
927
+ function sleep(ms) {
928
+ return new Promise((resolve) => setTimeout(resolve, ms));
929
+ }
930
+ var MIN_PART_SIZE = 5 * 1024 * 1024;
931
+ var DEFAULT_PART_SIZE = 16 * 1024 * 1024;
932
+ var DEFAULT_UPLOAD_CONCURRENCY = 4;
933
+ var MAX_PARTS_PER_UPLOAD = 1e4;
934
+ function effectiveUploadPartSize(totalBytes, requestedPartSize) {
935
+ if (Math.ceil(totalBytes / requestedPartSize) <= MAX_PARTS_PER_UPLOAD) {
936
+ return requestedPartSize;
937
+ }
938
+ const MiB = 1024 * 1024;
939
+ const needed = Math.ceil(totalBytes / MAX_PARTS_PER_UPLOAD);
940
+ return Math.ceil(needed / MiB) * MiB;
941
+ }
942
+ function toBytes(body) {
943
+ if (typeof body === "string") return new TextEncoder().encode(body);
944
+ if (body instanceof Uint8Array) return body;
945
+ return new Uint8Array(body);
946
+ }
947
+ function escapeXml(value) {
948
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
949
+ }
950
+ function buildDeleteObjectsXml(keys, quiet) {
951
+ const objects = keys.map((k) => `<Object><Key>${escapeXml(k)}</Key></Object>`).join("");
952
+ const quietTag = quiet ? "<Quiet>true</Quiet>" : "";
953
+ return `<?xml version="1.0" encoding="UTF-8"?><Delete>${objects}${quietTag}</Delete>`;
954
+ }
955
+ function buildCompleteMultipartUploadXml(parts) {
956
+ const body = parts.map(
957
+ (p) => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>${escapeXml(p.etag)}</ETag></Part>`
958
+ ).join("");
959
+ return `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload>${body}</CompleteMultipartUpload>`;
960
+ }
961
+ function asArray(value) {
962
+ if (value === void 0 || value === null) return [];
963
+ return Array.isArray(value) ? value : [value];
964
+ }
965
+ function optionalNumber(value) {
966
+ return value === void 0 || value === null ? void 0 : Number(value);
967
+ }
618
968
  function optionalString(value) {
619
969
  return value === void 0 || value === null ? void 0 : String(value);
620
970
  }
@@ -643,6 +993,60 @@ function parseListObjectsResult(xml) {
643
993
  prefix: optionalString(root.Prefix)
644
994
  };
645
995
  }
996
+ function xmlIsTruncated(value) {
997
+ return value === "true" || value === true;
998
+ }
999
+ function parseDeleteObjectsResult(xml) {
1000
+ const root = parseXml(xml).DeleteResult ?? {};
1001
+ const deleted = asArray(root.Deleted).map((d) => optionalString(d.Key)).filter((k) => k !== void 0);
1002
+ const errors = asArray(root.Error).map((e) => ({
1003
+ key: String(e.Key ?? ""),
1004
+ code: optionalString(e.Code),
1005
+ message: optionalString(e.Message)
1006
+ }));
1007
+ return { deleted, errors };
1008
+ }
1009
+ function parseListPartsResult(xml) {
1010
+ const root = parseXml(xml).ListPartsResult ?? {};
1011
+ const parts = asArray(root.Part).map((p) => ({
1012
+ partNumber: Number(p.PartNumber ?? 0),
1013
+ etag: optionalString(p.ETag),
1014
+ size: Number(p.Size ?? 0),
1015
+ lastModified: optionalDate(p.LastModified)
1016
+ }));
1017
+ return {
1018
+ bucket: optionalString(root.Bucket),
1019
+ key: optionalString(root.Key),
1020
+ uploadId: optionalString(root.UploadId),
1021
+ parts,
1022
+ isTruncated: xmlIsTruncated(root.IsTruncated),
1023
+ partNumberMarker: Number(root.PartNumberMarker ?? 0),
1024
+ nextPartNumberMarker: optionalNumber(root.NextPartNumberMarker),
1025
+ maxParts: Number(root.MaxParts ?? parts.length)
1026
+ };
1027
+ }
1028
+ function parseListMultipartUploadsResult(xml) {
1029
+ const root = parseXml(xml).ListMultipartUploadsResult ?? {};
1030
+ const uploads = asArray(root.Upload).map((u) => ({
1031
+ key: String(u.Key ?? ""),
1032
+ uploadId: String(u.UploadId ?? ""),
1033
+ initiated: optionalDate(u.Initiated)
1034
+ }));
1035
+ const commonPrefixes = asArray(root.CommonPrefixes).map((cp) => optionalString(cp.Prefix)).filter((p) => p !== void 0);
1036
+ return {
1037
+ bucket: optionalString(root.Bucket),
1038
+ uploads,
1039
+ commonPrefixes,
1040
+ isTruncated: xmlIsTruncated(root.IsTruncated),
1041
+ keyMarker: optionalString(root.KeyMarker),
1042
+ uploadIdMarker: optionalString(root.UploadIdMarker),
1043
+ nextKeyMarker: optionalString(root.NextKeyMarker),
1044
+ nextUploadIdMarker: optionalString(root.NextUploadIdMarker),
1045
+ prefix: optionalString(root.Prefix),
1046
+ delimiter: optionalString(root.Delimiter),
1047
+ maxUploads: optionalNumber(root.MaxUploads)
1048
+ };
1049
+ }
646
1050
 
647
1051
  // src/disks.ts
648
1052
  var Disks = class {
@@ -836,6 +1240,7 @@ function exec(opts) {
836
1240
  ArchilError,
837
1241
  ArchilS3Error,
838
1242
  Disk,
1243
+ DiskMultipart,
839
1244
  Disks,
840
1245
  Tokens,
841
1246
  USER_AGENT,
@@ -844,6 +1249,7 @@ function exec(opts) {
844
1249
  createApiKey,
845
1250
  createDisk,
846
1251
  deleteApiKey,
1252
+ effectiveUploadPartSize,
847
1253
  exec,
848
1254
  getDisk,
849
1255
  listApiKeys,