@cloudbitmaps/gcs 0.22.0 → 0.23.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/dist/index.js CHANGED
@@ -2,7 +2,10 @@
2
2
  import {
3
3
  ValidationError as ValidationError5,
4
4
  brandAsBackend,
5
- refuseUnknownOptions
5
+ normalizeObjectPrefix as normalizeObjectPrefix3,
6
+ prefixPart,
7
+ refuseUnknownOptions,
8
+ requireNonEmptyString
6
9
  } from "@cloudbitmaps/core/driver-kit";
7
10
  import { Storage as GcsClient } from "@google-cloud/storage";
8
11
 
@@ -11,111 +14,63 @@ function defined(values) {
11
14
  return Object.fromEntries(Object.entries(values).filter(([, v]) => v !== void 0));
12
15
  }
13
16
 
14
- // src/storage.ts
15
- import {
16
- errorKind,
17
- NotFoundError,
18
- TransientError as TransientError2,
19
- ValidationError as ValidationError3,
20
- WriteConflictError,
21
- concatBytes,
22
- normalizeObjectPrefix,
23
- parseGenerationFromKey,
24
- requireBool,
25
- requirePositiveInt,
26
- segmentObjectPrefix,
27
- storageObjectKey,
28
- toTransient
29
- } from "@cloudbitmaps/core/driver-kit";
30
- import { createHash, randomBytes } from "node:crypto";
31
- import { once as once2 } from "node:events";
32
-
33
- // src/bucket-presence.ts
34
- import { TransientError } from "@cloudbitmaps/core/driver-kit";
35
-
36
- // src/gcs-errors.ts
37
- import {
38
- ValidationError,
39
- isTransportFault as isNodeTransportFault
40
- } from "@cloudbitmaps/core/driver-kit";
41
- function httpStatus(err) {
42
- const e = err;
43
- if (typeof e?.code === "number") return e.code;
44
- if (typeof e?.response?.status === "number") return e.response.status;
45
- return void 0;
46
- }
47
- function networkCode(err) {
48
- const code = err?.code;
49
- return typeof code === "string" ? code : void 0;
50
- }
51
- function isPreconditionFailed(err) {
52
- return httpStatus(err) === 412;
53
- }
54
- function isThrottle(err) {
55
- const status = httpStatus(err);
56
- return status === 429 || status === 503;
57
- }
58
- function isNotFound(err) {
59
- return httpStatus(err) === 404 && !isMissingBucket(err);
60
- }
61
- function isMissingBucket(err) {
62
- if (httpStatus(err) !== 404) return false;
63
- const e = err;
64
- const says = (m) => typeof m === "string" && /bucket does not exist/i.test(m);
65
- if (says(e.message)) return true;
66
- return Array.isArray(e.errors) && e.errors.some((inner) => says(inner?.message));
67
- }
68
- function missingBucketError(bucket, cause) {
69
- return Object.assign(
70
- new ValidationError(
71
- `the GCS bucket does not exist: ${bucket}; check the bucket name and the project the client is built for`
72
- ),
73
- { cause }
74
- );
75
- }
76
- function asMissingBucket(err, bucket) {
77
- return isMissingBucket(err) ? missingBucketError(bucket, err) : err;
78
- }
79
- function isInvalidRange(err) {
80
- return httpStatus(err) === 416;
81
- }
82
- var SDK_RETRIED_STATUSES = [408, 429, 500, 502, 503, 504];
83
- function isSdkConnectionProblem(reason) {
84
- return reason.includes("eai_again") || reason === "econnreset" || reason === "unexpected connection closure" || reason === "epipe" || reason === "socket connection timeout";
85
- }
86
- function isConnectionFault(err) {
87
- return isNodeTransportFault(err) || networkCode(err) === "ERR_STREAM_PREMATURE_CLOSE";
17
+ // src/read-timeout.ts
18
+ import { ValidationError } from "@cloudbitmaps/core/driver-kit";
19
+ var DEFAULT_READ_TIMEOUT_MS = 0;
20
+ var MAX_TIMER_MS = 2147483647;
21
+ function resolveReadTimeoutMs(value) {
22
+ if (value === void 0) return DEFAULT_READ_TIMEOUT_MS;
23
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > MAX_TIMER_MS) {
24
+ throw new ValidationError(
25
+ `readTimeoutMs must be a non-negative safe integer no larger than ${MAX_TIMER_MS}; got ${describe(value)}`
26
+ );
27
+ }
28
+ return value;
88
29
  }
89
- function isTransportFault(err) {
90
- return httpStatus(err) === void 0 && isConnectionFault(err);
30
+ function describe(value) {
31
+ if (typeof value === "string") return JSON.stringify(value);
32
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
33
+ return String(value);
34
+ }
35
+ return value === null ? "null" : typeof value;
91
36
  }
92
- function isDownloadRetryable(err) {
93
- if (err === null || typeof err !== "object") return false;
94
- if (isTransportFault(err)) return true;
95
- const e = err;
96
- if (typeof e.code === "number" && SDK_RETRIED_STATUSES.includes(e.code)) return true;
97
- if (typeof e.code === "string") {
98
- if (SDK_RETRIED_STATUSES.map(String).includes(e.code)) return true;
99
- if (isSdkConnectionProblem(e.code.toLowerCase())) return true;
37
+ var ReadTimedOut = class extends Error {
38
+ code = "ETIMEDOUT";
39
+ constructor(read, ms, cause) {
40
+ super(`GCS ${read} timed out after ${ms} ms`, cause === void 0 ? void 0 : { cause });
41
+ this.name = "ReadTimedOut";
100
42
  }
101
- if (Array.isArray(e.errors)) {
102
- for (const inner of e.errors) {
103
- const reason = inner?.reason?.toString().toLowerCase();
104
- if (reason !== void 0 && isSdkConnectionProblem(reason)) return true;
105
- }
43
+ };
44
+ var Deadline = class {
45
+ constructor(ms, read) {
46
+ this.ms = ms;
47
+ this.read = read;
48
+ this.endsAt = performance.now() + ms;
106
49
  }
107
- return false;
108
- }
109
- function isTransient(err) {
110
- if (isPreconditionFailed(err) || isNotFound(err) || isInvalidRange(err)) return false;
111
- const status = httpStatus(err);
112
- if (status === 408 || status === 429 || status !== void 0 && status >= 500 && status < 600)
113
- return true;
114
- return isConnectionFault(err);
50
+ ms;
51
+ read;
52
+ endsAt;
53
+ /** What is left of it, in milliseconds: 0 once it has passed. */
54
+ remaining() {
55
+ return Math.max(0, this.endsAt - performance.now());
56
+ }
57
+ /** The error a read fails with once this has passed; `cause` is the last attempt's own error, if one failed. */
58
+ expired(cause) {
59
+ return new ReadTimedOut(this.read, this.ms, cause);
60
+ }
61
+ };
62
+ function startDeadline(ms, read) {
63
+ return ms === 0 ? void 0 : new Deadline(ms, read);
115
64
  }
116
- function transientFaultMessage(err) {
117
- const e = err;
118
- return `transient GCS fault: ${String(e?.code ?? e?.name ?? "unknown")}`;
65
+ function withDeadline(send, deadline) {
66
+ if (deadline === void 0) return send();
67
+ if (deadline.remaining() === 0) return Promise.reject(deadline.expired());
68
+ const request = send();
69
+ let timer;
70
+ const timedOut = new Promise((_, reject) => {
71
+ timer = setTimeout(() => reject(deadline.expired()), deadline.remaining());
72
+ });
73
+ return Promise.race([request, timedOut]).finally(() => clearTimeout(timer));
119
74
  }
120
75
 
121
76
  // src/scrub-error.ts
@@ -475,6 +430,219 @@ function scrubCredentials(err) {
475
430
  return ctx.changed && copy !== OMIT ? copy : err;
476
431
  }
477
432
 
433
+ // src/lifecycle.ts
434
+ var MAX_MESSAGE = 200;
435
+ var MAX_FIELD = 64;
436
+ var MAX_CONDITION = 120;
437
+ var MAX_NAMED_RULES = 3;
438
+ var cut = (text, max) => text.length > max ? text.slice(0, max) : text;
439
+ var SAFE_ACTIONS = /* @__PURE__ */ new Set([
440
+ "SetStorageClass",
441
+ "AbortIncompleteMultipartUpload"
442
+ ]);
443
+ var NAME_SUFFIXES = [".crbm", ".reg"];
444
+ var prefixRelated = (a, b) => a.startsWith(b) || b.startsWith(a);
445
+ var suffixRelated = (a, b) => a.endsWith(b) || b.endsWith(a);
446
+ var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
447
+ var record = (value) => typeof value === "object" && value !== null ? value : {};
448
+ var NONCURRENT_ONLY = ["daysSinceNoncurrentTime", "noncurrentTimeBefore"];
449
+ var metByLiveObject = (value) => typeof value !== "number" || value < 1;
450
+ var actionOf = (rule) => {
451
+ const type = record(record(rule).action).type;
452
+ return typeof type === "string" && type !== "" ? type : void 0;
453
+ };
454
+ var isRule = (rule) => typeof rule === "object" && rule !== null;
455
+ function deletesOurs(rule, prefix) {
456
+ if (!isRule(rule)) return false;
457
+ const type = actionOf(rule);
458
+ if (type !== void 0 && SAFE_ACTIONS.has(type)) return false;
459
+ const when = record(record(rule).condition);
460
+ if (when.isLive === false) return false;
461
+ if (NONCURRENT_ONLY.some((key) => when[key] !== void 0)) return false;
462
+ if (!metByLiveObject(when.numNewerVersions)) return false;
463
+ const prefixes = strings(when.matchesPrefix);
464
+ if (prefixes.length > 0 && !prefixes.some((p) => prefixRelated(p, prefix))) return false;
465
+ const suffixes = strings(when.matchesSuffix);
466
+ return suffixes.length === 0 || suffixes.some((s) => NAME_SUFFIXES.some((ours) => suffixRelated(s, ours)));
467
+ }
468
+ function sitsInside(rule, prefix) {
469
+ const prefixes = strings(record(record(rule).condition).matchesPrefix);
470
+ return prefix !== "" && prefixes.length > 0 && !prefixes.some((p) => p !== prefix && prefix.startsWith(p));
471
+ }
472
+ function judgeMetadata(metadata, prefix) {
473
+ const rules = record(record(metadata).lifecycle).rule;
474
+ const found = (Array.isArray(rules) ? rules : []).filter(
475
+ (r) => deletesOurs(r, prefix)
476
+ );
477
+ if (found.length > 0) {
478
+ const named = found.slice(0, MAX_NAMED_RULES).map(
479
+ (r) => `${cut(actionOf(r) ?? "no action type", MAX_FIELD)} when ${cut(JSON.stringify(record(r).condition ?? {}), MAX_CONDITION)}`
480
+ ).join("; ");
481
+ const more = found.length > MAX_NAMED_RULES ? ` and ${found.length - MAX_NAMED_RULES} more` : "";
482
+ const quoted = JSON.stringify(cut(prefix, MAX_FIELD));
483
+ const narrow = `Narrow the rule with matchesPrefix to exclude ${quoted}, or restrict it to noncurrent versions.`;
484
+ const retarget = `A rule that sits inside ${quoted} cannot be narrowed to exclude it: remove it or retarget it to another prefix, or restrict it to noncurrent versions.`;
485
+ const where = prefix === "" ? "The store has no prefix, so a rule on any prefix can reach it: give the store a prefix, or scope the rule away from its paths." : [
486
+ found.some((r) => !sitsInside(r, prefix)) ? narrow : "",
487
+ found.some((r) => sitsInside(r, prefix)) ? retarget : ""
488
+ ].filter((x) => x !== "").join(" ");
489
+ return {
490
+ status: "problem",
491
+ detail: `${found.length} bucket lifecycle rule(s) with a Delete action, or one this check does not know, can match objects under ${quoted}: ${named}${more}. The store keeps current objects it must be able to read back. ${where}`
492
+ };
493
+ }
494
+ return {
495
+ status: "ok",
496
+ detail: `no bucket lifecycle rule deletes live objects under ${JSON.stringify(cut(prefix, MAX_FIELD))}`
497
+ };
498
+ }
499
+ function unchecked(err) {
500
+ if (err instanceof ReadTimedOut) {
501
+ return {
502
+ status: "unchecked",
503
+ detail: "the read of the bucket metadata got no answer in time (readTimeoutMs); run the check again"
504
+ };
505
+ }
506
+ const scrubbed = scrubCredentials(err);
507
+ const { code, message, name } = record(scrubbed);
508
+ if (code === 403) {
509
+ return {
510
+ status: "unchecked",
511
+ detail: "the identity may not read the bucket metadata: grant storage.buckets.get on the bucket to check its lifecycle rules, or confirm by hand that no rule deletes current objects under this prefix"
512
+ };
513
+ }
514
+ const shown = typeof message === "string" ? `: ${message.slice(0, MAX_MESSAGE)}` : "";
515
+ const label = typeof name === "string" ? cut(name, MAX_FIELD) : "Error";
516
+ const status = typeof code === "number" ? ` ${code}` : "";
517
+ return {
518
+ status: "unchecked",
519
+ detail: `could not read the bucket metadata (${label}${status}${shown}); check the lifecycle rules by hand`
520
+ };
521
+ }
522
+ async function checkGcsLifecycle(client, bucket, prefix, readTimeoutMs = 0) {
523
+ try {
524
+ const [metadata] = await withDeadline(
525
+ () => client.bucket(bucket).getMetadata(),
526
+ startDeadline(readTimeoutMs, "bucket metadata read")
527
+ );
528
+ return judgeMetadata(metadata, prefix);
529
+ } catch (err) {
530
+ try {
531
+ return unchecked(err);
532
+ } catch {
533
+ return { status: "unchecked", detail: "could not read the bucket metadata" };
534
+ }
535
+ }
536
+ }
537
+
538
+ // src/storage.ts
539
+ import {
540
+ errorKind,
541
+ NotFoundError,
542
+ TransientError as TransientError2,
543
+ ValidationError as ValidationError3,
544
+ WriteConflictError,
545
+ concatBytes,
546
+ normalizeObjectPrefix,
547
+ parseGenerationFromKey,
548
+ checkDeleteOptions,
549
+ requireBool,
550
+ requirePositiveInt,
551
+ segmentObjectPrefix,
552
+ storageObjectKey,
553
+ toTransient
554
+ } from "@cloudbitmaps/core/driver-kit";
555
+ import { createHash, randomBytes } from "node:crypto";
556
+ import { once as once2 } from "node:events";
557
+
558
+ // src/bucket-presence.ts
559
+ import { TransientError } from "@cloudbitmaps/core/driver-kit";
560
+
561
+ // src/gcs-errors.ts
562
+ import {
563
+ ValidationError as ValidationError2,
564
+ isTransportFault as isNodeTransportFault
565
+ } from "@cloudbitmaps/core/driver-kit";
566
+ function httpStatus(err) {
567
+ const e = err;
568
+ if (typeof e?.code === "number") return e.code;
569
+ if (typeof e?.response?.status === "number") return e.response.status;
570
+ return void 0;
571
+ }
572
+ function networkCode(err) {
573
+ const code = err?.code;
574
+ return typeof code === "string" ? code : void 0;
575
+ }
576
+ function isPreconditionFailed(err) {
577
+ return httpStatus(err) === 412;
578
+ }
579
+ function isThrottle(err) {
580
+ const status = httpStatus(err);
581
+ return status === 429 || status === 503;
582
+ }
583
+ function isNotFound(err) {
584
+ return httpStatus(err) === 404 && !isMissingBucket(err);
585
+ }
586
+ function isMissingBucket(err) {
587
+ if (httpStatus(err) !== 404) return false;
588
+ const e = err;
589
+ const says = (m) => typeof m === "string" && /bucket does not exist/i.test(m);
590
+ if (says(e.message)) return true;
591
+ return Array.isArray(e.errors) && e.errors.some((inner) => says(inner?.message));
592
+ }
593
+ function missingBucketError(bucket, cause) {
594
+ return Object.assign(
595
+ new ValidationError2(
596
+ `the GCS bucket does not exist: ${bucket}; check the bucket name and the project the client is built for`
597
+ ),
598
+ { cause }
599
+ );
600
+ }
601
+ function asMissingBucket(err, bucket) {
602
+ return isMissingBucket(err) ? missingBucketError(bucket, err) : err;
603
+ }
604
+ function isInvalidRange(err) {
605
+ return httpStatus(err) === 416;
606
+ }
607
+ var SDK_RETRIED_STATUSES = [408, 429, 500, 502, 503, 504];
608
+ function isSdkConnectionProblem(reason) {
609
+ return reason.includes("eai_again") || reason === "econnreset" || reason === "unexpected connection closure" || reason === "epipe" || reason === "socket connection timeout";
610
+ }
611
+ function isConnectionFault(err) {
612
+ return isNodeTransportFault(err) || networkCode(err) === "ERR_STREAM_PREMATURE_CLOSE";
613
+ }
614
+ function isTransportFault(err) {
615
+ return httpStatus(err) === void 0 && isConnectionFault(err);
616
+ }
617
+ function isDownloadRetryable(err) {
618
+ if (err === null || typeof err !== "object") return false;
619
+ if (isTransportFault(err)) return true;
620
+ const e = err;
621
+ if (typeof e.code === "number" && SDK_RETRIED_STATUSES.includes(e.code)) return true;
622
+ if (typeof e.code === "string") {
623
+ if (SDK_RETRIED_STATUSES.map(String).includes(e.code)) return true;
624
+ if (isSdkConnectionProblem(e.code.toLowerCase())) return true;
625
+ }
626
+ if (Array.isArray(e.errors)) {
627
+ for (const inner of e.errors) {
628
+ const reason = inner?.reason?.toString().toLowerCase();
629
+ if (reason !== void 0 && isSdkConnectionProblem(reason)) return true;
630
+ }
631
+ }
632
+ return false;
633
+ }
634
+ function isTransient(err) {
635
+ if (isPreconditionFailed(err) || isNotFound(err) || isInvalidRange(err)) return false;
636
+ const status = httpStatus(err);
637
+ if (status === 408 || status === 429 || status !== void 0 && status >= 500 && status < 600)
638
+ return true;
639
+ return isConnectionFault(err);
640
+ }
641
+ function transientFaultMessage(err) {
642
+ const e = err;
643
+ return `transient GCS fault: ${String(e?.code ?? e?.name ?? "unknown")}`;
644
+ }
645
+
478
646
  // src/bucket-presence.ts
479
647
  var BucketPresence = class {
480
648
  constructor(storage, bucket) {
@@ -520,65 +688,6 @@ var BucketPresence = class {
520
688
  }
521
689
  };
522
690
 
523
- // src/read-timeout.ts
524
- import { ValidationError as ValidationError2 } from "@cloudbitmaps/core/driver-kit";
525
- var DEFAULT_READ_TIMEOUT_MS = 0;
526
- var MAX_TIMER_MS = 2147483647;
527
- function resolveReadTimeoutMs(value) {
528
- if (value === void 0) return DEFAULT_READ_TIMEOUT_MS;
529
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > MAX_TIMER_MS) {
530
- throw new ValidationError2(
531
- `readTimeoutMs must be a non-negative safe integer no larger than ${MAX_TIMER_MS}; got ${describe(value)}`
532
- );
533
- }
534
- return value;
535
- }
536
- function describe(value) {
537
- if (typeof value === "string") return JSON.stringify(value);
538
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
539
- return String(value);
540
- }
541
- return value === null ? "null" : typeof value;
542
- }
543
- var ReadTimedOut = class extends Error {
544
- code = "ETIMEDOUT";
545
- constructor(read, ms, cause) {
546
- super(`GCS ${read} timed out after ${ms} ms`, cause === void 0 ? void 0 : { cause });
547
- this.name = "ReadTimedOut";
548
- }
549
- };
550
- var Deadline = class {
551
- constructor(ms, read) {
552
- this.ms = ms;
553
- this.read = read;
554
- this.endsAt = performance.now() + ms;
555
- }
556
- ms;
557
- read;
558
- endsAt;
559
- /** What is left of it, in milliseconds: 0 once it has passed. */
560
- remaining() {
561
- return Math.max(0, this.endsAt - performance.now());
562
- }
563
- /** The error a read fails with once this has passed; `cause` is the last attempt's own error, if one failed. */
564
- expired(cause) {
565
- return new ReadTimedOut(this.read, this.ms, cause);
566
- }
567
- };
568
- function startDeadline(ms, read) {
569
- return ms === 0 ? void 0 : new Deadline(ms, read);
570
- }
571
- function withDeadline(send, deadline) {
572
- if (deadline === void 0) return send();
573
- if (deadline.remaining() === 0) return Promise.reject(deadline.expired());
574
- const request = send();
575
- let timer;
576
- const timedOut = new Promise((_, reject) => {
577
- timer = setTimeout(() => reject(deadline.expired()), deadline.remaining());
578
- });
579
- return Promise.race([request, timedOut]).finally(() => clearTimeout(timer));
580
- }
581
-
582
691
  // src/download-retry.ts
583
692
  var RETRIES = 3;
584
693
  var BASE_DELAY_MS = 100;
@@ -932,7 +1041,7 @@ var GcsStorageDriver = class {
932
1041
  }
933
1042
  async delete(key, options) {
934
1043
  const objectName = storageObjectKey(this.prefix, key);
935
- const ifVersion = options?.ifVersion;
1044
+ const ifVersion = checkDeleteOptions(options);
936
1045
  const conditional = ifVersion !== void 0 && this.conditionalDelete;
937
1046
  try {
938
1047
  await this.file(objectName).delete(
@@ -1040,6 +1149,10 @@ var GcsUploadSink = class _GcsUploadSink {
1040
1149
  bufferedLen = 0;
1041
1150
  stream;
1042
1151
  // set once we cross the threshold → resumable mode
1152
+ /** The SDK's resumable upload under {@link stream}, which {@link abort} ends with an error: see there. */
1153
+ upload;
1154
+ /** Set by {@link abort}: the held upload starts no request from then on. */
1155
+ abandoned = false;
1043
1156
  done;
1044
1157
  failure;
1045
1158
  static WRITE_OPTS = {
@@ -1080,6 +1193,7 @@ var GcsUploadSink = class _GcsUploadSink {
1080
1193
  }
1081
1194
  /** Cross into resumable streaming: open the stream, flush the buffered bytes, keep only ~one threshold resident. */
1082
1195
  startResumable() {
1196
+ this.holdUpload();
1083
1197
  const stream = this.file.createWriteStream({ resumable: true, ...this.tagged() });
1084
1198
  this.stream = stream;
1085
1199
  this.done = once2(stream, "finish");
@@ -1145,8 +1259,56 @@ var GcsUploadSink = class _GcsUploadSink {
1145
1259
  throw err;
1146
1260
  }
1147
1261
  }
1262
+ /**
1263
+ * Keep the SDK's resumable upload, the writable its write stream pipes into, so {@link abort} can end its request.
1264
+ * The SDK hands it to the stream's duplex through `setWritable` in `startResumableUpload_`, a method of the `File`
1265
+ * its types declare; this wraps that method on this sink's own `File`, which no other write shares. Where the method
1266
+ * or the duplex's `setWritable` is missing, the SDK runs unchanged, nothing is kept, and an abort destroys the
1267
+ * stream alone, as it did before.
1268
+ */
1269
+ holdUpload() {
1270
+ const file = this.file;
1271
+ const start = file.startResumableUpload_;
1272
+ if (typeof start !== "function") return;
1273
+ file.startResumableUpload_ = (dup, options) => {
1274
+ const setWritable = dup.setWritable;
1275
+ if (typeof setWritable === "function") {
1276
+ dup.setWritable = (writable) => {
1277
+ this.hold(writable);
1278
+ setWritable.call(dup, writable);
1279
+ };
1280
+ }
1281
+ start.call(file, dup, options);
1282
+ };
1283
+ }
1284
+ /**
1285
+ * Keep `upload`, and let it start no request once this sink is abandoned. The SDK registers the listener that
1286
+ * aborts an upload's request only as that request starts, after the session is created, so an upload abandoned
1287
+ * before then would start its request anyway, with nothing left to end it. Each request that sends the upload's
1288
+ * bytes, its first and every retry, begins in `startUploading`, a public method of the SDK's `Upload`. On an
1289
+ * abandoned upload it resolves at once: the upload is destroyed already, and some of the SDK's callers do not
1290
+ * catch a rejection.
1291
+ */
1292
+ hold(upload) {
1293
+ this.upload = upload;
1294
+ const held = upload;
1295
+ const startUploading = held.startUploading;
1296
+ if (typeof startUploading !== "function") return;
1297
+ held.startUploading = () => this.abandoned ? Promise.resolve() : startUploading.call(held);
1298
+ }
1299
+ /**
1300
+ * End an upload that will not be committed. Destroying the write stream alone leaves the resumable upload's request
1301
+ * open: the SDK destroys its upload with no error, aborts the request only when the upload errors, and its request
1302
+ * body waits for bytes that never come, so the socket stays open and the process alive until the service drops the
1303
+ * connection. So the upload is destroyed with an error first, which aborts its request; the body was never ended, so
1304
+ * no object is finalized. An upload whose request has not started yet starts none: see {@link hold}. The error is
1305
+ * the sink's own and reaches no caller.
1306
+ */
1148
1307
  async abort() {
1149
- if (this.stream !== void 0 && !this.stream.destroyed) this.stream.destroy();
1308
+ this.abandoned = true;
1309
+ const abandoned = new Error("the upload was abandoned before its object was committed");
1310
+ if (this.upload !== void 0 && !this.upload.destroyed) this.upload.destroy(abandoned);
1311
+ if (this.stream !== void 0 && !this.stream.destroyed) this.stream.destroy(abandoned);
1150
1312
  }
1151
1313
  };
1152
1314
 
@@ -1452,6 +1614,17 @@ var GcsStorage = class {
1452
1614
  registry;
1453
1615
  /** The client both halves share — built here unless one was supplied. */
1454
1616
  client;
1617
+ bucket;
1618
+ /** The name prefix every object sits under: `''`, or `trimmed/`. */
1619
+ keyPrefix;
1620
+ readTimeoutMs;
1621
+ /**
1622
+ * Builds the GCS backend over one bucket, which `bucket` names. Pass `client`, or `projectId` and `apiEndpoint` to
1623
+ * have one built, not both. `prefix` puts every object under a name prefix. Throws `ValidationError` for an unknown
1624
+ * option, no bucket, a `now` that is not a function, a client setting beside `client`, a `maxObjectBytes` or
1625
+ * `simpleUploadThresholdBytes` that is not a positive integer, a `conditionalDelete` that is not a boolean, an
1626
+ * empty or non-string `apiEndpoint` or `projectId`, or an invalid `prefix` or `readTimeoutMs`.
1627
+ */
1455
1628
  constructor(options) {
1456
1629
  let readClient;
1457
1630
  refuseUnknownOptions(
@@ -1460,7 +1633,7 @@ var GcsStorage = class {
1460
1633
  GCS_STORAGE_OPTION_KEYS,
1461
1634
  "a @google-cloud/storage client goes in `client`"
1462
1635
  );
1463
- if (typeof options.bucket !== "string" || options.bucket.length === 0) {
1636
+ if (typeof options.bucket !== "string" || options.bucket.trim().length === 0) {
1464
1637
  throw new ValidationError5("GcsStorage needs a `bucket`: the name of one, a non-empty string");
1465
1638
  }
1466
1639
  if (options.now !== void 0 && typeof options.now !== "function") {
@@ -1478,11 +1651,21 @@ var GcsStorage = class {
1478
1651
  this.client = options.client;
1479
1652
  readClient = downloadClient(options.client, true);
1480
1653
  } else {
1654
+ const apiEndpoint = requireNonEmptyString("GcsStorage: `apiEndpoint`", options.apiEndpoint);
1655
+ if (apiEndpoint !== void 0 && /^[a-z][a-z0-9+.-]*:\/*$/i.test(apiEndpoint.trim())) {
1656
+ throw new ValidationError5(
1657
+ 'GcsStorage: `apiEndpoint` must name a host, such as "http://localhost:4443" or "storage.googleapis.com"'
1658
+ );
1659
+ }
1660
+ requireNonEmptyString("GcsStorage: `projectId`", options.projectId);
1481
1661
  this.client = new GcsClient(
1482
1662
  defined({ projectId: options.projectId, apiEndpoint: options.apiEndpoint })
1483
1663
  );
1484
1664
  readClient = downloadClient(this.client, false);
1485
1665
  }
1666
+ this.bucket = options.bucket;
1667
+ this.keyPrefix = prefixPart(normalizeObjectPrefix3(options.prefix));
1668
+ this.readTimeoutMs = resolveReadTimeoutMs(options.readTimeoutMs);
1486
1669
  const shared = {
1487
1670
  storage: this.client,
1488
1671
  readStorage: readClient,
@@ -1503,6 +1686,17 @@ var GcsStorage = class {
1503
1686
  });
1504
1687
  brandAsBackend(this);
1505
1688
  }
1689
+ /**
1690
+ * Reads the bucket's metadata (one request, plus the SDK's own retries, bounded by `readTimeoutMs` when it is set) and
1691
+ * reports a lifecycle rule with a `Delete` action (or one it does not know) that applies to live
1692
+ * objects whose names can be this store's: generations and registry rows alike. Needs `storage.buckets.get`, which
1693
+ * a load never uses (`store.preflight()`'s lifecycle step does) and `roles/storage.objectAdmin` does not include: without it the answer is
1694
+ * `unchecked`, naming the permission. Never rejects; a `detail` carries no credential. Rule reading: see the
1695
+ * production guide.
1696
+ */
1697
+ checkLifecycle() {
1698
+ return checkGcsLifecycle(this.client, this.bucket, this.keyPrefix, this.readTimeoutMs);
1699
+ }
1506
1700
  };
1507
1701
  export {
1508
1702
  GcsStorage