working site
This commit is contained in:
parent
08d1278f11
commit
bcdceb615c
7
docker/.dockerignore
Normal file
7
docker/.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.next
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
40
docker/Dockerfile
Normal file
40
docker/Dockerfile
Normal 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
20
docker/docker-compose.yml
Normal 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
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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!");
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
52
src/components/SharePage.tsx
Normal file
52
src/components/SharePage.tsx
Normal 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
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
|
||||
18
src/utils/notifyClients.ts
Normal file
18
src/utils/notifyClients.ts
Normal 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.
Loading…
x
Reference in New Issue
Block a user