mirror of
https://gitbruv.vercel.app/api/git/bruv/gitbruv.git
synced 2025-12-20 23:24:09 +01:00
146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { db } from "@/db";
|
|
import { users, repositories } from "@/db/schema";
|
|
import { eq, and } from "drizzle-orm";
|
|
import git from "isomorphic-git";
|
|
import { createR2Fs, getRepoPrefix } from "@/lib/r2-fs";
|
|
import { rateLimit } from "@/lib/rate-limit";
|
|
import { authenticateRequest } from "@/lib/api-auth";
|
|
|
|
const CHUNK_SIZE = 64 * 1024;
|
|
|
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
|
const rateLimitResult = rateLimit(request, "file", { limit: 120, windowMs: 60000 });
|
|
if (!rateLimitResult.success) {
|
|
return new NextResponse("Too Many Requests", {
|
|
status: 429,
|
|
headers: { "Retry-After": Math.ceil((rateLimitResult.resetAt - Date.now()) / 1000).toString() },
|
|
});
|
|
}
|
|
|
|
const { path } = await params;
|
|
|
|
if (path.length < 4) {
|
|
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
}
|
|
|
|
const [username, repoName, branch, ...fileParts] = path;
|
|
const filePath = fileParts.join("/");
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.username, username),
|
|
});
|
|
|
|
if (!user) {
|
|
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
|
}
|
|
|
|
const repo = await db.query.repositories.findFirst({
|
|
where: and(eq(repositories.ownerId, user.id), eq(repositories.name, repoName)),
|
|
});
|
|
|
|
if (!repo) {
|
|
return NextResponse.json({ error: "Repository not found" }, { status: 404 });
|
|
}
|
|
|
|
if (repo.visibility === "private") {
|
|
const authenticatedUser = await authenticateRequest(request);
|
|
if (!authenticatedUser || authenticatedUser.id !== repo.ownerId) {
|
|
return new NextResponse("Unauthorized", {
|
|
status: 401,
|
|
headers: { "WWW-Authenticate": 'Basic realm="gitbruv"' },
|
|
});
|
|
}
|
|
}
|
|
|
|
const repoPrefix = getRepoPrefix(user.id, `${repoName}.git`);
|
|
const fs = createR2Fs(repoPrefix);
|
|
|
|
try {
|
|
const commits = await git.log({
|
|
fs,
|
|
gitdir: "/",
|
|
ref: branch,
|
|
depth: 1,
|
|
});
|
|
|
|
if (commits.length === 0) {
|
|
return NextResponse.json({ error: "Branch not found" }, { status: 404 });
|
|
}
|
|
|
|
const commitOid = commits[0].oid;
|
|
const parts = filePath.split("/").filter(Boolean);
|
|
const fileName = parts.pop()!;
|
|
|
|
let currentTree = (await git.readTree({ fs, gitdir: "/", oid: commitOid })).tree;
|
|
|
|
for (const part of parts) {
|
|
const entry = currentTree.find((e) => e.path === part && e.type === "tree");
|
|
if (!entry) {
|
|
return NextResponse.json({ error: "Path not found" }, { status: 404 });
|
|
}
|
|
currentTree = (await git.readTree({ fs, gitdir: "/", oid: entry.oid })).tree;
|
|
}
|
|
|
|
const fileEntry = currentTree.find((e) => e.path === fileName && e.type === "blob");
|
|
if (!fileEntry) {
|
|
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
|
}
|
|
|
|
const { blob } = await git.readBlob({
|
|
fs,
|
|
gitdir: "/",
|
|
oid: fileEntry.oid,
|
|
});
|
|
|
|
const rangeHeader = request.headers.get("range");
|
|
|
|
if (rangeHeader) {
|
|
const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
|
|
if (match) {
|
|
const start = parseInt(match[1], 10);
|
|
const end = match[2] ? parseInt(match[2], 10) : Math.min(start + CHUNK_SIZE - 1, blob.length - 1);
|
|
const chunk = blob.slice(start, end + 1);
|
|
|
|
return new NextResponse(chunk, {
|
|
status: 206,
|
|
headers: {
|
|
"Content-Range": `bytes ${start}-${end}/${blob.length}`,
|
|
"Accept-Ranges": "bytes",
|
|
"Content-Length": chunk.length.toString(),
|
|
"Content-Type": "application/octet-stream",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const stream = new ReadableStream({
|
|
start(controller) {
|
|
let offset = 0;
|
|
const push = () => {
|
|
if (offset >= blob.length) {
|
|
controller.close();
|
|
return;
|
|
}
|
|
const chunk = blob.slice(offset, offset + CHUNK_SIZE);
|
|
controller.enqueue(chunk);
|
|
offset += CHUNK_SIZE;
|
|
setTimeout(push, 0);
|
|
};
|
|
push();
|
|
},
|
|
});
|
|
|
|
return new NextResponse(stream, {
|
|
headers: {
|
|
"Content-Type": "application/octet-stream",
|
|
"Content-Length": blob.length.toString(),
|
|
"Accept-Ranges": "bytes",
|
|
"X-Total-Size": blob.length.toString(),
|
|
},
|
|
});
|
|
} catch (err) {
|
|
console.error("File streaming error:", err);
|
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
}
|
|
}
|