UNPKG

39.1 kBJavaScriptView Raw
1"use strict";
2// Copyright 2022 Google LLC
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16 if (k2 === undefined) k2 = k;
17 var desc = Object.getOwnPropertyDescriptor(m, k);
18 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19 desc = { enumerable: true, get: function() { return m[k]; } };
20 }
21 Object.defineProperty(o, k2, desc);
22}) : (function(o, m, k, k2) {
23 if (k2 === undefined) k2 = k;
24 o[k2] = m[k];
25}));
26var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27 Object.defineProperty(o, "default", { enumerable: true, value: v });
28}) : function(o, v) {
29 o["default"] = v;
30});
31var __importStar = (this && this.__importStar) || function (mod) {
32 if (mod && mod.__esModule) return mod;
33 var result = {};
34 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
35 __setModuleDefault(result, mod);
36 return result;
37};
38var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
39 if (kind === "m") throw new TypeError("Private method is not writable");
40 if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
41 if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
42 return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
43};
44var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
45 if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
46 if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
47 return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
48};
49var __importDefault = (this && this.__importDefault) || function (mod) {
50 return (mod && mod.__esModule) ? mod : { "default": mod };
51};
52var _Upload_instances, _Upload_gcclGcsCmd, _Upload_resetLocalBuffersCache, _Upload_addLocalBufferCache;
53Object.defineProperty(exports, "__esModule", { value: true });
54exports.Upload = exports.PROTOCOL_REGEX = void 0;
55exports.upload = upload;
56exports.createURI = createURI;
57exports.checkUploadStatus = checkUploadStatus;
58const abort_controller_1 = __importDefault(require("abort-controller"));
59const crypto_1 = require("crypto");
60const gaxios = __importStar(require("gaxios"));
61const google_auth_library_1 = require("google-auth-library");
62const stream_1 = require("stream");
63const async_retry_1 = __importDefault(require("async-retry"));
64const uuid = __importStar(require("uuid"));
65const util_js_1 = require("./util.js");
66const util_js_2 = require("./nodejs-common/util.js");
67// eslint-disable-next-line @typescript-eslint/ban-ts-comment
68// @ts-ignore
69const package_json_helper_cjs_1 = require("./package-json-helper.cjs");
70const NOT_FOUND_STATUS_CODE = 404;
71const RESUMABLE_INCOMPLETE_STATUS_CODE = 308;
72const packageJson = (0, package_json_helper_cjs_1.getPackageJSON)();
73exports.PROTOCOL_REGEX = /^(\w*):\/\//;
74class Upload extends stream_1.Writable {
75 constructor(cfg) {
76 var _a;
77 super(cfg);
78 _Upload_instances.add(this);
79 this.numBytesWritten = 0;
80 this.numRetries = 0;
81 this.currentInvocationId = {
82 checkUploadStatus: uuid.v4(),
83 chunk: uuid.v4(),
84 uri: uuid.v4(),
85 };
86 /**
87 * A cache of buffers written to this instance, ready for consuming
88 */
89 this.writeBuffers = [];
90 this.numChunksReadInRequest = 0;
91 /**
92 * An array of buffers used for caching the most recent upload chunk.
93 * We should not assume that the server received all bytes sent in the request.
94 * - https://cloud.google.com/storage/docs/performing-resumable-uploads#chunked-upload
95 */
96 this.localWriteCache = [];
97 this.localWriteCacheByteLength = 0;
98 this.upstreamEnded = false;
99 _Upload_gcclGcsCmd.set(this, void 0);
100 cfg = cfg || {};
101 if (!cfg.bucket || !cfg.file) {
102 throw new Error('A bucket and file name are required');
103 }
104 if (cfg.offset && !cfg.uri) {
105 throw new RangeError('Cannot provide an `offset` without providing a `uri`');
106 }
107 if (cfg.isPartialUpload && !cfg.chunkSize) {
108 throw new RangeError('Cannot set `isPartialUpload` without providing a `chunkSize`');
109 }
110 cfg.authConfig = cfg.authConfig || {};
111 cfg.authConfig.scopes = [
112 'https://www.googleapis.com/auth/devstorage.full_control',
113 ];
114 this.authClient = cfg.authClient || new google_auth_library_1.GoogleAuth(cfg.authConfig);
115 const universe = cfg.universeDomain || google_auth_library_1.DEFAULT_UNIVERSE;
116 this.apiEndpoint = `https://storage.${universe}`;
117 if (cfg.apiEndpoint && cfg.apiEndpoint !== this.apiEndpoint) {
118 this.apiEndpoint = this.sanitizeEndpoint(cfg.apiEndpoint);
119 const hostname = new URL(this.apiEndpoint).hostname;
120 // check if it is a domain of a known universe
121 const isDomain = hostname === universe;
122 const isDefaultUniverseDomain = hostname === google_auth_library_1.DEFAULT_UNIVERSE;
123 // check if it is a subdomain of a known universe
124 // by checking a last (universe's length + 1) of a hostname
125 const isSubDomainOfUniverse = hostname.slice(-(universe.length + 1)) === `.${universe}`;
126 const isSubDomainOfDefaultUniverse = hostname.slice(-(google_auth_library_1.DEFAULT_UNIVERSE.length + 1)) ===
127 `.${google_auth_library_1.DEFAULT_UNIVERSE}`;
128 if (!isDomain &&
129 !isDefaultUniverseDomain &&
130 !isSubDomainOfUniverse &&
131 !isSubDomainOfDefaultUniverse) {
132 // a custom, non-universe domain,
133 // use gaxios
134 this.authClient = gaxios;
135 }
136 }
137 this.baseURI = `${this.apiEndpoint}/upload/storage/v1/b`;
138 this.bucket = cfg.bucket;
139 const cacheKeyElements = [cfg.bucket, cfg.file];
140 if (typeof cfg.generation === 'number') {
141 cacheKeyElements.push(`${cfg.generation}`);
142 }
143 this.cacheKey = cacheKeyElements.join('/');
144 this.customRequestOptions = cfg.customRequestOptions || {};
145 this.file = cfg.file;
146 this.generation = cfg.generation;
147 this.kmsKeyName = cfg.kmsKeyName;
148 this.metadata = cfg.metadata || {};
149 this.offset = cfg.offset;
150 this.origin = cfg.origin;
151 this.params = cfg.params || {};
152 this.userProject = cfg.userProject;
153 this.chunkSize = cfg.chunkSize;
154 this.retryOptions = cfg.retryOptions;
155 this.isPartialUpload = (_a = cfg.isPartialUpload) !== null && _a !== void 0 ? _a : false;
156 if (cfg.key) {
157 const base64Key = Buffer.from(cfg.key).toString('base64');
158 this.encryption = {
159 key: base64Key,
160 hash: (0, crypto_1.createHash)('sha256').update(cfg.key).digest('base64'),
161 };
162 }
163 this.predefinedAcl = cfg.predefinedAcl;
164 if (cfg.private)
165 this.predefinedAcl = 'private';
166 if (cfg.public)
167 this.predefinedAcl = 'publicRead';
168 const autoRetry = cfg.retryOptions.autoRetry;
169 this.uriProvidedManually = !!cfg.uri;
170 this.uri = cfg.uri;
171 if (this.offset) {
172 // we're resuming an incomplete upload
173 this.numBytesWritten = this.offset;
174 }
175 this.numRetries = 0; // counter for number of retries currently executed
176 if (!autoRetry) {
177 cfg.retryOptions.maxRetries = 0;
178 }
179 this.timeOfFirstRequest = Date.now();
180 const contentLength = cfg.metadata
181 ? Number(cfg.metadata.contentLength)
182 : NaN;
183 this.contentLength = isNaN(contentLength) ? '*' : contentLength;
184 __classPrivateFieldSet(this, _Upload_gcclGcsCmd, cfg[util_js_2.GCCL_GCS_CMD_KEY], "f");
185 this.once('writing', () => {
186 if (this.uri) {
187 this.continueUploading();
188 }
189 else {
190 this.createURI(err => {
191 if (err) {
192 return this.destroy(err);
193 }
194 this.startUploading();
195 return;
196 });
197 }
198 });
199 }
200 /**
201 * Prevent 'finish' event until the upload has succeeded.
202 *
203 * @param fireFinishEvent The finish callback
204 */
205 _final(fireFinishEvent = () => { }) {
206 this.upstreamEnded = true;
207 this.once('uploadFinished', fireFinishEvent);
208 process.nextTick(() => {
209 this.emit('upstreamFinished');
210 // it's possible `_write` may not be called - namely for empty object uploads
211 this.emit('writing');
212 });
213 }
214 /**
215 * Handles incoming data from upstream
216 *
217 * @param chunk The chunk to append to the buffer
218 * @param encoding The encoding of the chunk
219 * @param readCallback A callback for when the buffer has been read downstream
220 */
221 _write(chunk, encoding, readCallback = () => { }) {
222 // Backwards-compatible event
223 this.emit('writing');
224 this.writeBuffers.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk);
225 this.once('readFromChunkBuffer', readCallback);
226 process.nextTick(() => this.emit('wroteToChunkBuffer'));
227 }
228 /**
229 * Prepends the local buffer to write buffer and resets it.
230 *
231 * @param keepLastBytes number of bytes to keep from the end of the local buffer.
232 */
233 prependLocalBufferToUpstream(keepLastBytes) {
234 // Typically, the upstream write buffers should be smaller than the local
235 // cache, so we can save time by setting the local cache as the new
236 // upstream write buffer array and appending the old array to it
237 let initialBuffers = [];
238 if (keepLastBytes) {
239 // we only want the last X bytes
240 let bytesKept = 0;
241 while (keepLastBytes > bytesKept) {
242 // load backwards because we want the last X bytes
243 // note: `localWriteCacheByteLength` is reset below
244 let buf = this.localWriteCache.pop();
245 if (!buf)
246 break;
247 bytesKept += buf.byteLength;
248 if (bytesKept > keepLastBytes) {
249 // we have gone over the amount desired, let's keep the last X bytes
250 // of this buffer
251 const diff = bytesKept - keepLastBytes;
252 buf = buf.subarray(diff);
253 bytesKept -= diff;
254 }
255 initialBuffers.unshift(buf);
256 }
257 }
258 else {
259 // we're keeping all of the local cache, simply use it as the initial buffer
260 initialBuffers = this.localWriteCache;
261 }
262 // Append the old upstream to the new
263 const append = this.writeBuffers;
264 this.writeBuffers = initialBuffers;
265 for (const buf of append) {
266 this.writeBuffers.push(buf);
267 }
268 // reset last buffers sent
269 __classPrivateFieldGet(this, _Upload_instances, "m", _Upload_resetLocalBuffersCache).call(this);
270 }
271 /**
272 * Retrieves data from upstream's buffer.
273 *
274 * @param limit The maximum amount to return from the buffer.
275 */
276 *pullFromChunkBuffer(limit) {
277 while (limit) {
278 const buf = this.writeBuffers.shift();
279 if (!buf)
280 break;
281 let bufToYield = buf;
282 if (buf.byteLength > limit) {
283 bufToYield = buf.subarray(0, limit);
284 this.writeBuffers.unshift(buf.subarray(limit));
285 limit = 0;
286 }
287 else {
288 limit -= buf.byteLength;
289 }
290 yield bufToYield;
291 // Notify upstream we've read from the buffer and we're able to consume
292 // more. It can also potentially send more data down as we're currently
293 // iterating.
294 this.emit('readFromChunkBuffer');
295 }
296 }
297 /**
298 * A handler for determining if data is ready to be read from upstream.
299 *
300 * @returns If there will be more chunks to read in the future
301 */
302 async waitForNextChunk() {
303 const willBeMoreChunks = await new Promise(resolve => {
304 // There's data available - it should be digested
305 if (this.writeBuffers.length) {
306 return resolve(true);
307 }
308 // The upstream writable ended, we shouldn't expect any more data.
309 if (this.upstreamEnded) {
310 return resolve(false);
311 }
312 // Nothing immediate seems to be determined. We need to prepare some
313 // listeners to determine next steps...
314 const wroteToChunkBufferCallback = () => {
315 removeListeners();
316 return resolve(true);
317 };
318 const upstreamFinishedCallback = () => {
319 removeListeners();
320 // this should be the last chunk, if there's anything there
321 if (this.writeBuffers.length)
322 return resolve(true);
323 return resolve(false);
324 };
325 // Remove listeners when we're ready to callback.
326 const removeListeners = () => {
327 this.removeListener('wroteToChunkBuffer', wroteToChunkBufferCallback);
328 this.removeListener('upstreamFinished', upstreamFinishedCallback);
329 };
330 // If there's data recently written it should be digested
331 this.once('wroteToChunkBuffer', wroteToChunkBufferCallback);
332 // If the upstream finishes let's see if there's anything to grab
333 this.once('upstreamFinished', upstreamFinishedCallback);
334 });
335 return willBeMoreChunks;
336 }
337 /**
338 * Reads data from upstream up to the provided `limit`.
339 * Ends when the limit has reached or no data is expected to be pushed from upstream.
340 *
341 * @param limit The most amount of data this iterator should return. `Infinity` by default.
342 */
343 async *upstreamIterator(limit = Infinity) {
344 // read from upstream chunk buffer
345 while (limit && (await this.waitForNextChunk())) {
346 // read until end or limit has been reached
347 for (const chunk of this.pullFromChunkBuffer(limit)) {
348 limit -= chunk.byteLength;
349 yield chunk;
350 }
351 }
352 }
353 createURI(callback) {
354 if (!callback) {
355 return this.createURIAsync();
356 }
357 this.createURIAsync().then(r => callback(null, r), callback);
358 }
359 async createURIAsync() {
360 const metadata = { ...this.metadata };
361 const headers = {};
362 // Delete content length and content type from metadata if they exist.
363 // These are headers and should not be sent as part of the metadata.
364 if (metadata.contentLength) {
365 headers['X-Upload-Content-Length'] = metadata.contentLength.toString();
366 delete metadata.contentLength;
367 }
368 if (metadata.contentType) {
369 headers['X-Upload-Content-Type'] = metadata.contentType;
370 delete metadata.contentType;
371 }
372 let googAPIClient = `${(0, util_js_1.getRuntimeTrackingString)()} gccl/${packageJson.version}-${(0, util_js_1.getModuleFormat)()} gccl-invocation-id/${this.currentInvocationId.uri}`;
373 if (__classPrivateFieldGet(this, _Upload_gcclGcsCmd, "f")) {
374 googAPIClient += ` gccl-gcs-cmd/${__classPrivateFieldGet(this, _Upload_gcclGcsCmd, "f")}`;
375 }
376 // Check if headers already exist before creating new ones
377 const reqOpts = {
378 method: 'POST',
379 url: [this.baseURI, this.bucket, 'o'].join('/'),
380 params: Object.assign({
381 name: this.file,
382 uploadType: 'resumable',
383 }, this.params),
384 data: metadata,
385 headers: {
386 'User-Agent': (0, util_js_1.getUserAgentString)(),
387 'x-goog-api-client': googAPIClient,
388 ...headers,
389 },
390 };
391 if (metadata.contentLength) {
392 reqOpts.headers['X-Upload-Content-Length'] =
393 metadata.contentLength.toString();
394 }
395 if (metadata.contentType) {
396 reqOpts.headers['X-Upload-Content-Type'] = metadata.contentType;
397 }
398 if (typeof this.generation !== 'undefined') {
399 reqOpts.params.ifGenerationMatch = this.generation;
400 }
401 if (this.kmsKeyName) {
402 reqOpts.params.kmsKeyName = this.kmsKeyName;
403 }
404 if (this.predefinedAcl) {
405 reqOpts.params.predefinedAcl = this.predefinedAcl;
406 }
407 if (this.origin) {
408 reqOpts.headers.Origin = this.origin;
409 }
410 const uri = await (0, async_retry_1.default)(async (bail) => {
411 var _a, _b, _c;
412 try {
413 const res = await this.makeRequest(reqOpts);
414 // We have successfully got a URI we can now create a new invocation id
415 this.currentInvocationId.uri = uuid.v4();
416 return res.headers.location;
417 }
418 catch (err) {
419 const e = err;
420 const apiError = {
421 code: (_a = e.response) === null || _a === void 0 ? void 0 : _a.status,
422 name: (_b = e.response) === null || _b === void 0 ? void 0 : _b.statusText,
423 message: (_c = e.response) === null || _c === void 0 ? void 0 : _c.statusText,
424 errors: [
425 {
426 reason: e.code,
427 },
428 ],
429 };
430 if (this.retryOptions.maxRetries > 0 &&
431 this.retryOptions.retryableErrorFn(apiError)) {
432 throw e;
433 }
434 else {
435 return bail(e);
436 }
437 }
438 }, {
439 retries: this.retryOptions.maxRetries,
440 factor: this.retryOptions.retryDelayMultiplier,
441 maxTimeout: this.retryOptions.maxRetryDelay * 1000, //convert to milliseconds
442 maxRetryTime: this.retryOptions.totalTimeout * 1000, //convert to milliseconds
443 });
444 this.uri = uri;
445 this.offset = 0;
446 // emit the newly generated URI for future reuse, if necessary.
447 this.emit('uri', uri);
448 return uri;
449 }
450 async continueUploading() {
451 var _a;
452 (_a = this.offset) !== null && _a !== void 0 ? _a : (await this.getAndSetOffset());
453 return this.startUploading();
454 }
455 async startUploading() {
456 const multiChunkMode = !!this.chunkSize;
457 let responseReceived = false;
458 this.numChunksReadInRequest = 0;
459 if (!this.offset) {
460 this.offset = 0;
461 }
462 // Check if the offset (server) is too far behind the current stream
463 if (this.offset < this.numBytesWritten) {
464 const delta = this.numBytesWritten - this.offset;
465 const message = `The offset is lower than the number of bytes written. The server has ${this.offset} bytes and while ${this.numBytesWritten} bytes has been uploaded - thus ${delta} bytes are missing. Stopping as this could result in data loss. Initiate a new upload to continue.`;
466 this.emit('error', new RangeError(message));
467 return;
468 }
469 // Check if we should 'fast-forward' to the relevant data to upload
470 if (this.numBytesWritten < this.offset) {
471 // 'fast-forward' to the byte where we need to upload.
472 // only push data from the byte after the one we left off on
473 const fastForwardBytes = this.offset - this.numBytesWritten;
474 for await (const _chunk of this.upstreamIterator(fastForwardBytes)) {
475 _chunk; // discard the data up until the point we want
476 }
477 this.numBytesWritten = this.offset;
478 }
479 let expectedUploadSize = undefined;
480 // Set `expectedUploadSize` to `contentLength - this.numBytesWritten`, if available
481 if (typeof this.contentLength === 'number') {
482 expectedUploadSize = this.contentLength - this.numBytesWritten;
483 }
484 // `expectedUploadSize` should be no more than the `chunkSize`.
485 // It's possible this is the last chunk request for a multiple
486 // chunk upload, thus smaller than the chunk size.
487 if (this.chunkSize) {
488 expectedUploadSize = expectedUploadSize
489 ? Math.min(this.chunkSize, expectedUploadSize)
490 : this.chunkSize;
491 }
492 // A queue for the upstream data
493 const upstreamQueue = this.upstreamIterator(expectedUploadSize);
494 // The primary read stream for this request. This stream retrieves no more
495 // than the exact requested amount from upstream.
496 const requestStream = new stream_1.Readable({
497 read: async () => {
498 // Don't attempt to retrieve data upstream if we already have a response
499 if (responseReceived)
500 requestStream.push(null);
501 const result = await upstreamQueue.next();
502 if (result.value) {
503 this.numChunksReadInRequest++;
504 if (multiChunkMode) {
505 // save ever buffer used in the request in multi-chunk mode
506 __classPrivateFieldGet(this, _Upload_instances, "m", _Upload_addLocalBufferCache).call(this, result.value);
507 }
508 else {
509 __classPrivateFieldGet(this, _Upload_instances, "m", _Upload_resetLocalBuffersCache).call(this);
510 __classPrivateFieldGet(this, _Upload_instances, "m", _Upload_addLocalBufferCache).call(this, result.value);
511 }
512 this.numBytesWritten += result.value.byteLength;
513 this.emit('progress', {
514 bytesWritten: this.numBytesWritten,
515 contentLength: this.contentLength,
516 });
517 requestStream.push(result.value);
518 }
519 if (result.done) {
520 requestStream.push(null);
521 }
522 },
523 });
524 let googAPIClient = `${(0, util_js_1.getRuntimeTrackingString)()} gccl/${packageJson.version}-${(0, util_js_1.getModuleFormat)()} gccl-invocation-id/${this.currentInvocationId.chunk}`;
525 if (__classPrivateFieldGet(this, _Upload_gcclGcsCmd, "f")) {
526 googAPIClient += ` gccl-gcs-cmd/${__classPrivateFieldGet(this, _Upload_gcclGcsCmd, "f")}`;
527 }
528 const headers = {
529 'User-Agent': (0, util_js_1.getUserAgentString)(),
530 'x-goog-api-client': googAPIClient,
531 };
532 // If using multiple chunk upload, set appropriate header
533 if (multiChunkMode) {
534 // We need to know how much data is available upstream to set the `Content-Range` header.
535 // https://cloud.google.com/storage/docs/performing-resumable-uploads#chunked-upload
536 for await (const chunk of this.upstreamIterator(expectedUploadSize)) {
537 // This will conveniently track and keep the size of the buffers.
538 // We will reach either the expected upload size or the remainder of the stream.
539 __classPrivateFieldGet(this, _Upload_instances, "m", _Upload_addLocalBufferCache).call(this, chunk);
540 }
541 // This is the sum from the `#addLocalBufferCache` calls
542 const bytesToUpload = this.localWriteCacheByteLength;
543 // Important: we want to know if the upstream has ended and the queue is empty before
544 // unshifting data back into the queue. This way we will know if this is the last request or not.
545 const isLastChunkOfUpload = !(await this.waitForNextChunk());
546 // Important: put the data back in the queue for the actual upload
547 this.prependLocalBufferToUpstream();
548 let totalObjectSize = this.contentLength;
549 if (typeof this.contentLength !== 'number' &&
550 isLastChunkOfUpload &&
551 !this.isPartialUpload) {
552 // Let's let the server know this is the last chunk of the object since we didn't set it before.
553 totalObjectSize = bytesToUpload + this.numBytesWritten;
554 }
555 // `- 1` as the ending byte is inclusive in the request.
556 const endingByte = bytesToUpload + this.numBytesWritten - 1;
557 // `Content-Length` for multiple chunk uploads is the size of the chunk,
558 // not the overall object
559 headers['Content-Length'] = bytesToUpload;
560 headers['Content-Range'] =
561 `bytes ${this.offset}-${endingByte}/${totalObjectSize}`;
562 }
563 else {
564 headers['Content-Range'] = `bytes ${this.offset}-*/${this.contentLength}`;
565 }
566 const reqOpts = {
567 method: 'PUT',
568 url: this.uri,
569 headers,
570 body: requestStream,
571 };
572 try {
573 const resp = await this.makeRequestStream(reqOpts);
574 if (resp) {
575 responseReceived = true;
576 await this.responseHandler(resp);
577 }
578 }
579 catch (e) {
580 const err = e;
581 if (this.retryOptions.retryableErrorFn(err)) {
582 this.attemptDelayedRetry({
583 status: NaN,
584 data: err,
585 });
586 return;
587 }
588 this.destroy(err);
589 }
590 }
591 // Process the API response to look for errors that came in
592 // the response body.
593 async responseHandler(resp) {
594 if (resp.data.error) {
595 this.destroy(resp.data.error);
596 return;
597 }
598 // At this point we can safely create a new id for the chunk
599 this.currentInvocationId.chunk = uuid.v4();
600 const moreDataToUpload = await this.waitForNextChunk();
601 const shouldContinueWithNextMultiChunkRequest = this.chunkSize &&
602 resp.status === RESUMABLE_INCOMPLETE_STATUS_CODE &&
603 resp.headers.range &&
604 moreDataToUpload;
605 /**
606 * This is true when we're expecting to upload more data in a future request,
607 * yet the upstream for the upload session has been exhausted.
608 */
609 const shouldContinueUploadInAnotherRequest = this.isPartialUpload &&
610 resp.status === RESUMABLE_INCOMPLETE_STATUS_CODE &&
611 !moreDataToUpload;
612 if (shouldContinueWithNextMultiChunkRequest) {
613 // Use the upper value in this header to determine where to start the next chunk.
614 // We should not assume that the server received all bytes sent in the request.
615 // https://cloud.google.com/storage/docs/performing-resumable-uploads#chunked-upload
616 const range = resp.headers.range;
617 this.offset = Number(range.split('-')[1]) + 1;
618 // We should not assume that the server received all bytes sent in the request.
619 // - https://cloud.google.com/storage/docs/performing-resumable-uploads#chunked-upload
620 const missingBytes = this.numBytesWritten - this.offset;
621 if (missingBytes) {
622 // As multi-chunk uploads send one chunk per request and pulls one
623 // chunk into the pipeline, prepending the missing bytes back should
624 // be fine for the next request.
625 this.prependLocalBufferToUpstream(missingBytes);
626 this.numBytesWritten -= missingBytes;
627 }
628 else {
629 // No bytes missing - no need to keep the local cache
630 __classPrivateFieldGet(this, _Upload_instances, "m", _Upload_resetLocalBuffersCache).call(this);
631 }
632 // continue uploading next chunk
633 this.continueUploading();
634 }
635 else if (!this.isSuccessfulResponse(resp.status) &&
636 !shouldContinueUploadInAnotherRequest) {
637 const err = new Error('Upload failed');
638 err.code = resp.status;
639 err.name = 'Upload failed';
640 if (resp === null || resp === void 0 ? void 0 : resp.data) {
641 err.errors = [resp === null || resp === void 0 ? void 0 : resp.data];
642 }
643 this.destroy(err);
644 }
645 else {
646 // no need to keep the cache
647 __classPrivateFieldGet(this, _Upload_instances, "m", _Upload_resetLocalBuffersCache).call(this);
648 if (resp && resp.data) {
649 resp.data.size = Number(resp.data.size);
650 }
651 this.emit('metadata', resp.data);
652 // Allow the object (Upload) to continue naturally so the user's
653 // "finish" event fires.
654 this.emit('uploadFinished');
655 }
656 }
657 /**
658 * Check the status of an existing resumable upload.
659 *
660 * @param cfg A configuration to use. `uri` is required.
661 * @returns the current upload status
662 */
663 async checkUploadStatus(config = {}) {
664 let googAPIClient = `${(0, util_js_1.getRuntimeTrackingString)()} gccl/${packageJson.version}-${(0, util_js_1.getModuleFormat)()} gccl-invocation-id/${this.currentInvocationId.checkUploadStatus}`;
665 if (__classPrivateFieldGet(this, _Upload_gcclGcsCmd, "f")) {
666 googAPIClient += ` gccl-gcs-cmd/${__classPrivateFieldGet(this, _Upload_gcclGcsCmd, "f")}`;
667 }
668 const opts = {
669 method: 'PUT',
670 url: this.uri,
671 headers: {
672 'Content-Length': 0,
673 'Content-Range': 'bytes */*',
674 'User-Agent': (0, util_js_1.getUserAgentString)(),
675 'x-goog-api-client': googAPIClient,
676 },
677 };
678 try {
679 const resp = await this.makeRequest(opts);
680 // Successfully got the offset we can now create a new offset invocation id
681 this.currentInvocationId.checkUploadStatus = uuid.v4();
682 return resp;
683 }
684 catch (e) {
685 if (config.retry === false ||
686 !(e instanceof Error) ||
687 !this.retryOptions.retryableErrorFn(e)) {
688 throw e;
689 }
690 const retryDelay = this.getRetryDelay();
691 if (retryDelay <= 0) {
692 throw e;
693 }
694 await new Promise(res => setTimeout(res, retryDelay));
695 return this.checkUploadStatus(config);
696 }
697 }
698 async getAndSetOffset() {
699 try {
700 // we want to handle retries in this method.
701 const resp = await this.checkUploadStatus({ retry: false });
702 if (resp.status === RESUMABLE_INCOMPLETE_STATUS_CODE) {
703 if (typeof resp.headers.range === 'string') {
704 this.offset = Number(resp.headers.range.split('-')[1]) + 1;
705 return;
706 }
707 }
708 this.offset = 0;
709 }
710 catch (e) {
711 const err = e;
712 if (this.retryOptions.retryableErrorFn(err)) {
713 this.attemptDelayedRetry({
714 status: NaN,
715 data: err,
716 });
717 return;
718 }
719 this.destroy(err);
720 }
721 }
722 async makeRequest(reqOpts) {
723 if (this.encryption) {
724 reqOpts.headers = reqOpts.headers || {};
725 reqOpts.headers['x-goog-encryption-algorithm'] = 'AES256';
726 reqOpts.headers['x-goog-encryption-key'] = this.encryption.key.toString();
727 reqOpts.headers['x-goog-encryption-key-sha256'] =
728 this.encryption.hash.toString();
729 }
730 if (this.userProject) {
731 reqOpts.params = reqOpts.params || {};
732 reqOpts.params.userProject = this.userProject;
733 }
734 // Let gaxios know we will handle a 308 error code ourselves.
735 reqOpts.validateStatus = (status) => {
736 return (this.isSuccessfulResponse(status) ||
737 status === RESUMABLE_INCOMPLETE_STATUS_CODE);
738 };
739 const combinedReqOpts = {
740 ...this.customRequestOptions,
741 ...reqOpts,
742 headers: {
743 ...this.customRequestOptions.headers,
744 ...reqOpts.headers,
745 },
746 };
747 const res = await this.authClient.request(combinedReqOpts);
748 if (res.data && res.data.error) {
749 throw res.data.error;
750 }
751 return res;
752 }
753 async makeRequestStream(reqOpts) {
754 const controller = new abort_controller_1.default();
755 const errorCallback = () => controller.abort();
756 this.once('error', errorCallback);
757 if (this.userProject) {
758 reqOpts.params = reqOpts.params || {};
759 reqOpts.params.userProject = this.userProject;
760 }
761 reqOpts.signal = controller.signal;
762 reqOpts.validateStatus = () => true;
763 const combinedReqOpts = {
764 ...this.customRequestOptions,
765 ...reqOpts,
766 headers: {
767 ...this.customRequestOptions.headers,
768 ...reqOpts.headers,
769 },
770 };
771 const res = await this.authClient.request(combinedReqOpts);
772 const successfulRequest = this.onResponse(res);
773 this.removeListener('error', errorCallback);
774 return successfulRequest ? res : null;
775 }
776 /**
777 * @return {bool} is the request good?
778 */
779 onResponse(resp) {
780 if (resp.status !== 200 &&
781 this.retryOptions.retryableErrorFn({
782 code: resp.status,
783 message: resp.statusText,
784 name: resp.statusText,
785 })) {
786 this.attemptDelayedRetry(resp);
787 return false;
788 }
789 this.emit('response', resp);
790 return true;
791 }
792 /**
793 * @param resp GaxiosResponse object from previous attempt
794 */
795 attemptDelayedRetry(resp) {
796 if (this.numRetries < this.retryOptions.maxRetries) {
797 if (resp.status === NOT_FOUND_STATUS_CODE &&
798 this.numChunksReadInRequest === 0) {
799 this.startUploading();
800 }
801 else {
802 const retryDelay = this.getRetryDelay();
803 if (retryDelay <= 0) {
804 this.destroy(new Error(`Retry total time limit exceeded - ${JSON.stringify(resp.data)}`));
805 return;
806 }
807 // Unshift the local cache back in case it's needed for the next request.
808 this.numBytesWritten -= this.localWriteCacheByteLength;
809 this.prependLocalBufferToUpstream();
810 // We don't know how much data has been received by the server.
811 // `continueUploading` will recheck the offset via `getAndSetOffset`.
812 // If `offset` < `numberBytesReceived` then we will raise a RangeError
813 // as we've streamed too much data that has been missed - this should
814 // not be the case for multi-chunk uploads as `lastChunkSent` is the
815 // body of the entire request.
816 this.offset = undefined;
817 setTimeout(this.continueUploading.bind(this), retryDelay);
818 }
819 this.numRetries++;
820 }
821 else {
822 this.destroy(new Error(`Retry limit exceeded - ${JSON.stringify(resp.data)}`));
823 }
824 }
825 /**
826 * The amount of time to wait before retrying the request, in milliseconds.
827 * If negative, do not retry.
828 *
829 * @returns the amount of time to wait, in milliseconds.
830 */
831 getRetryDelay() {
832 const randomMs = Math.round(Math.random() * 1000);
833 const waitTime = Math.pow(this.retryOptions.retryDelayMultiplier, this.numRetries) *
834 1000 +
835 randomMs;
836 const maxAllowableDelayMs = this.retryOptions.totalTimeout * 1000 -
837 (Date.now() - this.timeOfFirstRequest);
838 const maxRetryDelayMs = this.retryOptions.maxRetryDelay * 1000;
839 return Math.min(waitTime, maxRetryDelayMs, maxAllowableDelayMs);
840 }
841 /*
842 * Prepare user-defined API endpoint for compatibility with our API.
843 */
844 sanitizeEndpoint(url) {
845 if (!exports.PROTOCOL_REGEX.test(url)) {
846 url = `https://${url}`;
847 }
848 return url.replace(/\/+$/, ''); // Remove trailing slashes
849 }
850 /**
851 * Check if a given status code is 2xx
852 *
853 * @param status The status code to check
854 * @returns if the status is 2xx
855 */
856 isSuccessfulResponse(status) {
857 return status >= 200 && status < 300;
858 }
859}
860exports.Upload = Upload;
861_Upload_gcclGcsCmd = new WeakMap(), _Upload_instances = new WeakSet(), _Upload_resetLocalBuffersCache = function _Upload_resetLocalBuffersCache() {
862 this.localWriteCache = [];
863 this.localWriteCacheByteLength = 0;
864}, _Upload_addLocalBufferCache = function _Upload_addLocalBufferCache(buf) {
865 this.localWriteCache.push(buf);
866 this.localWriteCacheByteLength += buf.byteLength;
867};
868function upload(cfg) {
869 return new Upload(cfg);
870}
871function createURI(cfg, callback) {
872 const up = new Upload(cfg);
873 if (!callback) {
874 return up.createURI();
875 }
876 up.createURI().then(r => callback(null, r), callback);
877}
878/**
879 * Check the status of an existing resumable upload.
880 *
881 * @param cfg A configuration to use. `uri` is required.
882 * @returns the current upload status
883 */
884function checkUploadStatus(cfg) {
885 const up = new Upload(cfg);
886 return up.checkUploadStatus();
887}