123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Uploader = void 0;
- const lodash_1 = require("lodash");
- const abort_controller_1 = require("abort-controller");
- const clc = require("colorette");
- const crypto = require("crypto");
- const fs = require("fs");
- const path = require("path");
- const zlib = require("zlib");
- const apiv2_1 = require("../../apiv2");
- const queue_1 = require("../../throttler/queue");
- const api_1 = require("../../api");
- const hashcache_1 = require("./hashcache");
- const logger_1 = require("../../logger");
- const error_1 = require("../../error");
- const MIN_UPLOAD_TIMEOUT = 30000;
- const MAX_UPLOAD_TIMEOUT = 7200000;
- function progressMessage(message, current, total) {
- current = Math.min(current, total);
- const percent = Math.floor(((current * 1.0) / total) * 100).toString();
- return `${message} [${current}/${total}] (${clc.bold(clc.green(`${percent}%`))})`;
- }
- class Uploader {
- constructor(options) {
- this.hashClient = new apiv2_1.Client({
- urlPrefix: api_1.hostingApiOrigin,
- auth: true,
- apiVersion: "v1beta1",
- });
- this.version = options.version;
- this.cwd = options.cwd || process.cwd();
- this.projectRoot = options.projectRoot;
- this.gzipLevel = options.gzipLevel || 9;
- this.hashQueue = new queue_1.Queue({
- name: "hashQueue",
- concurrency: options.hashConcurrency || 50,
- handler: this.hashHandler.bind(this),
- });
- this.populateBatchSize = options.populateBatchSize || 1000;
- this.populateBatch = {};
- this.populateQueue = new queue_1.Queue({
- name: "populateQueue",
- concurrency: options.populateConcurrency || 10,
- handler: this.populateHandler.bind(this),
- retries: 3,
- });
- this.uploadQueue = new queue_1.Queue({
- name: "uploadQueue",
- concurrency: options.uploadConcurrency || 200,
- handler: this.uploadHandler.bind(this),
- retries: 5,
- });
- this.public = options.public || this.cwd;
- this.files = options.files;
- this.fileCount = this.files.length;
- this.cache = (0, hashcache_1.load)(this.projectRoot, this.hashcacheName());
- this.cacheNew = new Map();
- this.sizeMap = {};
- this.hashMap = {};
- this.pathMap = {};
- }
- hashcacheName() {
- return Buffer.from(path.relative(this.projectRoot, this.public))
- .toString("base64")
- .replace(/=+$/, "");
- }
- async start() {
- if (this.files.length === 0) {
- return;
- }
- for (const f of this.files) {
- this.hashQueue.add(f);
- }
- this.hashQueue.close();
- this.hashQueue.process();
- this.hashQueue
- .wait()
- .then(this.queuePopulate.bind(this))
- .then(() => {
- (0, hashcache_1.dump)(this.projectRoot, this.hashcacheName(), this.cacheNew);
- logger_1.logger.debug("[hosting][hash queue][FINAL]", this.hashQueue.stats());
- this.populateQueue.close();
- return this.populateQueue.wait();
- })
- .then(() => {
- logger_1.logger.debug("[hosting][populate queue][FINAL]", this.populateQueue.stats());
- logger_1.logger.debug("[hosting] uploads queued:", this.uploadQueue.stats().total);
- this.uploadQueue.close();
- });
- this.uploadQueue.wait().catch((err) => {
- if (err.message.includes("content hash")) {
- logger_1.logger.debug("[hosting][upload queue] upload failed with content hash error. Deleting hash cache");
- (0, hashcache_1.dump)(this.projectRoot, this.hashcacheName(), new Map());
- }
- });
- const fin = (err) => {
- logger_1.logger.debug("[hosting][upload queue][FINAL]", this.uploadQueue.stats());
- if (err) {
- throw err;
- }
- };
- return this.wait().then(fin).catch(fin);
- }
- async wait() {
- await Promise.all([this.hashQueue.wait(), this.populateQueue.wait(), this.uploadQueue.wait()]);
- }
- statusMessage() {
- if (!this.hashQueue.finished) {
- return progressMessage("hashing files", this.hashQueue.complete, this.fileCount);
- }
- else if (!this.populateQueue.finished) {
- return progressMessage("adding files to version", this.populateQueue.complete * 1000, this.fileCount);
- }
- else if (!this.uploadQueue.finished) {
- return progressMessage("uploading new files", this.uploadQueue.complete, this.uploadQueue.stats().total);
- }
- else {
- return "upload complete";
- }
- }
- async hashHandler(filePath) {
- const stats = fs.statSync(path.resolve(this.public, filePath));
- const mtime = stats.mtime.getTime();
- this.sizeMap[filePath] = stats.size;
- const cached = this.cache.get(filePath);
- if (cached && cached.mtime === mtime) {
- this.cacheNew.set(filePath, cached);
- this.addHash(filePath, cached.hash);
- return;
- }
- const fstream = this.zipStream(filePath);
- const hash = crypto.createHash("sha256");
- fstream.pipe(hash);
- return new Promise((resolve, reject) => {
- fstream.on("end", resolve);
- fstream.on("error", reject);
- }).then(() => {
- const hashVal = hash.read().toString("hex");
- this.cacheNew.set(filePath, { mtime: mtime, hash: hashVal });
- this.addHash(filePath, hashVal);
- });
- }
- addHash(filePath, hash) {
- this.hashMap[hash] = filePath;
- this.pathMap[filePath] = hash;
- this.populateBatch["/" + filePath] = hash;
- const curBatchSize = (0, lodash_1.size)(this.populateBatch);
- if (curBatchSize > 0 && curBatchSize % this.populateBatchSize === 0) {
- this.queuePopulate();
- }
- }
- queuePopulate() {
- const pop = this.populateBatch;
- this.populateQueue.add(pop);
- this.populateBatch = {};
- this.populateQueue.process();
- }
- async populateHandler(batch) {
- const res = await this.hashClient.post(`/${this.version}:populateFiles`, { files: batch });
- this.uploadUrl = res.body.uploadUrl;
- this.uploadClient = new apiv2_1.Client({ urlPrefix: this.uploadUrl, auth: true });
- this.addUploads(res.body.uploadRequiredHashes || []);
- }
- addUploads(hashes) {
- for (const hash of hashes) {
- this.uploadQueue.add(hash);
- }
- this.uploadQueue.process();
- }
- async uploadHandler(toUpload) {
- if (!this.uploadClient) {
- throw new error_1.FirebaseError("No upload client available.", { exit: 2 });
- }
- const controller = new abort_controller_1.default();
- const timeout = setTimeout(() => {
- controller.abort();
- }, this.uploadTimeout(this.hashMap[toUpload]));
- const res = await this.uploadClient.request({
- method: "POST",
- path: `/${toUpload}`,
- body: this.zipStream(this.hashMap[toUpload]),
- resolveOnHTTPError: true,
- responseType: "stream",
- signal: controller.signal,
- });
- clearTimeout(timeout);
- if (this.uploadQueue.cursor % 100 === 0) {
- logger_1.logger.debug("[hosting][upload]", this.uploadQueue.stats());
- }
- if (res.status !== 200) {
- const errorMessage = await res.response.text();
- logger_1.logger.debug(`[hosting][upload] ${this.hashMap[toUpload]} (${toUpload}) HTTP ERROR ${res.status}: headers=${JSON.stringify(res.response.headers)} ${errorMessage}`);
- throw new Error(`Unexpected error while uploading file: ${errorMessage}`);
- }
- }
- zipStream(filePath) {
- const file = fs.createReadStream(path.resolve(this.public, filePath));
- const gzip = zlib.createGzip({ level: this.gzipLevel });
- return file.pipe(gzip);
- }
- uploadTimeout(filePath) {
- const size = this.sizeMap[filePath] || 0;
- return Math.min(Math.max(Math.round(size / 1000) * 20, MIN_UPLOAD_TIMEOUT), MAX_UPLOAD_TIMEOUT);
- }
- }
- exports.Uploader = Uploader;
|