update: api router, to use new encryption and support new feature

This commit is contained in:
mbaharip
2025-01-21 21:59:58 +07:00
parent bc0467cd97
commit 1023229410
10 changed files with 485 additions and 396 deletions
+145
View File
@@ -0,0 +1,145 @@
import { type NextRequest, NextResponse } from "next/server";
import { encryptionService, gdrive } from "~/lib/utils.server";
import { GetFile } from "~/actions/files";
import { CheckIndexPassword, CheckPagePassword } from "~/actions/password";
import { ValidatePaths } from "~/actions/paths";
import { ValidateFileToken } from "~/actions/token";
import config from "config";
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest, { params }: { params: Promise<{ rest: string[] }> }) {
const { rest } = await params;
const sp = new URL(request.nextUrl).searchParams;
const forceRedirect = sp.get("redirect") === "1";
const token = sp.get("token");
const paths = rest.map((path) => {
if (path.startsWith("/")) return decodeURIComponent(path.slice(1));
return decodeURIComponent(path);
});
try {
if (!config.apiConfig.allowDownloadProtectedFile) {
if (!token) {
throw new Error("[401] Token not found", {
cause: "This endpoint requires a token to download the file",
});
}
const validateToken = await ValidateFileToken(token);
if (!validateToken.success) {
throw new Error(`[401] ${validateToken.message}`, {
cause: validateToken.error,
});
}
}
const validatedPaths = await ValidatePaths(paths);
if (!validatedPaths.success) {
throw new Error(`[404] ${validatedPaths.message}`, {
cause: validatedPaths.error,
});
}
const currentFile = validatedPaths.data.pop();
if (!currentFile) {
throw new Error("[404] File not found", {
cause: "Failed to get current file",
});
}
if (config.siteConfig.privateIndex && !config.apiConfig.allowDownloadProtectedFile) {
const [indexUnlocked, pageUnlocked] = await Promise.all([
CheckIndexPassword(),
CheckPagePassword(validatedPaths.data),
]);
if (!indexUnlocked.success) {
throw new Error(`[401] ${indexUnlocked.message}`, {
cause: indexUnlocked.error,
});
}
if (!pageUnlocked.success) {
throw new Error(`[401] ${pageUnlocked.message}`, {
cause: pageUnlocked.error,
});
}
}
const file = await GetFile(currentFile.id);
if (!file.success) {
throw new Error(`[404] ${file.message}`, {
cause: file.error,
});
}
if (!file.data?.encryptedWebContentLink) {
throw new Error("[500] No download link found", {
cause: "No download link found",
});
}
const fileSize = Number(file.data?.size ?? 0);
if ((config.apiConfig.maxFileSize > 0 && fileSize > config.apiConfig.maxFileSize) || forceRedirect) {
const decryptedContentUrl = await encryptionService.decrypt(file.data.encryptedWebContentLink);
const contentUrl = new URL(decryptedContentUrl);
contentUrl.searchParams.set("confirm", "1");
return new NextResponse(null, {
status: 302,
headers: {
Location: contentUrl.toString(),
},
});
}
const content = await gdrive.files.get(
{
fileId: await encryptionService.decrypt(file.data.encryptedId),
alt: "media",
supportsAllDrives: config.apiConfig.isTeamDrive,
acknowledgeAbuse: true,
},
{
responseType: "stream",
},
);
const fileBuffer = await new Promise<Buffer>((res, rej) => {
const chunks: Buffer<ArrayBufferLike>[] = [];
content.data.on("data", (chunk) => {
chunks.push(Buffer.from(chunk as ArrayBufferLike));
});
content.data.on("end", () => {
res(Buffer.concat(chunks));
});
content.data.on("error", (err) => {
rej(err);
});
});
return new NextResponse(fileBuffer, {
status: 200,
headers: {
"Content-Type": file.data.mimeType,
"Content-Length": fileBuffer.length.toString(),
"Content-Disposition": `attachment; filename="${file.data.name}"`,
"Cache-Control": config.cacheControl,
},
});
} catch (error) {
const e = error as Error;
const message = e.message.replace(/\[.*\]/, "").trim();
const status = /\[.*\]/.exec(e.message)?.[0].replace(/\[|\]/g, "").trim() ?? 500;
return NextResponse.json(
{
scope: "api/download",
message,
cause: e.cause ?? "Unknown",
},
{
status: Number(status),
},
);
}
}
@@ -0,0 +1,158 @@
import { type NextRequest, NextResponse } from "next/server";
import { encryptionService } from "~/lib/utils.server";
import { gdriveNoCache as gdrive } from "~/utils/gdriveInstance";
import { GetFile } from "~/actions/files";
import { CheckIndexPassword, CheckPagePassword } from "~/actions/password";
import { ValidatePaths } from "~/actions/paths";
import { GetSearchResultPath } from "~/actions/search";
import { ValidateFileToken } from "~/actions/token";
import config from "config";
export const dynamic = "force-dynamic";
export async function GET(
request: NextRequest,
{
params,
}: {
params: Promise<{
encryptedId: string;
}>;
},
) {
const { encryptedId } = await params;
const sp = new URL(request.nextUrl).searchParams;
const token = sp.get("token");
try {
if (!token) {
throw new Error("[401] Token not found", {
cause: "This endpoint requires a token to download the file",
});
}
const decryptedId = await encryptionService.decrypt(encryptedId);
console.log(decryptedId);
const filePath = await GetSearchResultPath(encryptedId);
console.log("filePath");
// const [decryptedId, filePath] = await Promise.all([
// encryptionService.decrypt(encryptedId),
// GetSearchResultPath(encryptedId),
// ]).then((data) => {
// console.log(data);
// return data;
// });
if (!filePath.success) {
throw new Error(`[404] ${filePath.message}`, {
cause: filePath.error,
});
}
if (config.siteConfig.privateIndex && !config.apiConfig.allowDownloadProtectedFile) {
const paths = await ValidatePaths(filePath.data.split("/"));
if (!paths.success) {
throw new Error(`[404] ${paths.message}`, {
cause: paths.error,
});
}
const [indexUnlocked, pageUnlocked] = await Promise.all([CheckIndexPassword(), CheckPagePassword(paths.data)]);
if (!indexUnlocked.success) {
throw new Error(`[401] ${indexUnlocked.message}`, {
cause: indexUnlocked.error,
});
}
if (!pageUnlocked.success) {
throw new Error(`[401] ${pageUnlocked.message}`, {
cause: pageUnlocked.error,
});
}
}
const validateToken = await ValidateFileToken(token);
if (!validateToken.success) {
throw new Error(`[401] ${validateToken.message}`, {
cause: validateToken.error,
});
}
const file = await GetFile(encryptedId);
if (!file.success) {
throw new Error(`[404] ${file.message}`, {
cause: file.error,
});
}
const fileSize = Number(file.data?.size ?? 0);
if (!file.data?.encryptedWebContentLink) {
throw new Error("[500] No download link found", {
cause: "No download link returned from file data",
});
}
if (config.apiConfig.maxFileSize && fileSize > config.apiConfig.maxFileSize) {
const decryptedContentUrl = await encryptionService.decrypt(file.data.encryptedWebContentLink);
const contentUrl = new URL(decryptedContentUrl);
contentUrl.searchParams.set("confirm", "1");
return new NextResponse(null, {
status: 302,
headers: {
Location: contentUrl.toString(),
},
});
}
const content = await gdrive.files.get(
{
fileId: decryptedId,
alt: "media",
supportsAllDrives: config.apiConfig.isTeamDrive,
acknowledgeAbuse: true,
},
{
responseType: "stream",
},
);
const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer<ArrayBufferLike>[] = [];
content.data.on("data", (chunk) => {
chunks.push(Buffer.from(chunk as ArrayBufferLike));
});
content.data.on("end", () => {
resolve(Buffer.concat(chunks));
});
content.data.on("error", (err) => {
reject(err);
});
});
return new NextResponse(fileBuffer, {
status: 200,
headers: {
"Content-Type": file.data.mimeType ?? "application/octet-stream",
"Content-Length": fileBuffer.length.toString(),
"Content-Disposition": `attachment; filename="${encodeURIComponent(
file.data.name ?? `Untitled.${file.data.fileExtension}`,
)}"`,
"Cache-Control": config.cacheControl,
},
});
} catch (error) {
const e = error as Error;
const message = e.message.replace(/\[.*\]/, "").trim();
const status = /\[.*\]/.exec(e.message)?.[0].replace(/\[|\]/g, "").trim() ?? 500;
return NextResponse.json(
{
scope: "api/download",
message,
cause: e.cause ?? "Unknown",
},
{
status: Number(status),
},
);
}
}
-133
View File
@@ -1,133 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { decryptData } from "~/utils/encryptionHelper";
import { gdriveNoCache as gdrive } from "~/utils/gdriveInstance";
import { CheckDownloadToken, CheckPassword, CheckPaths, CheckSitePassword, RedirectSearchFile } from "actions";
import config from "config";
export const dynamic = "force-dynamic";
export async function GET(
request: NextRequest,
{
params: { encryptedId },
}: {
params: {
encryptedId: string;
};
},
) {
try {
const sp = new URL(request.nextUrl).searchParams;
const token = sp.get("token");
if (!token) throw new Error("Token not found");
const tokenValidity = await CheckDownloadToken(token);
if (!tokenValidity.success) throw new Error(tokenValidity.message);
if (config.siteConfig.privateIndex && !config.apiConfig.allowDownloadProtectedFile) {
const unlocked = await CheckSitePassword();
if (!unlocked.success) {
return new NextResponse(
`It seems like this site is protected by password, and you haven't entered the password yet.
If you've already entered the password, please make sure your browser is not blocking cookies from this site.`,
{
status: 401,
},
);
}
}
const decryptedId = await decryptData(encryptedId);
const _filePaths = RedirectSearchFile(encryptedId);
const _fileMeta = gdrive.files.get({
fileId: decryptedId,
fields: "id, name, mimeType, size, fileExtension, webContentLink",
supportsAllDrives: config.apiConfig.isTeamDrive,
});
const [fileMeta, filePaths] = await Promise.all([_fileMeta, _filePaths]);
if (!config.apiConfig.allowDownloadProtectedFile) {
const checkPath = await CheckPaths(filePaths.split("/"));
if (!checkPath.success) throw new Error("File not found");
const unlocked = await CheckPassword(checkPath.data);
if (!unlocked.success) {
if (!unlocked.path) throw new Error("No path returned from password checking");
const lockedIndex = checkPath.data.findIndex((path) => path.id === unlocked.path);
// Get all path until the locked index, then join them
const path = checkPath.data
.slice(0, lockedIndex + 1)
.map((path) => path.path)
.join("/");
return new NextResponse(
`The file you're trying to access is protected by password.
Please open the file link and enter the password to access the file, then try to download the file again.
Protected Path: ${new URL(path, config.basePath).toString()}
If you've already entered the password, please make sure your browser is not blocking cookies from this site.`,
{
status: 401,
},
);
}
}
const fileSize = Number(fileMeta.data.size || 0);
if (!fileMeta.data.webContentLink) throw new Error("No download link found");
if (config.apiConfig.maxFileSize && fileSize > config.apiConfig.maxFileSize) {
const contentUrl = new URL(fileMeta.data.webContentLink);
contentUrl.searchParams.set("confirm", "1");
return NextResponse.redirect(contentUrl, {
status: 302,
});
}
const fileContent = await gdrive.files.get(
{
fileId: decryptedId,
alt: "media",
supportsAllDrives: config.apiConfig.isTeamDrive,
},
{
responseType: "stream",
},
);
const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
fileContent.data.on("data", (chunk) => {
chunks.push(chunk);
});
fileContent.data.on("end", () => {
resolve(Buffer.concat(chunks));
});
fileContent.data.on("error", (err) => {
reject(err);
});
});
return new NextResponse(fileBuffer, {
status: 200,
headers: {
"Content-Type": fileMeta.data.mimeType || "application/octet-stream",
"Content-Length": fileBuffer.length.toString(),
"Content-Disposition": `attachment; filename="${encodeURIComponent(
fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`,
)}"`,
"Cache-Control": config.cacheControl,
},
});
} catch (error) {
const e = error as Error;
console.error(e.message);
return new NextResponse(e.message, {
status: 500,
});
}
}
+4 -2
View File
@@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { encryptionService } from "~/lib/utils.server";
@@ -12,11 +12,13 @@ export async function GET(request: NextRequest) {
if (!query) return new NextResponse("Add query parameter 'q' with the value to encrypt", { status: 400 });
const encrypted = await encryptionService.encrypt(query, key ?? undefined);
const decrypted = await encryptionService.decrypt(encrypted, key ?? undefined);
return NextResponse.json(
{
message: key ? "Encrypted with provided key" : "Encrypted with environment key",
value: encrypted,
encryptedValue: encrypted,
decryptedValue: decrypted,
key: key ?? process.env.ENCRYPTION_KEY,
},
{ status: 200 },
+4 -5
View File
@@ -1,7 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { type NextRequest, NextResponse } from "next/server";
import { decryptData } from "~/utils/encryptionHelper";
import gdrive from "~/utils/gdriveInstance";
import { encryptionService, gdrive } from "~/lib/utils.server";
import config from "config";
@@ -13,7 +12,7 @@ type Props = {
export async function GET(request: NextRequest, { params: { encryptedId } }: Props) {
try {
const decryptedId = await decryptData(encryptedId);
const decryptedId = await encryptionService.decrypt(encryptedId);
const { data } = await gdrive.files.get({
fileId: decryptedId,
fields: "id, name, mimeType, webContentLink",
@@ -29,7 +28,7 @@ export async function GET(request: NextRequest, { params: { encryptedId } }: Pro
return new NextResponse(bufferData, {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
"Content-Type": data.mimeType || "application/octet-stream",
"Content-Type": data.mimeType ?? "application/octet-stream",
"Content-Length": bufferData.length.toString(),
"Content-Disposition": `inline; filename="${data.name}"`,
},
+120
View File
@@ -0,0 +1,120 @@
import { type NextRequest, NextResponse } from "next/server";
import { IS_DEV } from "~/constant";
import { encryptionService, gdriveNoCache } from "~/lib/utils.server";
import { GetFile } from "~/actions/files";
import config from "config";
export const dynamic = "force-dynamic";
export async function GET(
request: NextRequest,
{
params,
}: {
params: Promise<{
encryptedId: string;
}>;
},
) {
const { encryptedId } = await params;
const sp = new URL(request.url).searchParams;
const isInline = sp.get("inline") === "1";
const isFull = sp.get("full") === "1";
const origin = request.headers.get("Origin") ?? request.headers.get("Referer") ?? request.headers.get("Host") ?? null;
try {
if (!origin) {
throw new Error("[500] Invalid request", {
cause: "Request headers is invalid",
});
}
// // Only allow if the request is from the same domain or the referer is the same domain
if (!IS_DEV && !origin?.toLowerCase().includes(config.basePath.toLowerCase())) {
throw new Error("[403] Unauthorized", {
cause: "Request is not allowed",
});
}
const decryptedId = await encryptionService.decrypt(encryptedId);
const file = await GetFile(encryptedId);
if (!file.success) {
throw new Error(`[404] ${file.message}`, {
cause: file.error,
});
}
if (!file.data?.encryptedWebContentLink) {
throw new Error("[500] No download link found", {
cause: "No download link found",
});
}
const fileSize = Number(file.data.size ?? 0);
const isFullyLoaded = isFull ? true : Number(request.headers.get("Range")?.split("-")[1] ?? 0) === fileSize - 1;
if (config.apiConfig.streamMaxSize && fileSize > config.apiConfig.streamMaxSize) {
throw new Error("[400] File is too large to stream", {
cause: "File size is larger than the maximum allowed size, please download the file instead",
});
}
const ranges = request.headers.get("Range") ?? "bytes=0-";
const chunkSize = 5 * 1024 * 1024; // Load 5MB at a time
let rangeStart = 0;
let rangeEnd = Math.min(chunkSize, fileSize - 1);
const rangeSize = /bytes=(\d+)-(\d+)?/.exec(ranges);
if (rangeSize) {
rangeStart = Number(rangeSize?.[1] ?? 0);
if (isFullyLoaded) {
rangeEnd = fileSize - 1;
} else {
rangeEnd = Math.min(rangeStart + chunkSize, fileSize - 1);
}
}
const content = await gdriveNoCache.files.get(
{
fileId: decryptedId,
alt: "media",
supportsAllDrives: config.apiConfig.isTeamDrive,
acknowledgeAbuse: true,
},
{
responseType: "stream",
headers: {
"Accept-Ranges": "bytes",
"Range": `bytes=${rangeStart}-${rangeEnd}`,
},
},
);
const headers = new Headers(content.headers);
if (isInline) {
headers.set("Content-Disposition", `inline; filename="${file.data.name}"`);
}
return new NextResponse(content.data as unknown as BodyInit, {
status: isFullyLoaded ? 200 : content.status,
headers: headers,
});
} catch (error) {
const e = error as Error;
const message = e.message.replace(/\[.*\]/, "").trim();
const status = /\[.*\]/.exec(e.message)?.[0].replace(/\[|\]/g, "").trim() ?? 500;
return NextResponse.json(
{
scope: "api/preview",
message,
cause: e.cause ?? "Unknown",
},
{
status: Number(status),
},
);
}
}
+53 -29
View File
@@ -1,49 +1,73 @@
import { type NextRequest, NextResponse } from "next/server";
import { decryptData } from "~/utils/encryptionHelper";
import { encryptionService } from "~/lib/utils.server";
import { CheckPassword, CheckPaths, GetFile } from "actions";
import config from "config";
import { GetFile } from "~/actions/files";
import { ValidatePaths } from "~/actions/paths";
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest, { params: { rest } }: { params: { rest: string[] } }) {
export async function GET(request: NextRequest, { params }: { params: Promise<{ rest: string[] }> }) {
const { rest } = await params;
const ALLOWED_TYPES = ["image/*", "video/*", "audio/*"]; // Based from mime types
const paths = rest.map((path) => {
if (path.startsWith("/")) return decodeURIComponent(path.slice(1));
return decodeURIComponent(path);
});
try {
const sp = new URL(request.nextUrl).searchParams;
const token = sp.get("token");
if (!token) throw new Error("Token not found");
const paths = await CheckPaths(rest);
if (!paths.success) throw new Error(paths.message);
if (!config.apiConfig.allowDownloadProtectedFile) {
const unlocked = await CheckPassword(paths.data);
if (!unlocked.success)
throw new Error(unlocked.path ? unlocked.message : "No path returned from password checking");
const validatedPaths = await ValidatePaths(paths);
if (!validatedPaths.success) {
throw new Error(`[404] ${validatedPaths.message}`, {
cause: validatedPaths.error,
});
}
const encryptedId = paths.data.pop()?.id;
if (!encryptedId) throw new Error("Failed to get encrypted ID, try to refresh the page.");
if (token !== encryptedId) throw new Error("Invalid token");
const currentFile = validatedPaths.data.pop();
if (!currentFile) {
throw new Error("[404] File not found", {
cause: "Failed to get current file",
});
}
if (ALLOWED_TYPES.every((type) => !new RegExp(type.replace("*", ".*")).test(currentFile.mimeType))) {
throw new Error("[400] Invalid file type", {
cause: "Raw link only available for video, image, and audio files",
});
}
const data = await GetFile(encryptedId);
if (data.mimeType?.includes("folder")) throw new Error("Can't download folder");
if (!data.mimeType.includes("video") && !data.mimeType.includes("image") && !data.mimeType.includes("audio"))
throw new Error("Raw link only available for video, image, and audio files");
if (!data.encryptedWebContentLink) throw new Error("No download link found");
const fileMeta = await GetFile(currentFile.id);
if (!fileMeta.success) {
throw new Error(`[500] ${fileMeta.message}`, {
cause: fileMeta.error,
});
}
if (!fileMeta.data?.encryptedWebContentLink) {
throw new Error("[500] No download link found", {
cause: "No download link found",
});
}
const decryptedWebContent = await decryptData(data.encryptedWebContentLink);
const decryptedLink = await encryptionService.decrypt(fileMeta.data.encryptedWebContentLink);
return new NextResponse(null, {
status: 302,
headers: {
Location: decryptedWebContent,
Location: decryptedLink,
},
});
} catch (error) {
const e = error as Error;
console.error(e.message);
return new NextResponse(e.message, {
status: 500,
});
const message = e.message.replace(/\[.*\]/, "").trim();
const status = /\[.*\]/.exec(e.message)?.[0].replace(/\[|\]/g, "").trim() ?? 500;
return NextResponse.json(
{
scope: "api/raw",
message,
cause: e.cause ?? "Unknown",
},
{
status: Number(status),
},
);
}
}
-67
View File
@@ -1,67 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { encryptionService } from "~/lib/utils.server";
import { GetFile } from "~/actions/files";
import { ValidatePaths } from "~/actions/paths";
// TODO: Barebone implementation, add more protection like token, etc
export async function GET(req: NextRequest) {
const sp = new URL(req.nextUrl).searchParams;
const url = sp.get("url");
try {
if (!url)
throw new Error("[400] Invalid raw api usage", {
cause: "No URL provided",
});
const paths = url.split("/").filter(Boolean);
const validatePaths = await ValidatePaths(paths);
if (!validatePaths.success)
throw new Error(`[404] ${validatePaths.message}`, {
cause: validatePaths.error,
});
const currentFile = validatePaths.data.pop();
if (!currentFile)
throw new Error("[404] File not found", {
cause: "Failed to get current file",
});
if (["image", "video", "audio"].every((type) => !currentFile.mimeType.includes(type)))
throw new Error("[400] Invalid file type", {
cause: "Raw link only available for video, image, and audio files",
});
const file = await GetFile(currentFile.id);
if (!file.success)
throw new Error(`[500] ${file.message}`, {
cause: file.error,
});
if (!file.data?.encryptedWebContentLink)
throw new Error("[500] No download link found", {
cause: "No download link found",
});
const decryptedLink = await encryptionService.decrypt(file.data.encryptedWebContentLink);
return new NextResponse(null, {
status: 302,
headers: {
Location: decryptedLink,
},
});
} catch (error) {
const e = error as Error;
const message = e.message.replace(/\[.*\]/, "").trim();
const status = /\[.*\]/.exec(e.message)?.[0].replace(/\[|\]/g, "").trim() ?? 500;
return NextResponse.json(
{
scope: "api/raw",
message,
cause: e.cause ?? "Unknown",
},
{
status: Number(status),
},
);
}
}
-159
View File
@@ -1,159 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { IS_DEV } from "~/constant";
import { decryptData } from "~/utils/encryptionHelper";
import { gdriveNoCache as gdrive } from "~/utils/gdriveInstance";
import { CheckDownloadToken, CheckPassword, CheckPaths, RedirectSearchFile } from "actions";
import config from "config";
export const dynamic = "force-dynamic";
export async function GET(
request: NextRequest,
{
params: { encryptedId },
}: {
params: {
encryptedId: string;
};
},
) {
try {
const sp = new URL(request.nextUrl).searchParams;
const token = sp.get("token");
if (!token) throw new Error("Token not found");
// Only allow if the request is from the same domain or the referer is the same domain
if (!IS_DEV && !request.headers.get("Referer")?.includes(config.basePath)) {
throw new Error("Invalid request");
}
const tokenValidity = await CheckDownloadToken(token);
if (!tokenValidity.success) throw new Error(tokenValidity.message);
const decryptedId = await decryptData(encryptedId);
const _filePaths = RedirectSearchFile(encryptedId);
const _fileMeta = gdrive.files.get(
{
fileId: decryptedId,
fields: "id, name, mimeType, size, fileExtension, webContentLink",
supportsAllDrives: config.apiConfig.isTeamDrive,
},
{
headers: {
"Accept-Ranges": "bytes",
"Range": request.headers.get("Range") || `bytes=0-${1024 * 1024 - 1}`,
},
},
);
const [filePaths, fileMeta] = await Promise.all([_filePaths, _fileMeta]);
const isFull = Number(request.headers.get("Range")?.split("-")[1] || 0) === Number(fileMeta.data.size || "1") - 1;
const fileSize = Number(fileMeta.data.size || 0);
if (!fileMeta.data.webContentLink) throw new Error("No download link found");
if (config.apiConfig.streamMaxSize && fileSize > config.apiConfig.streamMaxSize) {
throw new Error("File is too large to stream");
}
if (!config.apiConfig.allowDownloadProtectedFile) {
const checkPath = await CheckPaths(filePaths.split("/"));
if (!checkPath.success) throw new Error("File not found");
const unlocked = await CheckPassword(checkPath.data);
if (!unlocked.success) {
if (!unlocked.path) throw new Error("No path returned from password checking");
const lockedIndex = checkPath.data.findIndex((path) => path.id === unlocked.path);
// Get all path until the locked index, then join them
const path = checkPath.data
.slice(0, lockedIndex + 1)
.map((path) => path.path)
.join("/");
return new NextResponse(
`The file you're trying to access is protected by password.
Please open the file link and enter the password to access the file, then try to download the file again.
Protected Path: ${new URL(path, config.basePath).toString()}
If you've already entered the password, please make sure your browser is not blocking cookies from this site.`,
{
status: 401,
},
);
}
}
const ranges = request.headers.get("Range") || "bytes=0-";
const chunkSize = 5 * 1024 * 1024; // Load 5MB at a time
let rangeStart = 0;
let rangeEnd = Math.min(chunkSize, fileSize - 1);
const rangeRegex = /bytes=(\d+)-(\d+)?/;
const rangeSize = rangeRegex.exec(ranges);
if (rangeSize) {
rangeStart = parseInt(rangeSize[1], 10);
if (isFull) {
rangeEnd = fileSize - 1;
} else {
rangeEnd = Math.min(rangeStart + chunkSize, fileSize - 1);
}
}
const contentRange = `bytes=${rangeStart}-${rangeEnd}/${fileSize}`;
const contentLength = rangeEnd ? rangeEnd - rangeStart + 1 : fileSize;
const fileContent = await gdrive.files.get(
{
fileId: decryptedId,
alt: "media",
supportsAllDrives: config.apiConfig.isTeamDrive,
acknowledgeAbuse: true,
},
{
responseType: "stream",
headers: {
"Accept-Ranges": "bytes",
"Range": `bytes=${rangeStart}-${rangeEnd}`,
},
},
);
const stream = fileContent.data as NodeJS.ReadableStream;
const fileRange = fileContent.headers["content-range"];
const fileLength = fileContent.headers["content-length"];
const readable = new ReadableStream({
start(controller) {
stream.on("data", (chunk) => {
controller.enqueue(chunk);
});
stream.on("end", () => {
controller.close();
});
stream.on("error", (error) => {
controller.error(error);
});
},
});
return new NextResponse(readable, {
status: 206,
headers: {
"Content-Range": fileRange || contentRange,
"Content-Length": fileLength || contentLength.toString(),
"Content-Type": fileMeta.data.mimeType || "application/octet-stream",
"Accept-Ranges": "bytes",
},
});
} catch (error) {
const e = error as Error;
console.error(e.message);
return new NextResponse(e.message, {
status: 500,
});
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ export async function GET(request: NextRequest, { params }: Props) {
const { encryptedId } = await params;
try {
const searchParams = new URL(request.nextUrl).searchParams;
const size = searchParams.get("size") || "512";
const size = searchParams.get("size") ?? "512";
// Only allow if the request is from the same domain or the referer is the same domain
if (!IS_DEV && !request.headers.get("Referer")?.includes(config.basePath)) {