diff --git a/src/AnonymizedFile.ts b/src/AnonymizedFile.ts index aa9cdc2..454a148 100644 --- a/src/AnonymizedFile.ts +++ b/src/AnonymizedFile.ts @@ -43,7 +43,10 @@ export default class AnonymizedFile { constructor(data: { repository: Repository; anonymizedPath: string }) { this.repository = data.repository; if (!this.repository.options.terms) - throw new AnonymousError("terms_not_specified"); + throw new AnonymousError("terms_not_specified", { + object: this, + httpStatus: 400, + }); this.anonymizedPath = data.anonymizedPath; } @@ -54,7 +57,11 @@ export default class AnonymizedFile { */ async originalPath(): Promise { if (this._originalPath) return this._originalPath; - if (!this.anonymizedPath) throw new AnonymousError("path_not_specified"); + if (!this.anonymizedPath) + throw new AnonymousError("path_not_specified", { + object: this, + httpStatus: 400, + }); const paths = this.anonymizedPath.trim().split("/"); @@ -70,7 +77,10 @@ export default class AnonymizedFile { continue; } if (!currentAnonymized[fileName]) { - throw new AnonymousError("file_not_found", this); + throw new AnonymousError("file_not_found", { + object: this, + httpStatus: 404, + }); } currentAnonymized = currentAnonymized[fileName]; @@ -103,7 +113,7 @@ export default class AnonymizedFile { currentAnonymized.sha === undefined || currentAnonymized.size === undefined ) { - throw new AnonymousError("folder_not_supported", this); + throw new AnonymousError("folder_not_supported", { object: this }); } const file: TreeFile = currentAnonymized as TreeFile; @@ -114,7 +124,10 @@ export default class AnonymizedFile { // it should never happen const shaTree = tree2sha(currentOriginal); if (!currentAnonymized.sha || !shaTree[file.sha]) { - throw new AnonymousError("file_not_found", this); + throw new AnonymousError("file_not_found", { + object: this, + httpStatus: 404, + }); } this._originalPath = path.join(currentOriginalPath, shaTree[file.sha]); @@ -147,7 +160,10 @@ export default class AnonymizedFile { async content(): Promise { if (this.fileSize && this.fileSize > config.MAX_FILE_SIZE) { - throw new AnonymousError("file_too_big", this); + throw new AnonymousError("file_too_big", { + object: this, + httpStatus: 403, + }); } if (await storage.exists(this.originalCachePath)) { return storage.read(this.originalCachePath); @@ -163,7 +179,11 @@ export default class AnonymizedFile { } get originalCachePath() { - if (!this.originalPath) throw new AnonymousError("path_not_defined"); + if (!this.originalPath) + throw new AnonymousError("path_not_defined", { + object: this, + httpStatus: 400, + }); return path.join(this.repository.originalCachePath, this._originalPath); } diff --git a/src/AnonymousError.ts b/src/AnonymousError.ts index 51bba96..09cf7ad 100644 --- a/src/AnonymousError.ts +++ b/src/AnonymousError.ts @@ -1,18 +1,53 @@ -import { CustomError } from 'ts-custom-error' +import { CustomError } from "ts-custom-error"; +import AnonymizedFile from "./AnonymizedFile"; +import Repository from "./Repository"; +import GitHubBase from "./source/GitHubBase"; +import { GitHubRepository } from "./source/GitHubRepository"; +import User from "./User"; /** * Custom error message */ export default class AnonymousError extends CustomError { - - value: any; + value?: any; + httpStatus?: number; + cause?: Error; - constructor(message: string, value?: any) { + constructor( + message: string, + opt?: { + httpStatus?: number; + cause?: Error; + object?: any; + } + ) { super(message); - this.value = value; + this.value = opt?.object; + this.httpStatus = opt?.httpStatus; + this.cause = opt?.cause; } toString(): string { - return this.message; + let out = ""; + let detail = this.value ? JSON.stringify(this.value) : null; + if (this.value instanceof Repository) { + detail = this.value.repoId; + } else if (this.value instanceof AnonymizedFile) { + detail = `/r/${this.value.repository.repoId}/${this.value.anonymizedPath}`; + } else if (this.value instanceof GitHubRepository) { + detail = `${this.value.fullName}`; + } else if (this.value instanceof User) { + detail = `${this.value.username}`; + } else if (this.value instanceof GitHubBase) { + detail = `${this.value.repository.repoId}`; + } + out += this.message; + if (detail) { + out += `: ${detail}`; + } + if (this.cause) { + out += `\n\tCause by ${this.cause}\n${this.cause.stack}`; + } + return out; } } diff --git a/src/Repository.ts b/src/Repository.ts index f6ffaa4..4663507 100644 --- a/src/Repository.ts +++ b/src/Repository.ts @@ -34,7 +34,10 @@ export default class Repository { this.source = new Zip(data.source, this); break; default: - throw new AnonymousError("unsupported_source", data.source.type); + throw new AnonymousError("unsupported_source", { + object: data.source.type, + httpStatus: 400, + }); } this.owner = new User(new UserModel({ _id: data.owner })); } @@ -110,7 +113,10 @@ export default class Repository { this.status == "removing" || this.status == "removed" ) { - throw new AnonymousError("repository_expired", this); + throw new AnonymousError("repository_expired", { + object: this, + httpStatus: 410, + }); } const fiveMinuteAgo = new Date(); fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5); @@ -119,7 +125,9 @@ export default class Repository { this.status == "preparing" || (this.status == "download" && this._model.statusDate > fiveMinuteAgo) ) { - throw new AnonymousError("repository_not_ready", this); + throw new AnonymousError("repository_not_ready", { + object: this, + }); } } @@ -170,7 +178,9 @@ export default class Repository { ); await this.updateStatus("error", "branch_not_found"); await this.resetSate(); - throw new AnonymousError("branch_not_found", this); + throw new AnonymousError("branch_not_found", { + object: this, + }); } this._model.anonymizeDate = new Date(); console.log(`${this._model.repoId} will be updated to ${newCommit}`); @@ -234,8 +244,9 @@ export default class Repository { /** * Reset/delete the state of the repository */ - async resetSate(status?: RepositoryStatus) { + async resetSate(status?: RepositoryStatus, statusMessage?: string) { if (status) this._model.status = status; + if (statusMessage) this._model.statusMessage = statusMessage; // remove attribute this._model.size = { storage: 0, file: 0 }; this._model.originalFiles = null; diff --git a/src/database/database.ts b/src/database/database.ts index fa5da49..8088378 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -24,6 +24,10 @@ export async function connect() { export async function getRepository(repoId: string) { const data = await AnonymizedRepositoryModel.findOne({ repoId }); - if (!data) throw new AnonymousError("repo_not_found", repoId); + if (!data) + throw new AnonymousError("repo_not_found", { + object: repoId, + httpStatus: 400, + }); return new Repository(data); } diff --git a/src/queue.ts b/src/queue.ts index 0185d73..586a87d 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -1,5 +1,6 @@ import * as Queue from "bull"; import config from "../config"; +import AnonymousError from "./AnonymousError"; import { getRepository } from "./database/database"; import Repository from "./Repository"; @@ -28,7 +29,15 @@ removeQueue.process(5, async (job) => { const repo = await getRepository(job.data.repoId); await repo.remove(); } catch (error) { - console.log("error", error); + if (error instanceof AnonymousError) { + console.error( + "[ERROR]", + error.toString(), + error.stack.split("\n")[1].trim() + ); + } else { + console.error(error); + } } finally { console.log(`${job.data.repoId} is removed`); } @@ -43,7 +52,15 @@ downloadQueue.process(2, async (job) => { job.progress("resetSate"); await repo.anonymize(); } catch (error) { - console.log("error", error); + if (error instanceof AnonymousError) { + console.error( + "[ERROR]", + error.toString(), + error.stack.split("\n")[1].trim() + ); + } else { + console.error(error); + } } finally { console.log(`${job.data.repoId} is downloaded`); } diff --git a/src/routes/conference.ts b/src/routes/conference.ts index 3b5fc16..02b1dfd 100644 --- a/src/routes/conference.ts +++ b/src/routes/conference.ts @@ -67,26 +67,79 @@ router.get("/", async (req: express.Request, res: express.Response) => { }); function validateConferenceForm(conf) { - if (!conf.name) throw new AnonymousError("conf_name_missing"); - if (!conf.conferenceID) throw new AnonymousError("conf_id_missing"); - if (!conf.startDate) throw new AnonymousError("conf_start_date_missing"); - if (!conf.endDate) throw new AnonymousError("conf_end_date_missing"); + if (!conf.name) + throw new AnonymousError("conf_name_missing", { + object: conf, + httpStatus: 400, + }); + if (!conf.conferenceID) + throw new AnonymousError("conf_id_missing", { + object: conf, + httpStatus: 400, + }); + if (!conf.startDate) + throw new AnonymousError("conf_start_date_missing", { + object: conf, + httpStatus: 400, + }); + if (!conf.endDate) + throw new AnonymousError("conf_end_date_missing", { + object: conf, + httpStatus: 400, + }); if (new Date(conf.startDate) > new Date(conf.endDate)) - throw new AnonymousError("conf_start_date_invalid"); + throw new AnonymousError("conf_start_date_invalid", { + object: conf, + httpStatus: 400, + }); if (new Date() > new Date(conf.endDate)) - throw new AnonymousError("conf_end_date_invalid"); + throw new AnonymousError("conf_end_date_invalid", { + object: conf, + httpStatus: 400, + }); if (plans.filter((p) => p.id == conf.plan.planID).length != 1) - throw new AnonymousError("invalid_plan"); + throw new AnonymousError("invalid_plan", { + object: conf, + httpStatus: 400, + }); const plan = plans.filter((p) => p.id == conf.plan.planID)[0]; if (plan.pricePerRepo > 0) { const billing = conf.billing; - if (!billing) throw new AnonymousError("billing_missing"); - if (!billing.name) throw new AnonymousError("billing_name_missing"); - if (!billing.email) throw new AnonymousError("billing_email_missing"); - if (!billing.address) throw new AnonymousError("billing_address_missing"); - if (!billing.city) throw new AnonymousError("billing_city_missing"); - if (!billing.zip) throw new AnonymousError("billing_zip_missing"); - if (!billing.country) throw new AnonymousError("billing_country_missing"); + if (!billing) + throw new AnonymousError("billing_missing", { + object: conf, + httpStatus: 400, + }); + if (!billing.name) + throw new AnonymousError("billing_name_missing", { + object: conf, + httpStatus: 400, + }); + if (!billing.email) + throw new AnonymousError("billing_email_missing", { + object: conf, + httpStatus: 400, + }); + if (!billing.address) + throw new AnonymousError("billing_address_missing", { + object: conf, + httpStatus: 400, + }); + if (!billing.city) + throw new AnonymousError("billing_city_missing", { + object: conf, + httpStatus: 400, + }); + if (!billing.zip) + throw new AnonymousError("billing_zip_missing", { + object: conf, + httpStatus: 400, + }); + if (!billing.country) + throw new AnonymousError("billing_country_missing", { + object: conf, + httpStatus: 400, + }); } } @@ -101,7 +154,10 @@ router.post( conferenceID: req.params.conferenceID, }); if (model.owners.indexOf(user.model.id) == -1) - throw new AnonymousError("not_authorized"); + throw new AnonymousError("not_authorized", { + object: req.params.conferenceID, + httpStatus: 401, + }); } validateConferenceForm(req.body); model.name = req.body.name; @@ -146,7 +202,13 @@ router.post( res.send("ok"); } catch (error) { if (error.message?.indexOf(" duplicate key") > -1) { - return handleError(new AnonymousError("conf_id_used"), res); + return handleError( + new AnonymousError("conf_id_used", { + object: req.params.conferenceID, + httpStatus: 400, + }), + res + ); } handleError(error, res); } @@ -161,10 +223,17 @@ router.get( const data = await ConferenceModel.findOne({ conferenceID: req.params.conferenceID, }); - if (!data) throw new AnonymousError("conf_not_found"); + if (!data) + throw new AnonymousError("conf_not_found", { + object: req.params.conferenceID, + httpStatus: 404, + }); const conference = new Conference(data); if (conference.ownerIDs.indexOf(user.model.id) == -1) - throw new AnonymousError("not_authorized"); + throw new AnonymousError("not_authorized", { + object: req.params.conferenceID, + httpStatus: 401, + }); const o: any = conference.toJSON(); o.repositories = (await conference.repositories()).map((r) => r.toJSON()); res.json(o); @@ -182,10 +251,17 @@ router.delete( const data = await ConferenceModel.findOne({ conferenceID: req.params.conferenceID, }); - if (!data) throw new AnonymousError("conf_not_found"); + if (!data) + throw new AnonymousError("conf_not_found", { + object: req.params.conferenceID, + httpStatus: 400, + }); const conference = new Conference(data); if (conference.ownerIDs.indexOf(user.model.id) == -1) - throw new AnonymousError("not_authorized"); + throw new AnonymousError("not_authorized", { + object: req.params.conferenceID, + httpStatus: 401, + }); await conference.remove(); res.send("ok"); } catch (error) { diff --git a/src/routes/file.ts b/src/routes/file.ts index ed63eb4..41e2df3 100644 --- a/src/routes/file.ts +++ b/src/routes/file.ts @@ -1,5 +1,6 @@ import * as express from "express"; import AnonymizedFile from "../AnonymizedFile"; +import AnonymousError from "../AnonymousError"; import { getRepo, handleError } from "./route-utils"; export const router = express.Router(); @@ -24,7 +25,10 @@ router.get( anonymizedPath, }); if (!(await f.isFileSupported())) { - return res.status(500).send({ error: "file_not_supported" }); + throw new AnonymousError("file_not_supported", { + httpStatus: 403, + object: f, + }); } res.attachment( anonymizedPath.substring(anonymizedPath.lastIndexOf("/") + 1) diff --git a/src/routes/repository-private.ts b/src/routes/repository-private.ts index 8cb5f85..365b2b1 100644 --- a/src/routes/repository-private.ts +++ b/src/routes/repository-private.ts @@ -24,15 +24,24 @@ router.post("/claim", async (req: express.Request, res: express.Response) => { const user = await getUser(req); try { if (!req.body.repoId) { - return res.status(500).json({ error: "repoId_not_defined" }); + throw new AnonymousError("repoId_not_defined", { + object: req.body, + httpStatus: 400, + }); } if (!req.body.repoUrl) { - return res.status(500).json({ error: "repoUrl_not_defined" }); + throw new AnonymousError("repoUrl_not_defined", { + object: req.body, + httpStatus: 400, + }); } const repoConfig = await db.getRepository(req.body.repoId); if (repoConfig == null) { - return res.status(500).json({ error: "repo_not_found" }); + throw new AnonymousError("repo_not_found", { + object: req.body, + httpStatus: 404, + }); } const r = gh(req.body.repoUrl); @@ -42,7 +51,10 @@ router.post("/claim", async (req: express.Request, res: express.Response) => { accessToken: user.accessToken, }); if ((repoConfig.source as GitHubBase).githubRepository.id != repo.id) { - return res.status(500).json({ error: "repo_not_found" }); + throw new AnonymousError("repo_not_found", { + object: req.body, + httpStatus: 404, + }); } console.log(`${user.username} claims ${r.repository}.`); @@ -70,7 +82,10 @@ router.post( const user = await getUser(req); if (repo.owner.id != user.id) { - return res.status(401).json({ error: "not_authorized" }); + throw new AnonymousError("not_authorized", { + object: req.params.repoId, + httpStatus: 401, + }); } await repo.updateIfNeeded({ force: true }); res.json({ status: repo.status }); @@ -88,10 +103,17 @@ router.delete( if (!repo) return; // if (repo.status == "removing") return res.json({ status: repo.status }); try { - if (repo.status == "removed") throw new AnonymousError("is_removed"); + if (repo.status == "removed") + throw new AnonymousError("is_removed", { + object: req.params.repoId, + httpStatus: 410, + }); const user = await getUser(req); if (repo.owner.id != user.id) { - return res.status(401).json({ error: "not_authorized" }); + throw new AnonymousError("not_authorized", { + object: req.params.repoId, + httpStatus: 401, + }); } await repo.updateStatus("removing"); await removeQueue.add(repo, { jobId: repo.repoId }); @@ -152,7 +174,12 @@ router.get( repo: req.params.repo, accessToken: user.accessToken, }); - if (!repo) return res.status(404).send({ error: "repo_not_found" }); + if (!repo) { + throw new AnonymousError("repo_not_found", { + object: req.params.repoId, + httpStatus: 404, + }); + } return res.send( await repo.readme({ accessToken: user.accessToken, @@ -174,7 +201,10 @@ router.get("/:repoId/", async (req: express.Request, res: express.Response) => { const user = await getUser(req); if (repo.owner.id != user.id) { - return res.status(401).send({ error: "not_authorized" }); + throw new AnonymousError("not_authorized", { + object: req.params.repoId, + httpStatus: 401, + }); } res.json((await db.getRepository(req.params.repoId)).toJSON()); } catch (error) { @@ -188,22 +218,40 @@ function validateNewRepo(repoUpdate) { !repoUpdate.repoId.match(validCharacters) || repoUpdate.repoId.length < 3 ) { - throw new AnonymousError("invalid_repoId", repoUpdate.repoId); + throw new AnonymousError("invalid_repoId", { + object: repoUpdate, + httpStatus: 400, + }); } if (!repoUpdate.source.branch) { - throw new AnonymousError("branch_not_specified"); + throw new AnonymousError("branch_not_specified", { + object: repoUpdate, + httpStatus: 400, + }); } if (!repoUpdate.source.commit) { - throw new AnonymousError("commit_not_specified"); + throw new AnonymousError("commit_not_specified", { + object: repoUpdate, + httpStatus: 400, + }); } if (!repoUpdate.options) { - throw new AnonymousError("options_not_provided"); + throw new AnonymousError("options_not_provided", { + object: repoUpdate, + httpStatus: 400, + }); } if (!Array.isArray(repoUpdate.terms)) { - throw new AnonymousError("invalid_terms_format", repoUpdate.terms); + throw new AnonymousError("invalid_terms_format", { + object: repoUpdate, + httpStatus: 400, + }); } if (!/^[a-fA-F0-9]+$/.test(repoUpdate.source.commit)) { - throw new AnonymousError("invalid_commit_format", repoUpdate.source.commit); + throw new AnonymousError("invalid_commit_format", { + object: repoUpdate, + httpStatus: 400, + }); } } @@ -248,7 +296,10 @@ router.post( const user = await getUser(req); if (repo.owner.id != user.id) { - return res.status(401).json({ error: "not_authorized" }); + throw new AnonymousError("not_authorized", { + object: req.params.repoId, + httpStatus: 401, + }); } const repoUpdate = req.body; @@ -289,7 +340,10 @@ router.post( new Date() > conf.endDate || conf.status !== "ready" ) { - throw new AnonymousError("conf_not_activated"); + throw new AnonymousError("conf_not_activated", { + object: conf, + httpStatus: 400, + }); } const f = conf.repositories.filter((r) => r.id == repo.model.id); if (f.length) { @@ -346,7 +400,10 @@ router.post("/", async (req: express.Request, res: express.Response) => { if (repo.source.type == "GitHubDownload") { // details.size is in kilobytes if (repository.size > config.MAX_REPO_SIZE) { - return res.status(500).send({ error: "invalid_mode" }); + throw new AnonymousError("invalid_mode", { + object: repository, + httpStatus: 400, + }); } } repo.conference = repoUpdate.conference; @@ -364,7 +421,10 @@ router.post("/", async (req: express.Request, res: express.Response) => { conf.status !== "ready" ) { await repo.remove(); - throw new AnonymousError("conf_not_activated"); + throw new AnonymousError("conf_not_activated", { + object: conf, + httpStatus: 400, + }); } conf.repositories.push({ id: repo.id, @@ -379,7 +439,11 @@ router.post("/", async (req: express.Request, res: express.Response) => { } catch (error) { if (error.message?.indexOf(" duplicate key") > -1) { return handleError( - new AnonymousError("repoId_already_used", repoUpdate.repoId), + new AnonymousError("repoId_already_used", { + httpStatus: 400, + cause: error, + object: repoUpdate, + }), res ); } diff --git a/src/routes/repository-public.ts b/src/routes/repository-public.ts index 87e33d9..39396be 100644 --- a/src/routes/repository-public.ts +++ b/src/routes/repository-public.ts @@ -12,14 +12,25 @@ const router = express.Router(); router.get( "/:repoId/zip", async (req: express.Request, res: express.Response) => { - if (!config.ENABLE_DOWNLOAD) - return res.status(403).send({ error: "download_not_enabled" }); - const repo = await getRepo(req, res); - if (!repo) return; - const pipeline = promisify(stream.pipeline); try { + if (!config.ENABLE_DOWNLOAD) { + throw new AnonymousError("download_not_enabled", { + httpStatus: 403, + object: req.params.repoId, + }); + } + const repo = await getRepo(req, res); + if (!repo) return; + + if (repo.source.type != "GitHubDownload") { + throw new AnonymousError("download_not_enabled", { + httpStatus: 403, + object: req.params.repoId, + }); + } + res.attachment(`${repo.repoId}.zip`); // cache the file for 6 hours @@ -67,7 +78,10 @@ router.get( repo.status == "removing" || repo.status == "removed" ) { - throw new AnonymousError("repository_expired", repo); + throw new AnonymousError("repository_expired", { + object: repo, + httpStatus: 410, + }); } const fiveMinuteAgo = new Date(); @@ -85,10 +99,16 @@ router.get( repo.model.statusMessage ? repo.model.statusMessage : "repository_not_available", - repo + { + object: repo, + httpStatus: 500, + } ); } - throw new AnonymousError("repository_not_ready", repo); + throw new AnonymousError("repository_not_ready", { + httpStatus: 404, + object: repo, + }); } await repo.updateIfNeeded(); diff --git a/src/routes/route-utils.ts b/src/routes/route-utils.ts index 683ec1e..1a5cb87 100644 --- a/src/routes/route-utils.ts +++ b/src/routes/route-utils.ts @@ -38,24 +38,11 @@ export async function getRepo( } function printError(error: any) { + io.notifyError(error, error.value); if (error instanceof AnonymousError) { - io.notifyError(error, error.value); - let detail = error.value ? JSON.stringify(error.value) : null; - if (error.value instanceof Repository) { - detail = error.value.repoId; - } else if (error.value instanceof AnonymizedFile) { - detail = `/r/${error.value.repository.repoId}/${error.value.anonymizedPath}`; - } else if (error.value instanceof GitHubRepository) { - detail = `${error.value.fullName}`; - } else if (error.value instanceof User) { - detail = `${error.value.username}`; - } else if (error.value instanceof GitHubBase) { - detail = `${error.value.repository.repoId}`; - } console.error( "[ERROR]", - error.message, - detail ? `'${detail}'` : null, + error.toString(), error.stack.split("\n")[1].trim() ); } else if (error instanceof Error) { @@ -72,7 +59,9 @@ export function handleError(error: any, res: express.Response) { message = error.message; } let status = 500; - if (message && message.indexOf("not_found") > -1) { + if (error.httpStatus) { + status = error.httpStatus; + } else if (message && message.indexOf("not_found") > -1) { status = 400; } else if (message && message.indexOf("not_connected") > -1) { status = 401; @@ -86,12 +75,16 @@ export async function getUser(req: express.Request) { const user = (req.user as any).user; if (!user) { req.logout(); - throw new AnonymousError("not_connected"); + throw new AnonymousError("not_connected", { + httpStatus: 401, + }); } const model = await UserModel.findById(user._id); if (!model) { req.logout(); - throw new AnonymousError("not_connected"); + throw new AnonymousError("not_connected", { + httpStatus: 401, + }); } return new User(model); } diff --git a/src/routes/webview.ts b/src/routes/webview.ts index 025da86..a0b94aa 100644 --- a/src/routes/webview.ts +++ b/src/routes/webview.ts @@ -12,14 +12,20 @@ async function webView(req: express.Request, res: express.Response) { if (!repo) return; try { if (!repo.options.page || !repo.options.pageSource) { - throw new AnonymousError("page_not_activated"); + throw new AnonymousError("page_not_activated", { + httpStatus: 400, + object: repo, + }); } if ( repo.options.pageSource?.branch != (repo.source as GitHubDownload).branch.name ) { - throw new AnonymousError("page_not_supported_on_different_branch"); + throw new AnonymousError("page_not_supported_on_different_branch", { + httpStatus: 400, + object: repo, + }); } let requestPath = path.join( @@ -37,7 +43,10 @@ async function webView(req: express.Request, res: express.Response) { anonymizedPath: requestPath, }); if (!(await f.isFileSupported())) { - return res.status(500).send({ error: "file_not_supported" }); + throw new AnonymousError("file_not_supported", { + httpStatus: 400, + object: f, + }); } f.send(res); } catch (error) { diff --git a/src/source/GitHubBase.ts b/src/source/GitHubBase.ts index ae4bac1..25de2b7 100644 --- a/src/source/GitHubBase.ts +++ b/src/source/GitHubBase.ts @@ -38,11 +38,17 @@ export default abstract class GitHubBase { } async getFileContent(file: AnonymizedFile): Promise { - throw new AnonymousError("Method not implemented."); + throw new AnonymousError("method_not_implemented", { + httpStatus: 501, + object: this, + }); } - + getFiles(): Promise { - throw new AnonymousError("Method not implemented."); + throw new AnonymousError("method_not_implemented", { + httpStatus: 501, + object: this, + }); } async getToken(owner?: string) { diff --git a/src/source/GitHubDownload.ts b/src/source/GitHubDownload.ts index e2c38bf..b46d95f 100644 --- a/src/source/GitHubDownload.ts +++ b/src/source/GitHubDownload.ts @@ -45,7 +45,10 @@ export default class GitHubDownload extends GitHubBase implements SourceBase { this.repository.status == "download" && this.repository.model.statusDate > fiveMinuteAgo ) - throw new AnonymousError("repo_in_download", this.repository); + throw new AnonymousError("repo_in_download", { + httpStatus: 404, + object: this.repository, + }); let response: OctokitResponse; try { response = await this._getZipUrl(await this.getToken()); @@ -55,11 +58,19 @@ export default class GitHubDownload extends GitHubBase implements SourceBase { response = await this._getZipUrl(config.GITHUB_TOKEN); } catch (error) { await this.repository.resetSate("error"); - throw new AnonymousError("repo_not_accessible", this.repository); + throw new AnonymousError("repo_not_accessible", { + httpStatus: 404, + cause: error, + object: this.repository, + }); } } else { await this.repository.resetSate("error"); - throw new AnonymousError("repo_not_accessible", this.repository); + throw new AnonymousError("repo_not_accessible", { + httpStatus: 404, + object: this.repository, + cause: error, + }); } } await this.repository.updateStatus("download"); @@ -89,7 +100,11 @@ export default class GitHubDownload extends GitHubBase implements SourceBase { await storage.extractTar(originalPath, downloadStream); } catch (error) { await this.repository.updateStatus("error", "unable_to_download"); - throw new AnonymousError("unable_to_download", error); + throw new AnonymousError("unable_to_download", { + httpStatus: 500, + cause: error, + object: this.repository, + }); } finally { inDownload = false; clearTimeout(progressTimeout); diff --git a/src/source/GitHubRepository.ts b/src/source/GitHubRepository.ts index 44ba941..8085fc4 100644 --- a/src/source/GitHubRepository.ts +++ b/src/source/GitHubRepository.ts @@ -117,7 +117,11 @@ export class GitHubRepository { selected.readme = readme; await model.save(); } catch (error) { - throw new AnonymousError("readme_not_available", this); + throw new AnonymousError("readme_not_available", { + httpStatus: 404, + cause: error, + object: this, + }); } } @@ -127,7 +131,10 @@ export class GitHubRepository { public get owner(): string { const repo = gh(this.fullName); if (!repo) { - throw new AnonymousError("invalid_repo", this); + throw new AnonymousError("invalid_repo", { + httpStatus: 400, + object: this, + }); } return repo.owner || this.fullName; } @@ -135,7 +142,10 @@ export class GitHubRepository { public get repo(): string { const repo = gh(this.fullName); if (!repo) { - throw new AnonymousError("invalid_repo", this); + throw new AnonymousError("invalid_repo", { + httpStatus: 400, + object: this, + }); } return repo.name || this.fullName; } @@ -159,18 +169,22 @@ export async function getRepositoryFromGitHub(opt: { }) ).data; } catch (error) { - if (error.status == 404) { - throw new AnonymousError("repo_not_found", { + throw new AnonymousError("repo_not_found", { + httpStatus: error.status, + object: { owner: opt.owner, repo: opt.repo, - }); - } - throw error; + }, + cause: error, + }); } if (!r) throw new AnonymousError("repo_not_found", { - owner: opt.owner, - repo: opt.repo, + httpStatus: 404, + object: { + owner: opt.owner, + repo: opt.repo, + }, }); let model = await RepositoryModel.findOne({ externalId: "gh_" + r.id }); if (!model) { diff --git a/src/source/GitHubStream.ts b/src/source/GitHubStream.ts index 973bb53..e1223c4 100644 --- a/src/source/GitHubStream.ts +++ b/src/source/GitHubStream.ts @@ -25,7 +25,11 @@ export default class GitHubStream extends GitHubBase implements SourceBase { } async getFileContent(file: AnonymizedFile): Promise { - if (!file.sha) throw new AnonymousError("file_sha_not_provided", file); + if (!file.sha) + throw new AnonymousError("file_sha_not_provided", { + httpStatus: 400, + object: file, + }); const octokit = new Octokit({ auth: await this.getToken(), }); @@ -37,7 +41,10 @@ export default class GitHubStream extends GitHubBase implements SourceBase { file_sha: file.sha, }); if (!ghRes.data.content && ghRes.data.size != 0) { - throw new AnonymousError("file_not_accessible", file); + throw new AnonymousError("file_not_accessible", { + httpStatus: 404, + object: file, + }); } // empty file let content: Buffer; @@ -54,12 +61,12 @@ export default class GitHubStream extends GitHubBase implements SourceBase { await storage.write(file.originalCachePath, content); return stream.Readable.from(content); } catch (error) { - if (error.status == 403) { - throw new AnonymousError("file_too_big", file); - } - console.error(error); + throw new AnonymousError("file_too_big", { + httpStatus: error.status, + cause: error, + object: file, + }); } - throw new AnonymousError("file_not_accessible", file); } async getFiles() { @@ -85,8 +92,16 @@ export default class GitHubStream extends GitHubBase implements SourceBase { recursive: "1", }); } catch (error) { - await this.repository.resetSate("error"); - throw new AnonymousError("repo_not_accessible", this.repository); + await this.repository.resetSate("error", "repo_not_accessible"); + throw new AnonymousError("repo_not_accessible", { + httpStatus: error.status, + cause: error, + object: { + owner: this.githubRepository.owner, + repo: this.githubRepository.repo, + tree_sha: sha, + }, + }); } const tree = this.tree2Tree(ghRes.data.tree, truncatedTree, parentPath); diff --git a/src/storage/S3.ts b/src/storage/S3.ts index 270e85a..b712975 100644 --- a/src/storage/S3.ts +++ b/src/storage/S3.ts @@ -20,7 +20,10 @@ export default class S3Storage implements StorageBase { client: S3; constructor() { - if (!config.S3_BUCKET) throw new AnonymousError("s3_config_not_provided"); + if (!config.S3_BUCKET) + throw new AnonymousError("s3_config_not_provided", { + httpStatus: 500, + }); this.client = new S3({ region: config.S3_REGION, endpoint: config.S3_ENDPOINT, @@ -179,7 +182,7 @@ export default class S3Storage implements StorageBase { header.name = header.name.substr(header.name.indexOf("/") + 1); originalArchiveStreamToS3Entry.call(toS3, header, stream, next); }; - + return new Promise((resolve, reject) => { toS3 = new ArchiveStreamToS3(config.S3_BUCKET, p, this.client); stream