diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 0000000..0d09073 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +Dockerfile +.dockerignore +.env +.env.local +.env.development.local \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..b8657de --- /dev/null +++ b/docker/Dockerfile @@ -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"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..2ce202d --- /dev/null +++ b/docker/docker-compose.yml @@ -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 diff --git a/next.config.js b/next.config.js index 707e640..4f12fea 100644 --- a/next.config.js +++ b/next.config.js @@ -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; diff --git a/src/app/_components/FileGrid.tsx b/src/app/_components/FileGrid.tsx index 759c680..491161c 100644 --- a/src/app/_components/FileGrid.tsx +++ b/src/app/_components/FileGrid.tsx @@ -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([]); const [error, setError] = useState(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
{error}
; } - const router = useRouter(); return (
diff --git a/src/app/_components/UploadForm.tsx b/src/app/_components/UploadForm.tsx index 1627bd8..29aae38 100644 --- a/src/app/_components/UploadForm.tsx +++ b/src/app/_components/UploadForm.tsx @@ -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!"); diff --git a/src/app/api/files/download/route.ts b/src/app/api/files/download/route.ts index 18f8114..c9fe334 100644 --- a/src/app/api/files/download/route.ts +++ b/src/app/api/files/download/route.ts @@ -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) { diff --git a/src/app/api/files/stream/route.ts b/src/app/api/files/stream/route.ts index fde07ac..ae8a8e6 100644 --- a/src/app/api/files/stream/route.ts +++ b/src/app/api/files/stream/route.ts @@ -1,6 +1,4 @@ -import { NextResponse } from "next/server"; - -const clients: Set = 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(); }); }, }); @@ -41,16 +32,4 @@ export async function GET() { Connection: "keep-alive", }, }); -} - -// 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); - } - }); } \ No newline at end of file diff --git a/src/app/api/remove/route.ts b/src/app/api/remove/route.ts index 83a533a..33c483c 100644 --- a/src/app/api/remove/route.ts +++ b/src/app/api/remove/route.ts @@ -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) { diff --git a/src/app/api/share/route.ts b/src/app/api/share/route.ts index 6105375..08dfc05 100644 --- a/src/app/api/share/route.ts +++ b/src/app/api/share/route.ts @@ -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); diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 3687d19..dec55ef 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -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: { @@ -46,7 +46,7 @@ export async function POST(req: Request) { const baseName = path.basename(fileName, fileExtension); fileName = `${baseName}-${Date.now()}${fileExtension}`; } - + file.on("data", (chunk) => { chunks.push(chunk); }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 146ea28..37629a1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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(); diff --git a/src/app/share/page.tsx b/src/app/share/page.tsx index 3a10dc5..38e0afe 100644 --- a/src/app/share/page.tsx +++ b/src/app/share/page.tsx @@ -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(null); const [error, setError] = useState(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) { @@ -108,23 +111,23 @@ export default function UploadsPage() { if (!fileDetails) { return ( -
- -
- -
-
-

- File Details -

-
-
- ); +
+ +
+ +
+
+

+ File Details +

+
+
+ ); } return ( @@ -142,17 +145,21 @@ export default function UploadsPage() {

File Details

-
- {(fileDetails.type.startsWith(".png") || fileDetails.type.startsWith(".jpg") || fileDetails.type.startsWith(".jpeg") || fileDetails.type.startsWith(".gif")) && ( - - )} - {(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 && Media preview)} + {(fileDetails.type.startsWith(".mp4") || + fileDetails.type.startsWith(".webm") || + fileDetails.type.startsWith(".ogg")) && + (mediasrc && - )} -
+ )} */} +

Name: {fileDetails.name} @@ -167,7 +174,6 @@ export default function UploadsPage() { Upload Date: {new Date(fileDetails.uploadDate).toLocaleString()}

- {/* Preview Section */}
}> + + ); -} \ No newline at end of file +} diff --git a/src/components/SharePage.tsx b/src/components/SharePage.tsx new file mode 100644 index 0000000..a2a13c3 --- /dev/null +++ b/src/components/SharePage.tsx @@ -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(null); + const [error, setError] = useState(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
{error}
; + } + + if (!imageSrc) { + return
Loading...
; + } + + return ( + imageSrc + ); +} \ No newline at end of file diff --git a/src/env.js b/src/env.js index 6b19f72..ecf0679 100644 --- a/src/env.js +++ b/src/env.js @@ -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 diff --git a/src/utils/notifyClients.ts b/src/utils/notifyClients.ts new file mode 100644 index 0000000..8952127 --- /dev/null +++ b/src/utils/notifyClients.ts @@ -0,0 +1,18 @@ +interface Client { + send: (data: string) => void; +} + +const clients: Set = new Set(); + +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 }; \ No newline at end of file diff --git a/uploads/naturaltanczy.mp4 b/uploads/naturaltanczy.mp4 deleted file mode 100644 index 8667f1a..0000000 Binary files a/uploads/naturaltanczy.mp4 and /dev/null differ