@codechecks/client
Version:
Open source platform for code review automation
306 lines • 11.6 kB
JavaScript
;
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