working site

This commit is contained in:
ZareMate 2025-04-14 08:20:36 +02:00
parent 08d1278f11
commit bcdceb615c
Signed by: zaremate
GPG Key ID: 369A0E45E03A81C3
17 changed files with 238 additions and 146 deletions

7
docker/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.next
Dockerfile
.dockerignore
.env
.env.local
.env.development.local

40
docker/Dockerfile Normal file
View File

@ -0,0 +1,40 @@
# 1. Install dependencies only when needed
FROM node:18-alpine AS deps
WORKDIR /app
# Install Prisma Client (important for runtime)
COPY package.json package-lock.json ./
RUN npm ci
# 2. Build the app with Prisma & Next.js
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
# Generate Prisma client
RUN npx prisma generate
# Build the app
RUN npm run build
# 3. Final image
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copy the built app + node_modules
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/prisma ./prisma
# Include the Prisma client
RUN npx prisma generate
# Expose port and run
EXPOSE 3000
CMD ["npm", "start"]

20
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: "mysql://filehost:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6@10.0.0.1:3306/file_hosting_db"
AUTH_SECRET: "lHpKepMT0dyBHcFQzeN9B5e4Rn/DG6Lc5aiMIKa9HdY="
AUTH_DISCORD_ID: "1360729915678392492"
AUTH_DISCORD_SECRET: "lIrkEwb2PpMpLZM7Yb9pGVeT7YLgIC_C"
AUTH_TRUST_HOST: "true"
SKIP_ENV_VALIDATION: "true"
volumes:
- type: bind
source: /mnt/0TB/DATA/AppData/file-hosting/uploads
target: /uploads

View File

@ -6,7 +6,19 @@ import "./src/env.js";
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
experimental: {}, eslint: {
ignoreDuringBuilds: true,
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.discordapp.com",
port: "",
pathname: "/**",
},
],
},
}; };
export default nextConfig; export default nextConfig;

View File

@ -1,3 +1,6 @@
//!eslint-disable @typescript-eslint/no-unsafe-assignment
//!eslint-disable @typescript-eslint/no-unsafe-argument
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -17,8 +20,7 @@ interface FileGridProps {
export default function FileGrid({ session }: FileGridProps) { export default function FileGrid({ session }: FileGridProps) {
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const router = useRouter();
const fetchFiles = async () => { const fetchFiles = async () => {
try { try {
@ -26,7 +28,7 @@ export default function FileGrid({ session }: FileGridProps) {
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch files"); throw new Error("Failed to fetch files");
} }
const data: { files: File[] } = await response.json(); const data = await response.json() as { files: File[] }; // Explicitly type the response
setFiles(data.files); setFiles(data.files);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -71,12 +73,14 @@ export default function FileGrid({ session }: FileGridProps) {
const eventSource = new EventSource("/api/files/stream"); const eventSource = new EventSource("/api/files/stream");
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
const data = JSON.parse(event.data); const data: { type: string; file?: File; fileId?: string } = JSON.parse(event.data); // Explicitly type the parsed data
if (data.type === "file-added") { if (data.type === "file-added" && data.file) {
setFiles((prevFiles) => [...prevFiles, data.file]); if (data.file) {
setFiles((prevFiles) => [...prevFiles, data.file as File]);
}
toast.success(`File "${data.file.name}" added!`); toast.success(`File "${data.file.name}" added!`);
} else if (data.type === "file-removed") { } else if (data.type === "file-removed" && data.fileId) {
setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId));
} }
}; };
@ -123,7 +127,6 @@ export default function FileGrid({ session }: FileGridProps) {
if (error) { if (error) {
return <div className="text-red-500">{error}</div>; return <div className="text-red-500">{error}</div>;
} }
const router = useRouter();
return ( return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-8"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-8">

View File

@ -40,7 +40,7 @@ export default function UploadForm() {
xhr.onload = () => { xhr.onload = () => {
if (xhr.status === 200) { if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText); const response: { url: string } = JSON.parse(xhr.responseText); // Explicitly type the response
setUploadedFileUrl(response.url); // Assume the API returns the uploaded file URL setUploadedFileUrl(response.url); // Assume the API returns the uploaded file URL
toast.success("File uploaded successfully!"); toast.success("File uploaded successfully!");

View File

@ -4,20 +4,20 @@ import { promises as fs } from "fs";
export async function GET(req: Request) { export async function GET(req: Request) {
const url = new URL(req.url); const url = new URL(req.url);
const fileName = url.searchParams.get("fileName"); const fileId = url.searchParams.get("fileName");
if (!fileName) { if (!fileId) {
return NextResponse.json({ error: "File name is required" }, { status: 400 }); return NextResponse.json({ error: "File id is required" }, { status: 400 });
} }
try { try {
const filePath = path.join(process.cwd(), "uploads", fileName); const filePath = path.join(process.cwd(), "uploads", fileId);
const fileBuffer = await fs.readFile(filePath); const fileBuffer = await fs.readFile(filePath);
return new Response(fileBuffer, { return new Response(fileBuffer, {
headers: { headers: {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`, "Content-Disposition": `attachment; filename="${fileId}"`,
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -1,6 +1,4 @@
import { NextResponse } from "next/server"; import { clients } from "~/utils/notifyClients";
const clients: Set<any> = new Set();
export async function GET() { export async function GET() {
const stream = new ReadableStream({ const stream = new ReadableStream({
@ -20,16 +18,9 @@ export async function GET() {
clients.add(client); clients.add(client);
// Remove the client when the stream is closed
const abortListener = () => {
clients.delete(client);
controller.close(); // Ensure the stream is closed when the client disconnects
};
signal.addEventListener("abort", abortListener);
// Cleanup the abort listener when the stream is closed
signal.addEventListener("abort", () => { signal.addEventListener("abort", () => {
signal.removeEventListener("abort", abortListener); clients.delete(client);
controller.close();
}); });
}, },
}); });
@ -42,15 +33,3 @@ export async function GET() {
}, },
}); });
} }
// Notify all connected clients about a file change
export function notifyClients(data: any) {
const message = JSON.stringify(data);
clients.forEach((client) => {
try {
client.send(message);
} catch (error) {
console.error("Failed to send message to a client:", error);
}
});
}

View File

@ -3,7 +3,7 @@ import { db } from "~/server/db";
import { auth } from "~/server/auth"; import { auth } from "~/server/auth";
import path from "path"; import path from "path";
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import { notifyClients } from "../files/stream/route"; import { notifyClients } from "~/utils/notifyClients";
export async function DELETE(req: Request) { export async function DELETE(req: Request) {
const session = await auth(); const session = await auth();
@ -13,15 +13,13 @@ export async function DELETE(req: Request) {
} }
try { try {
const body = await req.json().catch(() => null); // Handle empty or invalid JSON const body = (await req.json()) as { id: string } | null;
if (!body || !body.id) { if (!body?.id) {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
} }
const resourceId = body.id;
const resource = await db.file.findUnique({ const resource = await db.file.findUnique({
where: { id: resourceId }, where: { id: body.id },
}); });
if (!resource || resource.uploadedById !== session.user.id) { if (!resource || resource.uploadedById !== session.user.id) {
@ -34,11 +32,10 @@ export async function DELETE(req: Request) {
}); });
await db.file.delete({ await db.file.delete({
where: { id: resourceId }, where: { id: body.id },
}); });
// Notify clients about the deleted file notifyClients({ type: "file-removed", fileId: body.id });
notifyClients({ type: "file-removed", fileId: resourceId });
return NextResponse.json({ message: "Resource deleted successfully" }); return NextResponse.json({ message: "Resource deleted successfully" });
} catch (error) { } catch (error) {

View File

@ -2,12 +2,10 @@ import { NextResponse } from "next/server";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { auth } from "~/server/auth"; import { auth } from "~/server/auth";
export async function GET(req: Request) { export async function GET(req: Request) {
const session = await auth(); const session = await auth();
const url = new URL(req.url); const url = new URL(req.url);
const fileName = url.searchParams.get("file"); const fileName = url.searchParams.get("id");
if (!fileName) { if (!fileName) {
return NextResponse.json({ error: "File name is required" }, { status: 400 }); return NextResponse.json({ error: "File name is required" }, { status: 400 });
@ -26,12 +24,12 @@ export async function GET(req: Request) {
return NextResponse.json({ return NextResponse.json({
name: file.name, name: file.name,
size: file.size, size: file.size,
owner: file.uploadedBy?.name || "Unknown", owner: file.uploadedBy?.name ?? "Unknown", // Use nullish coalescing
uploadDate: file.uploadDate, uploadDate: file.uploadDate,
id: file.id, id: file.id,
isOwner: session?.user?.id === file.uploadedById, // Check if the current user is the owner isOwner: session?.user?.id === file.uploadedById,
type: file.extension, // Add file type type: file.extension,
url: file.url, // Add file URL url: file.url,
}); });
} catch (error) { } catch (error) {
console.error("Error fetching file details:", error); console.error("Error fetching file details:", error);

View File

@ -5,7 +5,7 @@ import { db } from "~/server/db";
import { auth } from "~/server/auth"; import { auth } from "~/server/auth";
import Busboy from "busboy"; import Busboy from "busboy";
import { Readable } from "stream"; import { Readable } from "stream";
import { notifyClients } from "../files/stream/route"; import { notifyClients } from "~/utils/notifyClients";
export const config = { export const config = {
api: { api: {

View File

@ -3,7 +3,7 @@ import { auth } from "~/server/auth";
import { HydrateClient } from "~/trpc/server"; import { HydrateClient } from "~/trpc/server";
import FileGrid from "~/app/_components/FileGrid"; import FileGrid from "~/app/_components/FileGrid";
import UploadForm from "~/app/_components/UploadForm"; import UploadForm from "~/app/_components/UploadForm";
import toast, { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
export default async function Home() { export default async function Home() {
const session = await auth(); const session = await auth();

View File

@ -1,41 +1,44 @@
"use client"; "use client";
import { Suspense } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation"; import { useSearchParams, useRouter } from "next/navigation";
import toast, { Toaster } from "react-hot-toast"; import toast, { Toaster } from "react-hot-toast";
// import { SharePage } from "~/components/SharePage";
interface FileDetails { interface FileDetails {
name: string; name: string;
size: number; size: number;
owner: string; owner: string;
uploadDate: string; uploadDate: string;
id: string; // Add an ID for the file id: string;
isOwner: boolean; // Add a flag to indicate ownership isOwner: boolean;
type: string; // Add a type field to differentiate between file types type: string;
url: string; // Add a URL field for the file url: string;
} }
export default function UploadsPage() { function UploadsPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const fileName = searchParams.get("file"); const fileId = searchParams.get("id");
const [fileDetails, setFileDetails] = useState<FileDetails | null>(null); const [fileDetails, setFileDetails] = useState<FileDetails | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// const mediasrc = SharePage() as string; // Replace with a valid string URL or logic to generate the URL
useEffect(() => { useEffect(() => {
if (!fileName) { if (!fileId) {
setError("File name is required."); setError("File name is required.");
return; return;
} }
const fetchFileDetails = async () => { const fetchFileDetails = async () => {
try { try {
const response = await fetch(`/api/share?file=${encodeURIComponent(fileName)}`); const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch file details"); throw new Error("Failed to fetch file details");
} }
const data = await response.json(); const data: FileDetails = await response.json(); // Explicitly type the response
setFileDetails(data); setFileDetails(data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -43,12 +46,12 @@ export default function UploadsPage() {
} }
}; };
fetchFileDetails(); void fetchFileDetails(); // Use `void` to mark the promise as intentionally ignored
}, [fileName]); }, [fileId]);
const handleDownload = async () => { const handleDownload = async () => {
try { try {
const response = await fetch(`/api/files/download?fileName=${encodeURIComponent(fileName || "")}`); const response = await fetch(`/api/files/download?fileId=${encodeURIComponent(fileId ?? "")}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to download file"); throw new Error("Failed to download file");
} }
@ -57,7 +60,7 @@ export default function UploadsPage() {
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = fileName || "downloaded-file"; a.download = fileId ?? "downloaded-file"; // Use nullish coalescing
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
@ -87,7 +90,7 @@ export default function UploadsPage() {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ id: fileDetails?.id }), // Use the ID of the file body: JSON.stringify({ id: fileDetails?.id }), // Use optional chaining
}); });
if (!response.ok) { if (!response.ok) {
@ -108,23 +111,23 @@ export default function UploadsPage() {
if (!fileDetails) { if (!fileDetails) {
return ( return (
<main className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> <main className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
<Toaster position="top-right" reverseOrder={false} /> <Toaster position="top-right" reverseOrder={false} />
<div className="absolute top-4 left-4"> <div className="absolute top-4 left-4">
<button <button
onClick={() => router.push("/")} onClick={() => router.push("/")}
className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20" className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20"
> >
Home Home
</button> </button>
</div> </div>
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16"> <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
<span className="text-[hsl(280,100%,70%)]">File</span> Details <span className="text-[hsl(280,100%,70%)]">File</span> Details
</h1> </h1>
</div> </div>
</main> </main>
); );
} }
return ( return (
@ -142,17 +145,21 @@ export default function UploadsPage() {
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
<span className="text-[hsl(280,100%,70%)]">File</span> Details <span className="text-[hsl(280,100%,70%)]">File</span> Details
</h1> </h1>
<div className=" mt-6"> <div className="mt-6">
{(fileDetails.type.startsWith(".png") || fileDetails.type.startsWith(".jpg") || fileDetails.type.startsWith(".jpeg") || fileDetails.type.startsWith(".gif")) && ( {/* {(fileDetails.type.startsWith(".png") ||
<SharePage /> fileDetails.type.startsWith(".jpg") ||
)} fileDetails.type.startsWith(".jpeg") ||
{(fileDetails.type.startsWith(".mp4") || fileDetails.type.startsWith(".webm") || fileDetails.type.startsWith(".ogg")) && ( fileDetails.type.startsWith(".gif")) && (mediasrc && <img src={mediasrc} alt="Media preview" />)}
{(fileDetails.type.startsWith(".mp4") ||
fileDetails.type.startsWith(".webm") ||
fileDetails.type.startsWith(".ogg")) &&
(mediasrc &&
<video controls className="max-w-full max-h-96 rounded-lg shadow-md"> <video controls className="max-w-full max-h-96 rounded-lg shadow-md">
<SharePage /> <source src={mediasrc} type="video" />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
)} )} */}
</div> </div>
<div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> <div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white">
<p> <p>
<strong>Name:</strong> {fileDetails.name} <strong>Name:</strong> {fileDetails.name}
@ -167,7 +174,6 @@ export default function UploadsPage() {
<strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()} <strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()}
</p> </p>
</div> </div>
{/* Preview Section */}
<div className="flex gap-4 mt-6"> <div className="flex gap-4 mt-6">
<button <button
onClick={handleDownload} onClick={handleDownload}
@ -197,52 +203,10 @@ export default function UploadsPage() {
); );
} }
export function SharePage() { export default function Page() {
const searchParams = useSearchParams();
const fileName = searchParams.get("file");
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchImage = async () => {
try {
if (!fileName) {
throw new Error("File name is required.");
}
const response = await fetch(`/api/serv?id=${encodeURIComponent(fileName)}`);
if (!response.ok) {
throw new Error("Failed to fetch image");
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
setImageSrc(objectUrl);
} catch (err) {
console.error(err);
setError("Failed to load image.");
console.log(err);
}
};
fetchImage();
return () => {
if (imageSrc) {
URL.revokeObjectURL(imageSrc); // Clean up the object URL
}
};
}, [fileName]);
if (error) {
return <div className="text-red-500">{error}</div>;
}
if (!imageSrc) {
return <div>Loading...</div>;
}
return ( return (
<source src={imageSrc} className="max-w-full max-h-96 rounded-lg shadow-md" /> <Suspense fallback={<div>Loading...</div>}>
<UploadsPage />
</Suspense>
); );
} }

View File

@ -0,0 +1,52 @@
"use client";
import { useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
export function SharePage() {
const searchParams = useSearchParams();
const fileId = searchParams.get("id");
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchImage = async () => {
try {
if (!fileId) {
throw new Error("File name is required.");
}
const response = await fetch(`/api/serv?id=${encodeURIComponent(fileId)}`);
if (!response.ok) {
throw new Error("Failed to fetch image");
}
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
setImageSrc(objectUrl);
} catch (err) {
console.error(err);
setError("Failed to load image.");
}
};
void fetchImage();
return () => {
if (imageSrc) {
URL.revokeObjectURL(imageSrc);
}
};
}, [fileId, imageSrc]);
if (error) {
return <div className="text-red-500">{error}</div>;
}
if (!imageSrc) {
return <div>Loading...</div>;
}
return (
imageSrc
);
}

View File

@ -1,6 +1,8 @@
import { createEnv } from "@t3-oss/env-nextjs"; import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod"; import { z } from "zod";
// console.log("Environment Variables:", process.env);
export const env = createEnv({ export const env = createEnv({
/** /**
* Specify your server-side environment variables schema here. This way you can ensure the app * Specify your server-side environment variables schema here. This way you can ensure the app

View File

@ -0,0 +1,18 @@
interface Client {
send: (data: string) => void;
}
const clients: Set<Client> = new Set<Client>();
export function notifyClients(data: unknown) {
const message = JSON.stringify(data);
clients.forEach((client) => {
try {
client.send(message);
} catch (error) {
console.error("Failed to send message to a client:", error);
}
});
}
export { clients };

Binary file not shown.