mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-01 22:29:47 +02:00
fix: restore public file streaming and contain ZIP failures
This commit is contained in:
1 parent
ecba15ce2e
commit
c9c729c2c5
7 files changed
+226
-22
No files matched your search
@@ -18,6 +18,7 @@ import FileModel from "./model/files/files.model";
|
||||
import { IFile } from "./model/files/files.types";
|
||||
import { FilterQuery } from "mongoose";
|
||||
import { createLogger, serializeError } from "./logger";
|
||||
import { githubTokenForStreamer } from "./github-token-context";
|
||||
|
||||
const logger = createLogger("anonymized-file");
|
||||
|
||||
@@ -356,7 +357,7 @@ export default class AnonymizedFile {
|
||||
return got.stream(join(config.STREAMER_ENTRYPOINT, "api"), {
|
||||
method: "POST",
|
||||
json: {
|
||||
token: await this.repository.getToken(),
|
||||
token: await githubTokenForStreamer(await this.repository.getToken(), this.repository.model.source.repositoryName),
|
||||
repoFullName: this.repository.model.source.repositoryName,
|
||||
commit: this.repository.model.source.commit,
|
||||
branch: this.repository.model.source.branch,
|
||||
@@ -403,7 +404,7 @@ export default class AnonymizedFile {
|
||||
json: {
|
||||
sha,
|
||||
size,
|
||||
token,
|
||||
token: await githubTokenForStreamer(token, this.repository.model.source.repositoryName),
|
||||
repoFullName: this.repository.model.source.repositoryName,
|
||||
commit: this.repository.model.source.commit,
|
||||
branch: this.repository.model.source.branch,
|
||||
|
||||
@@ -11,6 +11,17 @@ export function githubTokenContext(token: string) {
|
||||
if (!context && token.startsWith("public-read:")) throw new Error("Public repository access context expired");
|
||||
return context;
|
||||
}
|
||||
// Public handles belong to this process. Revalidate here, then let the
|
||||
// streamer fetch public bytes anonymously without forwarding the owner's token.
|
||||
export async function githubTokenForStreamer(token: string, repository: string | undefined): Promise<string> {
|
||||
const context = githubTokenContext(token);
|
||||
if (!context?.publicRepository) return token;
|
||||
if (!repository || context.publicRepository.toLowerCase() !== repository.toLowerCase()) {
|
||||
throw new Error("Public repository access context mismatch");
|
||||
}
|
||||
await context.renew();
|
||||
return "";
|
||||
}
|
||||
export function githubQuotaKey(token: string) {
|
||||
return contexts.get(token)?.quotaKey || createHash("sha256").update(token).digest("hex").slice(0, 24);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export default class GitHubStream extends GitHubBase {
|
||||
logger.debug("downloading via raw URL (LFS)", { url });
|
||||
return got.stream(url, {
|
||||
hooks: { beforeRequest: [async () => { await githubTokenContext(token)?.renew(); }] },
|
||||
headers: githubTokenContext(token)?.publicRepository ? {} : { authorization: `token ${token}` },
|
||||
headers: !token || githubTokenContext(token)?.publicRepository ? {} : { authorization: `token ${token}` },
|
||||
followRedirect: true,
|
||||
});
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export default class GitHubStream extends GitHubBase {
|
||||
): Promise<stream.Readable> {
|
||||
// Public raw downloads need no bearer token and do not consume the
|
||||
// unauthenticated REST API quota. GitHub also resolves LFS pointers here.
|
||||
if (githubTokenContext(token)?.publicRepository) {
|
||||
if (!token || githubTokenContext(token)?.publicRepository) {
|
||||
return Promise.resolve(this.downloadFileViaRaw(token, filePath));
|
||||
}
|
||||
return new Promise<stream.Readable>((resolve) => {
|
||||
|
||||
+18
-17
@@ -124,9 +124,14 @@ export async function streamAnonymizedZip(
|
||||
// opens). Destroy the response instead so the client sees a connection
|
||||
// drop and knows the download failed. Same class of silent-truncation
|
||||
// bug as #694.
|
||||
let upstreamSucceeded = false;
|
||||
let failed = false;
|
||||
const parser = Parse();
|
||||
const fail = (error: Error) => {
|
||||
if (failed) return;
|
||||
failed = true;
|
||||
logger.error("upstream zipball failed", serializeError(error));
|
||||
downloadStream.destroy();
|
||||
downloadStream.unpipe(parser);
|
||||
archive.abort();
|
||||
const destroyable = res as unknown as {
|
||||
destroy?: (err?: Error) => void;
|
||||
@@ -138,10 +143,13 @@ export async function streamAnonymizedZip(
|
||||
destroyable.end();
|
||||
}
|
||||
};
|
||||
// pipe() returns the destination. Listen on the archive itself as well,
|
||||
// including errors emitted after the upstream ZIP has finished downloading.
|
||||
archive.on("error", fail);
|
||||
|
||||
downloadStream
|
||||
.on("error", fail)
|
||||
.pipe(Parse())
|
||||
.pipe(parser)
|
||||
.on("entry", (entry: NodeJS.ReadableStream & { type: string; path: string; autodrain: () => void }) => {
|
||||
if (entry.type === "File") {
|
||||
try {
|
||||
@@ -161,11 +169,13 @@ export async function streamAnonymizedZip(
|
||||
...opt.anonymizerOptions,
|
||||
filePath: entry.path,
|
||||
});
|
||||
entry.on("error", fail);
|
||||
anonymizer.on("error", fail);
|
||||
const st = entry.pipe(anonymizer);
|
||||
archive.append(st, { name: fileName });
|
||||
} catch (error) {
|
||||
entry.autodrain();
|
||||
logger.error("entry transform failed", serializeError(error));
|
||||
fail(error as Error);
|
||||
}
|
||||
} else {
|
||||
entry.autodrain();
|
||||
@@ -173,22 +183,13 @@ export async function streamAnonymizedZip(
|
||||
})
|
||||
.on("error", fail)
|
||||
.on("finish", () => {
|
||||
upstreamSucceeded = true;
|
||||
if (failed) return;
|
||||
try {
|
||||
archive.finalize();
|
||||
} catch {
|
||||
/* ignored */
|
||||
archive.finalize().catch(fail);
|
||||
} catch (error) {
|
||||
fail(error as Error);
|
||||
}
|
||||
});
|
||||
|
||||
archive.pipe(res).on("error", (error) => {
|
||||
logger.error("archive pipe error", serializeError(error));
|
||||
if (!upstreamSucceeded) {
|
||||
// archive errored while we were still depending on upstream bytes:
|
||||
// treat as failure rather than truncating.
|
||||
fail(error);
|
||||
return;
|
||||
}
|
||||
(res as { end?: () => void }).end?.();
|
||||
});
|
||||
archive.pipe(res).on("error", fail);
|
||||
}
|
||||
Reference in new issue
Block a user