mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-09-13 14:18:57 +02:00
135 lines
4.3 KiB
TypeScript
135 lines
4.3 KiB
TypeScript
import * as express from "express";
|
|
import GitHubStream from "../core/source/GitHubStream";
|
|
import {
|
|
AnonymizeTransformer,
|
|
isTextFile,
|
|
} from "../core/anonymize-utils";
|
|
import { handleError } from "../server/routes/route-utils";
|
|
import { lookup } from "mime-types";
|
|
import { streamAnonymizedZip } from "../core/zipStream";
|
|
|
|
export const router = express.Router();
|
|
|
|
router.post(
|
|
"/download",
|
|
async (req: express.Request, res: express.Response) => {
|
|
req.body = req.body || {};
|
|
const token: string = req.body.token;
|
|
if (typeof req.body.repoFullName !== "string" || typeof token !== "string") {
|
|
return res.status(400).json({ error: "invalid_request" });
|
|
}
|
|
const repoFullName = req.body.repoFullName.split("/");
|
|
const repoId = req.body.repoId;
|
|
const commit = req.body.commit;
|
|
const anonymizerOptions = req.body.anonymizerOptions;
|
|
const contentOptions = req.body.contentOptions;
|
|
|
|
try {
|
|
await streamAnonymizedZip(
|
|
{
|
|
repoId,
|
|
organization: repoFullName[0],
|
|
repoName: repoFullName[1],
|
|
commit,
|
|
getToken: () => token,
|
|
anonymizerOptions,
|
|
contentOptions,
|
|
},
|
|
res
|
|
);
|
|
} catch (error) {
|
|
handleError(error, res);
|
|
if (res.headersSent && !res.writableEnded) res.destroy();
|
|
}
|
|
}
|
|
);
|
|
router.post("/", async (req: express.Request, res: express.Response) => {
|
|
req.body = req.body || {};
|
|
const token: string = req.body.token;
|
|
if (typeof req.body.repoFullName !== "string" || typeof token !== "string") {
|
|
return res.status(400).json({ error: "invalid_request" });
|
|
}
|
|
const repoFullName = req.body.repoFullName.split("/");
|
|
const repoId = req.body.repoId;
|
|
const fileSha = req.body.sha;
|
|
const fileSize: number | undefined =
|
|
typeof req.body.size === "number" ? req.body.size : undefined;
|
|
const commit = req.body.commit;
|
|
const filePath: string = req.body.filePath;
|
|
const anonymizerOptions = req.body.anonymizerOptions;
|
|
const anonymizer = new AnonymizeTransformer(anonymizerOptions);
|
|
|
|
// Defence in depth: the parent server validates filePath against
|
|
// FileModel before calling us, but the streamer joins this directly
|
|
// into the storage path on disk. Reject any segment that could escape
|
|
// the repo root, in case the streamer is ever exposed beyond the
|
|
// internal network or a buggy caller forwards an unvalidated path.
|
|
if (
|
|
typeof filePath !== "string" ||
|
|
filePath.length === 0 ||
|
|
filePath
|
|
.split(/[\\/]/)
|
|
.some((segment) => segment === ".." || segment === "")
|
|
) {
|
|
return res.status(400).json({ error: "invalid_file_path" });
|
|
}
|
|
|
|
const source = new GitHubStream({
|
|
repoId,
|
|
organization: repoFullName[0],
|
|
repoName: repoFullName[1],
|
|
commit: commit,
|
|
getToken: () => token,
|
|
});
|
|
try {
|
|
const content = await source.getFileContentCache(
|
|
filePath,
|
|
repoId,
|
|
() => ({ sha: fileSha, size: fileSize })
|
|
);
|
|
const mime = lookup(filePath);
|
|
if (mime && !filePath.endsWith(".ts")) {
|
|
res.contentType(mime);
|
|
} else if (isTextFile(filePath)) {
|
|
res.contentType("text/plain");
|
|
}
|
|
// Only declare Accept-Ranges: none for text files — they get rewritten on
|
|
// the fly so byte ranges aren't meaningful. For binary entries the
|
|
// transformer is a passthrough; let <video>/<audio> fall back to a full
|
|
// download instead of refusing to play (#538).
|
|
if (isTextFile(filePath)) {
|
|
res.header("Accept-Ranges", "none");
|
|
}
|
|
anonymizer.once("transform", (data) => {
|
|
if (!mime && data.isText) {
|
|
res.contentType("text/plain");
|
|
} else if (!mime && !data.isText) {
|
|
res.contentType("application/octet-stream");
|
|
}
|
|
});
|
|
function handleStreamError(error: Error) {
|
|
if (!content.closed && !content.destroyed) {
|
|
content.destroy();
|
|
}
|
|
handleError(error, res);
|
|
if (res.headersSent && !res.writableEnded) res.destroy();
|
|
anonymizer.destroy();
|
|
}
|
|
content
|
|
.on("error", handleStreamError)
|
|
.pipe(anonymizer)
|
|
.on("error", handleStreamError)
|
|
.pipe(res)
|
|
.on("error", handleStreamError)
|
|
.on("close", () => {
|
|
if (!content.closed && !content.destroyed) {
|
|
content.destroy();
|
|
}
|
|
});
|
|
} catch (error) {
|
|
handleError(error, res);
|
|
}
|
|
});
|
|
|
|
export default router;
|