mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-02-12 18:32:44 +00:00
254 lines
7.5 KiB
TypeScript
254 lines
7.5 KiB
TypeScript
import { join, basename } from "path";
|
|
import { Response } from "express";
|
|
import { Readable } from "stream";
|
|
import Repository from "./Repository";
|
|
import { Tree, TreeElement, TreeFile } from "./types";
|
|
import storage from "./storage";
|
|
import config from "../config";
|
|
import {
|
|
anonymizePath,
|
|
AnonymizeTransformer,
|
|
isTextFile,
|
|
} from "./anonymize-utils";
|
|
import AnonymousError from "./AnonymousError";
|
|
import { handleError } from "./routes/route-utils";
|
|
import { lookup } from "mime-types";
|
|
|
|
/**
|
|
* Represent a file in a anonymized repository
|
|
*/
|
|
export default class AnonymizedFile {
|
|
private _originalPath: string | undefined;
|
|
private fileSize?: number;
|
|
|
|
repository: Repository;
|
|
anonymizedPath: string;
|
|
_sha?: string;
|
|
|
|
constructor(data: { repository: Repository; anonymizedPath: string }) {
|
|
this.repository = data.repository;
|
|
if (!this.repository.options.terms)
|
|
throw new AnonymousError("terms_not_specified", {
|
|
object: this,
|
|
httpStatus: 400,
|
|
});
|
|
this.anonymizedPath = data.anonymizedPath;
|
|
}
|
|
|
|
async sha() {
|
|
if (this._sha) return this._sha.replace(/"/g, "");
|
|
await this.originalPath();
|
|
return this._sha?.replace(/"/g, "");
|
|
}
|
|
|
|
/**
|
|
* De-anonymize the path
|
|
*
|
|
* @returns the origin relative path of the file
|
|
*/
|
|
async originalPath(): Promise<string> {
|
|
if (this._originalPath) return this._originalPath;
|
|
if (!this.anonymizedPath)
|
|
throw new AnonymousError("path_not_specified", {
|
|
object: this,
|
|
httpStatus: 400,
|
|
});
|
|
|
|
const paths = this.anonymizedPath.trim().split("/");
|
|
let currentOriginal = (await this.repository.files({
|
|
force: false,
|
|
})) as TreeElement;
|
|
let currentOriginalPath = "";
|
|
for (let i = 0; i < paths.length; i++) {
|
|
const fileName = paths[i];
|
|
if (fileName == "") {
|
|
continue;
|
|
}
|
|
if (!(currentOriginal as Tree)[fileName]) {
|
|
// anonymize all the file in the folder and check if there is one that match the current filename
|
|
const options = [];
|
|
for (let originalFileName in currentOriginal) {
|
|
if (
|
|
anonymizePath(originalFileName, this.repository.options.terms) ==
|
|
fileName
|
|
) {
|
|
options.push(originalFileName);
|
|
}
|
|
}
|
|
|
|
// if only one option we found the original filename
|
|
if (options.length == 1) {
|
|
currentOriginalPath = join(currentOriginalPath, options[0]);
|
|
currentOriginal = (currentOriginal as Tree)[options[0]];
|
|
} else if (options.length == 0) {
|
|
throw new AnonymousError("file_not_found", {
|
|
object: this,
|
|
httpStatus: 404,
|
|
});
|
|
} else {
|
|
const nextName = paths[i + 1];
|
|
if (!nextName) {
|
|
// if there is no next name we can't find the file and we return the first option
|
|
currentOriginalPath = join(currentOriginalPath, options[0]);
|
|
currentOriginal = (currentOriginal as Tree)[options[0]];
|
|
}
|
|
let found = false;
|
|
for (const option of options) {
|
|
const optionTree = (currentOriginal as Tree)[option];
|
|
if ((optionTree as Tree).child) {
|
|
const optionTreeChild = (optionTree as Tree).child;
|
|
if ((optionTreeChild as Tree)[nextName]) {
|
|
currentOriginalPath = join(currentOriginalPath, option);
|
|
currentOriginal = optionTreeChild;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
// if we didn't find the next name we return the first option
|
|
currentOriginalPath = join(currentOriginalPath, options[0]);
|
|
currentOriginal = (currentOriginal as Tree)[options[0]];
|
|
}
|
|
}
|
|
} else {
|
|
currentOriginalPath = join(currentOriginalPath, fileName);
|
|
currentOriginal = (currentOriginal as Tree)[fileName];
|
|
}
|
|
}
|
|
|
|
if (
|
|
currentOriginal.sha === undefined ||
|
|
currentOriginal.size === undefined
|
|
) {
|
|
throw new AnonymousError("folder_not_supported", { object: this });
|
|
}
|
|
|
|
const file = currentOriginal as TreeFile;
|
|
this.fileSize = file.size;
|
|
this._sha = file.sha;
|
|
|
|
this._originalPath = currentOriginalPath;
|
|
return this._originalPath;
|
|
}
|
|
extension() {
|
|
const filename = basename(this.anonymizedPath);
|
|
const extensions = filename.split(".").reverse();
|
|
return extensions[0].toLowerCase();
|
|
}
|
|
isImage() {
|
|
const extension = this.extension();
|
|
return [
|
|
"png",
|
|
"jpg",
|
|
"jpeg",
|
|
"gif",
|
|
"svg",
|
|
"ico",
|
|
"bmp",
|
|
"tiff",
|
|
"tif",
|
|
"webp",
|
|
"avif",
|
|
"heif",
|
|
"heic",
|
|
].includes(extension);
|
|
}
|
|
isFileSupported() {
|
|
const extension = this.extension();
|
|
if (!this.repository.options.pdf && extension == "pdf") {
|
|
return false;
|
|
}
|
|
if (!this.repository.options.image && this.isImage()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async content(): Promise<Readable> {
|
|
if (this.anonymizedPath.includes(config.ANONYMIZATION_MASK)) {
|
|
await this.originalPath();
|
|
}
|
|
if (this.fileSize && this.fileSize > config.MAX_FILE_SIZE) {
|
|
throw new AnonymousError("file_too_big", {
|
|
object: this,
|
|
httpStatus: 403,
|
|
});
|
|
}
|
|
if (await storage.exists(this.originalCachePath)) {
|
|
return storage.read(this.originalCachePath);
|
|
}
|
|
return await this.repository.source?.getFileContent(this);
|
|
}
|
|
|
|
async anonymizedContent() {
|
|
return (await this.content()).pipe(new AnonymizeTransformer(this));
|
|
}
|
|
|
|
get originalCachePath() {
|
|
if (!this.originalPath)
|
|
throw new AnonymousError("path_not_defined", {
|
|
object: this,
|
|
httpStatus: 400,
|
|
});
|
|
if (!this._originalPath) {
|
|
if (this.anonymizedPath.includes(config.ANONYMIZATION_MASK)) {
|
|
throw new AnonymousError("path_not_defined", {
|
|
object: this,
|
|
httpStatus: 400,
|
|
});
|
|
} else {
|
|
return join(this.repository.originalCachePath, this.anonymizedPath);
|
|
}
|
|
}
|
|
|
|
return join(this.repository.originalCachePath, this._originalPath);
|
|
}
|
|
|
|
async send(res: Response): Promise<void> {
|
|
const mime = lookup(this.anonymizedPath);
|
|
if (mime && this.extension() != "ts") {
|
|
res.contentType(mime);
|
|
} else if (this.extension() != "ts") {
|
|
res.contentType("application/x-typescript");
|
|
} else if (isTextFile(this.anonymizedPath)) {
|
|
res.contentType("text/plain");
|
|
} else {
|
|
res.contentType("application/octet-stream");
|
|
}
|
|
res.header("Accept-Ranges", "none");
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const content = await this.content();
|
|
try {
|
|
const fileInfo = await storage.fileInfo(this.originalCachePath);
|
|
if (fileInfo.size) {
|
|
res.header("Content-Length", fileInfo.size.toString());
|
|
}
|
|
} catch (error) {
|
|
// unable to get file size
|
|
console.error(error);
|
|
}
|
|
content
|
|
.pipe(new AnonymizeTransformer(this))
|
|
.pipe(res)
|
|
.on("close", () => {
|
|
if (!content.closed && !content.destroyed) {
|
|
content.destroy();
|
|
}
|
|
resolve();
|
|
})
|
|
.on("error", (error) => {
|
|
if (!content.closed && !content.destroyed) {
|
|
content.destroy();
|
|
}
|
|
reject(error);
|
|
handleError(error, res);
|
|
});
|
|
} catch (error) {
|
|
handleError(error, res);
|
|
}
|
|
});
|
|
}
|
|
}
|