From 1254d83fb0e6cec490b099912e61b47d01f52dfb Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Sun, 5 May 2024 16:46:27 +0700 Subject: [PATCH 01/27] Test stream --- package.json | 1 + src/app/@preview.image.tsx | 27 +----- src/app/@preview.layout.tsx | 5 +- src/app/@preview.video.tsx | 94 +++++++++++++------ src/app/@rich-header.tsx | 58 +++++++----- src/app/api/stream/[encryptedId]/helper.ts | 24 +++++ src/app/api/stream/[encryptedId]/route.ts | 101 +++++++++++++++++++++ src/app/layout.tsx | 1 + yarn.lock | 79 ++++++++++++++++ 9 files changed, 318 insertions(+), 72 deletions(-) create mode 100644 src/app/api/stream/[encryptedId]/helper.ts create mode 100644 src/app/api/stream/[encryptedId]/route.ts diff --git a/package.json b/package.json index b25bd8f..6822503 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "next": "^14.1.4", "next-themes": "^0.3.0", "nextjs-toploader": "^1.6.11", + "plyr-react": "^5.3.0", "react": "^18", "react-colorful": "^5.6.1", "react-day-picker": "^8.10.0", diff --git a/src/app/@preview.image.tsx b/src/app/@preview.image.tsx index 033a882..88a9eeb 100644 --- a/src/app/@preview.image.tsx +++ b/src/app/@preview.image.tsx @@ -25,28 +25,7 @@ export default function PreviewImage({ file }: Props) { return; } const token = await CreateDownloadToken(); - await fetch(`/api/download/${file.encryptedId}?token=${token}`) - .then((res) => { - if (!res.ok) throw new Error("Failed to fetch image"); - return res.blob(); - }) - .then((blob) => { - const reader = new FileReader(); - reader.onload = () => { - setImgSrc(reader.result as string); - }; - reader.onerror = (e) => { - console.error(e); - setError( - "Could not preview this image, try downloading the file", - ); - }; - reader.readAsDataURL(blob); - }) - .catch((e) => { - console.error(e.message); - setError(e.message); - }); + setImgSrc(`/api/download/${file.encryptedId}?token=${token}&media=1`); } catch (error) { const e = error as Error; console.error(e); @@ -87,6 +66,10 @@ export default function PreviewImage({ file }: Props) { src={imgSrc} alt={file.name} className='max-h-[70dvh] w-full rounded-[var(--radius)] bg-muted object-contain object-center' + onError={(e) => { + console.error(e); + setError("Could not preview this image, try downloading the file"); + }} /> )} diff --git a/src/app/@preview.layout.tsx b/src/app/@preview.layout.tsx index 287dfce..f34bf0d 100644 --- a/src/app/@preview.layout.tsx +++ b/src/app/@preview.layout.tsx @@ -26,12 +26,13 @@ export default function FilePreviewLayout({ data, fileType }: Props) { const [view, setView] = useState<"markdown" | "raw">("markdown"); return ( - <> +
@@ -70,6 +71,6 @@ export default function FilePreviewLayout({ data, fileType }: Props) { - +
); } diff --git a/src/app/@preview.video.tsx b/src/app/@preview.video.tsx index 7c4d8af..df6e10c 100644 --- a/src/app/@preview.video.tsx +++ b/src/app/@preview.video.tsx @@ -1,15 +1,21 @@ "use client"; +import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; -import ReactPlayer from "react-player"; import { z } from "zod"; import { Schema_File } from "~/schema"; import { cn } from "~/utils"; import Icon from "~/components/Icon"; +import { decryptData } from "~/utils/encryptionHelper/hash"; + import { CreateDownloadToken } from "./actions"; +const Plyr = dynamic(() => import("plyr-react"), { + ssr: false, +}); + type Props = { file: z.infer; }; @@ -26,7 +32,11 @@ export default function PreviewVideo({ file }: Props) { return; } const token = await CreateDownloadToken(); - setVideoSrc(`/api/download/${file.encryptedId}?token=${token}`); + const id = await decryptData(file.encryptedId); + // setVideoSrc( + // `https://drive.usercontent.google.com/download?id=${id}&export=download&authuser=0`, + // ); + setVideoSrc(`/api/stream/${file.encryptedId}?token=${token}`); } catch (error) { const e = error as Error; console.error(e); @@ -63,30 +73,62 @@ export default function PreviewVideo({ file }: Props) { {error}
) : ( - ( -
- {children} -
- )} - style={{ - width: "100%", - height: "100%", - maxHeight: "60vh", - }} - onError={(error) => { - console.error(error.message); - if (error instanceof Error) { - setError(error.message); - } else { - setError("Failed to load video. (Probably not supported?)"); - } - }} - /> +
+ +
+ // ( + //
+ // {children} + //
+ // )} + // style={{ + // width: "100%", + // height: "100%", + // maxHeight: "60vh", + // }} + // onError={(error) => { + // console.error(error.message); + // if (error instanceof Error) { + // setError(error.message); + // } else { + // setError("Failed to load video. (Probably not supported?)"); + // } + // }} + // /> )} ); diff --git a/src/app/@rich-header.tsx b/src/app/@rich-header.tsx index e3a2179..3c984f5 100644 --- a/src/app/@rich-header.tsx +++ b/src/app/@rich-header.tsx @@ -1,37 +1,51 @@ "use client"; import { Button } from "~/components/ui/button"; -import { CardHeader, CardTitle } from "~/components/ui/card"; +import { CardHeader } from "~/components/ui/card"; import { Separator } from "~/components/ui/separator"; +import { getFileType } from "~/utils/previewHelper"; + type Props = { title: string; view: "markdown" | "raw"; onViewChange: (value: "markdown" | "raw") => void; + fileType: ReturnType | "unknown"; }; -export default function RichHeader({ title, view, onViewChange }: Props) { +export default function RichHeader({ + title, + view, + onViewChange, + fileType, +}: Props) { return ( -
- {title} -
- - -
+
+ {/* */} +

+ {title} +

+ {/*
*/} + {["markdown", "code", "text"].includes(fileType) && ( +
+ + +
+ )}
diff --git a/src/app/api/stream/[encryptedId]/helper.ts b/src/app/api/stream/[encryptedId]/helper.ts new file mode 100644 index 0000000..d12e947 --- /dev/null +++ b/src/app/api/stream/[encryptedId]/helper.ts @@ -0,0 +1,24 @@ +import { Readable } from "stream"; + +async function* nodeStreamToIterator(stream: any) { + for await (const chunk of stream) { + yield chunk; + } +} + +function iteratorToStream(iterator: AsyncGenerator) { + return new ReadableStream({ + async pull(controller) { + const { done, value } = await iterator.next(); + if (done) { + controller.close(); + } else { + controller.enqueue(value); + } + }, + }); +} +export function streamFile(body: Readable): ReadableStream { + const data: ReadableStream = iteratorToStream(nodeStreamToIterator(body)); + return data; +} diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts new file mode 100644 index 0000000..f720c12 --- /dev/null +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { CheckDownloadToken } from "~/app/actions"; + +import { decryptData } from "~/utils/encryptionHelper/hash"; +import { gdriveNoCache as gdrive } from "~/utils/gdriveInstance"; + +import config from "~/config/gIndex.config"; + +import { streamFile } from "./helper"; + +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 referrer is from the same site + if (!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 _fileMeta = gdrive.files.get({ + fileId: decryptedId, + fields: "id, name, mimeType, size, fileExtension, webContentLink", + supportsAllDrives: config.apiConfig.isTeamDrive, + }); + const _fileContent = gdrive.files.get( + { + fileId: decryptedId, + alt: "media", + supportsAllDrives: config.apiConfig.isTeamDrive, + }, + { + responseType: "stream", + }, + ); + + const [fileMeta, fileContent] = await Promise.all([ + _fileMeta, + _fileContent, + ]); + + const fileSize = Number(fileMeta.data.size || 0); + if (!fileMeta.data.webContentLink) + throw new Error("No download link found"); + + const stream: ReadableStream = streamFile(fileContent.data); + const ranges = request.headers.get("Range"); + if (ranges) { + const [start, end] = ranges.replace(/bytes=/, "").split("-"); + const startByte = parseInt(start, 10); + const endByte = end ? parseInt(end, 10) : fileSize - 1; + return new NextResponse(stream, { + status: 206, + headers: { + "Content-Type": fileMeta.data.mimeType || "application/octet-stream", + "Content-Disposition": `attachment; filename="${encodeURIComponent( + fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, + )}"`, + "Content-Length": (endByte - startByte + 1).toString(), + "Accept-Ranges": "bytes", + "Content-Range": `bytes ${startByte}-${endByte}/${fileSize}`, + }, + }); + } + + return new NextResponse(stream, { + status: 200, + headers: { + "Content-Type": fileMeta.data.mimeType || "application/octet-stream", + "Content-Disposition": `attachment; filename="${encodeURIComponent( + fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, + )}"`, + "Content-Length": fileSize.toString(), + "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/layout.tsx b/src/app/layout.tsx index 79c57fa..8050acd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import { Metadata } from "next"; import { JetBrains_Mono, Outfit, Source_Sans_3 } from "next/font/google"; +import "plyr-react/plyr.css"; import { cn } from "~/utils"; import { formatFooter } from "~/utils/footerFormatter"; diff --git a/yarn.lock b/yarn.lock index de412a0..eeb60d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2859,6 +2859,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^3.26.1": + version: 3.37.0 + resolution: "core-js@npm:3.37.0" + checksum: 10c0/7e00331f346318ca3f595c08ce9e74ddae744715aef137486c1399163afd79792fb94c3161280863adfdc3e30f8026912d56bd3036f93cacfc689d33e185f2ee + languageName: node + linkType: hard + "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -2918,6 +2925,13 @@ __metadata: languageName: node linkType: hard +"custom-event-polyfill@npm:^1.0.7": + version: 1.0.7 + resolution: "custom-event-polyfill@npm:1.0.7" + checksum: 10c0/b73c90d646d78f4acdff5453fa0f165f6d5506e32d074ca57d27f2bb7d9412356dab8cec7c00a0956b069e4987a8546a2c2c4b866d155e9fc4c27d223a225b78 + languageName: node + linkType: hard + "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -5181,6 +5195,13 @@ __metadata: languageName: node linkType: hard +"loadjs@npm:^4.2.0": + version: 4.3.0 + resolution: "loadjs@npm:4.3.0" + checksum: 10c0/8884520a7c5f3b0f6e4d3bc01d200c73b9c468986bea26acb54939d4a3f5da08f40f712812fadc1ff6030fca936e8c9eeb842aaafd287e32ca0ce6ae9f10e759 + languageName: node + linkType: hard + "locate-path@npm:^6.0.0": version: 6.0.0 resolution: "locate-path@npm:6.0.0" @@ -6229,6 +6250,7 @@ __metadata: next: "npm:^14.1.4" next-themes: "npm:^0.3.0" nextjs-toploader: "npm:^1.6.11" + plyr-react: "npm:^5.3.0" postcss: "npm:^8.4.38" prettier: "npm:3.0.0" prettier-plugin-tailwindcss: "npm:0.5.12" @@ -6750,6 +6772,37 @@ __metadata: languageName: node linkType: hard +"plyr-react@npm:^5.3.0": + version: 5.3.0 + resolution: "plyr-react@npm:5.3.0" + dependencies: + plyr: "npm:^3.7.7" + react-aptor: "npm:^2.0.0" + peerDependencies: + plyr: ^3.7.7 + react: ">=16.8" + peerDependenciesMeta: + plyr: + optional: false + react: + optional: true + checksum: 10c0/b338c5f07277c124663aa4f820dffb0700a67ac6eab0015b77985bb70c308da96bdffff6d103033544de6516cade63027f4c077871a0fa0feba996dfd3b6f2c0 + languageName: node + linkType: hard + +"plyr@npm:^3.7.7": + version: 3.7.8 + resolution: "plyr@npm:3.7.8" + dependencies: + core-js: "npm:^3.26.1" + custom-event-polyfill: "npm:^1.0.7" + loadjs: "npm:^4.2.0" + rangetouch: "npm:^2.0.1" + url-polyfill: "npm:^1.1.12" + checksum: 10c0/75c3e070f7829f76409e0d34784bf8070b827ad99c4713a36338af7ce8dff7cf38998403f34a4a0b4d6e99efd1856ee056443c7e838db1dbb98ed38828110a97 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.0.0 resolution: "possible-typed-array-names@npm:1.0.0" @@ -7001,6 +7054,25 @@ __metadata: languageName: node linkType: hard +"rangetouch@npm:^2.0.1": + version: 2.0.1 + resolution: "rangetouch@npm:2.0.1" + checksum: 10c0/5f7947d1c5e95f50630ed1e0cbaeb4c32a6be37b66d864a68f7e70a4e86f782eb869df9fa2357b981b1301d2158eff50002a199b0fbbbf6e1746bc9d213914d9 + languageName: node + linkType: hard + +"react-aptor@npm:^2.0.0": + version: 2.0.0 + resolution: "react-aptor@npm:2.0.0" + peerDependencies: + react: ">=16.8" + peerDependenciesMeta: + react: + optional: true + checksum: 10c0/f2f00b494a2cb93b0ee73949d2031c2f5a8233d169c3d16792e7c6a5177a75310155e85282209a6f74883b3564d1f3a46d5d4d03ee1c16224fbdb5d5665504f8 + languageName: node + linkType: hard + "react-colorful@npm:^5.6.1": version: 5.6.1 resolution: "react-colorful@npm:5.6.1" @@ -8554,6 +8626,13 @@ __metadata: languageName: node linkType: hard +"url-polyfill@npm:^1.1.12": + version: 1.1.12 + resolution: "url-polyfill@npm:1.1.12" + checksum: 10c0/69633c42e3182271d01d2f2f4acd889a9f54b88fd7b2f45fd84fed19eca7cfa98fb6bfbd43d9cd9fad222a11a2dffb562b8435cdaa67fca5e25e3da638741679 + languageName: node + linkType: hard + "url-template@npm:^2.0.8": version: 2.0.8 resolution: "url-template@npm:2.0.8" From eb0804826bea61b0f476cb242860053789717db7 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Sun, 5 May 2024 16:46:39 +0700 Subject: [PATCH 02/27] Remove error message when password is not set --- src/app/actions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/actions.ts b/src/app/actions.ts index 97a075b..747cf34 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -268,7 +268,8 @@ export async function CheckPassword( const currentFolder = paths[folderIndex]; if (!cookiesValue[currentFolder.id]) throw { - message: `Password for '${currentFolder.path}' is not set, please enter the password`, + // message: `Password for '${currentFolder.path}' is not set, please enter the password`, + message: null, path: currentFolder.id, }; From b08d6cedde63d1082467f5b494b015335a547473 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Sun, 5 May 2024 16:50:46 +0700 Subject: [PATCH 03/27] Fix missing props for `RichHeader` --- src/app/@readme.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/@readme.tsx b/src/app/@readme.tsx index 31d4fb3..8990720 100644 --- a/src/app/@readme.tsx +++ b/src/app/@readme.tsx @@ -24,6 +24,7 @@ export default function Readme({ content, title }: Props) { title={title} view={view} onViewChange={setView} + fileType={"markdown"} /> Date: Sun, 5 May 2024 23:47:17 +0700 Subject: [PATCH 04/27] Test new stream logic --- src/app/@preview.video.tsx | 7 +- src/app/api/stream/[encryptedId]/route.ts | 150 ++++++++++++++++------ 2 files changed, 110 insertions(+), 47 deletions(-) diff --git a/src/app/@preview.video.tsx b/src/app/@preview.video.tsx index df6e10c..73f15aa 100644 --- a/src/app/@preview.video.tsx +++ b/src/app/@preview.video.tsx @@ -8,8 +8,6 @@ import { cn } from "~/utils"; import Icon from "~/components/Icon"; -import { decryptData } from "~/utils/encryptionHelper/hash"; - import { CreateDownloadToken } from "./actions"; const Plyr = dynamic(() => import("plyr-react"), { @@ -32,10 +30,6 @@ export default function PreviewVideo({ file }: Props) { return; } const token = await CreateDownloadToken(); - const id = await decryptData(file.encryptedId); - // setVideoSrc( - // `https://drive.usercontent.google.com/download?id=${id}&export=download&authuser=0`, - // ); setVideoSrc(`/api/stream/${file.encryptedId}?token=${token}`); } catch (error) { const e = error as Error; @@ -85,6 +79,7 @@ export default function PreviewVideo({ file }: Props) { }, ], }} + // crossOrigin='anonymous' options={{ toggleInvert: true, settings: ["quality", "speed"], diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index f720c12..71448c0 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -7,8 +7,6 @@ import { gdriveNoCache as gdrive } from "~/utils/gdriveInstance"; import config from "~/config/gIndex.config"; -import { streamFile } from "./helper"; - export const dynamic = "force-dynamic"; export async function GET( @@ -35,12 +33,33 @@ export async function GET( if (!tokenValidity.success) throw new Error(tokenValidity.message); const decryptedId = await decryptData(encryptedId); - const _fileMeta = gdrive.files.get({ + const fileMeta = await gdrive.files.get({ fileId: decryptedId, fields: "id, name, mimeType, size, fileExtension, webContentLink", supportsAllDrives: config.apiConfig.isTeamDrive, }); - const _fileContent = gdrive.files.get( + + const fileSize = Number(fileMeta.data.size || 0); + if (!fileMeta.data.webContentLink) + throw new Error("No download link found"); + + const ranges = request.headers.get("Range") || "bytes=0-"; + let rangeStart = 0; + let rangeEnd = fileSize - 1; + const rangeRegex = /bytes=(\d+)-(\d+)?/; + const rangeSize = ranges.match(rangeRegex); + if (rangeSize) { + rangeStart = parseInt(rangeSize[1], 10); + rangeEnd = rangeSize ? parseInt(rangeSize[2], 10) : fileSize - 1; + } + + const contentRange = `bytes=${rangeStart}-${Math.min( + rangeEnd, + fileSize - 1, + )}/${fileSize}`; + const contentLength = rangeEnd ? rangeEnd - rangeStart + 1 : fileSize; + + const fileContent = await gdrive.files.get( { fileId: decryptedId, alt: "media", @@ -48,49 +67,98 @@ export async function GET( }, { responseType: "stream", + headers: { + "Accept-Ranges": "bytes", + "Range": ranges, + }, }, ); - const [fileMeta, fileContent] = await Promise.all([ - _fileMeta, - _fileContent, - ]); + const stream = fileContent.data as NodeJS.ReadableStream; + const fileRange = fileContent.headers["content-range"]; + const fileLength = fileContent.headers["content-length"]; - const fileSize = Number(fileMeta.data.size || 0); - if (!fileMeta.data.webContentLink) - throw new Error("No download link found"); - - const stream: ReadableStream = streamFile(fileContent.data); - const ranges = request.headers.get("Range"); - if (ranges) { - const [start, end] = ranges.replace(/bytes=/, "").split("-"); - const startByte = parseInt(start, 10); - const endByte = end ? parseInt(end, 10) : fileSize - 1; - return new NextResponse(stream, { - status: 206, - headers: { - "Content-Type": fileMeta.data.mimeType || "application/octet-stream", - "Content-Disposition": `attachment; filename="${encodeURIComponent( - fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, - )}"`, - "Content-Length": (endByte - startByte + 1).toString(), - "Accept-Ranges": "bytes", - "Content-Range": `bytes ${startByte}-${endByte}/${fileSize}`, - }, - }); - } - - return new NextResponse(stream, { - status: 200, - headers: { - "Content-Type": fileMeta.data.mimeType || "application/octet-stream", - "Content-Disposition": `attachment; filename="${encodeURIComponent( - fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, - )}"`, - "Content-Length": fileSize.toString(), - "Accept-Ranges": "bytes", + 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", + // 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`)}"`, + }, + }); + + // async function* readStream() { + // for await (const chunk of fileContent.data as any) { + // yield chunk; + // } + // } + // function iteratorToStream(iterator: AsyncGenerator) { + // return new ReadableStream({ + // async pull(controller) { + // const { done, value } = await iterator.next(); + // if (done) { + // controller.close(); + // } else { + // controller.enqueue(value); + // } + // }, + // }); + // } + // const stream: ReadableStream = iteratorToStream(readStream()); + + // const res = new Response(stream, { + // status: 206, + // headers: { + // "Content-Type": fileMeta.data.mimeType || "application/octet-stream", + // "Content-Disposition": `attachment; filename="${encodeURIComponent( + // fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, + // )}"`, + // "Content-Length": contentLength.toString(), + // "Content-Range": ranges ? contentRange : "", + // }, + // }); + // NextResponse.next(res); + // new NextResponse(stream, { + // status: request.headers.get("Range") ? 206 : 200, + // headers: { + // "Content-Type": fileMeta.data.mimeType || "application/octet-stream", + // "Content-Disposition": `attachment; filename="${encodeURIComponent( + // fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, + // )}"`, + // "Content-Length": fileSize.toString(), + // "Accept-Ranges": "bytes", + // "Range": ranges ? `bytes ${rangeStart}-${rangeEnd}/${fileSize}` : "", + // }, + // }); + + // return new NextResponse(stream, { + // status: 206, + // headers: { + // "Content-Type": fileMeta.data.mimeType || "application/octet-stream", + // "Content-Disposition": `attachment; filename="${encodeURIComponent( + // fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, + // )}"`, + // "Content-Length": fileSize.toString(), + // "Accept-Ranges": "bytes", + // "Range": ranges ? `bytes ${rangeStart}-${rangeEnd}/${fileSize}` : "", + // }, + // }); } catch (error) { const e = error as Error; console.error(e.message); From 04439d74efcfe31b1899679a9e1140058ce14a90 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Sun, 5 May 2024 23:47:55 +0700 Subject: [PATCH 05/27] Use `/thumbnail` endpoint and add long cache --- src/app/api/thumb/[encryptedId]/route.ts | 92 +++++++----------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/src/app/api/thumb/[encryptedId]/route.ts b/src/app/api/thumb/[encryptedId]/route.ts index ed0cd05..39eeccd 100644 --- a/src/app/api/thumb/[encryptedId]/route.ts +++ b/src/app/api/thumb/[encryptedId]/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; import { decryptData } from "~/utils/encryptionHelper/hash"; import gdrive from "~/utils/gdriveInstance"; @@ -16,6 +17,14 @@ export async function GET( { params: { encryptedId } }: Props, ) { try { + const searchParams = new URL(request.nextUrl).searchParams; + const size = searchParams.get("size") || "512"; + + const validSize = z.coerce.number().safeParse(size); + if (!validSize.success) { + throw new Error("Invalid size"); + } + const defaultImage = NextResponse.redirect( new URL("/og.png", config.basePath), { @@ -23,86 +32,39 @@ export async function GET( }, ); const decryptedId = await decryptData(encryptedId); - const _fileMeta = gdrive.files.get({ + + const fileMeta = await gdrive.files.get({ fileId: decryptedId, fields: "id, name, mimeType, fileExtension, webContentLink, thumbnailLink", supportsAllDrives: config.apiConfig.isTeamDrive, }); - const _fileContent = gdrive.files.get( - { - fileId: decryptedId, - alt: "media", - supportsAllDrives: config.apiConfig.isTeamDrive, - }, - { - responseType: "stream", - }, - ); - const [fileMeta, fileContent] = await Promise.all([ - _fileMeta, - _fileContent, - ]); - const fileSize = Number(fileMeta.data.size || 0); - - if (!fileMeta.data.webContentLink) return defaultImage; - if (!fileMeta.data.thumbnailLink) return defaultImage; if ( !fileMeta.data.mimeType?.startsWith("image") && !fileMeta.data.mimeType?.startsWith("video") - ) - return defaultImage; - - // If svg, return actual image since there is no thumbnail for svg - if ( - fileMeta.data.mimeType?.includes("svg") && - fileSize <= config.apiConfig.maxFileSize ) { - 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, { - headers: { - "Cache-Control": "public, max-age=31536000, immutable", - "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}`, - )}"`, - }, - }); + return defaultImage; } - if (config.apiConfig.proxyThumbnail) { - const downloadThumb = await fetch(fileMeta.data.thumbnailLink, { - cache: "force-cache", - }); - const buffer = await downloadThumb.arrayBuffer(); + const url = `https://drive.google.com/thumbnail?id=${decryptedId}&sz=w${size}`; - return new NextResponse(buffer, { - headers: { - "Cache-Control": "public, max-age=31536000, immutable", - "Content-Type": fileMeta.data.mimeType || "application/octet-stream", - "Content-Length": buffer.byteLength.toString(), - "Content-Disposition": `attachment; filename="${encodeURIComponent( - fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, - )}"`, - }, - }); + if (!config.apiConfig.proxyThumbnail) { + return NextResponse.redirect(url); } - return NextResponse.redirect(fileMeta.data.thumbnailLink); + const downloadThumb = await fetch(url, { + cache: "force-cache", + }); + const buffer = await downloadThumb.arrayBuffer(); + + return new NextResponse(buffer, { + headers: { + "Cache-Control": "public, max-age=31536000, immutable", + "Content-Type": "image/jpeg", + "Content-Length": buffer.byteLength.toString(), + }, + }); } catch (error) { const e = error as Error; console.error(e.message); From b4383c73364745652f25fc3bf96c216a143f81d9 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Sun, 5 May 2024 23:58:48 +0700 Subject: [PATCH 06/27] disable ref header --- src/app/api/stream/[encryptedId]/route.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index 71448c0..921f894 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -25,14 +25,15 @@ export async function GET( if (!token) throw new Error("Token not found"); // Only allow if referrer is from the same site - if (!request.headers.get("Referer")?.includes(config.basePath)) { - throw new Error("Invalid request"); - } + // if (!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 fileMeta = await gdrive.files.get({ fileId: decryptedId, fields: "id, name, mimeType, size, fileExtension, webContentLink", From 50b1ccec964956daa67a41a1e0f76301094967c7 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Mon, 6 May 2024 01:41:06 +0700 Subject: [PATCH 07/27] Add range header to file request --- src/app/api/stream/[encryptedId]/route.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index 921f894..f0b1ba6 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -34,11 +34,19 @@ export async function GET( const decryptedId = await decryptData(encryptedId); - const fileMeta = await gdrive.files.get({ - fileId: decryptedId, - fields: "id, name, mimeType, size, fileExtension, webContentLink", - supportsAllDrives: config.apiConfig.isTeamDrive, - }); + const fileMeta = await 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-", + }, + }, + ); const fileSize = Number(fileMeta.data.size || 0); if (!fileMeta.data.webContentLink) From e1c2e09c9ea6a4e750dfecbba027d36943608b50 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Mon, 6 May 2024 01:47:25 +0700 Subject: [PATCH 08/27] If there are no range, request first 1MB --- src/app/api/stream/[encryptedId]/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index f0b1ba6..486f534 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -43,7 +43,7 @@ export async function GET( { headers: { "Accept-Ranges": "bytes", - "Range": request.headers.get("Range") || "bytes=0-", + "Range": request.headers.get("Range") || `bytes=0-${1024 * 1024 - 1}`, }, }, ); From 3c0a1fdc8e15111cfb80ee6272932cddd2fd8f72 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Mon, 6 May 2024 01:53:31 +0700 Subject: [PATCH 09/27] Fix range header, added on the wrong request --- src/app/api/stream/[encryptedId]/route.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index 486f534..3039dc9 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -78,7 +78,8 @@ export async function GET( responseType: "stream", headers: { "Accept-Ranges": "bytes", - "Range": ranges, + "Range": + ranges === "bytes=0-" ? `bytes=0-${1024 * 1024 - 1}` : ranges, }, }, ); From cd20ad722297d22459627b92d1777ea5b5a96b9d Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Mon, 6 May 2024 02:02:46 +0700 Subject: [PATCH 10/27] Update the range end to load 1MB at a time instead of requesting the whole file --- src/app/api/stream/[encryptedId]/route.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index 3039dc9..6da6d63 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -59,7 +59,9 @@ export async function GET( const rangeSize = ranges.match(rangeRegex); if (rangeSize) { rangeStart = parseInt(rangeSize[1], 10); - rangeEnd = rangeSize ? parseInt(rangeSize[2], 10) : fileSize - 1; + + const chunkSize = 1024 * 1024; // Load 1MB at a time + rangeEnd = Math.min(rangeStart + chunkSize, fileSize - 1); } const contentRange = `bytes=${rangeStart}-${Math.min( From 40be06c79d7880f907a199a81cba6f937ac9791b Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Mon, 6 May 2024 02:12:53 +0700 Subject: [PATCH 11/27] Fix header to use calculated range instead of request header range --- src/app/api/stream/[encryptedId]/route.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index 6da6d63..8843ab3 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -53,21 +53,18 @@ export async function GET( throw new Error("No download link found"); const ranges = request.headers.get("Range") || "bytes=0-"; + const chunkSize = 1 * 1024 * 1024; // Load 1MB at a time let rangeStart = 0; - let rangeEnd = fileSize - 1; + let rangeEnd = Math.min(chunkSize, fileSize - 1); const rangeRegex = /bytes=(\d+)-(\d+)?/; const rangeSize = ranges.match(rangeRegex); if (rangeSize) { rangeStart = parseInt(rangeSize[1], 10); - const chunkSize = 1024 * 1024; // Load 1MB at a time rangeEnd = Math.min(rangeStart + chunkSize, fileSize - 1); } - const contentRange = `bytes=${rangeStart}-${Math.min( - rangeEnd, - fileSize - 1, - )}/${fileSize}`; + const contentRange = `bytes=${rangeStart}-${rangeEnd}/${fileSize}`; const contentLength = rangeEnd ? rangeEnd - rangeStart + 1 : fileSize; const fileContent = await gdrive.files.get( @@ -80,8 +77,7 @@ export async function GET( responseType: "stream", headers: { "Accept-Ranges": "bytes", - "Range": - ranges === "bytes=0-" ? `bytes=0-${1024 * 1024 - 1}` : ranges, + "Range": `bytes=${rangeStart}-${rangeEnd}`, }, }, ); From 6be8f159962fab8d2b12e5c3b3ab77a63e8336dc Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:50:11 +0700 Subject: [PATCH 12/27] Switch `jszip` with `fflate` --- package.json | 2 +- yarn.lock | 107 +++++---------------------------------------------- 2 files changed, 11 insertions(+), 98 deletions(-) diff --git a/package.json b/package.json index 6822503..06ccf5e 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,10 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.0.2", + "fflate": "^0.8.2", "googleapis": "^118.0.0", "input-otp": "^1.2.3", "jsonwebtoken": "^9.0.0", - "jszip": "^3.10.1", "lucide-react": "^0.363.0", "next": "^14.1.4", "next-themes": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index eeb60d9..e71512b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2866,13 +2866,6 @@ __metadata: languageName: node linkType: hard -"core-util-is@npm:~1.0.0": - version: 1.0.3 - resolution: "core-util-is@npm:1.0.3" - checksum: 10c0/90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 - languageName: node - linkType: hard - "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -3739,6 +3732,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 10c0/03448d630c0a583abea594835a9fdb2aaf7d67787055a761515bf4ed862913cfd693b4c4ffd5c3f3b355a70cf1e19033e9ae5aedcca103188aaff91b8bd6e293 + languageName: node + linkType: hard + "fflate@npm:~0.6.9": version: 0.6.10 resolution: "fflate@npm:0.6.10" @@ -4470,13 +4470,6 @@ __metadata: languageName: node linkType: hard -"immediate@npm:~3.0.5": - version: 3.0.6 - resolution: "immediate@npm:3.0.6" - checksum: 10c0/f8ba7ede69bee9260241ad078d2d535848745ff5f6995c7c7cb41cfdc9ccc213f66e10fa5afb881f90298b24a3f7344b637b592beb4f54e582770cdce3f1f039 - languageName: node - linkType: hard - "import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -4511,7 +4504,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:^2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -4869,13 +4862,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:~1.0.0": - version: 1.0.0 - resolution: "isarray@npm:1.0.0" - checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -5044,18 +5030,6 @@ __metadata: languageName: node linkType: hard -"jszip@npm:^3.10.1": - version: 3.10.1 - resolution: "jszip@npm:3.10.1" - dependencies: - lie: "npm:~3.3.0" - pako: "npm:~1.0.2" - readable-stream: "npm:~2.3.6" - setimmediate: "npm:^1.0.5" - checksum: 10c0/58e01ec9c4960383fb8b38dd5f67b83ccc1ec215bf74c8a5b32f42b6e5fb79fada5176842a11409c4051b5b94275044851814a31076bf49e1be218d3ef57c863 - languageName: node - linkType: hard - "jwa@npm:^1.4.1": version: 1.4.1 resolution: "jwa@npm:1.4.1" @@ -5151,15 +5125,6 @@ __metadata: languageName: node linkType: hard -"lie@npm:~3.3.0": - version: 3.3.0 - resolution: "lie@npm:3.3.0" - dependencies: - immediate: "npm:~3.0.5" - checksum: 10c0/56dd113091978f82f9dc5081769c6f3b947852ecf9feccaf83e14a123bc630c2301439ce6182521e5fbafbde88e88ac38314327a4e0493a1bea7e0699a7af808 - languageName: node - linkType: hard - "lil-gui@npm:~0.17.0": version: 0.17.0 resolution: "lil-gui@npm:0.17.0" @@ -6242,10 +6207,10 @@ __metadata: encoding: "npm:^0.1.13" eslint: "npm:8.38.0" eslint-config-next: "npm:^14.1.4" + fflate: "npm:^0.8.2" googleapis: "npm:^118.0.0" input-otp: "npm:^1.2.3" jsonwebtoken: "npm:^9.0.0" - jszip: "npm:^3.10.1" lucide-react: "npm:^0.363.0" next: "npm:^14.1.4" next-themes: "npm:^0.3.0" @@ -6615,13 +6580,6 @@ __metadata: languageName: node linkType: hard -"pako@npm:~1.0.2": - version: 1.0.11 - resolution: "pako@npm:1.0.11" - checksum: 10c0/86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe - languageName: node - linkType: hard - "papaparse@npm:^5.4.1": version: 5.4.1 resolution: "papaparse@npm:5.4.1" @@ -6989,13 +6947,6 @@ __metadata: languageName: node linkType: hard -"process-nextick-args@npm:~2.0.0": - version: 2.0.1 - resolution: "process-nextick-args@npm:2.0.1" - checksum: 10c0/bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 - languageName: node - linkType: hard - "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -7341,21 +7292,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:~2.3.6": - version: 2.3.8 - resolution: "readable-stream@npm:2.3.8" - dependencies: - core-util-is: "npm:~1.0.0" - inherits: "npm:~2.0.3" - isarray: "npm:~1.0.0" - process-nextick-args: "npm:~2.0.0" - safe-buffer: "npm:~5.1.1" - string_decoder: "npm:~1.1.1" - util-deprecate: "npm:~1.0.1" - checksum: 10c0/7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa - languageName: node - linkType: hard - "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -7673,13 +7609,6 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": - version: 5.1.2 - resolution: "safe-buffer@npm:5.1.2" - checksum: 10c0/780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 - languageName: node - linkType: hard - "safe-regex-test@npm:^1.0.3": version: 1.0.3 resolution: "safe-regex-test@npm:1.0.3" @@ -7760,13 +7689,6 @@ __metadata: languageName: node linkType: hard -"setimmediate@npm:^1.0.5": - version: 1.0.5 - resolution: "setimmediate@npm:1.0.5" - checksum: 10c0/5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 - languageName: node - linkType: hard - "shallowequal@npm:1.1.0": version: 1.1.0 resolution: "shallowequal@npm:1.1.0" @@ -8008,15 +7930,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:~1.1.1": - version: 1.1.1 - resolution: "string_decoder@npm:1.1.1" - dependencies: - safe-buffer: "npm:~5.1.0" - checksum: 10c0/b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e - languageName: node - linkType: hard - "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -8680,7 +8593,7 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 From 372d27b5b99d43d64b19fb3fa56173c3b971dfcb Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:50:32 +0700 Subject: [PATCH 13/27] Add `streamMaxSize` to config --- src/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/schema.ts b/src/schema.ts index 5aa1e31..c626c4f 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -95,6 +95,7 @@ export const Schema_Config_API = z itemsPerPage: z.number().positive(), searchResult: z.number().positive(), proxyThumbnail: z.boolean(), + streamMaxSize: z.number(), specialFile: z.object({ password: z.string(), From cadddc1482b4dbe14e5fb29faff04b4c7c40f5b3 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:51:16 +0700 Subject: [PATCH 14/27] Load low resolution thumbnail before full thumbnail --- src/app/@file.grid.tsx | 17 +++++++++++++++-- src/app/@file.list.tsx | 25 +++++++++++++++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/app/@file.grid.tsx b/src/app/@file.grid.tsx index 236a60d..ac5d9ba 100644 --- a/src/app/@file.grid.tsx +++ b/src/app/@file.grid.tsx @@ -51,6 +51,9 @@ export default function FileGrid({ data }: Props) { return new URL(path, config.basePath).pathname; }, [data, pathname]); + const [thumbnailURL, setThumbnailURL] = useState( + `/api/thumb/${data.encryptedId}?size=2`, + ); const [actionOpen, setActionOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 768px)"); @@ -220,13 +223,23 @@ export default function FileGrid({ data }: Props) { data.mimeType.startsWith("image")) ? ( <> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} className='rounded-top-[var(--radius)] absolute -z-0 h-32 w-full flex-shrink-0 flex-grow-0 object-cover opacity-50' /> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} className='relative z-0 h-32 w-full flex-shrink-0 flex-grow-0 object-contain backdrop-blur' /> diff --git a/src/app/@file.list.tsx b/src/app/@file.list.tsx index 87041dd..0513e51 100644 --- a/src/app/@file.list.tsx +++ b/src/app/@file.list.tsx @@ -29,6 +29,7 @@ import { import useMediaQuery from "~/hooks/useMediaQuery"; import bytesToReadable from "~/utils/bytesFormat"; +import { durationToReadable } from "~/utils/durationFormat"; import { getPreviewIcon } from "~/utils/previewHelper"; import config from "~/config/gIndex.config"; @@ -42,14 +43,16 @@ export default function FileList({ data }: Props) { const pathname = usePathname(); const filePath = useMemo(() => { - // const currentPath = pathname.startsWith("/e") ? pathname : `/e${pathname}`; - // Set to pathname to remove the /e prefix const path = [pathname, encodeURIComponent(data.name)] .join("/") .replace(/\/+/g, "/"); return new URL(path, config.basePath).pathname; }, [data, pathname]); + + const [thumbnailURL, setThumbnailURL] = useState( + `/api/thumb/${data.encryptedId}?size=2`, + ); const [actionOpen, setActionOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 768px)"); @@ -218,9 +221,14 @@ export default function FileList({ data }: Props) { data.mimeType.startsWith("image")) ? ( <> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} + className='size-16 flex-shrink-0 flex-grow-0 rounded-[var(--radius)] object-cover tablet:size-20' /> {data.mimeType.startsWith("video") && ( @@ -230,6 +238,11 @@ export default function FileList({ data }: Props) { className='absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2 rounded-full bg-muted-foreground fill-muted p-1.5 text-muted opacity-75' size={24} /> +
+ {durationToReadable( + data.videoMediaMetadata?.durationMillis || 0, + )} +
)} @@ -240,13 +253,13 @@ export default function FileList({ data }: Props) { ? "Folder" : getPreviewIcon(data.fileExtension || "", data.mimeType) } - className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-12' + className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-20' /> )}
{/* File data */} -
+
{config.siteConfig.showFileExtension ? data.name From 56639325aee6cb3a10c38ae027fd577a56bf8487 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:52:04 +0700 Subject: [PATCH 15/27] Load low resolution thumbnail before full thumbnail --- src/app/@header.button.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/@header.button.tsx b/src/app/@header.button.tsx index c8e99f1..680dfc3 100644 --- a/src/app/@header.button.tsx +++ b/src/app/@header.button.tsx @@ -503,6 +503,9 @@ export default function HeaderButton({ children }: PropsWithChildren) { } function SearchResultItem({ data }: { data: z.infer }) { + const [thumbnailURL, setThumbnailURL] = useState( + `/api/thumb/${data.encryptedId}?size=2`, + ); const router = useRouter(); return (
}) { data.mimeType.startsWith("image")) ? ( <> {data.name} { + if (thumbnailURL.includes("size=2")) { + setThumbnailURL(`/api/thumb/${data.encryptedId}`); + } + }} + className='size-16 flex-shrink-0 flex-grow-0 rounded-[var(--radius)] object-cover tablet:size-20' /> {data.mimeType.startsWith("video") && ( @@ -564,7 +572,7 @@ function SearchResultItem({ data }: { data: z.infer }) { ? "Folder" : getPreviewIcon(data.fileExtension || "", data.mimeType) } - className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-12' + className='size-16 flex-shrink-0 flex-grow-0 p-3 tablet:size-20' /> )}
From 0c721ecbf32c233c51a9f8a802f6546a190e5e0b Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:52:30 +0700 Subject: [PATCH 16/27] Remove small text, and balance the head3 --- src/app/@password.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/@password.tsx b/src/app/@password.tsx index 7bc2cac..06e285f 100644 --- a/src/app/@password.tsx +++ b/src/app/@password.tsx @@ -105,14 +105,14 @@ export default function Password({ path, checkPaths, errorMessage }: Props) { className={cn("h-48 w-64 object-contain")} />
-

+

{path === "global" ? "This site are password protected" : "The folder or file you are trying to access is password protected"}

- + {/* Please enter the password to access the content - + */}
Date: Wed, 8 May 2024 08:52:55 +0700 Subject: [PATCH 17/27] Update message when password not found --- src/app/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/actions.ts b/src/app/actions.ts index 747cf34..90ffc18 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -269,7 +269,7 @@ export async function CheckPassword( if (!cookiesValue[currentFolder.id]) throw { // message: `Password for '${currentFolder.path}' is not set, please enter the password`, - message: null, + message: `Please enter password for '${currentFolder.path}'`, path: currentFolder.id, }; From ede772f81e4dd5e96a287e5f31ee7486dc590feb Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:53:12 +0700 Subject: [PATCH 18/27] Add `streamMaxSize` to config template --- src/app/[...rest]/deploy/docs.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/[...rest]/deploy/docs.tsx b/src/app/[...rest]/deploy/docs.tsx index 465972f..8afd52f 100644 --- a/src/app/[...rest]/deploy/docs.tsx +++ b/src/app/[...rest]/deploy/docs.tsx @@ -412,6 +412,21 @@ const config: z.input = { * Default: true */ proxyThumbnail: ${configuration.api.proxyThumbnail ? "true" : "false"}, + + /** + * Only show preview for files that are smaller than this size + * If the file is larger than this size, it will show "can't preview" message instead + * + * Why? + * Since the stream endpoint are counted as a bandwidth usage + * I want to limit the preview to only small files + * It also to prevent abuse from the user + * + * You can also set this to 0 to disable the limit + * + * Default: 100MB + */ + streamMaxSize: ${100 * 1024 * 1024}, /** * Special file name that will be used for certain purposes From 48d93e96cba827230bfcc0f3f134a2e0fb1253d7 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:53:28 +0700 Subject: [PATCH 19/27] add `streamMaxSize` --- src/config/gIndex.config.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/config/gIndex.config.ts b/src/config/gIndex.config.ts index 860fafb..1a298ef 100644 --- a/src/config/gIndex.config.ts +++ b/src/config/gIndex.config.ts @@ -108,6 +108,21 @@ const config: z.input = { */ proxyThumbnail: true, + /** + * Only show preview for files that are smaller than this size + * If the file is larger than this size, it will show "can't preview" message instead + * + * Why? + * Since the stream endpoint are counted as a bandwidth usage + * I want to limit the preview to only small files + * It also to prevent abuse from the user + * + * You can also set this to 0 to disable the limit + * + * Default: 100MB + */ + streamMaxSize: 100 * 1024 * 1024, + /** * Special file name that will be used for certain purposes * These files will be ignored when searching for files From e2a8ffef5100cc4ebe78159481cc703f3c1bc25f Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:53:51 +0700 Subject: [PATCH 20/27] Add `Progress` components from `shadcn/ui` --- src/components/ui/progress.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/components/ui/progress.tsx diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 0000000..cdacbbd --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "~/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } From 4762ba33da5d9c7a7ea2aafe21fc12578a09141b Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:54:25 +0700 Subject: [PATCH 21/27] Only allows to be accessed from same domain as base path --- src/app/api/thumb/[encryptedId]/route.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/app/api/thumb/[encryptedId]/route.ts b/src/app/api/thumb/[encryptedId]/route.ts index 39eeccd..ea13612 100644 --- a/src/app/api/thumb/[encryptedId]/route.ts +++ b/src/app/api/thumb/[encryptedId]/route.ts @@ -2,7 +2,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { decryptData } from "~/utils/encryptionHelper/hash"; -import gdrive from "~/utils/gdriveInstance"; import config from "~/config/gIndex.config"; @@ -20,6 +19,14 @@ export async function GET( const searchParams = new URL(request.nextUrl).searchParams; const size = searchParams.get("size") || "512"; + // Only allow if the request is from the same domain or the referer is the same domain + if ( + process.env.NODE_ENV === "production" && + !request.headers.get("Referer")?.includes(config.basePath) + ) { + throw new Error("Invalid request"); + } + const validSize = z.coerce.number().safeParse(size); if (!validSize.success) { throw new Error("Invalid size"); @@ -33,20 +40,6 @@ export async function GET( ); const decryptedId = await decryptData(encryptedId); - const fileMeta = await gdrive.files.get({ - fileId: decryptedId, - fields: - "id, name, mimeType, fileExtension, webContentLink, thumbnailLink", - supportsAllDrives: config.apiConfig.isTeamDrive, - }); - - if ( - !fileMeta.data.mimeType?.startsWith("image") && - !fileMeta.data.mimeType?.startsWith("video") - ) { - return defaultImage; - } - const url = `https://drive.google.com/thumbnail?id=${decryptedId}&sz=w${size}`; if (!config.apiConfig.proxyThumbnail) { From 79cafb821a3276270b0448416010be2b4d344b8c Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:55:03 +0700 Subject: [PATCH 22/27] Add password check, and change the chunk size --- src/app/api/stream/[encryptedId]/route.ts | 131 +++++++++++----------- 1 file changed, 65 insertions(+), 66 deletions(-) diff --git a/src/app/api/stream/[encryptedId]/route.ts b/src/app/api/stream/[encryptedId]/route.ts index 8843ab3..767078e 100644 --- a/src/app/api/stream/[encryptedId]/route.ts +++ b/src/app/api/stream/[encryptedId]/route.ts @@ -1,6 +1,11 @@ import { NextRequest, NextResponse } from "next/server"; -import { CheckDownloadToken } from "~/app/actions"; +import { + CheckDownloadToken, + CheckPassword, + CheckPaths, + RedirectSearchFile, +} from "~/app/actions"; import { decryptData } from "~/utils/encryptionHelper/hash"; import { gdriveNoCache as gdrive } from "~/utils/gdriveInstance"; @@ -24,17 +29,21 @@ export async function GET( const token = sp.get("token"); if (!token) throw new Error("Token not found"); - // Only allow if referrer is from the same site - // if (!request.headers.get("Referer")?.includes(config.basePath)) { - // throw new Error("Invalid request"); - // } + // Only allow if the request is from the same domain or the referer is the same domain + if ( + process.env.NODE_ENV === "production" && + !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 fileMeta = await gdrive.files.get( + const _filePaths = RedirectSearchFile(encryptedId); + const _fileMeta = gdrive.files.get( { fileId: decryptedId, fields: "id, name, mimeType, size, fileExtension, webContentLink", @@ -48,12 +57,55 @@ export async function GET( }, ); + 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 = 1 * 1024 * 1024; // Load 1MB at a time + 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+)?/; @@ -61,7 +113,11 @@ export async function GET( if (rangeSize) { rangeStart = parseInt(rangeSize[1], 10); - rangeEnd = Math.min(rangeStart + chunkSize, fileSize - 1); + if (isFull) { + rangeEnd = fileSize - 1; + } else { + rangeEnd = Math.min(rangeStart + chunkSize, fileSize - 1); + } } const contentRange = `bytes=${rangeStart}-${rangeEnd}/${fileSize}`; @@ -72,6 +128,7 @@ export async function GET( fileId: decryptedId, alt: "media", supportsAllDrives: config.apiConfig.isTeamDrive, + acknowledgeAbuse: true, }, { responseType: "stream", @@ -107,66 +164,8 @@ export async function GET( "Content-Length": fileLength || contentLength.toString(), "Content-Type": fileMeta.data.mimeType || "application/octet-stream", "Accept-Ranges": "bytes", - // 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`)}"`, }, }); - - // async function* readStream() { - // for await (const chunk of fileContent.data as any) { - // yield chunk; - // } - // } - // function iteratorToStream(iterator: AsyncGenerator) { - // return new ReadableStream({ - // async pull(controller) { - // const { done, value } = await iterator.next(); - // if (done) { - // controller.close(); - // } else { - // controller.enqueue(value); - // } - // }, - // }); - // } - // const stream: ReadableStream = iteratorToStream(readStream()); - - // const res = new Response(stream, { - // status: 206, - // headers: { - // "Content-Type": fileMeta.data.mimeType || "application/octet-stream", - // "Content-Disposition": `attachment; filename="${encodeURIComponent( - // fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, - // )}"`, - // "Content-Length": contentLength.toString(), - // "Content-Range": ranges ? contentRange : "", - // }, - // }); - // NextResponse.next(res); - // new NextResponse(stream, { - // status: request.headers.get("Range") ? 206 : 200, - // headers: { - // "Content-Type": fileMeta.data.mimeType || "application/octet-stream", - // "Content-Disposition": `attachment; filename="${encodeURIComponent( - // fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, - // )}"`, - // "Content-Length": fileSize.toString(), - // "Accept-Ranges": "bytes", - // "Range": ranges ? `bytes ${rangeStart}-${rangeEnd}/${fileSize}` : "", - // }, - // }); - - // return new NextResponse(stream, { - // status: 206, - // headers: { - // "Content-Type": fileMeta.data.mimeType || "application/octet-stream", - // "Content-Disposition": `attachment; filename="${encodeURIComponent( - // fileMeta.data.name || `Untitled.${fileMeta.data.fileExtension}`, - // )}"`, - // "Content-Length": fileSize.toString(), - // "Accept-Ranges": "bytes", - // "Range": ranges ? `bytes ${rangeStart}-${rangeEnd}/${fileSize}` : "", - // }, - // }); } catch (error) { const e = error as Error; console.error(e.message); From 3978928995badd4d7c9c58e0fa380b63c20dd974 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:55:23 +0700 Subject: [PATCH 23/27] Load meta and check path first before loading the file --- src/app/api/download/[encryptedId]/route.ts | 37 +++++++++------------ 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/app/api/download/[encryptedId]/route.ts b/src/app/api/download/[encryptedId]/route.ts index 7f69a7b..15cb631 100644 --- a/src/app/api/download/[encryptedId]/route.ts +++ b/src/app/api/download/[encryptedId]/route.ts @@ -57,22 +57,8 @@ If you've already entered the password, please make sure your browser is not blo fields: "id, name, mimeType, size, fileExtension, webContentLink", supportsAllDrives: config.apiConfig.isTeamDrive, }); - const _fileContent = gdrive.files.get( - { - fileId: decryptedId, - alt: "media", - supportsAllDrives: config.apiConfig.isTeamDrive, - }, - { - responseType: "stream", - }, - ); - const [fileMeta, fileContent, filePaths] = await Promise.all([ - _fileMeta, - _fileContent, - _filePaths, - ]); + const [fileMeta, filePaths] = await Promise.all([_fileMeta, _filePaths]); if (!config.apiConfig.allowDownloadProtectedFile) { const checkPath = await CheckPaths(filePaths.split("/")); @@ -112,16 +98,24 @@ If you've already entered the password, please make sure your browser is not blo config.apiConfig.maxFileSize && fileSize > config.apiConfig.maxFileSize ) { - console.log("File size is too large, redirecting to webContentLink"); - return NextResponse.redirect(fileMeta.data.webContentLink, { + const contentUrl = new URL(fileMeta.data.webContentLink); + contentUrl.searchParams.set("confirm", "1"); + return NextResponse.redirect(contentUrl, { status: 302, - headers: { - ...request.headers, - "Cache-Control": config.cacheControl, - }, }); } + 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) => { @@ -138,7 +132,6 @@ If you've already entered the password, please make sure your browser is not blo return new NextResponse(fileBuffer, { status: 200, headers: { - ...request.headers, "Content-Type": fileMeta.data.mimeType || "application/octet-stream", "Content-Length": fileBuffer.length.toString(), "Content-Disposition": `attachment; filename="${encodeURIComponent( From c9431dcffcc463fec6b48fb82c8ace6bf688fda8 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Wed, 8 May 2024 08:55:40 +0700 Subject: [PATCH 24/27] Update preview logic --- src/app/@preview.audio.tsx | 75 +++++++++--- src/app/@preview.doc.tsx | 55 +++++++-- src/app/@preview.image.tsx | 75 ++++++++++-- src/app/@preview.layout.tsx | 95 ++++++++++----- src/app/@preview.manga.tsx | 229 ++++++++++++++++++++++++++++-------- src/app/@preview.rich.tsx | 21 ++-- src/app/@preview.video.tsx | 48 ++++---- 7 files changed, 447 insertions(+), 151 deletions(-) diff --git a/src/app/@preview.audio.tsx b/src/app/@preview.audio.tsx index eaf7e51..ba5d2dc 100644 --- a/src/app/@preview.audio.tsx +++ b/src/app/@preview.audio.tsx @@ -1,7 +1,7 @@ "use client"; +import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; -import AudioPlayer from "react-h5-audio-player"; import "react-h5-audio-player/lib/styles.css"; import { z } from "zod"; import { Schema_File } from "~/schema"; @@ -10,7 +10,27 @@ import { cn } from "~/utils"; import Icon from "~/components/Icon"; import { CreateDownloadToken } from "./actions"; -import "./r5-style.css"; + +// import "./r5-style.css"; + +const Plyr = dynamic(() => import("plyr-react"), { + ssr: false, + loading: () => ( +
+ +

Loading player...

+
+ ), +}); type Props = { file: z.infer; @@ -28,7 +48,7 @@ export default function PreviewAudio({ file }: Props) { return; } const token = await CreateDownloadToken(); - setAudioSrc(`/api/download/${file.encryptedId}?token=${token}`); + setAudioSrc(`/api/stream/${file.encryptedId}?token=${token}`); } catch (error) { const e = error as Error; console.error(e); @@ -66,20 +86,43 @@ export default function PreviewAudio({ file }: Props) {
) : (
- { - console.error(e); - setError( - "Could not preview this audio, try downloading the file", - ); + options={{ + controls: [ + "play-large", + "play", + "progress", + "current-time", + "duration", + "mute", + "volume", + "settings", + "fullscreen", + ], + volume: 0.5, + muted: false, + loop: { + active: false, + }, + speed: { + selected: 1, + options: [0.5, 1, 1.5, 2], + }, + keyboard: { + focused: true, + global: false, + }, }} />
diff --git a/src/app/@preview.doc.tsx b/src/app/@preview.doc.tsx index 3b6380a..327370c 100644 --- a/src/app/@preview.doc.tsx +++ b/src/app/@preview.doc.tsx @@ -1,6 +1,6 @@ "use client"; -import DocViewer, { DocViewerRenderers } from "@cyntler/react-doc-viewer"; +import DocViewer, { DocRenderer } from "@cyntler/react-doc-viewer"; import { useEffect, useState } from "react"; import { z } from "zod"; import { Schema_File } from "~/schema"; @@ -8,15 +8,49 @@ import { cn } from "~/utils"; import Icon from "~/components/Icon"; +import config from "~/config/gIndex.config"; + import { CreateDownloadToken } from "./actions"; +const GoogleDocsViewerRenderer: DocRenderer = ({ + mainState: { currentDocument }, +}) => { + if (!currentDocument || !currentDocument.uri) return null; + + const viewerUrl = new URL(`/gview`, "https://docs.google.com"); + viewerUrl.searchParams.set("url", currentDocument.uri); + viewerUrl.searchParams.set("embedded", "true"); + + return ( +