resumable-upload.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  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.
  15. var __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. }));
  26. var __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. });
  31. var __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. };
  38. var __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. };
  44. var __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. };
  49. var __importDefault = (this && this.__importDefault) || function (mod) {
  50. return (mod && mod.__esModule) ? mod : { "default": mod };
  51. };
  52. var _Upload_instances, _Upload_gcclGcsCmd, _Upload_resetLocalBuffersCache, _Upload_addLocalBufferCache;
  53. Object.defineProperty(exports, "__esModule", { value: true });
  54. exports.Upload = exports.PROTOCOL_REGEX = void 0;
  55. exports.upload = upload;
  56. exports.createURI = createURI;
  57. exports.checkUploadStatus = checkUploadStatus;
  58. const abort_controller_1 = __importDefault(require("abort-controller"));
  59. const crypto_1 = require("crypto");
  60. const gaxios = __importStar(require("gaxios"));
  61. const google_auth_library_1 = require("google-auth-library");
  62. const stream_1 = require("stream");
  63. const async_retry_1 = __importDefault(require("async-retry"));
  64. const uuid = __importStar(require("uuid"));
  65. const util_js_1 = require("./util.js");
  66. const util_js_2 = require("./nodejs-common/util.js");
  67. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  68. // @ts-ignore
  69. const package_json_helper_cjs_1 = require("./package-json-helper.cjs");
  70. const NOT_FOUND_STATUS_CODE = 404;
  71. const RESUMABLE_INCOMPLETE_STATUS_CODE = 308;
  72. const packageJson = (0, package_json_helper_cjs_1.getPackageJSON)();
  73. exports.PROTOCOL_REGEX = /^(\w*):\/\//;
  74. class 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. }
  860. exports.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. };
  868. function upload(cfg) {
  869. return new Upload(cfg);
  870. }
  871. function 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. */
  884. function checkUploadStatus(cfg) {
  885. const up = new Upload(cfg);
  886. return up.checkUploadStatus();
  887. }