Files
anonymous_github/src/streamer/route.ts
T

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;