mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
Test stream
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,12 +26,13 @@ export default function FilePreviewLayout({ data, fileType }: Props) {
|
||||
const [view, setView] = useState<"markdown" | "raw">("markdown");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<Card>
|
||||
<RichHeader
|
||||
title={data.name}
|
||||
view={view}
|
||||
onViewChange={setView}
|
||||
fileType={fileType}
|
||||
/>
|
||||
<CardContent className='p-1.5 pt-0 tablet:p-3 tablet:pt-0'>
|
||||
<div className='px-3'>
|
||||
@@ -70,6 +71,6 @@ export default function FilePreviewLayout({ data, fileType }: Props) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<PreviewAction file={data} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+68
-26
@@ -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<typeof Schema_File>;
|
||||
};
|
||||
@@ -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) {
|
||||
<span className='text-center text-destructive'>{error}</span>
|
||||
</div>
|
||||
) : (
|
||||
<ReactPlayer
|
||||
key={file.encryptedId}
|
||||
url={videoSrc}
|
||||
controls
|
||||
pip
|
||||
wrapper={({ children }) => (
|
||||
<div className='h-[60dvh] w-full overflow-hidden rounded-[var(--radius)] bg-muted'>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
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?)");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className='h-full w-full'>
|
||||
<Plyr
|
||||
source={{
|
||||
type: "video",
|
||||
sources: [
|
||||
{
|
||||
src: videoSrc,
|
||||
type: file.mimeType,
|
||||
size: file.size,
|
||||
},
|
||||
],
|
||||
}}
|
||||
options={{
|
||||
toggleInvert: true,
|
||||
settings: ["quality", "speed"],
|
||||
ratio: "16:9",
|
||||
controls: [
|
||||
"play-large",
|
||||
"play",
|
||||
"progress",
|
||||
"current-time",
|
||||
"duration",
|
||||
"mute",
|
||||
"volume",
|
||||
"settings",
|
||||
"pip",
|
||||
"download",
|
||||
"fullscreen",
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
// <ReactPlayer
|
||||
// key={file.encryptedId}
|
||||
// url={videoSrc}
|
||||
// controls
|
||||
// pip
|
||||
// wrapper={({ children }) => (
|
||||
// <div className='h-[60dvh] w-full overflow-hidden rounded-[var(--radius)] bg-muted'>
|
||||
// {children}
|
||||
// </div>
|
||||
// )}
|
||||
// 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?)");
|
||||
// }
|
||||
// }}
|
||||
// />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+36
-22
@@ -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<typeof getFileType> | "unknown";
|
||||
};
|
||||
export default function RichHeader({ title, view, onViewChange }: Props) {
|
||||
export default function RichHeader({
|
||||
title,
|
||||
view,
|
||||
onViewChange,
|
||||
fileType,
|
||||
}: Props) {
|
||||
return (
|
||||
<CardHeader className='pb-0'>
|
||||
<div className='flex flex-col gap-3 mobile:flex-row mobile:items-center mobile:justify-between'>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<div className='flex w-full items-center mobile:w-fit'>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={view === "markdown" ? "default" : "secondary"}
|
||||
onClick={() => onViewChange("markdown")}
|
||||
className='w-full rounded-r-none mobile:w-fit'
|
||||
>
|
||||
Markdown
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={view === "raw" ? "default" : "secondary"}
|
||||
onClick={() => onViewChange("raw")}
|
||||
className='w-full rounded-l-none mobile:w-fit'
|
||||
>
|
||||
Raw
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-col gap-3 overflow-hidden mobile:flex-row mobile:items-center mobile:justify-between'>
|
||||
{/* <CardTitle> */}
|
||||
<h3 className='line-clamp-1 flex-grow whitespace-pre-wrap break-all'>
|
||||
{title}
|
||||
</h3>
|
||||
{/* </CardTitle> */}
|
||||
{["markdown", "code", "text"].includes(fileType) && (
|
||||
<div className='flex w-full items-center mobile:w-fit'>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={view === "markdown" ? "default" : "secondary"}
|
||||
onClick={() => onViewChange("markdown")}
|
||||
className='w-full rounded-r-none mobile:w-fit'
|
||||
>
|
||||
Markdown
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={view === "raw" ? "default" : "secondary"}
|
||||
onClick={() => onViewChange("raw")}
|
||||
className='w-full rounded-l-none mobile:w-fit'
|
||||
>
|
||||
Raw
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
</CardHeader>
|
||||
|
||||
@@ -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<any>) {
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user