Test stream

This commit is contained in:
mbaharip
2024-05-05 16:46:27 +07:00
parent e3f79a14df
commit 1254d83fb0
9 changed files with 318 additions and 72 deletions
+1
View File
@@ -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",
+5 -22
View File
@@ -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>
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+101
View File
@@ -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
View File
@@ -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";
+79
View File
@@ -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"