From 1023229410d7a1b0b55acf4b121a2d008548fe34 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:59:58 +0700 Subject: [PATCH] update: api router, to use new encryption and support new feature --- src/app/api/download/[...rest]/route.ts | 145 ++++++++++++++++ .../api/download/[encryptedId]/route.old.ts | 158 +++++++++++++++++ src/app/api/download/[encryptedId]/route.ts | 133 --------------- src/app/api/internal/encrypt/route.ts | 6 +- src/app/api/og/[encryptedId]/route.ts | 9 +- src/app/api/preview/[encryptedId]/route.ts | 120 +++++++++++++ src/app/api/raw/[...rest]/route.ts | 82 +++++---- src/app/api/raw/route.ts | 67 -------- src/app/api/stream/[encryptedId]/route.ts | 159 ------------------ src/app/api/thumb/[encryptedId]/route.ts | 2 +- 10 files changed, 485 insertions(+), 396 deletions(-) create mode 100644 src/app/api/download/[...rest]/route.ts create mode 100644 src/app/api/download/[encryptedId]/route.old.ts delete mode 100644 src/app/api/download/[encryptedId]/route.ts create mode 100644 src/app/api/preview/[encryptedId]/route.ts delete mode 100644 src/app/api/raw/route.ts delete mode 100644 src/app/api/stream/[encryptedId]/route.ts diff --git a/src/app/api/download/[...rest]/route.ts b/src/app/api/download/[...rest]/route.ts new file mode 100644 index 0000000..bc6fe20 --- /dev/null +++ b/src/app/api/download/[...rest]/route.ts @@ -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((res, rej) => { + const chunks: Buffer[] = []; + 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), + }, + ); + } +} diff --git a/src/app/api/download/[encryptedId]/route.old.ts b/src/app/api/download/[encryptedId]/route.old.ts new file mode 100644 index 0000000..6c14c04 --- /dev/null +++ b/src/app/api/download/[encryptedId]/route.old.ts @@ -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((resolve, reject) => { + const chunks: Buffer[] = []; + 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), + }, + ); + } +} diff --git a/src/app/api/download/[encryptedId]/route.ts b/src/app/api/download/[encryptedId]/route.ts deleted file mode 100644 index 39956d5..0000000 --- a/src/app/api/download/[encryptedId]/route.ts +++ /dev/null @@ -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((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, - }); - } -} diff --git a/src/app/api/internal/encrypt/route.ts b/src/app/api/internal/encrypt/route.ts index 31f026a..87cb1b7 100644 --- a/src/app/api/internal/encrypt/route.ts +++ b/src/app/api/internal/encrypt/route.ts @@ -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 }, diff --git a/src/app/api/og/[encryptedId]/route.ts b/src/app/api/og/[encryptedId]/route.ts index 61f5ddf..5ae3139 100644 --- a/src/app/api/og/[encryptedId]/route.ts +++ b/src/app/api/og/[encryptedId]/route.ts @@ -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}"`, }, diff --git a/src/app/api/preview/[encryptedId]/route.ts b/src/app/api/preview/[encryptedId]/route.ts new file mode 100644 index 0000000..03324f7 --- /dev/null +++ b/src/app/api/preview/[encryptedId]/route.ts @@ -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), + }, + ); + } +} diff --git a/src/app/api/raw/[...rest]/route.ts b/src/app/api/raw/[...rest]/route.ts index eeec52b..03dee29 100644 --- a/src/app/api/raw/[...rest]/route.ts +++ b/src/app/api/raw/[...rest]/route.ts @@ -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), + }, + ); } } diff --git a/src/app/api/raw/route.ts b/src/app/api/raw/route.ts deleted file mode 100644 index 35bda28..0000000 --- a/src/app/api/raw/route.ts +++ /dev/null @@ -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), - }, - ); - } -} diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts deleted file mode 100644 index 32fe381..0000000 --- a/src/app/api/stream/[encryptedId]/route.ts +++ /dev/null @@ -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, - }); - } -} diff --git a/src/app/api/thumb/[encryptedId]/route.ts b/src/app/api/thumb/[encryptedId]/route.ts index 663dc0e..103499b 100644 --- a/src/app/api/thumb/[encryptedId]/route.ts +++ b/src/app/api/thumb/[encryptedId]/route.ts @@ -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)) {