mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
update: api router, to use new encryption and support new feature
This commit is contained in:
@@ -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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
@@ -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}"`,
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user