UNPKG

@codechecks/client

Version:

Open source platform for code review automation

306 lines 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const requestPromise = require("request-promise"); const graceful_fs_1 = require("graceful-fs"); // it handles max open files limits nicely const path_1 = require("path"); const glob = require("glob"); const mkdirp = require("mkdirp"); const utils_1 = require("./utils"); const errors_1 = require("request-promise/errors"); const stream_1 = require("stream"); const bluebird_1 = require("bluebird"); const errors_2 = require("./utils/errors"); const logger_1 = require("./logger"); const request = require("request"); const DEFAULT_HOST = "https://api.codechecks.io/v1"; // we limit concurrent connections to avoid DOSing our own backend // @note: since we use two different request objects total number of connections can reach 2x this value but this should not be a problem const MAX_CONNECTIONS = 20; // we automatically retry failed getFiles and saveFiles calls const MAX_RETRY = 10; class Api { constructor(options) { this.requestPromise = requestPromise.defaults({ headers: { Authorization: `Bearer ${options.token}`, }, baseUrl: options.host || DEFAULT_HOST, pool: { maxSockets: MAX_CONNECTIONS, }, timeout: 10000, }); this.requestRaw = request.defaults({ headers: { Authorization: `Bearer ${options.token}`, }, baseUrl: options.host || DEFAULT_HOST, pool: { maxSockets: MAX_CONNECTIONS, }, timeout: 10000, }); } async saveFileFromStream(fullKey, getContents, projectSlug, contentType) { return this.saveFileWithRetry(fullKey, getContents, MAX_RETRY, projectSlug, contentType); } async getFileAsString(fullKey, projectSlug) { return await this.getFileWithRetry(fullKey, MAX_RETRY, projectSlug); } async makeCommitCheck(sha, baseSha, checks, projectSlug) { return await this.requestPromise.post(projectSlug ? "/public/gh/commit-check" : "/gh/commit-check", { qs: { projectSlug, }, body: { sha, baseSha, checks: checks.map(c => { let detailsUrl = undefined; if (c.detailsUrl) { if (typeof c.detailsUrl === "string") { detailsUrl = { url: c.detailsUrl, }; } else { detailsUrl = { url: c.detailsUrl.url, label: c.detailsUrl.label, }; } } return Object.assign({}, c, { detailsUrl }); }), }, json: true, }); } async prInfo(prId) { return await this.requestPromise.get("/gh/pr-info", { qs: { id: prId, }, json: true, }); } async prInfoPublic(prId, projectSlug) { return await this.requestPromise.get("/public/gh/pr-info", { qs: { id: prId, projectSlug, }, json: true, }); } async projectInfo() { return await this.requestPromise.get("/projects/info", { json: true, }); } async projectInfoPublic(projectSlug) { return await this.requestPromise.get("/public/projects/info", { qs: { projectSlug, }, json: true, }); } /* #region get/save JSON values */ async getValue(name, key, projectSlug) { try { const res = await this.getFileAsString(`${key}/${name}.json`, projectSlug); if (!res) { return undefined; } return JSON.parse(res).value; } catch (e) { if (e instanceof errors_1.StatusCodeError && e.statusCode === 404) { return undefined; } throw e; } } async saveValue(name, value, key, projectSlug) { await this.saveFileFromStream(`${key}/${name}.json`, () => { const stream = new stream_1.Readable(); stream.push(JSON.stringify({ value })); stream.push(null); return stream; }, projectSlug, "application/json"); } /* #endregion */ /* #region get/save files */ async getFile(fullKey, destinationPath, projectSlug) { utils_1.ensureAbsolutePath(destinationPath); try { const fileString = await this.getFileAsString(fullKey, projectSlug); writeFile(destinationPath, fileString || ""); } catch (e) { if (e.statusCode !== 404) { throw e; } } } async saveFile(fullKey, sourcePath, projectSlug) { utils_1.ensureAbsolutePath(sourcePath); return await this.saveFileFromStream(fullKey, () => graceful_fs_1.createReadStream(sourcePath), projectSlug); } /* #endregion */ /* #region get/save directories */ async getDirectory(name, destinationPath, key, projectSlug) { const fullKeys = await this.getCollectionList(`${key}/${name}`, projectSlug); const promises = bluebird_1.Promise.map(fullKeys, async (fullKey) => { const file = await this.getFileAsString(fullKey, projectSlug); // fullKey looks like: SHA/name/x/y/z so we need to slice `SHA/name` path const finalPath = path_1.join(destinationPath, fullKey.substring(key.length + name.length + "/".length)); writeFile(finalPath, file || ""); }, { concurrency: MAX_CONNECTIONS }); await bluebird_1.Promise.all(promises); } async getCollectionList(path, projectSlug) { let continuationToken; let finalData = []; do { let response; if (projectSlug) { response = JSON.parse(await this.requestPromise.get(`public/directories/paged/${path}`, { qs: { projectSlug, continuationToken, }, })); } else { response = JSON.parse(await this.requestPromise.get(`directories/paged/${path}`, { qs: { continuationToken, }, })); } continuationToken = response.continuationToken; logger_1.logger.debug("Continuation token for directory listing: ", continuationToken); finalData.push(...response.data); } while (continuationToken); return finalData; } async saveDirectory(name, directoryPath, key, projectSlug) { utils_1.ensureAbsolutePath(directoryPath); const result = utils_1.runOrCatchError(() => graceful_fs_1.lstatSync(directoryPath)); if (result && !result.isDirectory) { throw errors_2.crash(`${directoryPath} is not a directory!`); } const allFiles = glob.sync(`${directoryPath}/**/*`, { absolute: true, follow: false, nodir: true, }); const promises = bluebird_1.Promise.map(allFiles, absPath => { const relPath = path_1.relative(directoryPath, absPath); return this.saveFileFromStream(`${key}/${name}/${relPath}`, () => graceful_fs_1.createReadStream(absPath), projectSlug); }, { concurrency: MAX_CONNECTIONS }); await bluebird_1.Promise.all(promises); } /* #endregion */ async getFileWithRetry(_fullKey, retry, projectSlug) { const fullKey = encodeURI(_fullKey); let file; try { logger_1.logger.debug("Getting file: ", fullKey); if (this.sharedCtx.isLocalMode && this.sharedCtx.isLocalMode.isOffline) { logger_1.logger.debug("Offline mode. Skipping..."); return undefined; } if (projectSlug) { file = await this.requestPromise.get(`public/files/${fullKey}`, { qs: { projectSlug, }, encoding: "binary", }); } else { file = await this.requestPromise.get(`files/${fullKey}`, { encoding: "binary", }); } logger_1.logger.debug("Getting file: ", fullKey, "DONE"); return file; } catch (e) { if (e.statusCode === 404 || retry === 0) { throw e; } else { await bluebird_1.delay(1000); logger_1.logger.debug("Getting file: ", fullKey, "RETRYING", retry); return this.getFileWithRetry(_fullKey, retry - 1); } } } async saveFileWithRetry(_fullKey, getContents, retry, projectSlug, contentType) { const fullKey = encodeURI(_fullKey); try { await new Promise((resolve, reject) => { logger_1.logger.debug("Saving file: ", fullKey); if (this.sharedCtx.isLocalMode && this.sharedCtx.isLocalMode.isOffline) { logger_1.logger.debug("Offline mode. Skipping..."); return; } getContents() .pipe(projectSlug ? this.requestRaw.post(`public/files/${fullKey}`, contentType ? { headers: { ["Content-Type"]: contentType }, qs: { projectSlug } } : { qs: { projectSlug } }) : this.requestRaw.post(`files/${fullKey}`, contentType ? { headers: { ["Content-Type"]: contentType } } : undefined)) .on("response", resp => { if (resp.statusCode >= 200 && resp.statusCode < 300) { logger_1.logger.debug("Saving file: ", fullKey, "DONE"); resolve(); } else { logger_1.logger.debug("Invalid response", resp.statusCode, resp.body); reject(resp); } }) .on("error", e => { reject(e); }); }); } catch (e) { if (retry === 0) { throw e; } else { await bluebird_1.delay(1000); logger_1.logger.debug("Saving file: ", fullKey, "RETRYING", retry); return this.saveFileWithRetry(_fullKey, getContents, retry - 1, projectSlug, contentType); } } } } exports.Api = Api; function getApiOptions(requiresSecret) { if (requiresSecret) { return { host: process.env["CC_HOST"], }; } else { return { token: utils_1.getRequiredEnv("CC_SECRET"), host: process.env["CC_HOST"], }; } } exports.getApiOptions = getApiOptions; function writeFile(path, fileAsString) { mkdirp.sync(path_1.dirname(path)); const writeStream = graceful_fs_1.createWriteStream(path); writeStream.write(fileAsString, "binary"); writeStream.end(); } //# sourceMappingURL=api.js.map