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 = {
reactStrictMode: true,
experimental: {},
eslint: {
ignoreDuringBuilds: true,
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.discordapp.com",
port: "",
pathname: "/**",
},
],
},
};
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";
import { useEffect, useState } from "react";
@ -17,8 +20,7 @@ interface FileGridProps {
export default function FileGrid({ session }: FileGridProps) {
const [files, setFiles] = useState<File[]>([]);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const fetchFiles = async () => {
try {
@ -26,7 +28,7 @@ export default function FileGrid({ session }: FileGridProps) {
if (!response.ok) {
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);
} catch (err) {
console.error(err);
@ -71,12 +73,14 @@ export default function FileGrid({ session }: FileGridProps) {
const eventSource = new EventSource("/api/files/stream");
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") {
setFiles((prevFiles) => [...prevFiles, data.file]);
if (data.type === "file-added" && data.file) {
if (data.file) {
setFiles((prevFiles) => [...prevFiles, data.file as File]);
}
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));
}
};
@ -123,7 +127,6 @@ export default function FileGrid({ session }: FileGridProps) {
if (error) {
return <div className="text-red-500">{error}</div>;
}
const router = useRouter();
return (
<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 = () => {
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
toast.success("File uploaded successfully!");

View File

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

View File

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

View File

@ -2,12 +2,10 @@ import { NextResponse } from "next/server";
import { db } from "~/server/db";
import { auth } from "~/server/auth";
export async function GET(req: Request) {
const session = await auth();
const url = new URL(req.url);
const fileName = url.searchParams.get("file");
const fileName = url.searchParams.get("id");
if (!fileName) {
return NextResponse.json({ error: "File name is required" }, { status: 400 });
@ -26,12 +24,12 @@ export async function GET(req: Request) {
return NextResponse.json({
name: file.name,
size: file.size,
owner: file.uploadedBy?.name || "Unknown",
owner: file.uploadedBy?.name ?? "Unknown", // Use nullish coalescing
uploadDate: file.uploadDate,
id: file.id,
isOwner: session?.user?.id === file.uploadedById, // Check if the current user is the owner
type: file.extension, // Add file type
url: file.url, // Add file URL
isOwner: session?.user?.id === file.uploadedById,
type: file.extension,
url: file.url,
});
} catch (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 Busboy from "busboy";
import { Readable } from "stream";
import { notifyClients } from "../files/stream/route";
import { notifyClients } from "~/utils/notifyClients";
export const config = {
api: {

View File

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

View File

@ -1,41 +1,44 @@
"use client";
import { Suspense } from "react";
import { useEffect, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import toast, { Toaster } from "react-hot-toast";
// import { SharePage } from "~/components/SharePage";
interface FileDetails {
name: string;
size: number;
owner: string;
uploadDate: string;
id: string; // Add an ID for the file
isOwner: boolean; // Add a flag to indicate ownership
type: string; // Add a type field to differentiate between file types
url: string; // Add a URL field for the file
id: string;
isOwner: boolean;
type: string;
url: string;
}
export default function UploadsPage() {
function UploadsPage() {
const searchParams = useSearchParams();
const router = useRouter();
const fileName = searchParams.get("file");
const fileId = searchParams.get("id");
const [fileDetails, setFileDetails] = useState<FileDetails | 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(() => {
if (!fileName) {
if (!fileId) {
setError("File name is required.");
return;
}
const fetchFileDetails = async () => {
try {
const response = await fetch(`/api/share?file=${encodeURIComponent(fileName)}`);
const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`);
if (!response.ok) {
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);
} catch (err) {
console.error(err);
@ -43,12 +46,12 @@ export default function UploadsPage() {
}
};
fetchFileDetails();
}, [fileName]);
void fetchFileDetails(); // Use `void` to mark the promise as intentionally ignored
}, [fileId]);
const handleDownload = async () => {
try {
const response = await fetch(`/api/files/download?fileName=${encodeURIComponent(fileName || "")}`);
const response = await fetch(`/api/files/download?fileId=${encodeURIComponent(fileId ?? "")}`);
if (!response.ok) {
throw new Error("Failed to download file");
}
@ -57,7 +60,7 @@ export default function UploadsPage() {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName || "downloaded-file";
a.download = fileId ?? "downloaded-file"; // Use nullish coalescing
document.body.appendChild(a);
a.click();
a.remove();
@ -87,7 +90,7 @@ export default function UploadsPage() {
headers: {
"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) {
@ -143,15 +146,19 @@ export default function UploadsPage() {
<span className="text-[hsl(280,100%,70%)]">File</span> Details
</h1>
<div className="mt-6">
{(fileDetails.type.startsWith(".png") || fileDetails.type.startsWith(".jpg") || fileDetails.type.startsWith(".jpeg") || fileDetails.type.startsWith(".gif")) && (
<SharePage />
)}
{(fileDetails.type.startsWith(".mp4") || fileDetails.type.startsWith(".webm") || fileDetails.type.startsWith(".ogg")) && (
{/* {(fileDetails.type.startsWith(".png") ||
fileDetails.type.startsWith(".jpg") ||
fileDetails.type.startsWith(".jpeg") ||
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">
<SharePage />
<source src={mediasrc} type="video" />
Your browser does not support the video tag.
</video>
)}
)} */}
</div>
<div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white">
<p>
@ -167,7 +174,6 @@ export default function UploadsPage() {
<strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()}
</p>
</div>
{/* Preview Section */}
<div className="flex gap-4 mt-6">
<button
onClick={handleDownload}
@ -197,52 +203,10 @@ export default function UploadsPage() {
);
}
export function SharePage() {
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>;
}
export default function Page() {
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 { z } from "zod";
// console.log("Environment Variables:", process.env);
export const env = createEnv({
/**
* 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.