chore: improve error messages

This commit is contained in:
tdurieux
2021-09-09 11:53:49 +02:00
parent efec854b46
commit a9d45a150c
16 changed files with 425 additions and 119 deletions
+27 -7
View File
@@ -43,7 +43,10 @@ export default class AnonymizedFile {
constructor(data: { repository: Repository; anonymizedPath: string }) { constructor(data: { repository: Repository; anonymizedPath: string }) {
this.repository = data.repository; this.repository = data.repository;
if (!this.repository.options.terms) 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; this.anonymizedPath = data.anonymizedPath;
} }
@@ -54,7 +57,11 @@ export default class AnonymizedFile {
*/ */
async originalPath(): Promise<string> { async originalPath(): Promise<string> {
if (this._originalPath) return this._originalPath; 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("/"); const paths = this.anonymizedPath.trim().split("/");
@@ -70,7 +77,10 @@ export default class AnonymizedFile {
continue; continue;
} }
if (!currentAnonymized[fileName]) { if (!currentAnonymized[fileName]) {
throw new AnonymousError("file_not_found", this); throw new AnonymousError("file_not_found", {
object: this,
httpStatus: 404,
});
} }
currentAnonymized = currentAnonymized[fileName]; currentAnonymized = currentAnonymized[fileName];
@@ -103,7 +113,7 @@ export default class AnonymizedFile {
currentAnonymized.sha === undefined || currentAnonymized.sha === undefined ||
currentAnonymized.size === 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; const file: TreeFile = currentAnonymized as TreeFile;
@@ -114,7 +124,10 @@ export default class AnonymizedFile {
// it should never happen // it should never happen
const shaTree = tree2sha(currentOriginal); const shaTree = tree2sha(currentOriginal);
if (!currentAnonymized.sha || !shaTree[file.sha]) { 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]); this._originalPath = path.join(currentOriginalPath, shaTree[file.sha]);
@@ -147,7 +160,10 @@ export default class AnonymizedFile {
async content(): Promise<stream.Readable> { async content(): Promise<stream.Readable> {
if (this.fileSize && this.fileSize > config.MAX_FILE_SIZE) { 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)) { if (await storage.exists(this.originalCachePath)) {
return storage.read(this.originalCachePath); return storage.read(this.originalCachePath);
@@ -163,7 +179,11 @@ export default class AnonymizedFile {
} }
get originalCachePath() { 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); return path.join(this.repository.originalCachePath, this._originalPath);
} }
+41 -6
View File
@@ -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 * Custom error message
*/ */
export default class AnonymousError extends CustomError { 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); super(message);
this.value = value; this.value = opt?.object;
this.httpStatus = opt?.httpStatus;
this.cause = opt?.cause;
} }
toString(): string { 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;
} }
} }
+16 -5
View File
@@ -34,7 +34,10 @@ export default class Repository {
this.source = new Zip(data.source, this); this.source = new Zip(data.source, this);
break; break;
default: 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 })); this.owner = new User(new UserModel({ _id: data.owner }));
} }
@@ -110,7 +113,10 @@ export default class Repository {
this.status == "removing" || this.status == "removing" ||
this.status == "removed" this.status == "removed"
) { ) {
throw new AnonymousError("repository_expired", this); throw new AnonymousError("repository_expired", {
object: this,
httpStatus: 410,
});
} }
const fiveMinuteAgo = new Date(); const fiveMinuteAgo = new Date();
fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5); fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
@@ -119,7 +125,9 @@ export default class Repository {
this.status == "preparing" || this.status == "preparing" ||
(this.status == "download" && this._model.statusDate > fiveMinuteAgo) (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.updateStatus("error", "branch_not_found");
await this.resetSate(); await this.resetSate();
throw new AnonymousError("branch_not_found", this); throw new AnonymousError("branch_not_found", {
object: this,
});
} }
this._model.anonymizeDate = new Date(); this._model.anonymizeDate = new Date();
console.log(`${this._model.repoId} will be updated to ${newCommit}`); 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 * Reset/delete the state of the repository
*/ */
async resetSate(status?: RepositoryStatus) { async resetSate(status?: RepositoryStatus, statusMessage?: string) {
if (status) this._model.status = status; if (status) this._model.status = status;
if (statusMessage) this._model.statusMessage = statusMessage;
// remove attribute // remove attribute
this._model.size = { storage: 0, file: 0 }; this._model.size = { storage: 0, file: 0 };
this._model.originalFiles = null; this._model.originalFiles = null;
+5 -1
View File
@@ -24,6 +24,10 @@ export async function connect() {
export async function getRepository(repoId: string) { export async function getRepository(repoId: string) {
const data = await AnonymizedRepositoryModel.findOne({ repoId }); 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); return new Repository(data);
} }
+19 -2
View File
@@ -1,5 +1,6 @@
import * as Queue from "bull"; import * as Queue from "bull";
import config from "../config"; import config from "../config";
import AnonymousError from "./AnonymousError";
import { getRepository } from "./database/database"; import { getRepository } from "./database/database";
import Repository from "./Repository"; import Repository from "./Repository";
@@ -28,7 +29,15 @@ removeQueue.process(5, async (job) => {
const repo = await getRepository(job.data.repoId); const repo = await getRepository(job.data.repoId);
await repo.remove(); await repo.remove();
} catch (error) { } 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 { } finally {
console.log(`${job.data.repoId} is removed`); console.log(`${job.data.repoId} is removed`);
} }
@@ -43,7 +52,15 @@ downloadQueue.process(2, async (job) => {
job.progress("resetSate"); job.progress("resetSate");
await repo.anonymize(); await repo.anonymize();
} catch (error) { } 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 { } finally {
console.log(`${job.data.repoId} is downloaded`); console.log(`${job.data.repoId} is downloaded`);
} }
+96 -20
View File
@@ -67,26 +67,79 @@ router.get("/", async (req: express.Request, res: express.Response) => {
}); });
function validateConferenceForm(conf) { function validateConferenceForm(conf) {
if (!conf.name) throw new AnonymousError("conf_name_missing"); if (!conf.name)
if (!conf.conferenceID) throw new AnonymousError("conf_id_missing"); throw new AnonymousError("conf_name_missing", {
if (!conf.startDate) throw new AnonymousError("conf_start_date_missing"); object: conf,
if (!conf.endDate) throw new AnonymousError("conf_end_date_missing"); 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)) 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)) 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) 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]; const plan = plans.filter((p) => p.id == conf.plan.planID)[0];
if (plan.pricePerRepo > 0) { if (plan.pricePerRepo > 0) {
const billing = conf.billing; const billing = conf.billing;
if (!billing) throw new AnonymousError("billing_missing"); if (!billing)
if (!billing.name) throw new AnonymousError("billing_name_missing"); throw new AnonymousError("billing_missing", {
if (!billing.email) throw new AnonymousError("billing_email_missing"); object: conf,
if (!billing.address) throw new AnonymousError("billing_address_missing"); httpStatus: 400,
if (!billing.city) throw new AnonymousError("billing_city_missing"); });
if (!billing.zip) throw new AnonymousError("billing_zip_missing"); if (!billing.name)
if (!billing.country) throw new AnonymousError("billing_country_missing"); 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, conferenceID: req.params.conferenceID,
}); });
if (model.owners.indexOf(user.model.id) == -1) 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); validateConferenceForm(req.body);
model.name = req.body.name; model.name = req.body.name;
@@ -146,7 +202,13 @@ router.post(
res.send("ok"); res.send("ok");
} catch (error) { } catch (error) {
if (error.message?.indexOf(" duplicate key") > -1) { 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); handleError(error, res);
} }
@@ -161,10 +223,17 @@ router.get(
const data = await ConferenceModel.findOne({ const data = await ConferenceModel.findOne({
conferenceID: req.params.conferenceID, 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); const conference = new Conference(data);
if (conference.ownerIDs.indexOf(user.model.id) == -1) 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(); const o: any = conference.toJSON();
o.repositories = (await conference.repositories()).map((r) => r.toJSON()); o.repositories = (await conference.repositories()).map((r) => r.toJSON());
res.json(o); res.json(o);
@@ -182,10 +251,17 @@ router.delete(
const data = await ConferenceModel.findOne({ const data = await ConferenceModel.findOne({
conferenceID: req.params.conferenceID, 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); const conference = new Conference(data);
if (conference.ownerIDs.indexOf(user.model.id) == -1) 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(); await conference.remove();
res.send("ok"); res.send("ok");
} catch (error) { } catch (error) {
+5 -1
View File
@@ -1,5 +1,6 @@
import * as express from "express"; import * as express from "express";
import AnonymizedFile from "../AnonymizedFile"; import AnonymizedFile from "../AnonymizedFile";
import AnonymousError from "../AnonymousError";
import { getRepo, handleError } from "./route-utils"; import { getRepo, handleError } from "./route-utils";
export const router = express.Router(); export const router = express.Router();
@@ -24,7 +25,10 @@ router.get(
anonymizedPath, anonymizedPath,
}); });
if (!(await f.isFileSupported())) { 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( res.attachment(
anonymizedPath.substring(anonymizedPath.lastIndexOf("/") + 1) anonymizedPath.substring(anonymizedPath.lastIndexOf("/") + 1)
+84 -20
View File
@@ -24,15 +24,24 @@ router.post("/claim", async (req: express.Request, res: express.Response) => {
const user = await getUser(req); const user = await getUser(req);
try { try {
if (!req.body.repoId) { 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) { 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); const repoConfig = await db.getRepository(req.body.repoId);
if (repoConfig == null) { 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); const r = gh(req.body.repoUrl);
@@ -42,7 +51,10 @@ router.post("/claim", async (req: express.Request, res: express.Response) => {
accessToken: user.accessToken, accessToken: user.accessToken,
}); });
if ((repoConfig.source as GitHubBase).githubRepository.id != repo.id) { 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}.`); console.log(`${user.username} claims ${r.repository}.`);
@@ -70,7 +82,10 @@ router.post(
const user = await getUser(req); const user = await getUser(req);
if (repo.owner.id != user.id) { 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 }); await repo.updateIfNeeded({ force: true });
res.json({ status: repo.status }); res.json({ status: repo.status });
@@ -88,10 +103,17 @@ router.delete(
if (!repo) return; if (!repo) return;
// if (repo.status == "removing") return res.json({ status: repo.status }); // if (repo.status == "removing") return res.json({ status: repo.status });
try { 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); const user = await getUser(req);
if (repo.owner.id != user.id) { 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 repo.updateStatus("removing");
await removeQueue.add(repo, { jobId: repo.repoId }); await removeQueue.add(repo, { jobId: repo.repoId });
@@ -152,7 +174,12 @@ router.get(
repo: req.params.repo, repo: req.params.repo,
accessToken: user.accessToken, 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( return res.send(
await repo.readme({ await repo.readme({
accessToken: user.accessToken, accessToken: user.accessToken,
@@ -174,7 +201,10 @@ router.get("/:repoId/", async (req: express.Request, res: express.Response) => {
const user = await getUser(req); const user = await getUser(req);
if (repo.owner.id != user.id) { 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()); res.json((await db.getRepository(req.params.repoId)).toJSON());
} catch (error) { } catch (error) {
@@ -188,22 +218,40 @@ function validateNewRepo(repoUpdate) {
!repoUpdate.repoId.match(validCharacters) || !repoUpdate.repoId.match(validCharacters) ||
repoUpdate.repoId.length < 3 repoUpdate.repoId.length < 3
) { ) {
throw new AnonymousError("invalid_repoId", repoUpdate.repoId); throw new AnonymousError("invalid_repoId", {
object: repoUpdate,
httpStatus: 400,
});
} }
if (!repoUpdate.source.branch) { if (!repoUpdate.source.branch) {
throw new AnonymousError("branch_not_specified"); throw new AnonymousError("branch_not_specified", {
object: repoUpdate,
httpStatus: 400,
});
} }
if (!repoUpdate.source.commit) { if (!repoUpdate.source.commit) {
throw new AnonymousError("commit_not_specified"); throw new AnonymousError("commit_not_specified", {
object: repoUpdate,
httpStatus: 400,
});
} }
if (!repoUpdate.options) { if (!repoUpdate.options) {
throw new AnonymousError("options_not_provided"); throw new AnonymousError("options_not_provided", {
object: repoUpdate,
httpStatus: 400,
});
} }
if (!Array.isArray(repoUpdate.terms)) { 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)) { 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); const user = await getUser(req);
if (repo.owner.id != user.id) { 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; const repoUpdate = req.body;
@@ -289,7 +340,10 @@ router.post(
new Date() > conf.endDate || new Date() > conf.endDate ||
conf.status !== "ready" 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); const f = conf.repositories.filter((r) => r.id == repo.model.id);
if (f.length) { if (f.length) {
@@ -346,7 +400,10 @@ router.post("/", async (req: express.Request, res: express.Response) => {
if (repo.source.type == "GitHubDownload") { if (repo.source.type == "GitHubDownload") {
// details.size is in kilobytes // details.size is in kilobytes
if (repository.size > config.MAX_REPO_SIZE) { 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; repo.conference = repoUpdate.conference;
@@ -364,7 +421,10 @@ router.post("/", async (req: express.Request, res: express.Response) => {
conf.status !== "ready" conf.status !== "ready"
) { ) {
await repo.remove(); await repo.remove();
throw new AnonymousError("conf_not_activated"); throw new AnonymousError("conf_not_activated", {
object: conf,
httpStatus: 400,
});
} }
conf.repositories.push({ conf.repositories.push({
id: repo.id, id: repo.id,
@@ -379,7 +439,11 @@ router.post("/", async (req: express.Request, res: express.Response) => {
} catch (error) { } catch (error) {
if (error.message?.indexOf(" duplicate key") > -1) { if (error.message?.indexOf(" duplicate key") > -1) {
return handleError( return handleError(
new AnonymousError("repoId_already_used", repoUpdate.repoId), new AnonymousError("repoId_already_used", {
httpStatus: 400,
cause: error,
object: repoUpdate,
}),
res res
); );
} }
+28 -8
View File
@@ -12,14 +12,25 @@ const router = express.Router();
router.get( router.get(
"/:repoId/zip", "/:repoId/zip",
async (req: express.Request, res: express.Response) => { 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); const pipeline = promisify(stream.pipeline);
try { 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`); res.attachment(`${repo.repoId}.zip`);
// cache the file for 6 hours // cache the file for 6 hours
@@ -67,7 +78,10 @@ router.get(
repo.status == "removing" || repo.status == "removing" ||
repo.status == "removed" repo.status == "removed"
) { ) {
throw new AnonymousError("repository_expired", repo); throw new AnonymousError("repository_expired", {
object: repo,
httpStatus: 410,
});
} }
const fiveMinuteAgo = new Date(); const fiveMinuteAgo = new Date();
@@ -85,10 +99,16 @@ router.get(
repo.model.statusMessage repo.model.statusMessage
? repo.model.statusMessage ? repo.model.statusMessage
: "repository_not_available", : "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(); await repo.updateIfNeeded();
+11 -18
View File
@@ -38,24 +38,11 @@ export async function getRepo(
} }
function printError(error: any) { function printError(error: any) {
io.notifyError(error, error.value);
if (error instanceof AnonymousError) { 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( console.error(
"[ERROR]", "[ERROR]",
error.message, error.toString(),
detail ? `'${detail}'` : null,
error.stack.split("\n")[1].trim() error.stack.split("\n")[1].trim()
); );
} else if (error instanceof Error) { } else if (error instanceof Error) {
@@ -72,7 +59,9 @@ export function handleError(error: any, res: express.Response) {
message = error.message; message = error.message;
} }
let status = 500; 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; status = 400;
} else if (message && message.indexOf("not_connected") > -1) { } else if (message && message.indexOf("not_connected") > -1) {
status = 401; status = 401;
@@ -86,12 +75,16 @@ export async function getUser(req: express.Request) {
const user = (req.user as any).user; const user = (req.user as any).user;
if (!user) { if (!user) {
req.logout(); req.logout();
throw new AnonymousError("not_connected"); throw new AnonymousError("not_connected", {
httpStatus: 401,
});
} }
const model = await UserModel.findById(user._id); const model = await UserModel.findById(user._id);
if (!model) { if (!model) {
req.logout(); req.logout();
throw new AnonymousError("not_connected"); throw new AnonymousError("not_connected", {
httpStatus: 401,
});
} }
return new User(model); return new User(model);
} }
+12 -3
View File
@@ -12,14 +12,20 @@ async function webView(req: express.Request, res: express.Response) {
if (!repo) return; if (!repo) return;
try { try {
if (!repo.options.page || !repo.options.pageSource) { if (!repo.options.page || !repo.options.pageSource) {
throw new AnonymousError("page_not_activated"); throw new AnonymousError("page_not_activated", {
httpStatus: 400,
object: repo,
});
} }
if ( if (
repo.options.pageSource?.branch != repo.options.pageSource?.branch !=
(repo.source as GitHubDownload).branch.name (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( let requestPath = path.join(
@@ -37,7 +43,10 @@ async function webView(req: express.Request, res: express.Response) {
anonymizedPath: requestPath, anonymizedPath: requestPath,
}); });
if (!(await f.isFileSupported())) { 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); f.send(res);
} catch (error) { } catch (error) {
+9 -3
View File
@@ -38,11 +38,17 @@ export default abstract class GitHubBase {
} }
async getFileContent(file: AnonymizedFile): Promise<stream.Readable> { async getFileContent(file: AnonymizedFile): Promise<stream.Readable> {
throw new AnonymousError("Method not implemented."); throw new AnonymousError("method_not_implemented", {
httpStatus: 501,
object: this,
});
} }
getFiles(): Promise<Tree> { getFiles(): Promise<Tree> {
throw new AnonymousError("Method not implemented."); throw new AnonymousError("method_not_implemented", {
httpStatus: 501,
object: this,
});
} }
async getToken(owner?: string) { async getToken(owner?: string) {
+19 -4
View File
@@ -45,7 +45,10 @@ export default class GitHubDownload extends GitHubBase implements SourceBase {
this.repository.status == "download" && this.repository.status == "download" &&
this.repository.model.statusDate > fiveMinuteAgo 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<unknown, number>; let response: OctokitResponse<unknown, number>;
try { try {
response = await this._getZipUrl(await this.getToken()); 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); response = await this._getZipUrl(config.GITHUB_TOKEN);
} catch (error) { } catch (error) {
await this.repository.resetSate("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 { } else {
await this.repository.resetSate("error"); 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"); await this.repository.updateStatus("download");
@@ -89,7 +100,11 @@ export default class GitHubDownload extends GitHubBase implements SourceBase {
await storage.extractTar(originalPath, downloadStream); await storage.extractTar(originalPath, downloadStream);
} catch (error) { } catch (error) {
await this.repository.updateStatus("error", "unable_to_download"); 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 { } finally {
inDownload = false; inDownload = false;
clearTimeout(progressTimeout); clearTimeout(progressTimeout);
+24 -10
View File
@@ -117,7 +117,11 @@ export class GitHubRepository {
selected.readme = readme; selected.readme = readme;
await model.save(); await model.save();
} catch (error) { } 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 { public get owner(): string {
const repo = gh(this.fullName); const repo = gh(this.fullName);
if (!repo) { if (!repo) {
throw new AnonymousError("invalid_repo", this); throw new AnonymousError("invalid_repo", {
httpStatus: 400,
object: this,
});
} }
return repo.owner || this.fullName; return repo.owner || this.fullName;
} }
@@ -135,7 +142,10 @@ export class GitHubRepository {
public get repo(): string { public get repo(): string {
const repo = gh(this.fullName); const repo = gh(this.fullName);
if (!repo) { if (!repo) {
throw new AnonymousError("invalid_repo", this); throw new AnonymousError("invalid_repo", {
httpStatus: 400,
object: this,
});
} }
return repo.name || this.fullName; return repo.name || this.fullName;
} }
@@ -159,18 +169,22 @@ export async function getRepositoryFromGitHub(opt: {
}) })
).data; ).data;
} catch (error) { } 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, owner: opt.owner,
repo: opt.repo, repo: opt.repo,
}); },
} cause: error,
throw error; });
} }
if (!r) if (!r)
throw new AnonymousError("repo_not_found", { throw new AnonymousError("repo_not_found", {
owner: opt.owner, httpStatus: 404,
repo: opt.repo, object: {
owner: opt.owner,
repo: opt.repo,
},
}); });
let model = await RepositoryModel.findOne({ externalId: "gh_" + r.id }); let model = await RepositoryModel.findOne({ externalId: "gh_" + r.id });
if (!model) { if (!model) {
+24 -9
View File
@@ -25,7 +25,11 @@ export default class GitHubStream extends GitHubBase implements SourceBase {
} }
async getFileContent(file: AnonymizedFile): Promise<stream.Readable> { async getFileContent(file: AnonymizedFile): Promise<stream.Readable> {
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({ const octokit = new Octokit({
auth: await this.getToken(), auth: await this.getToken(),
}); });
@@ -37,7 +41,10 @@ export default class GitHubStream extends GitHubBase implements SourceBase {
file_sha: file.sha, file_sha: file.sha,
}); });
if (!ghRes.data.content && ghRes.data.size != 0) { 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 // empty file
let content: Buffer; let content: Buffer;
@@ -54,12 +61,12 @@ export default class GitHubStream extends GitHubBase implements SourceBase {
await storage.write(file.originalCachePath, content); await storage.write(file.originalCachePath, content);
return stream.Readable.from(content); return stream.Readable.from(content);
} catch (error) { } catch (error) {
if (error.status == 403) { throw new AnonymousError("file_too_big", {
throw new AnonymousError("file_too_big", file); httpStatus: error.status,
} cause: error,
console.error(error); object: file,
});
} }
throw new AnonymousError("file_not_accessible", file);
} }
async getFiles() { async getFiles() {
@@ -85,8 +92,16 @@ export default class GitHubStream extends GitHubBase implements SourceBase {
recursive: "1", recursive: "1",
}); });
} catch (error) { } catch (error) {
await this.repository.resetSate("error"); await this.repository.resetSate("error", "repo_not_accessible");
throw new AnonymousError("repo_not_accessible", this.repository); 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); const tree = this.tree2Tree(ghRes.data.tree, truncatedTree, parentPath);
+5 -2
View File
@@ -20,7 +20,10 @@ export default class S3Storage implements StorageBase {
client: S3; client: S3;
constructor() { 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({ this.client = new S3({
region: config.S3_REGION, region: config.S3_REGION,
endpoint: config.S3_ENDPOINT, endpoint: config.S3_ENDPOINT,
@@ -179,7 +182,7 @@ export default class S3Storage implements StorageBase {
header.name = header.name.substr(header.name.indexOf("/") + 1); header.name = header.name.substr(header.name.indexOf("/") + 1);
originalArchiveStreamToS3Entry.call(toS3, header, stream, next); originalArchiveStreamToS3Entry.call(toS3, header, stream, next);
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
toS3 = new ArchiveStreamToS3(config.S3_BUCKET, p, this.client); toS3 = new ArchiveStreamToS3(config.S3_BUCKET, p, this.client);
stream stream