GSI - Employe Self Service Mobile
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

207 lines
8.2 KiB

"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;