@cloudbitmaps/gcs 0.21.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/README.md +54 -10
- package/dist/backend.d.ts +44 -14
- package/dist/index.js +623 -150
- package/dist/index.js.map +4 -4
- package/package.json +4 -3
- package/dist/bucket-presence.d.ts +0 -37
- package/dist/crc32c.d.ts +0 -5
- package/dist/defined.d.ts +0 -8
- package/dist/download-retry.d.ts +0 -8
- package/dist/gcs-errors.d.ts +0 -60
- package/dist/read-client.d.ts +0 -6
- package/dist/read-once.d.ts +0 -52
- package/dist/read-timeout.d.ts +0 -28
- package/dist/registry.d.ts +0 -90
- package/dist/scrub-error.d.ts +0 -18
- package/dist/send-once.d.ts +0 -5
- package/dist/storage.d.ts +0 -109
package/dist/index.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ValidationError as ValidationError5,
|
|
4
4
|
brandAsBackend,
|
|
5
|
-
|
|
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,18 +14,538 @@ function defined(values) {
|
|
|
11
14
|
return Object.fromEntries(Object.entries(values).filter(([, v]) => v !== void 0));
|
|
12
15
|
}
|
|
13
16
|
|
|
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;
|
|
29
|
+
}
|
|
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;
|
|
36
|
+
}
|
|
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";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var Deadline = class {
|
|
45
|
+
constructor(ms, read) {
|
|
46
|
+
this.ms = ms;
|
|
47
|
+
this.read = read;
|
|
48
|
+
this.endsAt = performance.now() + ms;
|
|
49
|
+
}
|
|
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);
|
|
64
|
+
}
|
|
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));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/scrub-error.ts
|
|
77
|
+
import { types } from "node:util";
|
|
78
|
+
var TRANSPORT = /* @__PURE__ */ new Set([
|
|
79
|
+
"request",
|
|
80
|
+
"agent",
|
|
81
|
+
"socket",
|
|
82
|
+
"_httpMessage",
|
|
83
|
+
"connection",
|
|
84
|
+
"req",
|
|
85
|
+
"res",
|
|
86
|
+
"config"
|
|
87
|
+
]);
|
|
88
|
+
var RESPONSE_FIELDS = ["status", "statusText", "statusCode", "statusMessage"];
|
|
89
|
+
var CREDENTIAL_NAMES = [
|
|
90
|
+
"authorization",
|
|
91
|
+
"proxy-authorization",
|
|
92
|
+
"cookie",
|
|
93
|
+
"set-cookie",
|
|
94
|
+
"x-api-key",
|
|
95
|
+
"x-goog-api-key",
|
|
96
|
+
"x-goog-encryption-key",
|
|
97
|
+
"x-goog-copy-source-encryption-key",
|
|
98
|
+
"encryption-key",
|
|
99
|
+
"encryption-key-base64",
|
|
100
|
+
"x-goog-signature",
|
|
101
|
+
"x-goog-credential",
|
|
102
|
+
"signature",
|
|
103
|
+
"signature-provided",
|
|
104
|
+
"canonical-request",
|
|
105
|
+
"token",
|
|
106
|
+
"access-token",
|
|
107
|
+
"refresh-token",
|
|
108
|
+
"id-token",
|
|
109
|
+
"subject-token",
|
|
110
|
+
"client-secret",
|
|
111
|
+
"private-key",
|
|
112
|
+
"assertion"
|
|
113
|
+
];
|
|
114
|
+
var fold = (name) => name.trim().replace(/:$/, "").replace(/[-_]/g, "").toLowerCase();
|
|
115
|
+
var CREDENTIAL_NAME = new Set(CREDENTIAL_NAMES.map(fold));
|
|
116
|
+
var LONGEST_NAME = 64;
|
|
117
|
+
var CREDENTIAL_LINES = /^((?:proxy-)?authorization|cookie|set-cookie|x-api-key|x-goog-api-key|x-goog-(?:copy-source-)?encryption-key):[^\r\n]*/gim;
|
|
118
|
+
var AUTHORIZATION_VALUE = /((?:proxy-)?authorization\\?["']?\s*(?::|=|%3D)\s*\\?["']?)(?:[A-Za-z][\w-]*(?:\s+|%20|\+))?[^\s&"'<>\\,;]+/gi;
|
|
119
|
+
var COOKIE_VALUE = /(?<![\w-])((?:set-)?cookie\\?["']?\s*:\s*\\?["']?)[^\r\n"'<>\\]+/gi;
|
|
120
|
+
var NAMED = "x-goog-(?:copy-source-)?encryption-key|x-goog-api-key|x-goog-signature|x-goog-credential|(?:access|refresh|id|subject)[-_]token|client[-_]secret";
|
|
121
|
+
var NAMED_SECRET = new RegExp(
|
|
122
|
+
`((?:${NAMED})\\\\?["']?\\s*(?::|=|%3D)\\s*\\\\?["']?)[^\\s&"'<>\\\\,;]+`,
|
|
123
|
+
"gi"
|
|
124
|
+
);
|
|
125
|
+
var BEARER = /\b(Bearer)(\s+|%20|\+)(?=[A-Za-z0-9\-._~+/]{16})[A-Za-z0-9\-._~+/]+=*/gi;
|
|
126
|
+
var TOKEN_TEXT = /ya29\.(?=[A-Za-z0-9_.-]{16})[A-Za-z0-9_.-]+|(?<![A-Za-z0-9_-])eyJ(?=[A-Za-z0-9_-]{8})[A-Za-z0-9_-]+\.eyJ(?=[A-Za-z0-9_-]{8})[A-Za-z0-9_-]+\.(?=[A-Za-z0-9_-]{16})[A-Za-z0-9_-]+/g;
|
|
127
|
+
var SIGNATURE_NAME = /Signature(?:=|%3D)/gi;
|
|
128
|
+
var SIGNATURE_RUN = /[A-Za-z0-9+/=_.-]*/y;
|
|
129
|
+
var SIGNATURE_MIN = 16;
|
|
130
|
+
var PEM_OPEN = /-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/y;
|
|
131
|
+
var PEM_CLOSE = /-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----/y;
|
|
132
|
+
var ELEMENT = /(<([A-Za-z][\w.:-]{0,63})>)([^<]*)(<\/\2>)/g;
|
|
133
|
+
var CELL = /(<([A-Za-z][\w-]{0,15})\b[^<>]{0,256}>\s{0,32}([^<>\s](?:[^<>]{0,62}[^<>\s])?)\s{0,32}<\/\2\s{0,32}>(?:\s{0,32}<\/[A-Za-z][\w-]{0,15}\s{0,32}>){0,4}(?:\s{0,32}<[A-Za-z][\w-]{0,15}\b[^<>]{0,256}>){1,4})([^<]*)/g;
|
|
134
|
+
var REDACTED = "[redacted]";
|
|
135
|
+
var MAX_DEPTH = 10;
|
|
136
|
+
var isPlain = (value) => {
|
|
137
|
+
const proto = Object.getPrototypeOf(value);
|
|
138
|
+
return proto === Object.prototype || proto === null;
|
|
139
|
+
};
|
|
140
|
+
var isError = (value) => value instanceof Error || types.isNativeError(value);
|
|
141
|
+
var isCredentialName = (text) => text.length <= LONGEST_NAME && CREDENTIAL_NAME.has(fold(text));
|
|
142
|
+
function redactText(text) {
|
|
143
|
+
try {
|
|
144
|
+
const named = redactPrivateKeys(text).replace(CREDENTIAL_LINES, `$1: ${REDACTED}`).replace(AUTHORIZATION_VALUE, `$1${REDACTED}`).replace(COOKIE_VALUE, `$1${REDACTED}`).replace(NAMED_SECRET, `$1${REDACTED}`).replace(BEARER, `$1$2${REDACTED}`).replace(TOKEN_TEXT, REDACTED);
|
|
145
|
+
return redactCells(
|
|
146
|
+
redactSignatures(named).replace(
|
|
147
|
+
ELEMENT,
|
|
148
|
+
(whole, open, tag, inner, close) => isCredentialName(tag) && inner.trim() !== "" ? `${open}${REDACTED}${close}` : whole
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
} catch {
|
|
152
|
+
return REDACTED;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function redactPrivateKeys(text) {
|
|
156
|
+
let out = "";
|
|
157
|
+
let from = 0;
|
|
158
|
+
for (let at = text.indexOf("-----BEGIN "); at !== -1; at = text.indexOf("-----BEGIN ", Math.max(at + 1, from))) {
|
|
159
|
+
PEM_OPEN.lastIndex = at;
|
|
160
|
+
const open = PEM_OPEN.exec(text);
|
|
161
|
+
if (open === null) continue;
|
|
162
|
+
const body = at + open[0].length;
|
|
163
|
+
const end = text.indexOf("-----END", body);
|
|
164
|
+
let close = "";
|
|
165
|
+
if (end !== -1) {
|
|
166
|
+
PEM_CLOSE.lastIndex = end;
|
|
167
|
+
close = PEM_CLOSE.exec(text)?.[0] ?? "";
|
|
168
|
+
}
|
|
169
|
+
out += `${text.slice(from, body)}${REDACTED}${close}`;
|
|
170
|
+
from = end === -1 ? text.length : end + close.length;
|
|
171
|
+
}
|
|
172
|
+
return from === 0 ? text : out + text.slice(from);
|
|
173
|
+
}
|
|
174
|
+
function redactSignatures(text) {
|
|
175
|
+
let out = "";
|
|
176
|
+
let from = 0;
|
|
177
|
+
SIGNATURE_NAME.lastIndex = 0;
|
|
178
|
+
for (let m = SIGNATURE_NAME.exec(text); m !== null; m = SIGNATURE_NAME.exec(text)) {
|
|
179
|
+
const start = m.index + m[0].length;
|
|
180
|
+
let end = start;
|
|
181
|
+
for (; ; ) {
|
|
182
|
+
SIGNATURE_RUN.lastIndex = end;
|
|
183
|
+
SIGNATURE_RUN.exec(text);
|
|
184
|
+
end = SIGNATURE_RUN.lastIndex;
|
|
185
|
+
if (text[end] !== "%" || text.startsWith("%26", end)) break;
|
|
186
|
+
end += 1;
|
|
187
|
+
}
|
|
188
|
+
if (end - start >= SIGNATURE_MIN) {
|
|
189
|
+
out += `${text.slice(from, start)}${REDACTED}`;
|
|
190
|
+
from = end;
|
|
191
|
+
}
|
|
192
|
+
SIGNATURE_NAME.lastIndex = end;
|
|
193
|
+
}
|
|
194
|
+
return from === 0 ? text : out + text.slice(from);
|
|
195
|
+
}
|
|
196
|
+
function redactCells(text) {
|
|
197
|
+
let out = "";
|
|
198
|
+
let from = 0;
|
|
199
|
+
CELL.lastIndex = 0;
|
|
200
|
+
for (let m = CELL.exec(text); m !== null; m = CELL.exec(text)) {
|
|
201
|
+
const [whole, head, , name, value] = m;
|
|
202
|
+
if (isCredentialName(name) && value.trim() !== "") {
|
|
203
|
+
out += `${text.slice(from, m.index)}${head}${REDACTED}`;
|
|
204
|
+
from = m.index + whole.length;
|
|
205
|
+
} else {
|
|
206
|
+
CELL.lastIndex = m.index + 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return from === 0 ? text : out + text.slice(from);
|
|
210
|
+
}
|
|
211
|
+
var OMIT = /* @__PURE__ */ Symbol("omit");
|
|
212
|
+
var INDEX = /^(?:0|[1-9]\d*)$/;
|
|
213
|
+
var isIndex = (key) => typeof key === "string" && INDEX.test(key) && Number(key) < 2 ** 32 - 1;
|
|
214
|
+
function hasNamedKeys(value) {
|
|
215
|
+
if (!Array.isArray(value)) return Reflect.ownKeys(value).length > 0;
|
|
216
|
+
return Reflect.ownKeys(value).some((key) => key !== "length" && !isIndex(key));
|
|
217
|
+
}
|
|
218
|
+
function define(copy, key, value, enumerable) {
|
|
219
|
+
Object.defineProperty(copy, key, { value, writable: true, configurable: true, enumerable });
|
|
220
|
+
}
|
|
221
|
+
function copyOf(value, ctx, depth) {
|
|
222
|
+
if (typeof value === "string") {
|
|
223
|
+
const clean = redactText(value);
|
|
224
|
+
if (clean !== value) ctx.changed = true;
|
|
225
|
+
return clean;
|
|
226
|
+
}
|
|
227
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
228
|
+
ctx.changed = true;
|
|
229
|
+
return OMIT;
|
|
230
|
+
}
|
|
231
|
+
if (typeof value !== "object" || value === null) return value;
|
|
232
|
+
if (ctx.copies.has(value)) return ctx.copies.get(value);
|
|
233
|
+
if (depth > MAX_DEPTH) {
|
|
234
|
+
ctx.changed = true;
|
|
235
|
+
return OMIT;
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(value)) return copyList(value, ctx, depth);
|
|
238
|
+
if (value instanceof Map) return copyMap(value, ctx, depth);
|
|
239
|
+
if (value instanceof Set) return copySet(value, ctx, depth);
|
|
240
|
+
return copyRecord(value, ctx, depth);
|
|
241
|
+
}
|
|
242
|
+
function copyList(value, ctx, depth) {
|
|
243
|
+
const list = new Array(value.length);
|
|
244
|
+
ctx.copies.set(value, list);
|
|
245
|
+
if (hasNamedKeys(value)) ctx.changed = true;
|
|
246
|
+
let hidden = false;
|
|
247
|
+
let last = -1;
|
|
248
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
249
|
+
if (!isIndex(key)) continue;
|
|
250
|
+
const index = Number(key);
|
|
251
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
252
|
+
if (descriptor === void 0 || !("value" in descriptor)) {
|
|
253
|
+
ctx.changed = true;
|
|
254
|
+
hidden = false;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const item = descriptor.value;
|
|
258
|
+
if (hidden && index === last + 1 && item != null) {
|
|
259
|
+
list[index] = REDACTED;
|
|
260
|
+
if (item !== REDACTED) ctx.changed = true;
|
|
261
|
+
} else {
|
|
262
|
+
const kept = copyOf(item, ctx, depth + 1);
|
|
263
|
+
if (kept === OMIT) delete list[index];
|
|
264
|
+
else list[index] = kept;
|
|
265
|
+
}
|
|
266
|
+
hidden = typeof item === "string" && isCredentialName(item);
|
|
267
|
+
last = index;
|
|
268
|
+
}
|
|
269
|
+
return list;
|
|
270
|
+
}
|
|
271
|
+
function copyMap(value, ctx, depth) {
|
|
272
|
+
const map = /* @__PURE__ */ new Map();
|
|
273
|
+
ctx.copies.set(value, map);
|
|
274
|
+
if (hasNamedKeys(value)) ctx.changed = true;
|
|
275
|
+
for (const [k, v] of value) {
|
|
276
|
+
const key = copyOf(k, ctx, depth + 1);
|
|
277
|
+
if (key === OMIT) continue;
|
|
278
|
+
if (typeof k === "string" && isCredentialName(k) && v != null) {
|
|
279
|
+
map.set(key, REDACTED);
|
|
280
|
+
if (v !== REDACTED) ctx.changed = true;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const kept = copyOf(v, ctx, depth + 1);
|
|
284
|
+
if (kept !== OMIT) map.set(key, kept);
|
|
285
|
+
}
|
|
286
|
+
return map;
|
|
287
|
+
}
|
|
288
|
+
function copySet(value, ctx, depth) {
|
|
289
|
+
const set = /* @__PURE__ */ new Set();
|
|
290
|
+
ctx.copies.set(value, set);
|
|
291
|
+
if (hasNamedKeys(value)) ctx.changed = true;
|
|
292
|
+
for (const v of value) {
|
|
293
|
+
const kept = copyOf(v, ctx, depth + 1);
|
|
294
|
+
if (kept !== OMIT) set.add(kept);
|
|
295
|
+
}
|
|
296
|
+
return set;
|
|
297
|
+
}
|
|
298
|
+
function copyRecord(value, ctx, depth) {
|
|
299
|
+
const errorLike = isError(value);
|
|
300
|
+
if (!errorLike && !isPlain(value)) {
|
|
301
|
+
ctx.changed = true;
|
|
302
|
+
return OMIT;
|
|
303
|
+
}
|
|
304
|
+
const copy = errorLike ? Object.setPrototypeOf(new Error(), Object.getPrototypeOf(value)) : Object.create(Object.getPrototypeOf(value));
|
|
305
|
+
ctx.copies.set(value, copy);
|
|
306
|
+
if (errorLike) delete copy.stack;
|
|
307
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
308
|
+
if (typeof key === "string" && TRANSPORT.has(key)) {
|
|
309
|
+
ctx.changed = true;
|
|
310
|
+
} else if (errorLike && key === "stack") {
|
|
311
|
+
copyStack(value, copy, ctx, depth);
|
|
312
|
+
} else {
|
|
313
|
+
copyField(value, copy, key, ctx, depth);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (errorLike) keepSlotFields(value, copy, ctx, depth);
|
|
317
|
+
redactPairs(value, copy, errorLike, ctx);
|
|
318
|
+
return copy;
|
|
319
|
+
}
|
|
320
|
+
var DESCRIBING = /* @__PURE__ */ new Set([
|
|
321
|
+
"message",
|
|
322
|
+
"domain",
|
|
323
|
+
"reason",
|
|
324
|
+
"location",
|
|
325
|
+
"locationType",
|
|
326
|
+
"extendedHelp"
|
|
327
|
+
]);
|
|
328
|
+
function redactPairs(original, copy, errorLike, ctx) {
|
|
329
|
+
const names = Reflect.ownKeys(original).filter((key) => {
|
|
330
|
+
if (typeof key !== "string" || DESCRIBING.has(key)) return false;
|
|
331
|
+
const held = Object.getOwnPropertyDescriptor(original, key)?.value;
|
|
332
|
+
return typeof held === "string" && isCredentialName(held);
|
|
333
|
+
});
|
|
334
|
+
if (names.length === 0) return;
|
|
335
|
+
const values = errorLike ? names.filter((key) => /name$/i.test(key)).map((key) => `${key.slice(0, -4)}value`.toLowerCase()) : void 0;
|
|
336
|
+
for (const key of Reflect.ownKeys(copy)) {
|
|
337
|
+
if (typeof key !== "string" || names.includes(key) || DESCRIBING.has(key)) continue;
|
|
338
|
+
if (values !== void 0 && !values.includes(key.toLowerCase())) continue;
|
|
339
|
+
const descriptor = Object.getOwnPropertyDescriptor(copy, key);
|
|
340
|
+
if (descriptor?.value == null || descriptor.value === REDACTED) continue;
|
|
341
|
+
if (typeof descriptor.value !== "string" && typeof descriptor.value !== "object") continue;
|
|
342
|
+
define(copy, key, REDACTED, descriptor.enumerable ?? false);
|
|
343
|
+
ctx.changed = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function isSummary(value) {
|
|
347
|
+
return isPlain(value) && Reflect.ownKeys(value).every((key) => {
|
|
348
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
349
|
+
const held = descriptor?.value;
|
|
350
|
+
return typeof key === "string" && RESPONSE_FIELDS.includes(key) && (typeof held === "number" || typeof held === "string");
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function copyResponse(response, ctx, depth) {
|
|
354
|
+
if (isSummary(response)) return copyOf(response, ctx, depth);
|
|
355
|
+
ctx.changed = true;
|
|
356
|
+
const summary = {};
|
|
357
|
+
for (const key of RESPONSE_FIELDS) {
|
|
358
|
+
let held;
|
|
359
|
+
try {
|
|
360
|
+
held = response[key];
|
|
361
|
+
} catch {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (typeof held === "number" || typeof held === "string") {
|
|
365
|
+
summary[key] = copyOf(held, ctx, depth + 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return summary;
|
|
369
|
+
}
|
|
370
|
+
function copyStack(original, copy, ctx, depth) {
|
|
371
|
+
let stack;
|
|
372
|
+
try {
|
|
373
|
+
stack = original.stack;
|
|
374
|
+
} catch {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const kept = copyOf(stack, ctx, depth + 1);
|
|
378
|
+
if (kept !== OMIT) define(copy, "stack", kept, false);
|
|
379
|
+
}
|
|
380
|
+
function copyField(original, copy, key, ctx, depth) {
|
|
381
|
+
const descriptor = Object.getOwnPropertyDescriptor(original, key);
|
|
382
|
+
if (descriptor === void 0) return;
|
|
383
|
+
if (!("value" in descriptor)) {
|
|
384
|
+
ctx.changed = true;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const name = typeof key === "string" ? redactText(key) : key;
|
|
388
|
+
if (name !== key) ctx.changed = true;
|
|
389
|
+
const held = descriptor.value;
|
|
390
|
+
let kept;
|
|
391
|
+
if (typeof key === "string" && isCredentialName(key) && held != null) {
|
|
392
|
+
kept = REDACTED;
|
|
393
|
+
if (held !== REDACTED) ctx.changed = true;
|
|
394
|
+
} else if (key === "response" && typeof held === "object" && held !== null) {
|
|
395
|
+
kept = copyResponse(held, ctx, depth + 1);
|
|
396
|
+
} else {
|
|
397
|
+
kept = copyOf(held, ctx, depth + 1);
|
|
398
|
+
if (kept === OMIT) return;
|
|
399
|
+
}
|
|
400
|
+
Object.defineProperty(copy, name, {
|
|
401
|
+
value: kept,
|
|
402
|
+
writable: descriptor.writable ?? true,
|
|
403
|
+
configurable: true,
|
|
404
|
+
enumerable: descriptor.enumerable ?? false
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function keepSlotFields(original, copy, ctx, depth) {
|
|
408
|
+
for (const key of ["name", "message", "code"]) {
|
|
409
|
+
if (Object.prototype.hasOwnProperty.call(original, key)) continue;
|
|
410
|
+
let held;
|
|
411
|
+
try {
|
|
412
|
+
held = original[key];
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
let read = OMIT;
|
|
417
|
+
try {
|
|
418
|
+
read = copy[key];
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
if (read === held) continue;
|
|
422
|
+
const kept = copyOf(held, ctx, depth + 1);
|
|
423
|
+
if (kept !== OMIT) define(copy, key, kept, false);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function scrubCredentials(err) {
|
|
427
|
+
if (typeof err !== "object" || err === null) return err;
|
|
428
|
+
const ctx = { copies: /* @__PURE__ */ new WeakMap(), changed: false };
|
|
429
|
+
const copy = copyOf(err, ctx, 0);
|
|
430
|
+
return ctx.changed && copy !== OMIT ? copy : err;
|
|
431
|
+
}
|
|
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
|
+
|
|
14
538
|
// src/storage.ts
|
|
15
539
|
import {
|
|
540
|
+
errorKind,
|
|
16
541
|
NotFoundError,
|
|
17
542
|
TransientError as TransientError2,
|
|
18
543
|
ValidationError as ValidationError3,
|
|
19
544
|
WriteConflictError,
|
|
20
545
|
concatBytes,
|
|
21
|
-
isNotFoundError,
|
|
22
|
-
isValidationError,
|
|
23
|
-
isWriteConflictError,
|
|
24
546
|
normalizeObjectPrefix,
|
|
25
547
|
parseGenerationFromKey,
|
|
548
|
+
checkDeleteOptions,
|
|
26
549
|
requireBool,
|
|
27
550
|
requirePositiveInt,
|
|
28
551
|
segmentObjectPrefix,
|
|
@@ -37,7 +560,7 @@ import { TransientError } from "@cloudbitmaps/core/driver-kit";
|
|
|
37
560
|
|
|
38
561
|
// src/gcs-errors.ts
|
|
39
562
|
import {
|
|
40
|
-
ValidationError,
|
|
563
|
+
ValidationError as ValidationError2,
|
|
41
564
|
isTransportFault as isNodeTransportFault
|
|
42
565
|
} from "@cloudbitmaps/core/driver-kit";
|
|
43
566
|
function httpStatus(err) {
|
|
@@ -69,7 +592,7 @@ function isMissingBucket(err) {
|
|
|
69
592
|
}
|
|
70
593
|
function missingBucketError(bucket, cause) {
|
|
71
594
|
return Object.assign(
|
|
72
|
-
new
|
|
595
|
+
new ValidationError2(
|
|
73
596
|
`the GCS bucket does not exist: ${bucket}; check the bucket name and the project the client is built for`
|
|
74
597
|
),
|
|
75
598
|
{ cause }
|
|
@@ -120,86 +643,6 @@ function transientFaultMessage(err) {
|
|
|
120
643
|
return `transient GCS fault: ${String(e?.code ?? e?.name ?? "unknown")}`;
|
|
121
644
|
}
|
|
122
645
|
|
|
123
|
-
// src/scrub-error.ts
|
|
124
|
-
var DROPPED = /* @__PURE__ */ new Set([
|
|
125
|
-
"request",
|
|
126
|
-
"agent",
|
|
127
|
-
"socket",
|
|
128
|
-
"_httpMessage",
|
|
129
|
-
"connection",
|
|
130
|
-
"req",
|
|
131
|
-
"res",
|
|
132
|
-
"config"
|
|
133
|
-
]);
|
|
134
|
-
var CREDENTIAL_NAME = /^(?:proxy-)?authorization$|^cookie$|^set-cookie$|^x-api-key$/i;
|
|
135
|
-
var CREDENTIAL_LINES = /^((?:proxy-)?authorization|cookie|set-cookie|x-api-key):[^\r\n]*/gim;
|
|
136
|
-
var REDACTED = "[redacted]";
|
|
137
|
-
var isPlain = (value) => {
|
|
138
|
-
const proto = Object.getPrototypeOf(value);
|
|
139
|
-
return proto === Object.prototype || proto === null;
|
|
140
|
-
};
|
|
141
|
-
function responseSummary(response) {
|
|
142
|
-
const out = {};
|
|
143
|
-
for (const key of ["status", "statusText"]) {
|
|
144
|
-
const value = response[key];
|
|
145
|
-
if (typeof value === "number" || typeof value === "string") out[key] = value;
|
|
146
|
-
}
|
|
147
|
-
return out;
|
|
148
|
-
}
|
|
149
|
-
function clean(value, copies) {
|
|
150
|
-
if (typeof value === "string") return value.replace(CREDENTIAL_LINES, `$1: ${REDACTED}`);
|
|
151
|
-
if (typeof value !== "object" || value === null) {
|
|
152
|
-
return typeof value === "function" || typeof value === "symbol" ? void 0 : value;
|
|
153
|
-
}
|
|
154
|
-
if (copies.has(value)) return copies.get(value);
|
|
155
|
-
if (Array.isArray(value)) {
|
|
156
|
-
const list = [];
|
|
157
|
-
copies.set(value, list);
|
|
158
|
-
for (const item of value) list.push(clean(item, copies));
|
|
159
|
-
return list;
|
|
160
|
-
}
|
|
161
|
-
const isError = value instanceof Error;
|
|
162
|
-
if (!isError && !isPlain(value)) return void 0;
|
|
163
|
-
const copy = Object.create(Object.getPrototypeOf(value));
|
|
164
|
-
copies.set(value, copy);
|
|
165
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
166
|
-
if (typeof key === "string" && DROPPED.has(key)) continue;
|
|
167
|
-
let held;
|
|
168
|
-
try {
|
|
169
|
-
held = value[key];
|
|
170
|
-
} catch {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
174
|
-
let kept;
|
|
175
|
-
if (typeof key === "string" && CREDENTIAL_NAME.test(key)) kept = held == null ? held : REDACTED;
|
|
176
|
-
else if (key === "response" && typeof held === "object" && held !== null) {
|
|
177
|
-
kept = responseSummary(held);
|
|
178
|
-
} else kept = clean(held, copies);
|
|
179
|
-
if (kept === void 0 && held !== void 0) continue;
|
|
180
|
-
Object.defineProperty(copy, key, {
|
|
181
|
-
value: kept,
|
|
182
|
-
writable: true,
|
|
183
|
-
configurable: true,
|
|
184
|
-
enumerable: descriptor?.enumerable ?? true
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
return copy;
|
|
188
|
-
}
|
|
189
|
-
function holdsTransport(err, seen) {
|
|
190
|
-
if (seen.has(err)) return false;
|
|
191
|
-
seen.add(err);
|
|
192
|
-
if (Object.getOwnPropertyNames(err).some((key) => DROPPED.has(key) || key === "response")) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
const cause = err.cause;
|
|
196
|
-
return typeof cause === "object" && cause !== null && holdsTransport(cause, seen);
|
|
197
|
-
}
|
|
198
|
-
function scrubCredentials(err) {
|
|
199
|
-
if (typeof err !== "object" || err === null) return err;
|
|
200
|
-
return holdsTransport(err, /* @__PURE__ */ new Set()) ? clean(err, /* @__PURE__ */ new WeakMap()) : err;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
646
|
// src/bucket-presence.ts
|
|
204
647
|
var BucketPresence = class {
|
|
205
648
|
constructor(storage, bucket) {
|
|
@@ -245,65 +688,6 @@ var BucketPresence = class {
|
|
|
245
688
|
}
|
|
246
689
|
};
|
|
247
690
|
|
|
248
|
-
// src/read-timeout.ts
|
|
249
|
-
import { ValidationError as ValidationError2 } from "@cloudbitmaps/core/driver-kit";
|
|
250
|
-
var DEFAULT_READ_TIMEOUT_MS = 0;
|
|
251
|
-
var MAX_TIMER_MS = 2147483647;
|
|
252
|
-
function resolveReadTimeoutMs(value) {
|
|
253
|
-
if (value === void 0) return DEFAULT_READ_TIMEOUT_MS;
|
|
254
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > MAX_TIMER_MS) {
|
|
255
|
-
throw new ValidationError2(
|
|
256
|
-
`readTimeoutMs must be a non-negative safe integer no larger than ${MAX_TIMER_MS}; got ${describe(value)}`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
return value;
|
|
260
|
-
}
|
|
261
|
-
function describe(value) {
|
|
262
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
263
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
264
|
-
return String(value);
|
|
265
|
-
}
|
|
266
|
-
return value === null ? "null" : typeof value;
|
|
267
|
-
}
|
|
268
|
-
var ReadTimedOut = class extends Error {
|
|
269
|
-
code = "ETIMEDOUT";
|
|
270
|
-
constructor(read, ms, cause) {
|
|
271
|
-
super(`GCS ${read} timed out after ${ms} ms`, cause === void 0 ? void 0 : { cause });
|
|
272
|
-
this.name = "ReadTimedOut";
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
var Deadline = class {
|
|
276
|
-
constructor(ms, read) {
|
|
277
|
-
this.ms = ms;
|
|
278
|
-
this.read = read;
|
|
279
|
-
this.endsAt = performance.now() + ms;
|
|
280
|
-
}
|
|
281
|
-
ms;
|
|
282
|
-
read;
|
|
283
|
-
endsAt;
|
|
284
|
-
/** What is left of it, in milliseconds: 0 once it has passed. */
|
|
285
|
-
remaining() {
|
|
286
|
-
return Math.max(0, this.endsAt - performance.now());
|
|
287
|
-
}
|
|
288
|
-
/** The error a read fails with once this has passed; `cause` is the last attempt's own error, if one failed. */
|
|
289
|
-
expired(cause) {
|
|
290
|
-
return new ReadTimedOut(this.read, this.ms, cause);
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
function startDeadline(ms, read) {
|
|
294
|
-
return ms === 0 ? void 0 : new Deadline(ms, read);
|
|
295
|
-
}
|
|
296
|
-
function withDeadline(send, deadline) {
|
|
297
|
-
if (deadline === void 0) return send();
|
|
298
|
-
if (deadline.remaining() === 0) return Promise.reject(deadline.expired());
|
|
299
|
-
const request = send();
|
|
300
|
-
let timer;
|
|
301
|
-
const timedOut = new Promise((_, reject) => {
|
|
302
|
-
timer = setTimeout(() => reject(deadline.expired()), deadline.remaining());
|
|
303
|
-
});
|
|
304
|
-
return Promise.race([request, timedOut]).finally(() => clearTimeout(timer));
|
|
305
|
-
}
|
|
306
|
-
|
|
307
691
|
// src/download-retry.ts
|
|
308
692
|
var RETRIES = 3;
|
|
309
693
|
var BASE_DELAY_MS = 100;
|
|
@@ -415,6 +799,10 @@ var REAL_TIME = {
|
|
|
415
799
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
416
800
|
};
|
|
417
801
|
var REAL_RNG = { next: () => Math.random() };
|
|
802
|
+
function isTypedPassThrough(err) {
|
|
803
|
+
const kind = errorKind(err);
|
|
804
|
+
return kind === "ValidationError" || kind === "WriteConflictError" || kind === "NotFoundError";
|
|
805
|
+
}
|
|
418
806
|
var DEFAULT_UPLOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
419
807
|
var GcsStorageDriver = class {
|
|
420
808
|
storage;
|
|
@@ -486,7 +874,7 @@ var GcsStorageDriver = class {
|
|
|
486
874
|
`generation already exists (write-once): ${key.segment}.${key.generation}`
|
|
487
875
|
);
|
|
488
876
|
}
|
|
489
|
-
if (
|
|
877
|
+
if (isTypedPassThrough(err)) throw err;
|
|
490
878
|
if (isNotFound(err)) await this.presence.confirm();
|
|
491
879
|
throw this.mapError(err);
|
|
492
880
|
}
|
|
@@ -653,7 +1041,7 @@ var GcsStorageDriver = class {
|
|
|
653
1041
|
}
|
|
654
1042
|
async delete(key, options) {
|
|
655
1043
|
const objectName = storageObjectKey(this.prefix, key);
|
|
656
|
-
const ifVersion = options
|
|
1044
|
+
const ifVersion = checkDeleteOptions(options);
|
|
657
1045
|
const conditional = ifVersion !== void 0 && this.conditionalDelete;
|
|
658
1046
|
try {
|
|
659
1047
|
await this.file(objectName).delete(
|
|
@@ -707,7 +1095,7 @@ var GcsStorageDriver = class {
|
|
|
707
1095
|
/** Map GCS read errors to the driver vocabulary; pass everything else through {@link mapError}. */
|
|
708
1096
|
mapReadError(err, key) {
|
|
709
1097
|
err = scrubCredentials(err);
|
|
710
|
-
if (
|
|
1098
|
+
if (errorKind(err) === "ValidationError") return err;
|
|
711
1099
|
if (isNotFound(err))
|
|
712
1100
|
return new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
|
|
713
1101
|
if (isInvalidRange(err)) {
|
|
@@ -761,6 +1149,10 @@ var GcsUploadSink = class _GcsUploadSink {
|
|
|
761
1149
|
bufferedLen = 0;
|
|
762
1150
|
stream;
|
|
763
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;
|
|
764
1156
|
done;
|
|
765
1157
|
failure;
|
|
766
1158
|
static WRITE_OPTS = {
|
|
@@ -801,6 +1193,7 @@ var GcsUploadSink = class _GcsUploadSink {
|
|
|
801
1193
|
}
|
|
802
1194
|
/** Cross into resumable streaming: open the stream, flush the buffered bytes, keep only ~one threshold resident. */
|
|
803
1195
|
startResumable() {
|
|
1196
|
+
this.holdUpload();
|
|
804
1197
|
const stream = this.file.createWriteStream({ resumable: true, ...this.tagged() });
|
|
805
1198
|
this.stream = stream;
|
|
806
1199
|
this.done = once2(stream, "finish");
|
|
@@ -866,8 +1259,56 @@ var GcsUploadSink = class _GcsUploadSink {
|
|
|
866
1259
|
throw err;
|
|
867
1260
|
}
|
|
868
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
|
+
*/
|
|
869
1307
|
async abort() {
|
|
870
|
-
|
|
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);
|
|
871
1312
|
}
|
|
872
1313
|
};
|
|
873
1314
|
|
|
@@ -1173,6 +1614,17 @@ var GcsStorage = class {
|
|
|
1173
1614
|
registry;
|
|
1174
1615
|
/** The client both halves share — built here unless one was supplied. */
|
|
1175
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
|
+
*/
|
|
1176
1628
|
constructor(options) {
|
|
1177
1629
|
let readClient;
|
|
1178
1630
|
refuseUnknownOptions(
|
|
@@ -1181,7 +1633,7 @@ var GcsStorage = class {
|
|
|
1181
1633
|
GCS_STORAGE_OPTION_KEYS,
|
|
1182
1634
|
"a @google-cloud/storage client goes in `client`"
|
|
1183
1635
|
);
|
|
1184
|
-
if (typeof options.bucket !== "string" || options.bucket.length === 0) {
|
|
1636
|
+
if (typeof options.bucket !== "string" || options.bucket.trim().length === 0) {
|
|
1185
1637
|
throw new ValidationError5("GcsStorage needs a `bucket`: the name of one, a non-empty string");
|
|
1186
1638
|
}
|
|
1187
1639
|
if (options.now !== void 0 && typeof options.now !== "function") {
|
|
@@ -1199,11 +1651,21 @@ var GcsStorage = class {
|
|
|
1199
1651
|
this.client = options.client;
|
|
1200
1652
|
readClient = downloadClient(options.client, true);
|
|
1201
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);
|
|
1202
1661
|
this.client = new GcsClient(
|
|
1203
1662
|
defined({ projectId: options.projectId, apiEndpoint: options.apiEndpoint })
|
|
1204
1663
|
);
|
|
1205
1664
|
readClient = downloadClient(this.client, false);
|
|
1206
1665
|
}
|
|
1666
|
+
this.bucket = options.bucket;
|
|
1667
|
+
this.keyPrefix = prefixPart(normalizeObjectPrefix3(options.prefix));
|
|
1668
|
+
this.readTimeoutMs = resolveReadTimeoutMs(options.readTimeoutMs);
|
|
1207
1669
|
const shared = {
|
|
1208
1670
|
storage: this.client,
|
|
1209
1671
|
readStorage: readClient,
|
|
@@ -1224,6 +1686,17 @@ var GcsStorage = class {
|
|
|
1224
1686
|
});
|
|
1225
1687
|
brandAsBackend(this);
|
|
1226
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
|
+
}
|
|
1227
1700
|
};
|
|
1228
1701
|
export {
|
|
1229
1702
|
GcsStorage
|