test new storage
This commit is contained in:
parent
eabe7f36c1
commit
6a5820dda1
3 changed files with 227 additions and 1 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
93
apps/infoalloggi/src/server/storage.ts
Normal file
93
apps/infoalloggi/src/server/storage.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue