@blamejs/core 0.7.18 → 0.7.19
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/CHANGELOG.md +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* gcs-bucket-ops — bucket-level operations for Google Cloud Storage.
|
|
4
|
-
*
|
|
5
|
-
* Per-object ops (put / get / list / delete) live in
|
|
6
|
-
* `lib/object-store/gcs.js` and are bound to a single bucket at
|
|
7
|
-
* create() time. Bucket lifecycle ops are at a different level — a
|
|
8
|
-
* project-scoped client that addresses arbitrary buckets — so they
|
|
9
|
-
* get their own factory.
|
|
10
|
-
*
|
|
11
|
-
* create(name, opts?) async; POST /storage/v1/b?project={projectId}
|
|
12
|
-
* opts: location ('US' / 'EU' / region) +
|
|
13
|
-
* storageClass + iamConfiguration
|
|
14
|
-
* delete(name) async; DELETE /storage/v1/b/{name}
|
|
15
|
-
* list() async; GET /storage/v1/b?project={projectId}
|
|
16
|
-
* -> [{ name, location, storageClass,
|
|
17
|
-
* timeCreated, updated }]
|
|
18
|
-
* setLifecycle(name, rules) async; PATCH /storage/v1/b/{name}
|
|
19
|
-
* -> body { lifecycle: { rule: [...] } }
|
|
20
|
-
* setCorsRules(name, rules) async; PATCH /storage/v1/b/{name}
|
|
21
|
-
* -> body { cors: [...] }
|
|
22
|
-
*
|
|
23
|
-
* Auth: same service-account JSON / RSA-SHA256-signed JWT exchanged
|
|
24
|
-
* for an OAuth2 access token as `lib/object-store/gcs.js`.
|
|
25
|
-
*/
|
|
26
|
-
var fs = require("node:fs");
|
|
27
|
-
var gcs = require("./gcs");
|
|
28
|
-
var authHeader = require("../auth-header");
|
|
29
|
-
var httpClient = require("../http-client");
|
|
30
|
-
var safeJson = require("../safe-json");
|
|
31
|
-
var safeUrl = require("../safe-url");
|
|
32
|
-
var C = require("../constants");
|
|
33
|
-
var requestHelpers = require("../request-helpers");
|
|
34
|
-
var { ObjectStoreError } = require("../framework-error");
|
|
35
|
-
|
|
36
|
-
var _err = ObjectStoreError.factory;
|
|
37
|
-
|
|
38
|
-
// HTTP status constants used in the bucket-ops expectStatus arrays.
|
|
39
|
-
var HTTP_OK = requestHelpers.HTTP_STATUS.OK;
|
|
40
|
-
var HTTP_NO_CONTENT = requestHelpers.HTTP_STATUS.NO_CONTENT;
|
|
41
|
-
var HTTP_NOT_FOUND = requestHelpers.HTTP_STATUS.NOT_FOUND;
|
|
42
|
-
var HTTP_CONFLICT = requestHelpers.HTTP_STATUS.CONFLICT;
|
|
43
|
-
|
|
44
|
-
// Internal URL builder — endpoint + path string from validated config.
|
|
45
|
-
// Routes through safeUrl.parse so the protocol allowlist + length cap
|
|
46
|
-
// apply uniformly.
|
|
47
|
-
function _internalUrl(input, allowedProtocols) {
|
|
48
|
-
return safeUrl.parse(input, {
|
|
49
|
-
allowedProtocols: allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
50
|
-
errorClass: ObjectStoreError,
|
|
51
|
-
maxUrlLength: C.BYTES.kib(32),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// GCS bucket names: 3-63 chars, lowercase letters / digits / hyphens /
|
|
56
|
-
// underscores / dots; can't start or end with hyphen; can't contain '..'
|
|
57
|
-
// or 'goog' prefix; can't be IP address. Most violations the API will
|
|
58
|
-
// reject for us — we sanity-check the basics.
|
|
59
|
-
var BUCKET_NAME_RE = /^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$/;
|
|
60
|
-
|
|
61
|
-
function _validateBucketName(name) {
|
|
62
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
63
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
64
|
-
"gcs bucketOps: bucket name must be a non-empty string", true);
|
|
65
|
-
}
|
|
66
|
-
if (name.length < 3 || name.length > 63) {
|
|
67
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
68
|
-
"gcs bucketOps: bucket name must be 3-63 chars (got " + name.length + ")", true);
|
|
69
|
-
}
|
|
70
|
-
// Length is bounded above (3..63) before the regex runs so the
|
|
71
|
-
// pattern can't be driven against an unbounded input.
|
|
72
|
-
if (name.length > 63 || !BUCKET_NAME_RE.test(name)) {
|
|
73
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
74
|
-
"gcs bucketOps: bucket name '" + name + "' is invalid; lowercase " +
|
|
75
|
-
"letters / digits / hyphens / underscores / dots only, must start " +
|
|
76
|
-
"and end with letter or digit", true);
|
|
77
|
-
}
|
|
78
|
-
if (name.indexOf("..") !== -1) {
|
|
79
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
80
|
-
"gcs bucketOps: bucket name '" + name + "' contains '..'", true);
|
|
81
|
-
}
|
|
82
|
-
if (name.indexOf("goog") === 0) {
|
|
83
|
-
throw _err("BUCKET_INVALID_NAME",
|
|
84
|
-
"gcs bucketOps: bucket name '" + name + "' starts with 'goog' " +
|
|
85
|
-
"(reserved by Google)", true);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// GCS lifecycle rules: { action: { type: "Delete"|"SetStorageClass", storageClass? },
|
|
90
|
-
// condition: { age, createdBefore, ...} }
|
|
91
|
-
function _validateLifecycleRule(rule, idx) {
|
|
92
|
-
function bad(msg) {
|
|
93
|
-
throw _err("INVALID_LIFECYCLE",
|
|
94
|
-
"gcs bucketOps: setLifecycle: rule[" + idx + "]: " + msg, true);
|
|
95
|
-
}
|
|
96
|
-
if (!rule || typeof rule !== "object") bad("must be an object");
|
|
97
|
-
if (!rule.action || typeof rule.action !== "object") {
|
|
98
|
-
bad("action object is required");
|
|
99
|
-
}
|
|
100
|
-
if (!rule.action.type) bad("action.type is required");
|
|
101
|
-
if (rule.action.type !== "Delete" && rule.action.type !== "SetStorageClass" &&
|
|
102
|
-
rule.action.type !== "AbortIncompleteMultipartUpload") {
|
|
103
|
-
bad("action.type must be 'Delete' / 'SetStorageClass' / " +
|
|
104
|
-
"'AbortIncompleteMultipartUpload' (got " +
|
|
105
|
-
JSON.stringify(rule.action.type) + ")");
|
|
106
|
-
}
|
|
107
|
-
if (rule.action.type === "SetStorageClass" && !rule.action.storageClass) {
|
|
108
|
-
bad("action.storageClass required when action.type='SetStorageClass'");
|
|
109
|
-
}
|
|
110
|
-
if (!rule.condition || typeof rule.condition !== "object") {
|
|
111
|
-
bad("condition object is required");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function _validateCorsRule(rule, idx) {
|
|
116
|
-
function bad(msg) {
|
|
117
|
-
throw _err("INVALID_CORS_RULE",
|
|
118
|
-
"gcs bucketOps: setCorsRules: rule[" + idx + "]: " + msg, true);
|
|
119
|
-
}
|
|
120
|
-
if (!rule || typeof rule !== "object") bad("must be an object");
|
|
121
|
-
if (!Array.isArray(rule.origin) || rule.origin.length === 0) {
|
|
122
|
-
bad("origin must be a non-empty array");
|
|
123
|
-
}
|
|
124
|
-
if (rule.method !== undefined && !Array.isArray(rule.method)) {
|
|
125
|
-
bad("method, if present, must be an array");
|
|
126
|
-
}
|
|
127
|
-
if (rule.responseHeader !== undefined && !Array.isArray(rule.responseHeader)) {
|
|
128
|
-
bad("responseHeader, if present, must be an array");
|
|
129
|
-
}
|
|
130
|
-
if (rule.maxAgeSeconds !== undefined &&
|
|
131
|
-
(typeof rule.maxAgeSeconds !== "number" || rule.maxAgeSeconds < 0)) {
|
|
132
|
-
bad("maxAgeSeconds, if present, must be a non-negative number");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function create(config) {
|
|
137
|
-
if (!config) throw _err("BAD_OPT", "gcs bucketOps: config required", true);
|
|
138
|
-
|
|
139
|
-
var serviceAccount = config.serviceAccount;
|
|
140
|
-
if (!serviceAccount && config.serviceAccountFile) {
|
|
141
|
-
try {
|
|
142
|
-
serviceAccount = safeJson.parse(fs.readFileSync(config.serviceAccountFile));
|
|
143
|
-
} catch (e) {
|
|
144
|
-
throw _err("BAD_OPT", "gcs bucketOps: failed to read serviceAccountFile '" +
|
|
145
|
-
config.serviceAccountFile + "': " + ((e && e.message) || String(e)), true);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (!serviceAccount || !serviceAccount.client_email || !serviceAccount.private_key) {
|
|
149
|
-
throw _err("BAD_OPT",
|
|
150
|
-
"gcs bucketOps: serviceAccount with { client_email, private_key } required " +
|
|
151
|
-
"(or serviceAccountFile pointing to one)", true);
|
|
152
|
-
}
|
|
153
|
-
var projectId = config.projectId || serviceAccount.project_id;
|
|
154
|
-
if (!projectId) {
|
|
155
|
-
throw _err("BAD_OPT",
|
|
156
|
-
"gcs bucketOps: projectId required (either config.projectId or " +
|
|
157
|
-
"serviceAccount.project_id)", true);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
var endpoint = config.endpoint || gcs.DEFAULT_ENDPOINT;
|
|
161
|
-
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
|
|
162
|
-
var tokenEndpoint = config.tokenEndpoint || "https://oauth2.googleapis.com/token";
|
|
163
|
-
// Bucket-level admin needs the full-control scope — list-buckets +
|
|
164
|
-
// create + delete + lifecycle + CORS are all admin operations beyond
|
|
165
|
-
// the per-object read_write scope used by gcs.js's per-blob client.
|
|
166
|
-
var scope = config.scope || "https://www.googleapis.com/auth/devstorage.full_control";
|
|
167
|
-
var timeoutMs = config.timeoutMs;
|
|
168
|
-
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
169
|
-
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
170
|
-
|
|
171
|
-
// Token cache — same shape as gcs.js's per-blob client, scoped to
|
|
172
|
-
// this factory instance so a parallel per-blob client doesn't share
|
|
173
|
-
// the admin-scoped token (different scope).
|
|
174
|
-
var cachedToken = null;
|
|
175
|
-
var TOKEN_REFRESH_BUFFER = C.TIME.minutes(5);
|
|
176
|
-
|
|
177
|
-
function _request(method, url, headers, body, expectStatus) {
|
|
178
|
-
var reqOpts = {
|
|
179
|
-
method: method,
|
|
180
|
-
url: url,
|
|
181
|
-
headers: headers,
|
|
182
|
-
body: body,
|
|
183
|
-
idleTimeoutMs: timeoutMs,
|
|
184
|
-
allowedProtocols: allowedProtocols,
|
|
185
|
-
errorClass: ObjectStoreError,
|
|
186
|
-
};
|
|
187
|
-
if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
|
|
188
|
-
return httpClient.request(reqOpts).then(function (res) {
|
|
189
|
-
if (expectStatus && expectStatus.indexOf(res.statusCode) === -1) {
|
|
190
|
-
var bodyText = res.body ?
|
|
191
|
-
(Buffer.isBuffer(res.body) ? res.body.toString("utf8") : String(res.body)) : "";
|
|
192
|
-
throw _err("UNEXPECTED_STATUS",
|
|
193
|
-
"gcs bucketOps: " + method + " " + url + " returned HTTP " +
|
|
194
|
-
res.statusCode + (bodyText ? " — " + bodyText.slice(0, 500) : ""), true);
|
|
195
|
-
}
|
|
196
|
-
return res;
|
|
197
|
-
}, function (e) {
|
|
198
|
-
// http-client rejects on 4xx/5xx; if the caller marked the
|
|
199
|
-
// status as semantically acceptable (404 on delete, 409 on
|
|
200
|
-
// create) surface it as a normal response.
|
|
201
|
-
var sc = e && e.statusCode;
|
|
202
|
-
if (sc && expectStatus && expectStatus.indexOf(sc) !== -1) {
|
|
203
|
-
return { statusCode: sc, headers: {}, body: Buffer.alloc(0) };
|
|
204
|
-
}
|
|
205
|
-
throw e;
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function _ensureToken() {
|
|
210
|
-
if (cachedToken && Date.now() < cachedToken.expiresAt - TOKEN_REFRESH_BUFFER) {
|
|
211
|
-
return cachedToken.accessToken;
|
|
212
|
-
}
|
|
213
|
-
var assertion = gcs._signJwt(serviceAccount, scope, tokenEndpoint);
|
|
214
|
-
var bodyStr = "grant_type=" + encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer") +
|
|
215
|
-
"&assertion=" + encodeURIComponent(assertion);
|
|
216
|
-
var bodyBuf = Buffer.from(bodyStr, "utf8");
|
|
217
|
-
var res = await _request("POST", _internalUrl(tokenEndpoint, allowedProtocols), {
|
|
218
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
219
|
-
"Content-Length": String(bodyBuf.length),
|
|
220
|
-
}, bodyBuf, [HTTP_OK]);
|
|
221
|
-
var tokenResp = safeJson.parse(res.body);
|
|
222
|
-
if (!tokenResp.access_token) {
|
|
223
|
-
throw _err("AUTH_FAILED",
|
|
224
|
-
"gcs bucketOps: token endpoint returned no access_token: " +
|
|
225
|
-
(res.body ? res.body.toString("utf8") : ""), true);
|
|
226
|
-
}
|
|
227
|
-
var expiresInMs = C.TIME.seconds(tokenResp.expires_in || 3600);
|
|
228
|
-
cachedToken = {
|
|
229
|
-
accessToken: tokenResp.access_token,
|
|
230
|
-
expiresAt: Date.now() + expiresInMs,
|
|
231
|
-
};
|
|
232
|
-
return cachedToken.accessToken;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Returns a parsed URL ready for `.searchParams` mutation. The bucket
|
|
236
|
-
// base path is "/storage/v1/b"; suffix (e.g., "/" + bucket name) is
|
|
237
|
-
// appended before parsing so safeUrl.parse owns every URL we hand to
|
|
238
|
-
// httpClient.
|
|
239
|
-
function _bucketBaseUrl(suffix) {
|
|
240
|
-
return _internalUrl(endpoint + "/storage/v1/b" + (suffix || ""), allowedProtocols);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function createBucket(name, opts) {
|
|
244
|
-
_validateBucketName(name);
|
|
245
|
-
opts = opts || {};
|
|
246
|
-
var token = await _ensureToken();
|
|
247
|
-
var url = _bucketBaseUrl();
|
|
248
|
-
url.searchParams.set("project", projectId);
|
|
249
|
-
var bodyObj = { name: name };
|
|
250
|
-
if (opts.location) bodyObj.location = opts.location;
|
|
251
|
-
if (opts.storageClass) bodyObj.storageClass = opts.storageClass;
|
|
252
|
-
if (opts.iamConfiguration) bodyObj.iamConfiguration = opts.iamConfiguration;
|
|
253
|
-
var bodyBuf = Buffer.from(JSON.stringify(bodyObj), "utf8");
|
|
254
|
-
var headers = Object.assign(authHeader.bearer(token), {
|
|
255
|
-
"Content-Type": "application/json",
|
|
256
|
-
"Content-Length": String(bodyBuf.length),
|
|
257
|
-
});
|
|
258
|
-
var res = await _request("POST", url, headers, bodyBuf, [HTTP_OK, HTTP_CONFLICT]);
|
|
259
|
-
if (res.statusCode === HTTP_CONFLICT) {
|
|
260
|
-
throw _err("BUCKET_ALREADY_OWNED",
|
|
261
|
-
"gcs bucketOps: bucket '" + name + "' already exists", true);
|
|
262
|
-
}
|
|
263
|
-
var parsed = safeJson.parse(res.body);
|
|
264
|
-
return {
|
|
265
|
-
name: parsed.name,
|
|
266
|
-
location: parsed.location || null,
|
|
267
|
-
storageClass: parsed.storageClass || null,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function deleteBucket(name) {
|
|
272
|
-
_validateBucketName(name);
|
|
273
|
-
var token = await _ensureToken();
|
|
274
|
-
var url = _bucketBaseUrl("/" + encodeURIComponent(name));
|
|
275
|
-
var headers = authHeader.bearer(token);
|
|
276
|
-
var res = await _request("DELETE", url, headers, null, [HTTP_NO_CONTENT, HTTP_NOT_FOUND]);
|
|
277
|
-
return res.statusCode === HTTP_NO_CONTENT;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async function listBuckets(opts) {
|
|
281
|
-
opts = opts || {};
|
|
282
|
-
var token = await _ensureToken();
|
|
283
|
-
var url = _bucketBaseUrl();
|
|
284
|
-
url.searchParams.set("project", projectId);
|
|
285
|
-
if (opts.prefix) url.searchParams.set("prefix", opts.prefix);
|
|
286
|
-
if (opts.maxResults) url.searchParams.set("maxResults", String(opts.maxResults));
|
|
287
|
-
if (opts.pageToken) url.searchParams.set("pageToken", opts.pageToken);
|
|
288
|
-
var headers = authHeader.bearer(token);
|
|
289
|
-
var res = await _request("GET", url, headers, null, [HTTP_OK]);
|
|
290
|
-
var parsed = safeJson.parse(res.body);
|
|
291
|
-
var items = Array.isArray(parsed.items) ? parsed.items : [];
|
|
292
|
-
return items.map(function (item) {
|
|
293
|
-
return {
|
|
294
|
-
name: item.name,
|
|
295
|
-
location: item.location || null,
|
|
296
|
-
storageClass: item.storageClass || null,
|
|
297
|
-
timeCreated: item.timeCreated || null,
|
|
298
|
-
updated: item.updated || null,
|
|
299
|
-
};
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function setLifecycle(name, rules) {
|
|
304
|
-
_validateBucketName(name);
|
|
305
|
-
if (!Array.isArray(rules)) {
|
|
306
|
-
throw _err("INVALID_LIFECYCLE",
|
|
307
|
-
"gcs bucketOps: setLifecycle: rules must be an array", true);
|
|
308
|
-
}
|
|
309
|
-
rules.forEach(_validateLifecycleRule);
|
|
310
|
-
var token = await _ensureToken();
|
|
311
|
-
var url = _bucketBaseUrl("/" + encodeURIComponent(name));
|
|
312
|
-
var bodyObj = { lifecycle: { rule: rules } };
|
|
313
|
-
var bodyBuf = Buffer.from(JSON.stringify(bodyObj), "utf8");
|
|
314
|
-
var headers = Object.assign(authHeader.bearer(token), {
|
|
315
|
-
"Content-Type": "application/json",
|
|
316
|
-
"Content-Length": String(bodyBuf.length),
|
|
317
|
-
});
|
|
318
|
-
await _request("PATCH", url, headers, bodyBuf, [HTTP_OK]);
|
|
319
|
-
return { rulesApplied: rules.length };
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function setCorsRules(name, rules) {
|
|
323
|
-
_validateBucketName(name);
|
|
324
|
-
if (!Array.isArray(rules)) {
|
|
325
|
-
throw _err("INVALID_CORS_RULE",
|
|
326
|
-
"gcs bucketOps: setCorsRules: rules must be an array", true);
|
|
327
|
-
}
|
|
328
|
-
rules.forEach(_validateCorsRule);
|
|
329
|
-
var token = await _ensureToken();
|
|
330
|
-
var url = _bucketBaseUrl("/" + encodeURIComponent(name));
|
|
331
|
-
var bodyObj = { cors: rules };
|
|
332
|
-
var bodyBuf = Buffer.from(JSON.stringify(bodyObj), "utf8");
|
|
333
|
-
var headers = Object.assign(authHeader.bearer(token), {
|
|
334
|
-
"Content-Type": "application/json",
|
|
335
|
-
"Content-Length": String(bodyBuf.length),
|
|
336
|
-
});
|
|
337
|
-
await _request("PATCH", url, headers, bodyBuf, [HTTP_OK]);
|
|
338
|
-
return { rulesApplied: rules.length };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
protocol: "gcs",
|
|
343
|
-
create: createBucket,
|
|
344
|
-
delete: deleteBucket,
|
|
345
|
-
list: listBuckets,
|
|
346
|
-
setLifecycle: setLifecycle,
|
|
347
|
-
setCorsRules: setCorsRules,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
module.exports = { create: create };
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* gcs-bucket-ops — bucket-level operations for Google Cloud Storage.
|
|
4
|
+
*
|
|
5
|
+
* Per-object ops (put / get / list / delete) live in
|
|
6
|
+
* `lib/object-store/gcs.js` and are bound to a single bucket at
|
|
7
|
+
* create() time. Bucket lifecycle ops are at a different level — a
|
|
8
|
+
* project-scoped client that addresses arbitrary buckets — so they
|
|
9
|
+
* get their own factory.
|
|
10
|
+
*
|
|
11
|
+
* create(name, opts?) async; POST /storage/v1/b?project={projectId}
|
|
12
|
+
* opts: location ('US' / 'EU' / region) +
|
|
13
|
+
* storageClass + iamConfiguration
|
|
14
|
+
* delete(name) async; DELETE /storage/v1/b/{name}
|
|
15
|
+
* list() async; GET /storage/v1/b?project={projectId}
|
|
16
|
+
* -> [{ name, location, storageClass,
|
|
17
|
+
* timeCreated, updated }]
|
|
18
|
+
* setLifecycle(name, rules) async; PATCH /storage/v1/b/{name}
|
|
19
|
+
* -> body { lifecycle: { rule: [...] } }
|
|
20
|
+
* setCorsRules(name, rules) async; PATCH /storage/v1/b/{name}
|
|
21
|
+
* -> body { cors: [...] }
|
|
22
|
+
*
|
|
23
|
+
* Auth: same service-account JSON / RSA-SHA256-signed JWT exchanged
|
|
24
|
+
* for an OAuth2 access token as `lib/object-store/gcs.js`.
|
|
25
|
+
*/
|
|
26
|
+
var fs = require("node:fs");
|
|
27
|
+
var gcs = require("./gcs");
|
|
28
|
+
var authHeader = require("../auth-header");
|
|
29
|
+
var httpClient = require("../http-client");
|
|
30
|
+
var safeJson = require("../safe-json");
|
|
31
|
+
var safeUrl = require("../safe-url");
|
|
32
|
+
var C = require("../constants");
|
|
33
|
+
var requestHelpers = require("../request-helpers");
|
|
34
|
+
var { ObjectStoreError } = require("../framework-error");
|
|
35
|
+
|
|
36
|
+
var _err = ObjectStoreError.factory;
|
|
37
|
+
|
|
38
|
+
// HTTP status constants used in the bucket-ops expectStatus arrays.
|
|
39
|
+
var HTTP_OK = requestHelpers.HTTP_STATUS.OK;
|
|
40
|
+
var HTTP_NO_CONTENT = requestHelpers.HTTP_STATUS.NO_CONTENT;
|
|
41
|
+
var HTTP_NOT_FOUND = requestHelpers.HTTP_STATUS.NOT_FOUND;
|
|
42
|
+
var HTTP_CONFLICT = requestHelpers.HTTP_STATUS.CONFLICT;
|
|
43
|
+
|
|
44
|
+
// Internal URL builder — endpoint + path string from validated config.
|
|
45
|
+
// Routes through safeUrl.parse so the protocol allowlist + length cap
|
|
46
|
+
// apply uniformly.
|
|
47
|
+
function _internalUrl(input, allowedProtocols) {
|
|
48
|
+
return safeUrl.parse(input, {
|
|
49
|
+
allowedProtocols: allowedProtocols || safeUrl.ALLOW_HTTP_TLS,
|
|
50
|
+
errorClass: ObjectStoreError,
|
|
51
|
+
maxUrlLength: C.BYTES.kib(32),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// GCS bucket names: 3-63 chars, lowercase letters / digits / hyphens /
|
|
56
|
+
// underscores / dots; can't start or end with hyphen; can't contain '..'
|
|
57
|
+
// or 'goog' prefix; can't be IP address. Most violations the API will
|
|
58
|
+
// reject for us — we sanity-check the basics.
|
|
59
|
+
var BUCKET_NAME_RE = /^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$/;
|
|
60
|
+
|
|
61
|
+
function _validateBucketName(name) {
|
|
62
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
63
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
64
|
+
"gcs bucketOps: bucket name must be a non-empty string", true);
|
|
65
|
+
}
|
|
66
|
+
if (name.length < 3 || name.length > 63) {
|
|
67
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
68
|
+
"gcs bucketOps: bucket name must be 3-63 chars (got " + name.length + ")", true);
|
|
69
|
+
}
|
|
70
|
+
// Length is bounded above (3..63) before the regex runs so the
|
|
71
|
+
// pattern can't be driven against an unbounded input.
|
|
72
|
+
if (name.length > 63 || !BUCKET_NAME_RE.test(name)) {
|
|
73
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
74
|
+
"gcs bucketOps: bucket name '" + name + "' is invalid; lowercase " +
|
|
75
|
+
"letters / digits / hyphens / underscores / dots only, must start " +
|
|
76
|
+
"and end with letter or digit", true);
|
|
77
|
+
}
|
|
78
|
+
if (name.indexOf("..") !== -1) {
|
|
79
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
80
|
+
"gcs bucketOps: bucket name '" + name + "' contains '..'", true);
|
|
81
|
+
}
|
|
82
|
+
if (name.indexOf("goog") === 0) {
|
|
83
|
+
throw _err("BUCKET_INVALID_NAME",
|
|
84
|
+
"gcs bucketOps: bucket name '" + name + "' starts with 'goog' " +
|
|
85
|
+
"(reserved by Google)", true);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// GCS lifecycle rules: { action: { type: "Delete"|"SetStorageClass", storageClass? },
|
|
90
|
+
// condition: { age, createdBefore, ...} }
|
|
91
|
+
function _validateLifecycleRule(rule, idx) {
|
|
92
|
+
function bad(msg) {
|
|
93
|
+
throw _err("INVALID_LIFECYCLE",
|
|
94
|
+
"gcs bucketOps: setLifecycle: rule[" + idx + "]: " + msg, true);
|
|
95
|
+
}
|
|
96
|
+
if (!rule || typeof rule !== "object") bad("must be an object");
|
|
97
|
+
if (!rule.action || typeof rule.action !== "object") {
|
|
98
|
+
bad("action object is required");
|
|
99
|
+
}
|
|
100
|
+
if (!rule.action.type) bad("action.type is required");
|
|
101
|
+
if (rule.action.type !== "Delete" && rule.action.type !== "SetStorageClass" &&
|
|
102
|
+
rule.action.type !== "AbortIncompleteMultipartUpload") {
|
|
103
|
+
bad("action.type must be 'Delete' / 'SetStorageClass' / " +
|
|
104
|
+
"'AbortIncompleteMultipartUpload' (got " +
|
|
105
|
+
JSON.stringify(rule.action.type) + ")");
|
|
106
|
+
}
|
|
107
|
+
if (rule.action.type === "SetStorageClass" && !rule.action.storageClass) {
|
|
108
|
+
bad("action.storageClass required when action.type='SetStorageClass'");
|
|
109
|
+
}
|
|
110
|
+
if (!rule.condition || typeof rule.condition !== "object") {
|
|
111
|
+
bad("condition object is required");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _validateCorsRule(rule, idx) {
|
|
116
|
+
function bad(msg) {
|
|
117
|
+
throw _err("INVALID_CORS_RULE",
|
|
118
|
+
"gcs bucketOps: setCorsRules: rule[" + idx + "]: " + msg, true);
|
|
119
|
+
}
|
|
120
|
+
if (!rule || typeof rule !== "object") bad("must be an object");
|
|
121
|
+
if (!Array.isArray(rule.origin) || rule.origin.length === 0) {
|
|
122
|
+
bad("origin must be a non-empty array");
|
|
123
|
+
}
|
|
124
|
+
if (rule.method !== undefined && !Array.isArray(rule.method)) {
|
|
125
|
+
bad("method, if present, must be an array");
|
|
126
|
+
}
|
|
127
|
+
if (rule.responseHeader !== undefined && !Array.isArray(rule.responseHeader)) {
|
|
128
|
+
bad("responseHeader, if present, must be an array");
|
|
129
|
+
}
|
|
130
|
+
if (rule.maxAgeSeconds !== undefined &&
|
|
131
|
+
(typeof rule.maxAgeSeconds !== "number" || rule.maxAgeSeconds < 0)) {
|
|
132
|
+
bad("maxAgeSeconds, if present, must be a non-negative number");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function create(config) {
|
|
137
|
+
if (!config) throw _err("BAD_OPT", "gcs bucketOps: config required", true);
|
|
138
|
+
|
|
139
|
+
var serviceAccount = config.serviceAccount;
|
|
140
|
+
if (!serviceAccount && config.serviceAccountFile) {
|
|
141
|
+
try {
|
|
142
|
+
serviceAccount = safeJson.parse(fs.readFileSync(config.serviceAccountFile));
|
|
143
|
+
} catch (e) {
|
|
144
|
+
throw _err("BAD_OPT", "gcs bucketOps: failed to read serviceAccountFile '" +
|
|
145
|
+
config.serviceAccountFile + "': " + ((e && e.message) || String(e)), true);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!serviceAccount || !serviceAccount.client_email || !serviceAccount.private_key) {
|
|
149
|
+
throw _err("BAD_OPT",
|
|
150
|
+
"gcs bucketOps: serviceAccount with { client_email, private_key } required " +
|
|
151
|
+
"(or serviceAccountFile pointing to one)", true);
|
|
152
|
+
}
|
|
153
|
+
var projectId = config.projectId || serviceAccount.project_id;
|
|
154
|
+
if (!projectId) {
|
|
155
|
+
throw _err("BAD_OPT",
|
|
156
|
+
"gcs bucketOps: projectId required (either config.projectId or " +
|
|
157
|
+
"serviceAccount.project_id)", true);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
var endpoint = config.endpoint || gcs.DEFAULT_ENDPOINT;
|
|
161
|
+
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
|
|
162
|
+
var tokenEndpoint = config.tokenEndpoint || "https://oauth2.googleapis.com/token";
|
|
163
|
+
// Bucket-level admin needs the full-control scope — list-buckets +
|
|
164
|
+
// create + delete + lifecycle + CORS are all admin operations beyond
|
|
165
|
+
// the per-object read_write scope used by gcs.js's per-blob client.
|
|
166
|
+
var scope = config.scope || "https://www.googleapis.com/auth/devstorage.full_control";
|
|
167
|
+
var timeoutMs = config.timeoutMs;
|
|
168
|
+
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
169
|
+
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
170
|
+
|
|
171
|
+
// Token cache — same shape as gcs.js's per-blob client, scoped to
|
|
172
|
+
// this factory instance so a parallel per-blob client doesn't share
|
|
173
|
+
// the admin-scoped token (different scope).
|
|
174
|
+
var cachedToken = null;
|
|
175
|
+
var TOKEN_REFRESH_BUFFER = C.TIME.minutes(5);
|
|
176
|
+
|
|
177
|
+
function _request(method, url, headers, body, expectStatus) {
|
|
178
|
+
var reqOpts = {
|
|
179
|
+
method: method,
|
|
180
|
+
url: url,
|
|
181
|
+
headers: headers,
|
|
182
|
+
body: body,
|
|
183
|
+
idleTimeoutMs: timeoutMs,
|
|
184
|
+
allowedProtocols: allowedProtocols,
|
|
185
|
+
errorClass: ObjectStoreError,
|
|
186
|
+
};
|
|
187
|
+
if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
|
|
188
|
+
return httpClient.request(reqOpts).then(function (res) {
|
|
189
|
+
if (expectStatus && expectStatus.indexOf(res.statusCode) === -1) {
|
|
190
|
+
var bodyText = res.body ?
|
|
191
|
+
(Buffer.isBuffer(res.body) ? res.body.toString("utf8") : String(res.body)) : "";
|
|
192
|
+
throw _err("UNEXPECTED_STATUS",
|
|
193
|
+
"gcs bucketOps: " + method + " " + url + " returned HTTP " +
|
|
194
|
+
res.statusCode + (bodyText ? " — " + bodyText.slice(0, 500) : ""), true);
|
|
195
|
+
}
|
|
196
|
+
return res;
|
|
197
|
+
}, function (e) {
|
|
198
|
+
// http-client rejects on 4xx/5xx; if the caller marked the
|
|
199
|
+
// status as semantically acceptable (404 on delete, 409 on
|
|
200
|
+
// create) surface it as a normal response.
|
|
201
|
+
var sc = e && e.statusCode;
|
|
202
|
+
if (sc && expectStatus && expectStatus.indexOf(sc) !== -1) {
|
|
203
|
+
return { statusCode: sc, headers: {}, body: Buffer.alloc(0) };
|
|
204
|
+
}
|
|
205
|
+
throw e;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function _ensureToken() {
|
|
210
|
+
if (cachedToken && Date.now() < cachedToken.expiresAt - TOKEN_REFRESH_BUFFER) {
|
|
211
|
+
return cachedToken.accessToken;
|
|
212
|
+
}
|
|
213
|
+
var assertion = gcs._signJwt(serviceAccount, scope, tokenEndpoint);
|
|
214
|
+
var bodyStr = "grant_type=" + encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer") +
|
|
215
|
+
"&assertion=" + encodeURIComponent(assertion);
|
|
216
|
+
var bodyBuf = Buffer.from(bodyStr, "utf8");
|
|
217
|
+
var res = await _request("POST", _internalUrl(tokenEndpoint, allowedProtocols), {
|
|
218
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
219
|
+
"Content-Length": String(bodyBuf.length),
|
|
220
|
+
}, bodyBuf, [HTTP_OK]);
|
|
221
|
+
var tokenResp = safeJson.parse(res.body);
|
|
222
|
+
if (!tokenResp.access_token) {
|
|
223
|
+
throw _err("AUTH_FAILED",
|
|
224
|
+
"gcs bucketOps: token endpoint returned no access_token: " +
|
|
225
|
+
(res.body ? res.body.toString("utf8") : ""), true);
|
|
226
|
+
}
|
|
227
|
+
var expiresInMs = C.TIME.seconds(tokenResp.expires_in || 3600);
|
|
228
|
+
cachedToken = {
|
|
229
|
+
accessToken: tokenResp.access_token,
|
|
230
|
+
expiresAt: Date.now() + expiresInMs,
|
|
231
|
+
};
|
|
232
|
+
return cachedToken.accessToken;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Returns a parsed URL ready for `.searchParams` mutation. The bucket
|
|
236
|
+
// base path is "/storage/v1/b"; suffix (e.g., "/" + bucket name) is
|
|
237
|
+
// appended before parsing so safeUrl.parse owns every URL we hand to
|
|
238
|
+
// httpClient.
|
|
239
|
+
function _bucketBaseUrl(suffix) {
|
|
240
|
+
return _internalUrl(endpoint + "/storage/v1/b" + (suffix || ""), allowedProtocols);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function createBucket(name, opts) {
|
|
244
|
+
_validateBucketName(name);
|
|
245
|
+
opts = opts || {};
|
|
246
|
+
var token = await _ensureToken();
|
|
247
|
+
var url = _bucketBaseUrl();
|
|
248
|
+
url.searchParams.set("project", projectId);
|
|
249
|
+
var bodyObj = { name: name };
|
|
250
|
+
if (opts.location) bodyObj.location = opts.location;
|
|
251
|
+
if (opts.storageClass) bodyObj.storageClass = opts.storageClass;
|
|
252
|
+
if (opts.iamConfiguration) bodyObj.iamConfiguration = opts.iamConfiguration;
|
|
253
|
+
var bodyBuf = Buffer.from(JSON.stringify(bodyObj), "utf8");
|
|
254
|
+
var headers = Object.assign(authHeader.bearer(token), {
|
|
255
|
+
"Content-Type": "application/json",
|
|
256
|
+
"Content-Length": String(bodyBuf.length),
|
|
257
|
+
});
|
|
258
|
+
var res = await _request("POST", url, headers, bodyBuf, [HTTP_OK, HTTP_CONFLICT]);
|
|
259
|
+
if (res.statusCode === HTTP_CONFLICT) {
|
|
260
|
+
throw _err("BUCKET_ALREADY_OWNED",
|
|
261
|
+
"gcs bucketOps: bucket '" + name + "' already exists", true);
|
|
262
|
+
}
|
|
263
|
+
var parsed = safeJson.parse(res.body);
|
|
264
|
+
return {
|
|
265
|
+
name: parsed.name,
|
|
266
|
+
location: parsed.location || null,
|
|
267
|
+
storageClass: parsed.storageClass || null,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function deleteBucket(name) {
|
|
272
|
+
_validateBucketName(name);
|
|
273
|
+
var token = await _ensureToken();
|
|
274
|
+
var url = _bucketBaseUrl("/" + encodeURIComponent(name));
|
|
275
|
+
var headers = authHeader.bearer(token);
|
|
276
|
+
var res = await _request("DELETE", url, headers, null, [HTTP_NO_CONTENT, HTTP_NOT_FOUND]);
|
|
277
|
+
return res.statusCode === HTTP_NO_CONTENT;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function listBuckets(opts) {
|
|
281
|
+
opts = opts || {};
|
|
282
|
+
var token = await _ensureToken();
|
|
283
|
+
var url = _bucketBaseUrl();
|
|
284
|
+
url.searchParams.set("project", projectId);
|
|
285
|
+
if (opts.prefix) url.searchParams.set("prefix", opts.prefix);
|
|
286
|
+
if (opts.maxResults) url.searchParams.set("maxResults", String(opts.maxResults));
|
|
287
|
+
if (opts.pageToken) url.searchParams.set("pageToken", opts.pageToken);
|
|
288
|
+
var headers = authHeader.bearer(token);
|
|
289
|
+
var res = await _request("GET", url, headers, null, [HTTP_OK]);
|
|
290
|
+
var parsed = safeJson.parse(res.body);
|
|
291
|
+
var items = Array.isArray(parsed.items) ? parsed.items : [];
|
|
292
|
+
return items.map(function (item) {
|
|
293
|
+
return {
|
|
294
|
+
name: item.name,
|
|
295
|
+
location: item.location || null,
|
|
296
|
+
storageClass: item.storageClass || null,
|
|
297
|
+
timeCreated: item.timeCreated || null,
|
|
298
|
+
updated: item.updated || null,
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function setLifecycle(name, rules) {
|
|
304
|
+
_validateBucketName(name);
|
|
305
|
+
if (!Array.isArray(rules)) {
|
|
306
|
+
throw _err("INVALID_LIFECYCLE",
|
|
307
|
+
"gcs bucketOps: setLifecycle: rules must be an array", true);
|
|
308
|
+
}
|
|
309
|
+
rules.forEach(_validateLifecycleRule);
|
|
310
|
+
var token = await _ensureToken();
|
|
311
|
+
var url = _bucketBaseUrl("/" + encodeURIComponent(name));
|
|
312
|
+
var bodyObj = { lifecycle: { rule: rules } };
|
|
313
|
+
var bodyBuf = Buffer.from(JSON.stringify(bodyObj), "utf8");
|
|
314
|
+
var headers = Object.assign(authHeader.bearer(token), {
|
|
315
|
+
"Content-Type": "application/json",
|
|
316
|
+
"Content-Length": String(bodyBuf.length),
|
|
317
|
+
});
|
|
318
|
+
await _request("PATCH", url, headers, bodyBuf, [HTTP_OK]);
|
|
319
|
+
return { rulesApplied: rules.length };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function setCorsRules(name, rules) {
|
|
323
|
+
_validateBucketName(name);
|
|
324
|
+
if (!Array.isArray(rules)) {
|
|
325
|
+
throw _err("INVALID_CORS_RULE",
|
|
326
|
+
"gcs bucketOps: setCorsRules: rules must be an array", true);
|
|
327
|
+
}
|
|
328
|
+
rules.forEach(_validateCorsRule);
|
|
329
|
+
var token = await _ensureToken();
|
|
330
|
+
var url = _bucketBaseUrl("/" + encodeURIComponent(name));
|
|
331
|
+
var bodyObj = { cors: rules };
|
|
332
|
+
var bodyBuf = Buffer.from(JSON.stringify(bodyObj), "utf8");
|
|
333
|
+
var headers = Object.assign(authHeader.bearer(token), {
|
|
334
|
+
"Content-Type": "application/json",
|
|
335
|
+
"Content-Length": String(bodyBuf.length),
|
|
336
|
+
});
|
|
337
|
+
await _request("PATCH", url, headers, bodyBuf, [HTTP_OK]);
|
|
338
|
+
return { rulesApplied: rules.length };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
protocol: "gcs",
|
|
343
|
+
create: createBucket,
|
|
344
|
+
delete: deleteBucket,
|
|
345
|
+
list: listBuckets,
|
|
346
|
+
setLifecycle: setLifecycle,
|
|
347
|
+
setCorsRules: setCorsRules,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = { create: create };
|