test new storage

This commit is contained in:
Marco Pedone 2025-10-22 15:41:53 +02:00
parent eabe7f36c1
commit 6a5820dda1
3 changed files with 227 additions and 1 deletions

View file

@ -59,6 +59,10 @@ async function createNextConfig(): Promise<NextConfig> {
reactStrictMode: true, reactStrictMode: true,
rewrites: async () => { rewrites: async () => {
return [ return [
{
source: "/storage/:slug*",
destination: "http://localhost:8080/:slug*",
},
{ {
destination: env.NODE_ENV === "production" ? "/404" : "/api/panel", destination: env.NODE_ENV === "production" ? "/404" : "/api/panel",
source: "/api/panel", source: "/api/panel",

View file

@ -147,6 +147,135 @@ export default Test;
*/ */
const Test: NextPage = () => { const Test: NextPage = () => {
return <div>Test</div>; return <FileManagementPage />;
}; };
export default Test; export default Test;
import { useEffect, useState } from "react";
import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import {
deleteFile,
type FileMetadata,
fetchFiles,
uploadFile,
} from "~/server/storage";
function FileManagementPage() {
const [files, setFiles] = useState<FileMetadata[]>([]);
const [loading, setLoading] = useState(true);
const loadData = async () => {
setLoading(true);
const fileList = await fetchFiles();
setFiles(fileList);
setLoading(false);
};
useEffect(() => {
loadData();
}, []);
const [inputFile, setInputFile] = useState<File | null>(null);
const [inputExpiresAt, setInputExpiresAt] = useState<string | undefined>(
undefined,
);
const handleFileUpload = async () => {
if (inputFile) {
const success = await uploadFile(inputFile, inputExpiresAt);
if (success) {
// Reload list after successful upload
await loadData();
}
}
};
const handleDelete = async (fileID: string) => {
const confirmDelete = window.confirm("Are you sure?");
if (confirmDelete) {
const success = await deleteFile(fileID);
if (success) {
await loadData();
}
}
};
return (
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center gap-4">
<h1 className="font-bold text-2xl">File Storage</h1>
<Button
onClick={async () => {
await loadData();
}}
>
Refresh
</Button>
</div>
<Input
className=""
onChange={(e) => {
setInputFile(e.currentTarget.files?.[0] || null);
}}
type="file"
/>
<Input
className=""
onChange={(e) => {
setInputExpiresAt(new Date(e.currentTarget.value).toISOString());
}}
type="datetime-local"
/>
<Button onClick={handleFileUpload}>Upload</Button>
{loading ? (
<LoadingPage />
) : (
<ul className="space-y-2">
{files.map((file) => (
<li
className="flex items-center justify-between gap-3 rounded-lg border p-3"
key={file.id}
>
{file.mimeType.includes("image") && (
<img
alt={file.originalName}
className="size-20"
src={`/storage/file/${file.id}`}
/>
)}
<div className="flex-1">
<p className="font-semibold">{file.originalName}</p>
<p className="text-gray-500 text-sm">
{file.mimeType} | Size: {(file.size / 1024 / 1024).toFixed(2)}{" "}
MB
</p>
<p>Uploaded At: {new Date(file.uploadedAt).toLocaleString()}</p>
{file.expiresAt && (
<p>Expires At: {new Date(file.expiresAt).toLocaleString()}</p>
)}
</div>
<div className="flex space-x-2">
<a
className="rounded bg-blue-500 px-3 py-1 text-sm text-white"
href={`/storage/file/${file.id}`}
>
Download
</a>
<button
className="rounded bg-red-500 px-3 py-1 text-sm text-white"
onClick={() => handleDelete(file.id)}
type="button"
>
Delete
</button>
</div>
</li>
))}
</ul>
)}
</div>
);
}

View file

@ -0,0 +1,93 @@
import z from "zod";
const FileMetadataSchema = z.object({
id: z.string(),
originalName: z.string(),
size: z.number(),
mimeType: z.string(),
blockCount: z.number(),
uploadedAt: z.string(),
expiresAt: z.string().nullable(),
});
export type FileMetadata = z.infer<typeof FileMetadataSchema>;
// Helper to fetch the list of all files
export async function fetchFiles(): Promise<FileMetadata[]> {
try {
const response = await fetch(`/storage/api/files`);
if (!response.ok) {
throw new Error(`Failed to fetch files: ${response.statusText}`);
}
const parse = FileMetadataSchema.array().safeParse(await response.json());
if (parse.success) {
return parse.data;
}
console.error("Failed to parse file metadata:", parse.error);
return [];
} catch (error) {
console.error("Error fetching file list:", error);
return [];
}
}
// Helper to handle file uploads
export async function uploadFile(
file: File,
expiresAt: string | undefined,
): Promise<boolean> {
const formData = new FormData();
formData.append("file", file);
if (expiresAt) {
formData.append("expires_at", expiresAt);
}
try {
const response = await fetch(`/storage/upload`, {
method: "POST",
body: formData,
});
// The Go server redirects on success (StatusSeeOther), which may cause CORS issues
// or be difficult to read in a browser environment.
// We'll treat any successful status (2xx or 3xx) as a success for now.
if (response.ok || (response.status >= 300 && response.status < 400)) {
console.log("File uploaded successfully.");
return true;
}
throw new Error(`Upload failed with status: ${response.status}`);
} catch (error) {
console.error("Error during file upload:", error);
return false;
}
}
// Helper to delete a file
export async function deleteFile(fileID: string): Promise<boolean> {
const formData = new URLSearchParams();
formData.append("id", fileID);
try {
// The Go server accepts POST with form data for delete
const response = await fetch(`/storage/delete`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
});
// Treat redirect as success
if (response.ok || (response.status >= 300 && response.status < 400)) {
console.log(`File ${fileID} deleted successfully.`);
return true;
}
throw new Error(`Delete failed with status: ${response.status}`);
} catch (error) {
console.error("Error during file deletion:", error);
return false;
}
}