mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-07-05 21:27:59 +02:00
perf: improve the perf of Anonymous GitHub
This commit is contained in:
+32
-14
@@ -38,7 +38,7 @@ export default class AnonymizedFile {
|
|||||||
|
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
anonymizedPath: string;
|
anonymizedPath: string;
|
||||||
sha?: string;
|
_sha?: string;
|
||||||
|
|
||||||
constructor(data: { repository: Repository; anonymizedPath: string }) {
|
constructor(data: { repository: Repository; anonymizedPath: string }) {
|
||||||
this.repository = data.repository;
|
this.repository = data.repository;
|
||||||
@@ -50,6 +50,12 @@ export default class AnonymizedFile {
|
|||||||
this.anonymizedPath = data.anonymizedPath;
|
this.anonymizedPath = data.anonymizedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sha() {
|
||||||
|
if (this._sha) return this._sha;
|
||||||
|
await this.originalPath();
|
||||||
|
return this._sha;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* De-anonymize the path
|
* De-anonymize the path
|
||||||
*
|
*
|
||||||
@@ -118,7 +124,7 @@ export default class AnonymizedFile {
|
|||||||
|
|
||||||
const file: TreeFile = currentAnonymized as TreeFile;
|
const file: TreeFile = currentAnonymized as TreeFile;
|
||||||
this.fileSize = file.size;
|
this.fileSize = file.size;
|
||||||
this.sha = file.sha;
|
this._sha = file.sha;
|
||||||
|
|
||||||
if (isAmbiguous) {
|
if (isAmbiguous) {
|
||||||
// it should never happen
|
// it should never happen
|
||||||
@@ -134,16 +140,15 @@ export default class AnonymizedFile {
|
|||||||
} else {
|
} else {
|
||||||
this._originalPath = currentOriginalPath;
|
this._originalPath = currentOriginalPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._originalPath;
|
return this._originalPath;
|
||||||
}
|
}
|
||||||
async extension() {
|
extension() {
|
||||||
const filename = basename(await this.originalPath());
|
const filename = basename(this.anonymizedPath);
|
||||||
const extensions = filename.split(".").reverse();
|
const extensions = filename.split(".").reverse();
|
||||||
return extensions[0].toLowerCase();
|
return extensions[0].toLowerCase();
|
||||||
}
|
}
|
||||||
async isImage(): Promise<boolean> {
|
isImage() {
|
||||||
const extension = await this.extension();
|
const extension = this.extension();
|
||||||
return [
|
return [
|
||||||
"png",
|
"png",
|
||||||
"jpg",
|
"jpg",
|
||||||
@@ -160,18 +165,21 @@ export default class AnonymizedFile {
|
|||||||
"heic",
|
"heic",
|
||||||
].includes(extension);
|
].includes(extension);
|
||||||
}
|
}
|
||||||
async isFileSupported() {
|
isFileSupported() {
|
||||||
const extension = await this.extension();
|
const extension = this.extension();
|
||||||
if (!this.repository.options.pdf && extension == "pdf") {
|
if (!this.repository.options.pdf && extension == "pdf") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.repository.options.image && (await this.isImage())) {
|
if (!this.repository.options.image && this.isImage()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async content(): Promise<Readable> {
|
async content(): Promise<Readable> {
|
||||||
|
if (this.anonymizedPath.includes(config.ANONYMIZATION_MASK)) {
|
||||||
|
await this.originalPath();
|
||||||
|
}
|
||||||
if (this.fileSize && this.fileSize > config.MAX_FILE_SIZE) {
|
if (this.fileSize && this.fileSize > config.MAX_FILE_SIZE) {
|
||||||
throw new AnonymousError("file_too_big", {
|
throw new AnonymousError("file_too_big", {
|
||||||
object: this,
|
object: this,
|
||||||
@@ -185,9 +193,8 @@ export default class AnonymizedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async anonymizedContent() {
|
async anonymizedContent() {
|
||||||
await this.originalPath();
|
|
||||||
const rs = await this.content();
|
const rs = await this.content();
|
||||||
return rs.pipe(anonymizeStream(await this.originalPath(), this.repository));
|
return rs.pipe(anonymizeStream(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
get originalCachePath() {
|
get originalCachePath() {
|
||||||
@@ -196,14 +203,25 @@ export default class AnonymizedFile {
|
|||||||
object: this,
|
object: this,
|
||||||
httpStatus: 400,
|
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);
|
return join(this.repository.originalCachePath, this._originalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(res: Response): Promise<void> {
|
async send(res: Response): Promise<void> {
|
||||||
const pipe = promisify(pipeline);
|
const pipe = promisify(pipeline);
|
||||||
try {
|
try {
|
||||||
if (await this.extension()) {
|
if (this.extension()) {
|
||||||
res.contentType(await this.extension());
|
res.contentType(this.extension());
|
||||||
}
|
}
|
||||||
await pipe(await this.anonymizedContent(), res);
|
await pipe(await this.anonymizedContent(), res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+21
-13
@@ -16,6 +16,7 @@ import ConferenceModel from "./database/conference/conferences.model";
|
|||||||
import AnonymousError from "./AnonymousError";
|
import AnonymousError from "./AnonymousError";
|
||||||
import { downloadQueue } from "./queue";
|
import { downloadQueue } from "./queue";
|
||||||
import { isConnected } from "./database/database";
|
import { isConnected } from "./database/database";
|
||||||
|
import AnonymizedFile from "./AnonymizedFile";
|
||||||
|
|
||||||
export default class Repository {
|
export default class Repository {
|
||||||
private _model: IAnonymizedRepositoryDocument;
|
private _model: IAnonymizedRepositoryDocument;
|
||||||
@@ -48,12 +49,17 @@ export default class Repository {
|
|||||||
* @param opt force to get an updated list of files
|
* @param opt force to get an updated list of files
|
||||||
* @returns The anonymized file tree
|
* @returns The anonymized file tree
|
||||||
*/
|
*/
|
||||||
async anonymizedFiles(opt?: {
|
async anonymizedFiles(
|
||||||
/** Force to refresh the file tree */
|
opt: {
|
||||||
force?: boolean;
|
/** Force to refresh the file tree */
|
||||||
/** Include the file sha in the response */
|
force?: boolean;
|
||||||
includeSha: boolean;
|
/** Include the file sha in the response */
|
||||||
}): Promise<Tree> {
|
includeSha: boolean;
|
||||||
|
} = {
|
||||||
|
force: false,
|
||||||
|
includeSha: false,
|
||||||
|
}
|
||||||
|
): Promise<Tree> {
|
||||||
const terms = this._model.options.terms || [];
|
const terms = this._model.options.terms || [];
|
||||||
|
|
||||||
function anonymizeTreeRecursive(tree: TreeElement): TreeElement {
|
function anonymizeTreeRecursive(tree: TreeElement): TreeElement {
|
||||||
@@ -78,11 +84,11 @@ export default class Repository {
|
|||||||
* @param opt force to get an updated list of files
|
* @param opt force to get an updated list of files
|
||||||
* @returns The file tree
|
* @returns The file tree
|
||||||
*/
|
*/
|
||||||
async files(opt?: { force?: boolean }) {
|
async files(opt: { force?: boolean } = { force: false }): Promise<Tree> {
|
||||||
if (
|
if (
|
||||||
this._model.originalFiles &&
|
this._model.originalFiles &&
|
||||||
Object.keys(this._model.originalFiles).length !== 0 &&
|
Object.keys(this._model.originalFiles).length !== 0 &&
|
||||||
!opt?.force
|
!opt.force
|
||||||
) {
|
) {
|
||||||
return this._model.originalFiles;
|
return this._model.originalFiles;
|
||||||
}
|
}
|
||||||
@@ -90,9 +96,6 @@ export default class Repository {
|
|||||||
this._model.originalFiles = files;
|
this._model.originalFiles = files;
|
||||||
this._model.size = { storage: 0, file: 0 };
|
this._model.size = { storage: 0, file: 0 };
|
||||||
await this.computeSize();
|
await this.computeSize();
|
||||||
await this._model.save();
|
|
||||||
|
|
||||||
this._model.originalFiles = files;
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,8 +143,13 @@ export default class Repository {
|
|||||||
zip(): Readable {
|
zip(): Readable {
|
||||||
return storage.archive(this.originalCachePath, {
|
return storage.archive(this.originalCachePath, {
|
||||||
format: "zip",
|
format: "zip",
|
||||||
fileTransformer: (filename) =>
|
fileTransformer: (filename: string) =>
|
||||||
anonymizeStream(filename, this) as Transformer,
|
anonymizeStream(
|
||||||
|
new AnonymizedFile({
|
||||||
|
repository: this,
|
||||||
|
anonymizedPath: filename,
|
||||||
|
})
|
||||||
|
) as Transformer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -119,9 +119,14 @@ export default class User {
|
|||||||
*/
|
*/
|
||||||
async getRepositories() {
|
async getRepositories() {
|
||||||
const repositories = (
|
const repositories = (
|
||||||
await AnonymizedRepositoryModel.find({
|
await AnonymizedRepositoryModel.find(
|
||||||
owner: this.id,
|
{
|
||||||
}).exec()
|
owner: this.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
originalFiles: 0,
|
||||||
|
}
|
||||||
|
).exec()
|
||||||
).map((d) => new Repository(d));
|
).map((d) => new Repository(d));
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (let repo of repositories) {
|
for (let repo of repositories) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { isText } from "istextorbinary";
|
|||||||
import { basename } from "path";
|
import { basename } from "path";
|
||||||
import { Transform } from "stream";
|
import { Transform } from "stream";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
|
import AnonymizedFile from "./AnonymizedFile";
|
||||||
|
|
||||||
const urlRegex =
|
const urlRegex =
|
||||||
/<?\b((https?|ftp|file):\/\/)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\b\/?>?/g;
|
/<?\b((https?|ftp|file):\/\/)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\b\/?>?/g;
|
||||||
@@ -31,7 +32,7 @@ export function isTextFile(filePath: string, content: Buffer) {
|
|||||||
return isText(filename, content);
|
return isText(filename, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function anonymizeStream(filename: string, repository: Repository) {
|
export function anonymizeStream(file: AnonymizedFile) {
|
||||||
const ts = new Transform();
|
const ts = new Transform();
|
||||||
var chunks = [],
|
var chunks = [],
|
||||||
len = 0,
|
len = 0,
|
||||||
@@ -43,8 +44,8 @@ export function anonymizeStream(filename: string, repository: Repository) {
|
|||||||
|
|
||||||
if (pos === 1) {
|
if (pos === 1) {
|
||||||
let data: any = Buffer.concat(chunks, len);
|
let data: any = Buffer.concat(chunks, len);
|
||||||
if (isTextFile(filename, data)) {
|
if (isTextFile(file.anonymizedPath, data)) {
|
||||||
data = anonymizeContent(data.toString(), repository);
|
data = anonymizeContent(data.toString(), file.repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks = [];
|
chunks = [];
|
||||||
@@ -60,8 +61,8 @@ export function anonymizeStream(filename: string, repository: Repository) {
|
|||||||
ts._flush = function _flush(cb) {
|
ts._flush = function _flush(cb) {
|
||||||
if (chunks.length) {
|
if (chunks.length) {
|
||||||
let data: any = Buffer.concat(chunks, len);
|
let data: any = Buffer.concat(chunks, len);
|
||||||
if (isText(filename, data)) {
|
if (isText(file.anonymizedPath, data)) {
|
||||||
data = anonymizeContent(data.toString(), repository);
|
data = anonymizeContent(data.toString(), file.repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.push(data);
|
this.push(data);
|
||||||
|
|||||||
@@ -21,14 +21,25 @@ export async function connect() {
|
|||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRepository(repoId: string) {
|
export async function getRepository(
|
||||||
|
repoId: string,
|
||||||
|
opts: {
|
||||||
|
includeFiles: boolean;
|
||||||
|
} = {
|
||||||
|
includeFiles: true,
|
||||||
|
}
|
||||||
|
) {
|
||||||
if (!repoId || repoId == "undefined") {
|
if (!repoId || repoId == "undefined") {
|
||||||
throw new AnonymousError("repo_not_found", {
|
throw new AnonymousError("repo_not_found", {
|
||||||
object: repoId,
|
object: repoId,
|
||||||
httpStatus: 404,
|
httpStatus: 404,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const data = await AnonymizedRepositoryModel.findOne({ repoId });
|
const project: any = {};
|
||||||
|
if (!opts.includeFiles) {
|
||||||
|
project.originalFiles = 0;
|
||||||
|
}
|
||||||
|
const data = await AnonymizedRepositoryModel.findOne({ repoId }, project);
|
||||||
if (!data)
|
if (!data)
|
||||||
throw new AnonymousError("repo_not_found", {
|
throw new AnonymousError("repo_not_found", {
|
||||||
object: repoId,
|
object: repoId,
|
||||||
|
|||||||
+2
-2
@@ -24,7 +24,7 @@ router.get(
|
|||||||
repository: repo,
|
repository: repo,
|
||||||
anonymizedPath,
|
anonymizedPath,
|
||||||
});
|
});
|
||||||
if (!(await f.isFileSupported())) {
|
if (!f.isFileSupported()) {
|
||||||
throw new AnonymousError("file_not_supported", {
|
throw new AnonymousError("file_not_supported", {
|
||||||
httpStatus: 403,
|
httpStatus: 403,
|
||||||
object: f,
|
object: f,
|
||||||
@@ -34,7 +34,7 @@ router.get(
|
|||||||
anonymizedPath.substring(anonymizedPath.lastIndexOf("/") + 1)
|
anonymizedPath.substring(anonymizedPath.lastIndexOf("/") + 1)
|
||||||
);
|
);
|
||||||
// cache the file for 5min
|
// cache the file for 5min
|
||||||
res.header('Cache-Control', 'max-age=300');
|
res.header("Cache-Control", "max-age=300");
|
||||||
await f.send(res);
|
await f.send(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError(error, res, req);
|
return handleError(error, res, req);
|
||||||
|
|||||||
@@ -109,7 +109,10 @@ router.post(
|
|||||||
"/:repoId/refresh",
|
"/:repoId/refresh",
|
||||||
async (req: express.Request, res: express.Response) => {
|
async (req: express.Request, res: express.Response) => {
|
||||||
try {
|
try {
|
||||||
const repo = await getRepo(req, res, { nocheck: true });
|
const repo = await getRepo(req, res, {
|
||||||
|
nocheck: true,
|
||||||
|
includeFiles: false,
|
||||||
|
});
|
||||||
if (!repo) return;
|
if (!repo) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -133,7 +136,10 @@ router.post(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/:repoId/",
|
"/:repoId/",
|
||||||
async (req: express.Request, res: express.Response) => {
|
async (req: express.Request, res: express.Response) => {
|
||||||
const repo = await getRepo(req, res, { nocheck: true });
|
const repo = await getRepo(req, res, {
|
||||||
|
nocheck: true,
|
||||||
|
includeFiles: false,
|
||||||
|
});
|
||||||
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 {
|
||||||
@@ -237,7 +243,10 @@ router.get(
|
|||||||
// get repository information
|
// get repository information
|
||||||
router.get("/:repoId/", async (req: express.Request, res: express.Response) => {
|
router.get("/:repoId/", async (req: express.Request, res: express.Response) => {
|
||||||
try {
|
try {
|
||||||
const repo = await getRepo(req, res, { nocheck: true });
|
const repo = await getRepo(req, res, {
|
||||||
|
nocheck: true,
|
||||||
|
includeFiles: false,
|
||||||
|
});
|
||||||
if (!repo) return;
|
if (!repo) return;
|
||||||
|
|
||||||
const user = await getUser(req);
|
const user = await getUser(req);
|
||||||
@@ -327,7 +336,10 @@ router.post(
|
|||||||
"/:repoId/",
|
"/:repoId/",
|
||||||
async (req: express.Request, res: express.Response) => {
|
async (req: express.Request, res: express.Response) => {
|
||||||
try {
|
try {
|
||||||
const repo = await getRepo(req, res, { nocheck: true });
|
const repo = await getRepo(req, res, {
|
||||||
|
nocheck: true,
|
||||||
|
includeFiles: false,
|
||||||
|
});
|
||||||
if (!repo) return;
|
if (!repo) return;
|
||||||
const user = await getUser(req);
|
const user = await getUser(req);
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ router.get(
|
|||||||
async (req: express.Request, res: express.Response) => {
|
async (req: express.Request, res: express.Response) => {
|
||||||
try {
|
try {
|
||||||
res.header("Cache-Control", "no-cache");
|
res.header("Cache-Control", "no-cache");
|
||||||
const repo = await getRepo(req, res, { nocheck: true });
|
const repo = await getRepo(req, res, { nocheck: true, includeFiles: false });
|
||||||
if (!repo) return;
|
if (!repo) return;
|
||||||
let redirectURL = null;
|
let redirectURL = null;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -37,11 +37,16 @@ export async function getPullRequest(
|
|||||||
export async function getRepo(
|
export async function getRepo(
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
res: express.Response,
|
res: express.Response,
|
||||||
opt?: { nocheck?: boolean }
|
opt: { nocheck?: boolean; includeFiles?: boolean } = {
|
||||||
|
nocheck: false,
|
||||||
|
includeFiles: true,
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const repo = await db.getRepository(req.params.repoId);
|
const repo = await db.getRepository(req.params.repoId, {
|
||||||
if (opt?.nocheck == true) {
|
includeFiles: opt.includeFiles,
|
||||||
|
});
|
||||||
|
if (opt.nocheck == true) {
|
||||||
} else {
|
} else {
|
||||||
// redirect if the repository is expired
|
// redirect if the repository is expired
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ async function webView(req: express.Request, res: express.Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let best_match = null;
|
let best_match = null;
|
||||||
indexSelector:
|
indexSelector: for (const p of indexPriority) {
|
||||||
for (const p of indexPriority) {
|
|
||||||
for (let filename in currentAnonymized) {
|
for (let filename in currentAnonymized) {
|
||||||
if (filename.toLowerCase() == p) {
|
if (filename.toLowerCase() == p) {
|
||||||
best_match = filename;
|
best_match = filename;
|
||||||
@@ -96,13 +95,13 @@ async function webView(req: express.Request, res: express.Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await f.isFileSupported())) {
|
if (!f.isFileSupported()) {
|
||||||
throw new AnonymousError("file_not_supported", {
|
throw new AnonymousError("file_not_supported", {
|
||||||
httpStatus: 400,
|
httpStatus: 400,
|
||||||
object: f,
|
object: f,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ((await f.extension()) == "md") {
|
if (f.extension() == "md") {
|
||||||
const content = await streamToString(await f.anonymizedContent());
|
const content = await streamToString(await f.anonymizedContent());
|
||||||
res.contentType("html").send(marked.marked(content));
|
res.contentType("html").send(marked.marked(content));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default class GitHubStream extends GitHubBase implements SourceBase {
|
|||||||
const ghRes = await octokit.rest.git.getBlob({
|
const ghRes = await octokit.rest.git.getBlob({
|
||||||
owner: this.githubRepository.owner,
|
owner: this.githubRepository.owner,
|
||||||
repo: this.githubRepository.repo,
|
repo: this.githubRepository.repo,
|
||||||
file_sha: file.sha,
|
file_sha: await file.sha(),
|
||||||
});
|
});
|
||||||
if (!ghRes.data.content && ghRes.data.size != 0) {
|
if (!ghRes.data.content && ghRes.data.size != 0) {
|
||||||
throw new AnonymousError("file_not_accessible", {
|
throw new AnonymousError("file_not_accessible", {
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ export default class S3Storage implements StorageBase {
|
|||||||
endpoint: config.S3_ENDPOINT,
|
endpoint: config.S3_ENDPOINT,
|
||||||
accessKeyId: config.S3_CLIENT_ID,
|
accessKeyId: config.S3_CLIENT_ID,
|
||||||
secretAccessKey: config.S3_CLIENT_SECRET,
|
secretAccessKey: config.S3_CLIENT_SECRET,
|
||||||
|
httpOptions: {
|
||||||
|
timeout: 1000 * 60 * 60 * 2, // 2 hour
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user