new storage for files

This commit is contained in:
Marco Pedone 2025-10-24 16:10:59 +02:00
parent f7003fc6b9
commit 6acfc08169
31 changed files with 1620 additions and 1958 deletions

View file

@ -13,6 +13,7 @@ TODOS:
- Modale conferma mostrare scorciatoia a doc lavoro o simile per facilitare
- Post modale conferma, mostrare se transitorio , modale con informativa tipo:" il saldo del servizio e il periodo di permanenza veranno adeguati alle condizioni stabilite insieme al locatore nel momento della trattativa e alla documentazione presentata come giustificazione di transitorietà" (modificato)
- border radius immagini carousel annuncio
NEW IDEA TODOS:

View file

@ -2,8 +2,7 @@ import dynamic from "next/dynamic";
import { useEffect, useRef, useState } from "react";
import { LoadingPage } from "~/components/loading";
import { cn } from "~/lib/utils";
import type { TempTokensToken } from "~/schemas/public/TempTokens";
import { api } from "~/utils/api";
import type { FileMetadata } from "~/server/services/storage.service";
const PDFViewer = dynamic(
() => import("./pdf-viewer").then((mod) => mod.PDFViewer),
@ -17,11 +16,10 @@ export const AllegatoIframe = ({
allegato,
}: {
className?: string;
allegato: string;
allegato: FileMetadata;
}) => {
const if_ref = useRef<HTMLIFrameElement>(null);
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const [token, setToken] = useState<TempTokensToken | null>(null);
const [isMobile, setIsMobile] = useState(false);
const [isIOS, setIsIOS] = useState(false);
const [isPDF, setIsPDF] = useState(false);
@ -37,7 +35,7 @@ export const AllegatoIframe = ({
setIsIOS(/iphone|ipad|ipod/i.test(userAgent));
// Try to detect if file is PDF from extension
setIsPDF(allegato.toLowerCase().endsWith(".pdf"));
setIsPDF(allegato.mimeType === "application/pdf");
}, [allegato]);
const handleIframeLoad = () => {
@ -52,17 +50,7 @@ export const AllegatoIframe = ({
}
};
useEffect(() => {
async function set() {
const token = await getToken();
setToken(token);
}
set().catch((e) => console.error(e));
}, []);
if (!token) return <LoadingPage />;
const requestUrl = `/go-api/storage/get/${allegato}?token=${String(token)}`;
const requestUrl = `/storage-api/get/${allegato.id}?mode=inline`;
// For PDF on iOS, provide download link instead
if (isPDF && (isIOS || isMobile)) {
return (

View file

@ -36,14 +36,11 @@ import {
} from "~/components/ui/dropdown-menu";
import { Separator } from "~/components/ui/separator";
import { UploadModal } from "~/components/upload_modal";
import { handleDownload } from "~/hooks/filesHooks";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { UsersId } from "~/schemas/public/Users";
import type { FileMetadata } from "~/server/services/storage.service";
import { api } from "~/utils/api";
export const AllegatiComp = ({
@ -55,27 +52,27 @@ export const AllegatiComp = ({
}) => {
const { t } = useTranslation();
const { data: allegati, isLoading: isLoadingAllegati } =
api.storage.getUserStorage.useQuery({ userId: userId });
api.storage.retrieveUserFileData.useQuery({ userId });
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { mutate: deleteFile } = api.storage.deleteUserStorage.useMutation({
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
const { mutate: deleteFile } = api.storage.deleteUserStorageEntry.useMutation(
{
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.retrieveUserFileData.invalidate({
userId: userId,
});
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
id: context?.toastId,
});
},
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate({
userId: userId,
});
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
id: context?.toastId,
});
},
});
);
const { mutate: renameFile } = api.storage.renameFile.useMutation({
onMutate: () => {
@ -86,7 +83,7 @@ export const AllegatiComp = ({
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate({
await utils.storage.retrieveUserFileData.invalidate({
userId: userId,
});
toast.success(t.successo, {
@ -96,7 +93,7 @@ export const AllegatiComp = ({
},
});
const [editData, setEditData] = useState<{
id: StorageindexId;
id: string;
filename: string;
} | null>(null);
const [editOpen, setEditOpen] = useState(false);
@ -109,7 +106,7 @@ export const AllegatiComp = ({
return;
}
renameFile({
fileId: formData.get("id") as StorageindexId,
fileId: formData.get("id") as string,
name: formData.get("filename") as string,
});
};
@ -128,13 +125,7 @@ export const AllegatiComp = ({
<h1 className="font-semibold text-xl">{t.allegati.da_info}</h1>
{from_admin && (
<FileList
delete_onClick={(id) => deleteFile({ fileId: id, userId })}
download_onClick={(file) =>
handleDownload({
file,
getToken: async () => await getToken(),
})
}
delete_onClick={(id) => deleteFile({ storageId: id, userId })}
files={from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
@ -158,13 +149,7 @@ export const AllegatiComp = ({
{not_from_admin && (
<FileList
delete_onClick={(id) => deleteFile({ fileId: id, userId })}
download_onClick={(file) =>
handleDownload({
file,
getToken: async () => await getToken(),
})
}
delete_onClick={(id) => deleteFile({ storageId: id, userId })}
files={not_from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
@ -184,45 +169,56 @@ export const AllegatiComp = ({
};
export const ExtIcon = ({
ext,
mimeType,
className,
}: {
ext: string | null | undefined;
mimeType: string;
className?: string;
}) => {
if (!ext) return <File className={cn("size-5 text-gray-500", className)} />;
if (ext.includes("pdf")) {
const lower = mimeType?.toLowerCase() || "";
if (!lower) return <File className={cn("size-5 text-gray-500", className)} />;
// PDF
if (lower.includes("pdf")) {
return <FileText className={cn("size-5 text-red-500", className)} />;
}
// Images
if (
ext.includes("image") ||
["jpg", "jpeg", "png", "gif", "svg", "webp"].some((ext) =>
ext.includes(ext),
)
lower.startsWith("image/") ||
/\b(jpg|jpeg|png|gif|svg|webp)\b/.test(lower)
) {
return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
}
if (
["html", "css", "js", "jsx", "ts", "tsx", "json", "xml"].some((ext) =>
ext.includes(ext),
)
) {
// Code files
if (/\b(html|css|js|jsx|ts|tsx|json|xml)\b/.test(lower)) {
return <FileCode className={cn("size-5 text-emerald-500", className)} />;
}
if (["xls", "xlsx", "csv"].some((ext) => ext.includes(ext))) {
// Spreadsheets
if (/\b(xls|xlsx|csv)\b/.test(lower)) {
return (
<FileSpreadsheet className={cn("size-5 text-green-600", className)} />
);
}
if (["zip", "rar", "7z", "tar", "gz"].some((ext) => ext.includes(ext))) {
// Archives
if (/\b(zip|rar|7z|tar|gz)\b/.test(lower)) {
return <FileArchive className={cn("size-5 text-amber-600", className)} />;
}
if (["mp3", "wav", "ogg", "flac"].some((ext) => ext.includes(ext))) {
// Audio
if (/\b(mp3|wav|ogg|flac)\b/.test(lower)) {
return <FileAudio className={cn("size-5 text-purple-500", className)} />;
}
if (["mp4", "avi", "mov", "wmv", "webm"].some((ext) => ext.includes(ext))) {
// Video
if (/\b(mp4|avi|mov|wmv|webm)\b/.test(lower)) {
return <FileVideo className={cn("size-5 text-pink-500", className)} />;
}
return <File className={cn("size-5 text-gray-500", className)} />;
};
@ -235,7 +231,7 @@ const EditFileDialog = ({
open: boolean;
onOpenChange: (status: boolean) => void;
data: {
id: StorageindexId;
id: string;
filename: string;
} | null;
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
@ -282,17 +278,15 @@ const EditFileDialog = ({
};
interface FileListProps {
files: Storageindex[];
delete_onClick: (id: StorageindexId) => void;
download_onClick: (file: Storageindex) => void;
files: FileMetadata[];
delete_onClick: (id: string) => void;
isAdmin: boolean;
selectHandler: (id: StorageindexId, filename: string) => void;
selectHandler: (id: string, filename: string) => void;
}
function FileList({
files,
delete_onClick,
download_onClick,
isAdmin,
selectHandler,
}: FileListProps) {
@ -312,7 +306,7 @@ function FileList({
>
<div className="col-span-6 flex items-center gap-2 md:col-span-8">
<div className="shrink-0">
<ExtIcon ext={file.ext} />
<ExtIcon mimeType={file.mimeType} />
</div>
<Link
aria-label="Allegato"
@ -320,30 +314,23 @@ function FileList({
href={`/area-riservata/allegato-view/${file.id}`}
target="_blank"
>
{file.filename}
{file.originalName}
</Link>
</div>
<div className="col-span-4 text-muted-foreground md:col-span-3">
{format(file.created_at, "d MMM yyyy")}
{format(new Date(file.uploadedAt), "d MMM yyyy")}
</div>
<div className="col-span-2 flex justify-end gap-1 md:col-span-1">
<Button
aria-label="Download"
asChild
className="size-8"
size="icon"
title="Download"
variant="ghost"
>
<Link href={`/storage-api/get/${file.id}?mode=download`}>
<Button
className="flex gap-1 text-sm"
onClick={() => download_onClick(file)}
className="flex size-8 gap-1 text-sm"
size="icon"
variant="outline"
>
<Download className="size-4" />
</Button>
</Button>
</Link>
{isAdmin && (
<DropdownMenu>
@ -357,7 +344,7 @@ function FileList({
<DropdownMenuItem
aria-label="Rinomina"
onClick={() =>
selectHandler(file.id, file.filename || "")
selectHandler(file.id, file.originalName || "")
}
>
<Pencil className="mr-2 size-4" />

View file

@ -36,7 +36,7 @@ export const ChatAttachments = ({
data: attachments,
isLoading: attachmentsLoading,
refetch,
} = api.storage.getUserStorage.useQuery(
} = api.storage.retrieveUserFileData.useQuery(
{ userId: chatUserData.id },
{ refetchOnMount: true },
);
@ -58,20 +58,20 @@ export const ChatAttachments = ({
<Paperclip className="text-muted-foreground" size={20} />
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaContent className="max-h-[90vh] md:max-w-xl">
<CredenzaHeader>
<CredenzaTitle className="sr-only">Allegati</CredenzaTitle>
<CredenzaDescription className="sr-only">
Chat Attachments
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] space-y-3 overflow-auto pb-5">
<CredenzaBody className="max-h-[80vh] space-y-5 overflow-auto pb-5 md:px-1">
<UploadComponent
isAdmin={userData.isAdmin}
userId={chatUserData.id}
/>
<div className="h-auto max-h-[40rem] max-w-lg overflow-auto rounded-lg border-2 border-gray-300 py-4">
<div className="h-auto max-h-[40rem] max-w-lg overflow-auto rounded-lg">
{attachmentsLoading ? (
<LoadingPage />
) : (
@ -84,7 +84,7 @@ export const ChatAttachments = ({
</div>
) : (
<ul className="px-2">
<h3 className="px-3 font-semibold text-gray-700 text-lg">
<h3 className="font-semibold text-gray-700 text-lg">
{t.allegati.allegati_recenti}
</h3>
{attachments?.map((file, idx) => {
@ -92,28 +92,30 @@ export const ChatAttachments = ({
return (
<li
className="flex items-center justify-between border-b px-3 py-1"
key={`${file.filename}-${idx}`}
className="flex flex-row items-center justify-between border-b py-1"
key={`${file.originalName}-${idx}`}
>
<Link
aria-label="Visualizza Allegato"
className={cn(
buttonVariants({
variant: "link",
}),
"flex h-8 items-center gap-2 truncate p-0 text-base",
)}
href={`/area-riservata/allegato-view/${file.id}`}
onTouchStart={() => console.log("touching")}
target="_blank"
>
{file.ext && <ExtIcon ext={file.ext} />}
<span className="truncate">{file.filename}</span>
<Button
className="flex h-8 items-center gap-2 truncate p-0 text-base has-[>svg]:px-0"
variant="link"
>
<ExtIcon mimeType={file.mimeType} />
<span className="max-w-[60vw] truncate">
{file.originalName}
</span>
</Button>
</Link>
<span
className="text-muted-foreground text-xs"
key={new Date().toString()}
>
{TimeSince(file.created_at)}
{TimeSince(new Date(file.uploadedAt))}
</span>
</li>
);
@ -123,8 +125,6 @@ export const ChatAttachments = ({
</>
)}
</div>
</CredenzaBody>
<CredenzaFooter className="flex flex-row items-center justify-between gap-2">
<Link
aria-label="Visualizza Allegati"
className={cn(
@ -142,6 +142,8 @@ export const ChatAttachments = ({
</span>
)}
</Link>
</CredenzaBody>
<CredenzaFooter className="flex flex-col items-center justify-between gap-2">
<CredenzaClose asChild>
<Button className="w-full">{t.chiudi}</Button>
</CredenzaClose>

File diff suppressed because it is too large Load diff

View file

@ -63,11 +63,8 @@ import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
import { api } from "~/utils/api";
export const AnnuncioActions = () => {
@ -495,7 +492,7 @@ const ContrattoSection = ({
}) => {
const { userId } = useServizio();
const { data } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>(
const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_contratto_ref,
);
@ -505,7 +502,7 @@ const ContrattoSection = ({
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
@ -519,7 +516,7 @@ const ContrattoSection = ({
});
if (selectedDoc) {
setUserStorage({
fileId: selectedDoc,
storageId: selectedDoc,
userId,
});
}
@ -587,7 +584,7 @@ const RicevutaSection = ({
}) => {
const { userId } = useServizio();
const { data } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>(
const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_registrazione_ref,
);
@ -597,7 +594,7 @@ const RicevutaSection = ({
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
@ -611,7 +608,7 @@ const RicevutaSection = ({
});
if (selectedDoc) {
setUserStorage({
fileId: selectedDoc,
storageId: selectedDoc,
userId,
});
}
@ -762,7 +759,7 @@ const DocSection = ({
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
@ -812,7 +809,7 @@ const DocSection = ({
});
setUserStorage({
fileId: f.id,
storageId: f.id,
userId,
});
}}
@ -828,10 +825,10 @@ const DocInfo = ({
storageId,
children,
}: {
storageId: StorageindexId;
storageId: string;
children: React.ReactNode;
}) => {
const { data: file_info } = api.storage.getStorageFromId.useQuery({
const { data: file_info } = api.storage.getFileInfos.useQuery({
storageId,
});
if (!file_info) return null;
@ -852,9 +849,9 @@ const DocInfo = ({
id="selected-file"
target="_blank"
>
{file_info.ext && <ExtIcon ext={file_info.ext} />}
<ExtIcon mimeType={file_info.mimeType} />
<span className="max-w-40 truncate sm:max-w-80">
{file_info.filename}
{file_info.originalName}
</span>
<ExternalLink className="size-4" />
</Link>
@ -870,9 +867,9 @@ const DocCombo = ({
onSelect,
current,
}: {
files: Storageindex[];
onSelect: (file: Storageindex) => void;
current: StorageindexId | null;
files: FileMetadataWithAdmin[];
onSelect: (file: FileMetadataWithAdmin) => void;
current: string | null;
}) => {
const [open, setOpen] = useState(false);
@ -909,8 +906,10 @@ const DocCombo = ({
value={file.id}
>
<span className="flex items-center gap-2">
{file.ext && <ExtIcon ext={file.ext} />}
<span className="block font-medium">{file.filename}</span>
<ExtIcon mimeType={file.mimeType} />
<span className="block font-medium">
{file.originalName}
</span>
{current === file.id && (
<Check className="size-4 text-accent-foreground" />
)}

View file

@ -1,38 +1,29 @@
import Link from "next/link";
import { AllegatoIframe } from "~/components/allegato-iframe";
import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import { handleDownload } from "~/hooks/filesHooks";
import { useTranslation } from "~/providers/I18nProvider";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { api } from "~/utils/api";
export const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
export const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId,
});
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { t } = useTranslation();
if (isLoading) return <LoadingPage />;
if (!fileInfos) return <p>{t.file_section.error}</p>;
return (
<>
<AllegatoIframe
allegato={fileInfos.id + fileInfos.ext}
allegato={fileInfos}
className="h-full max-h-[70vh] min-h-[60vh]"
/>
<Button
<Link
className="underline underline-offset-1 after:content-['_↗']"
onClick={() =>
handleDownload({
file: fileInfos,
getToken: async () => await getToken(),
})
}
variant="link"
href={`/storage-api/get/${fileInfos.id}?mode=download`}
>
{t.file_section.download}
</Button>
</Link>
</>
);
};

View file

@ -38,28 +38,27 @@ import { Input } from "~/components/ui/input";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useStorageTable } from "~/providers/StorageTableProvider";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import {
editFile,
type FileMetadataWithAdmin,
} from "~/server/services/storage.service";
import { api } from "~/utils/api";
export const StorageTable = ({
data,
handleDelete,
}: {
data: Storageindex[];
data: FileMetadataWithAdmin[];
handleDelete: (id: StorageindexId) => void;
handleDelete: (id: string) => void;
}) => {
const { setTable, cb_onStateChange } = useStorageTable();
const [deleteConfirmId, setDeleteConfirmId] = useState<StorageindexId | null>(
null,
);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const [renameTarget, setRenameTarget] = useState<{
allegatoId: StorageindexId;
allegatoId: string;
filename: string | null;
} | null>(null);
@ -74,23 +73,13 @@ export const StorageTable = ({
}
}, [tableInstance, setTable]);
const tabledata = data.map((row) => {
return {
actions: "Azioni",
created_at: row.created_at,
expires_at: row.expires_at,
ext: row.ext,
filename: row.filename,
from_admin: row.from_admin,
id: row.id,
};
});
const tabledata = data;
const columns_titles = {
actions: "Azioni",
created_at: "Data creazione",
ext: "Estensione",
filename: "Nome file",
uploadedAt: "Data creazione",
type: "Tipo file",
originalName: "Nome file",
from_admin: "Caricato da",
id: "Codice",
};
@ -120,7 +109,7 @@ export const StorageTable = ({
id: "select",
},
{
accessorKey: "filename",
accessorKey: "originalName",
cell: ({ row }) => {
return (
<Link
@ -129,7 +118,7 @@ export const StorageTable = ({
href={`/area-riservata/allegato-view/${row.original.id}`}
target="_blank"
>
{row.original.filename}
{row.original.originalName}
</Link>
);
},
@ -137,14 +126,14 @@ export const StorageTable = ({
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.filename}
title={columns_titles.originalName}
/>
),
},
{
accessorKey: "created_at",
accessorKey: "uploadedAt",
cell: ({ row }) => {
const date = row.getValue("created_at");
const date = row.getValue("uploadedAt");
return new Date(date as Date).toLocaleDateString();
},
@ -152,7 +141,7 @@ export const StorageTable = ({
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.created_at}
title={columns_titles.uploadedAt}
/>
),
},
@ -203,7 +192,7 @@ export const StorageTable = ({
onClick={() =>
setRenameTarget({
allegatoId: data.id,
filename: data.filename,
filename: data.originalName,
})
}
role="button"
@ -231,7 +220,7 @@ export const StorageTable = ({
];
const searchFiltro: SearchFiltro = {
columnName: "filename",
columnName: "originalName",
placeholder: "Cerca file ...",
};
return (
@ -240,7 +229,7 @@ export const StorageTable = ({
columns={columns}
columns_titles={columns_titles}
data={tabledata}
defaultSort={[{ desc: true, id: "created_at" }]}
defaultSort={[{ desc: true, id: "uploadedAt" }]}
onStateChange={cb_onStateChange}
onTableInit={setTableInstance}
pinnedFiltri={undefined}
@ -289,42 +278,32 @@ const RinominaDialog = ({
renameTarget,
setRenameTarget,
}: {
renameTarget: { allegatoId: StorageindexId; filename: string | null };
renameTarget: { allegatoId: string; filename: string | null };
setRenameTarget: (
target: { allegatoId: StorageindexId; filename: string | null } | null,
target: { allegatoId: string; filename: string | null } | null,
) => void;
}) => {
const { t } = useTranslation();
const utils = api.useUtils();
const { mutate: renameFile } = api.storage.renameFile.useMutation({
onMutate: () => {
setRenameTarget(null);
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getAll.invalidate();
toast.success(t.successo, {
icon: "👍",
id: context?.toastId,
});
},
});
const handleEditSubmit = (e: FormEvent<HTMLFormElement>) => {
const handleEditSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
if (!formData.get("filename")) {
return;
}
renameFile({
fileId: renameTarget.allegatoId,
name: formData.get("filename") as string,
});
const rename = await editFile(
renameTarget.allegatoId,
formData.get("filename") as string,
);
if (rename.success) {
setRenameTarget(null);
toast.success("File rinominato con successo", { icon: "👍" });
await utils.storage.getAll.invalidate();
} else {
toast.error(rename.error);
}
};
return (
<Dialog

View file

@ -31,7 +31,7 @@ const buttonVariants = cva(
grey: "bg-accent/30 text-accent-foreground shadow-sm hover:bg-accent/80 dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white",
info: "bg-sky-500 text-white hover:bg-sky-500/90 dark:bg-sky-900 dark:text-white dark:hover:bg-sky-900/90",
link: "text-primary underline-offset-4 hover:underline",
link: "text-primary underline-offset-4 hover:underline active:underline",
orange:
"bg-orange-500 text-white hover:bg-orange-500/90 dark:bg-orange-900 dark:text-white dark:hover:bg-orange-900/90",
outline:

View file

@ -22,9 +22,8 @@ import {
FileUploadTrigger,
} from "~/components/custom_ui/fileUpload";
import { Button } from "~/components/ui/button";
import { Upload } from "~/lib/storage_manager";
import { uploadHandler } from "~/hooks/storageClienSideHooks";
import { useTranslation } from "~/providers/I18nProvider";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
@ -88,19 +87,18 @@ export const UploadComponent = ({
const [files, setFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
const { mutateAsync: getToken } = api.storage.getStorageTokenM.useMutation();
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onMutate: () => {
toast(t.allegati.allegato_caricato, { icon: "📤" });
},
onSettled: async () => {
await utils.storage.getAll.invalidate();
await utils.storage.getUserStorage.invalidate({
userId,
});
},
});
const { mutateAsync: setUserStorage } =
api.storage.addUserStorage.useMutation({
onMutate: () => {
toast(t.allegati.allegato_caricato, { icon: "📤" });
},
onSettled: async () => {
await utils.storage.getAll.invalidate();
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
},
});
const acceptedMimeTypes = [
"image/jpeg", // jpg, jpeg
@ -159,19 +157,24 @@ export const UploadComponent = ({
onProgress: (file: File, progress: number) => void;
},
) => {
if (files.length === 0) {
return;
}
try {
const from_admin = isAdmin ?? true;
setIsUploading(true);
const { status, attachments } = await Upload({
files: files,
from_admin: isAdmin ?? true,
getToken,
const { status, attachments } = await uploadHandler({
files,
from_admin,
});
if (userId) {
for (const storageId of attachments) {
setUserStorage({
fileId: storageId as StorageindexId,
await setUserStorage({
storageId,
userId,
from_admin,
});
}
}

View file

@ -28,6 +28,7 @@ export const env = createEnv({
SKEBBY_USER: z.string(),
SKEBBY_PASS: z.string(),
REVALIDATION_SECRET: z.string(),
STORAGE_URL: z.string(),
STORAGE_TOKEN: z.string(),
},
client: {

View file

@ -1,34 +0,0 @@
import type { Storageindex } from "~/schemas/public/Storageindex";
import type { TempTokensToken } from "~/schemas/public/TempTokens";
export const handleDownload = async ({
file,
getToken,
}: {
file: Storageindex;
getToken: () => Promise<TempTokensToken | TempTokensToken[]>;
}) => {
const auth_token = (await getToken()) as TempTokensToken;
try {
const response = await fetch(
`/go-api/storage/get/${file.id}${file.ext}?token=${auth_token}`,
);
if (!response.ok) {
throw new Error("File fetch failed");
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
// biome-ignore lint/style/noNonNullAssertion: <known to be non-null>
a.download = file.filename!;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch {
throw new Error("File download failed");
}
};

View file

@ -0,0 +1,83 @@
import { add } from "date-fns";
import z from "zod";
type UploadResponse =
| {
success: true;
id: string;
}
| {
success: false;
error: string;
};
// Helper to handle file uploads
export async function uploadFile(
file: File,
expiresAt: string | undefined,
): Promise<UploadResponse> {
const formData = new FormData();
formData.append("file", file);
if (expiresAt) {
formData.append("expires_at", expiresAt);
}
try {
const response = await fetch(`/storage-api/upload`, {
method: "POST",
body: formData,
});
if (response.ok) {
const res = await response.json();
const parsed = z
.object({
id: z.string(),
})
.parse(res);
const fileID = parsed.id;
console.log("File uploaded successfully.");
return { success: true, id: fileID };
}
return {
success: false,
error: `Caricamento fallito con status: ${response.status}`,
};
} catch (e) {
return {
success: false,
error: `Errore caricamento file: ${(e as Error).message}`,
};
}
}
export const uploadHandler = async ({
files,
from_admin,
}: {
files: File[];
from_admin: boolean;
}) => {
const attachments = [];
const failed: string[] = [];
const expirationDate = add(new Date(), { days: 60 }).toISOString();
if (files.length === 0) {
console.error("No files provided");
return { attachments: [], error: "No files provided", status: false };
}
for (const file of files) {
const upload = await uploadFile(
file,
from_admin ? expirationDate : undefined,
);
if (upload.success) {
attachments.push(upload.id);
} else {
console.error(`Error uploading file ${file.name}: ${upload.error}`);
failed.push(file.name);
}
}
return { attachments, failed, status: true };
};

View file

@ -1,96 +0,0 @@
import type { TempTokensToken } from "~/schemas/public/TempTokens";
export const Upload = async ({
files,
getToken,
from_admin,
}: {
files: File[];
getToken: ({ num }: { num: number }) => Promise<TempTokensToken[]>;
from_admin: boolean;
}) => {
const attachments = [];
if (files.length === 0) {
console.error("No files provided");
return { attachments: [], error: "No files provided", status: false };
}
if (files.length > 1) {
const authTokens = await getToken({
num: files.length,
});
if (authTokens.length !== files.length) {
console.error(
"Error: number of tokens does not match number of files",
authTokens.length,
files.length,
);
return {
attachments: [],
error: "Error: number of tokens does not match number of files",
status: false,
};
}
for (const file of files) {
const attach = await uploadFile({
file,
from_admin,
// biome-ignore lint/style/noNonNullAssertion: <intended>
token: authTokens.pop()!,
});
attachments.push(attach);
}
} else {
const authTokens = (
await getToken({
num: 1,
})
)[0];
if (!authTokens) {
console.error("Error: no token received");
return {
attachments: [],
error: "Error: no token received",
status: false,
};
}
const attach = await uploadFile({
// biome-ignore lint/style/noNonNullAssertion: <intended>
file: files[0]!,
from_admin,
token: authTokens,
});
attachments.push(attach);
}
return { attachments, status: true };
};
const uploadFile = async ({
file,
token,
from_admin,
}: {
file: File;
token: TempTokensToken;
from_admin: boolean;
}) => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(
`/go-api/storage/upload?token=${token}&from_admin=${from_admin}`,
{
body: formData,
method: "POST",
},
);
if (!response.ok) {
throw new Error(
`Error uploading file: ${response.status} ${response.statusText}`,
);
}
return response.text();
};

View file

@ -38,10 +38,7 @@ import {
type StorageTable as StorageTableType,
useStorageTable,
} from "~/providers/StorageTableProvider";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { Users } from "~/schemas/public/Users";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { api } from "~/utils/api";
@ -50,7 +47,7 @@ const AdminStorage: NextPageWithLayout = () => {
const { data, isLoading, refetch } = api.storage.getAll.useQuery();
const { t } = useTranslation();
const utils = api.useUtils();
const { mutate: deleteFile } = api.storage.deleteStorage.useMutation({
const { mutate: deleteFiles } = api.storage.deleteFile.useMutation({
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
@ -67,8 +64,8 @@ const AdminStorage: NextPageWithLayout = () => {
},
});
const handleDelete = (id: StorageindexId) => {
deleteFile({ storageId: id });
const handleDelete = (id: string) => {
deleteFiles({ storageId: id });
};
const handleRefetch = async () => {
@ -117,7 +114,7 @@ const StorageTableHeader = ({
handleDelete,
}: {
handleRefetch: () => Promise<void>;
handleDelete: (id: StorageindexId) => void;
handleDelete: (id: string) => void;
}) => {
const { table } = useStorageTable();
if (!table) return null;
@ -158,7 +155,7 @@ const Actions = ({
handleDelete,
table,
}: {
handleDelete: (id: StorageindexId) => void;
handleDelete: (id: string) => void;
table: StorageTableType;
}) => {
const { selectedRows } = useStorageTable();
@ -190,9 +187,9 @@ const Actions = ({
</Button>
{visbOpen && (
<VisibComponent
allegati={selectedRows}
open={visbOpen}
setOpen={setVisbOpen}
storageIds={selectedRows.map((r) => r.id)}
/>
)}
</div>
@ -200,21 +197,21 @@ const Actions = ({
};
const VisibComponent = ({
allegati,
storageIds,
open,
setOpen,
}: {
allegati: Storageindex[];
storageIds: string[];
open: boolean;
setOpen: (v: boolean) => void;
}) => {
const utils = api.useUtils();
const { data, isLoading } = api.storage.getStorageVisibility.useQuery({
storageIds: allegati.map((a) => a.id),
storageIds,
});
const { data: users } = api.users.getUsers.useQuery();
const { mutate: remove } = api.storage.removeStorageVisibility.useMutation({
const { mutate: remove } = api.storage.deleteUserStorageEntry.useMutation({
onMutate: () => {
const toastId = toast.loading("Rimozione utente in corso", {
icon: "🪛",
@ -229,7 +226,7 @@ const VisibComponent = ({
});
},
});
const { mutate: add } = api.storage.addStorageVisibility.useMutation({
const { mutate: add } = api.storage.addUserStorage.useMutation({
onMutate: () => {
const toastId = toast.loading("Aggiunta utente in corso", {
icon: "🪛",
@ -245,7 +242,7 @@ const VisibComponent = ({
},
});
const { mutate: reset } = api.storage.resetStorageVisibility.useMutation({
const { mutate: reset } = api.storage.deleteFilesFromUserStorage.useMutation({
onMutate: () => {
const toastId = toast.loading("Reset visibilità in corso", {
icon: "🪛",
@ -280,17 +277,19 @@ const VisibComponent = ({
<p>Seleziona utente:</p>
<UtentiCombo
onSelect={(user) => {
for (const allegato of allegati) {
add({ storageId: allegato.id, userId: user.id });
for (const allegato of storageIds) {
add({
storageId: allegato,
userId: user.id,
from_admin: true,
});
}
}}
users={users || []}
/>
<Button
className="ml-auto"
onClick={() =>
reset({ storageId: allegati.map((a) => a.id) })
}
onClick={() => reset({ storageIds })}
variant="destructive"
>
Reset
@ -305,22 +304,22 @@ const VisibComponent = ({
key={allegato.id}
>
<p className="font-semibold text-lg">
File: {allegato.filename}
File: {allegato.originalName}
</p>
<div className="flex items-center gap-3">
<p>Visibile a:</p>
{allegato.users_storages.length === 0 ? (
{allegato.users.length === 0 ? (
<p>Nessuno</p>
) : (
<ul className="flex gap-2">
{allegato.users_storages.map((user) => (
<li key={user.userid}>
{allegato.users.map((user) => (
<li key={user.id}>
<Button
className="after:content-['X']"
onClick={() =>
remove({
storageId: allegato.id,
userId: user.userid,
userId: user.id,
})
}
variant="outline"

View file

@ -40,7 +40,7 @@ export const getServerSideProps = (async (context) => {
const helpers = await TrpcAuthedFetchingIstance({ access_token });
if (helpers) {
await helpers.trpc.storage.getUserStorage.prefetch({
await helpers.trpc.storage.retrieveUserFileData.prefetch({
userId: parsed.data,
});
return {

View file

@ -1,9 +1,11 @@
import { Download, Pen, Trash } from "lucide-react";
import type { GetServerSideProps } from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type FormEvent, useState } from "react";
import toast from "react-hot-toast";
import z from "zod";
import { AllegatoIframe } from "~/components/allegato-iframe";
import { Confirm } from "~/components/confirm";
import { DocNotFoundPage } from "~/components/doc_not_found";
@ -19,17 +21,14 @@ import {
DialogTrigger,
} from "~/components/ui/dialog";
import { Input } from "~/components/ui/input";
import { handleDownload } from "~/hooks/filesHooks";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import { useEnforcedSession } from "~/providers/SessionProvider";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zStorageIndexId } from "~/server/utils/zod_types";
import { api } from "~/utils/api";
type AllegatoViewProps = {
allegatoId: StorageindexId;
allegatoId: string;
};
const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
@ -37,13 +36,12 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
}: AllegatoViewProps) => {
const { t } = useTranslation();
const session = useEnforcedSession();
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId: allegatoId,
});
const utils = api.useUtils();
const router = useRouter();
const { mutate: deleteFile } = api.storage.deleteStorage.useMutation({
const { mutate: deleteFile } = api.storage.deleteFile.useMutation({
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🪛",
@ -51,7 +49,7 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate();
await utils.storage.retrieveUserFileData.invalidate();
toast.success(t.allegati.allegato_rimosso, {
icon: "🗑️",
@ -69,29 +67,25 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
return (
<>
<Head>
<title>Infoalloggi.it | {fileInfos.filename}</title>
<title>Infoalloggi.it | {fileInfos.originalName}</title>
</Head>
<div className="flex h-full w-full grow flex-col md:flex-row">
<div className="flex grow flex-col justify-center space-y-2 p-4">
<div className="mx-auto flex flex-wrap items-center gap-4">
<span>{fileInfos.filename}</span>
<span>caricato il {fileInfos.created_at.toLocaleString("it")}</span>
<Button
className="flex gap-2"
onClick={() =>
handleDownload({
file: fileInfos,
getToken: async () => await getToken(),
})
}
>
<Download size={16} />
Download
</Button>
<span>{fileInfos.originalName}</span>
<span>
caricato il {new Date(fileInfos.uploadedAt).toLocaleString("it")}
</span>
<Link href={`/storage-api/get/${fileInfos.id}?mode=download`}>
<Button className="flex gap-2">
<Download size={16} />
Download
</Button>
</Link>
<RinominaDialog
allegatoId={allegatoId}
filename={fileInfos.filename}
filename={fileInfos.originalName}
/>
{session.user.isAdmin && (
<Confirm
@ -106,7 +100,7 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
</Confirm>
)}
</div>
<AllegatoIframe allegato={fileInfos.id + fileInfos.ext} />
<AllegatoIframe allegato={fileInfos} />
</div>
</div>
</>
@ -125,7 +119,7 @@ export const getServerSideProps = (async (context) => {
};
}
const parsedStorageId = zStorageIndexId.safeParse(allegatoId);
const parsedStorageId = z.string().safeParse(allegatoId);
if (!parsedStorageId.success) {
return {
@ -137,7 +131,7 @@ export const getServerSideProps = (async (context) => {
const helper = await TrpcAuthedFetchingIstance({ access_token });
if (helper) {
await helper.trpc.storage.getStorageFromId.prefetch({
await helper.trpc.storage.getFileInfos.prefetch({
storageId: parsedStorageId.data,
});
@ -162,7 +156,7 @@ const RinominaDialog = ({
allegatoId,
filename,
}: {
allegatoId: StorageindexId;
allegatoId: string;
filename: string | null;
}) => {
const { t } = useTranslation();
@ -178,7 +172,7 @@ const RinominaDialog = ({
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getStorageFromId.invalidate({
await utils.storage.getFileInfos.invalidate({
storageId: allegatoId,
});
toast.success(t.successo, {

View file

@ -91,7 +91,7 @@ const ConfirmSection = ({
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.getUserStorage.invalidate();
await utils.storage.retrieveUserFileData.invalidate();
toast.success("Confermato", {
icon: "👍",

View file

@ -1,16 +1,14 @@
import type { GetServerSideProps } from "next";
import Link from "next/link";
import { AllegatoIframe } from "~/components/allegato-iframe";
import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
import { handleDownload } from "~/hooks/filesHooks";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
import { api } from "~/utils/api";
@ -49,33 +47,23 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
);
};
const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId,
});
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { t } = useTranslation();
if (isLoading) return <LoadingPage />;
if (!fileInfos) return <p>{t.file_section.error}</p>;
return (
<>
<AllegatoIframe
allegato={fileInfos.id + fileInfos.ext}
className="h-full max-h-[70vh]"
/>
<AllegatoIframe allegato={fileInfos} className="h-full max-h-[70vh]" />
<Button
<Link
className="underline underline-offset-1 after:content-['_↗']"
onClick={() =>
handleDownload({
file: fileInfos,
getToken: async () => await getToken(),
})
}
variant="link"
href={`/storage-api/get/${fileInfos.id}?mode=download`}
>
{t.file_section.download}
</Button>
</Link>
</>
);
};

View file

@ -1,16 +1,14 @@
import type { GetServerSideProps } from "next";
import Link from "next/link";
import { AllegatoIframe } from "~/components/allegato-iframe";
import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
import { handleDownload } from "~/hooks/filesHooks";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
import { api } from "~/utils/api";
@ -45,33 +43,23 @@ const ContrattoViewer: NextPageWithLayout<ContrattoViewerProps> = ({
);
};
const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
const { data: fileInfos, isLoading } = api.storage.getStorageFromId.useQuery({
const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId,
});
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
const { t } = useTranslation();
if (isLoading) return <LoadingPage />;
if (!fileInfos) return <p>{t.file_section.error}</p>;
return (
<>
<AllegatoIframe
allegato={fileInfos.id + fileInfos.ext}
className="h-full max-h-[70vh]"
/>
<AllegatoIframe allegato={fileInfos} className="h-full max-h-[70vh]" />
<Button
<Link
className="underline underline-offset-1 after:content-['_↗']"
onClick={() =>
handleDownload({
file: fileInfos,
getToken: async () => await getToken(),
})
}
variant="link"
href={`/storage-api/get/${fileInfos.id}?mode=download`}
>
{t.file_section.download}
</Button>
</Link>
</>
);
};

View file

@ -147,135 +147,6 @@ export default Test;
*/
const Test: NextPage = () => {
return <FileManagementPage />;
return <div>Test page</div>;
};
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-api/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-api/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

@ -6,27 +6,16 @@ import {
useContext,
useState,
} from "react";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
type TData = {
id: StorageindexId;
filename: string | null;
ext: string | null;
created_at: Date;
from_admin: boolean;
actions: string;
expires_at: Date | null;
};
type TData = FileMetadataWithAdmin;
export type StorageTable = Table<TData>;
const StorageTableContext = createContext<{
table: StorageTable | undefined;
setTable: (table: StorageTable | undefined) => void;
selectedRows: Storageindex[];
selectedRows: TData[];
cb_onStateChange: () => void;
}>({
cb_onStateChange: () => {},
@ -39,7 +28,7 @@ const StorageTableContext = createContext<{
export const StorageTableProvider = ({ children }: { children: ReactNode }) => {
const [table, setTable] = useState<StorageTable | undefined>(undefined);
const [selectedRows, setSelectedRows] = useState<Storageindex[]>([]);
const [selectedRows, setSelectedRows] = useState<TData[]>([]);
const cb_onStateChange = useCallback(() => {
if (table) {
@ -55,16 +44,7 @@ export const StorageTableProvider = ({ children }: { children: ReactNode }) => {
},
[],
);
setSelectedRows(
selected.map((r) => ({
created_at: r.created_at,
expires_at: r.expires_at,
ext: r.ext,
filename: r.filename,
from_admin: r.from_admin,
id: r.id,
})),
);
setSelectedRows(selected);
}
}, [table]);

View file

@ -19,7 +19,6 @@ import type { default as PrezziarioTable } from './Prezziario';
import type { default as RatelimiterTable } from './Ratelimiter';
import type { default as ChatsTable } from './Chats';
import type { default as EtichetteTable } from './Etichette';
import type { default as StorageindexTable } from './Storageindex';
import type { default as MessagesTable } from './Messages';
import type { default as UsersAnagraficaTable } from './UsersAnagrafica';
import type { default as BanlistTable } from './Banlist';
@ -63,8 +62,6 @@ export default interface PublicSchema {
etichette: EtichetteTable;
storageindex: StorageindexTable;
messages: MessagesTable;
users_anagrafica: UsersAnagraficaTable;

View file

@ -3,7 +3,6 @@
import type { ServizioServizioId } from './Servizio';
import type { AnnunciId } from './Annunci';
import type { StorageindexId } from './Storageindex';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Represents the table public.servizio_annunci */
@ -16,7 +15,7 @@ export default interface ServizioAnnunciTable {
open_contatti_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
doc_conferma_ref: ColumnType<StorageindexId | null, StorageindexId | null, StorageindexId | null>;
doc_conferma_ref: ColumnType<string | null, string | null, string | null>;
user_confirmed_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
@ -32,7 +31,7 @@ export default interface ServizioAnnunciTable {
hasConfermaAdmin: ColumnType<boolean, boolean | undefined, boolean>;
doc_contratto_ref: ColumnType<StorageindexId | null, StorageindexId | null, StorageindexId | null>;
doc_contratto_ref: ColumnType<string | null, string | null, string | null>;
gestionale_id: ColumnType<number | null, number | null, number | null>;
@ -42,7 +41,7 @@ export default interface ServizioAnnunciTable {
contratto_tipo: ColumnType<string | null, string | null, string | null>;
doc_registrazione_ref: ColumnType<StorageindexId | null, StorageindexId | null, StorageindexId | null>;
doc_registrazione_ref: ColumnType<string | null, string | null, string | null>;
doc_conferma_added: ColumnType<boolean, boolean | undefined, boolean>;

View file

@ -1,28 +0,0 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.storageindex */
export type StorageindexId = string & { __brand: 'public.storageindex' };
/** Represents the table public.storageindex */
export default interface StorageindexTable {
id: ColumnType<StorageindexId, StorageindexId | undefined, StorageindexId>;
created_at: ColumnType<Date, Date | string | undefined, Date | string>;
filename: ColumnType<string | null, string | null, string | null>;
ext: ColumnType<string | null, string | null, string | null>;
from_admin: ColumnType<boolean, boolean | undefined, boolean>;
expires_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
}
export type Storageindex = Selectable<StorageindexTable>;
export type NewStorageindex = Insertable<StorageindexTable>;
export type StorageindexUpdate = Updateable<StorageindexTable>;

View file

@ -2,14 +2,15 @@
// This file is automatically generated by Kanel. Do not modify manually.
import type { UsersId } from './Users';
import type { StorageindexId } from './Storageindex';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Represents the table public.users_storage */
export default interface UsersStorageTable {
userid: ColumnType<UsersId, UsersId, UsersId>;
userId: ColumnType<UsersId, UsersId, UsersId>;
storageid: ColumnType<StorageindexId, StorageindexId, StorageindexId>;
storageId: ColumnType<string, string, string>;
from_admin: ColumnType<boolean, boolean | undefined, boolean>;
}
export type UsersStorage = Selectable<UsersStorageTable>;

View file

@ -1,151 +1,90 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import z from "zod";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "~/server/api/trpc";
import {
addUserStorageHandler,
deleteUserStorageHandler,
getAllHandler,
getFromIdHandler,
addUserStorage,
deleteUserStorageEntry,
getAllWithFromAdmin,
getMultipleWithRelations,
getUserStorageHandler,
purgeFileFromUserStorages,
retrieveUserFiles,
} from "~/server/controllers/storage.controller";
import { db } from "~/server/db";
import { RateLimiterHandler } from "~/server/services/ratelimiter";
import {
addUserStorageJunc,
deleteHandler,
deleteUserStorageJunc,
getNewTokenHandler,
updateHandler,
deleteFile,
editFile,
getFileInfo,
} from "~/server/services/storage.service";
import { zStorageIndexId, zUserId } from "~/server/utils/zod_types";
const ratelimit = new RateLimiterHandler({
analytics: false,
maxRequests: 50,
windowSize: 60,
});
import { zUserId } from "~/server/utils/zod_types";
export const storageRouter = createTRPCRouter({
addStorageVisibility: adminProcedure
.input(z.object({ storageId: zStorageIndexId, userId: zUserId }))
getFileInfos: protectedProcedure
.input(z.object({ storageId: z.string() }))
.query(async ({ input }) => {
return (await getFileInfo(input.storageId)) || undefined;
}),
deleteFile: protectedProcedure
.input(z.object({ storageId: z.string() }))
.mutation(async ({ input }) => {
await addUserStorageJunc({
db,
storageId: input.storageId,
userId: input.userId,
const deletion = await deleteFile(input.storageId);
if (!deletion.success) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: deletion.error,
});
}
await purgeFileFromUserStorages({
...input,
});
}),
deleteUserStorageEntry: protectedProcedure
.input(z.object({ storageId: z.string(), userId: zUserId }))
.mutation(async ({ input }) => {
return await deleteUserStorageEntry(input);
}),
deleteFilesFromUserStorage: adminProcedure
.input(z.object({ storageIds: z.string().array() }))
.mutation(async ({ input }) => {
for (const storageId of input.storageIds) {
await purgeFileFromUserStorages({ storageId });
}
return "ok";
}),
retrieveUserFileData: protectedProcedure
.input(z.object({ userId: zUserId }))
.query(async ({ input }) => {
return await retrieveUserFiles(input);
}),
//OLD
addUserStorage: protectedProcedure
.input(
z.object({
fileId: zStorageIndexId,
userId: zUserId,
}),
)
.input(z.custom<NewUsersStorage>())
.mutation(async ({ input }) => {
return await addUserStorageHandler({
db,
storageId: input.fileId,
userId: input.userId,
});
}),
deleteStorage: protectedProcedure
.input(z.object({ storageId: zStorageIndexId }))
.mutation(async ({ input }) => {
return await deleteHandler({
cb: async (fileId) => {
await db
.deleteFrom("storageindex")
.where("id", "=", fileId)
.execute();
},
db,
fileId: input.storageId,
});
}),
deleteUserStorage: protectedProcedure
.input(z.object({ fileId: zStorageIndexId, userId: zUserId }))
.mutation(async ({ input }) => {
return await deleteUserStorageHandler({
db,
storageId: input.fileId,
userId: input.userId,
});
return await addUserStorage(input);
}),
getAll: protectedProcedure.query(async () => {
return await getAllHandler({ db });
return await getAllWithFromAdmin();
}),
getStorageFromId: protectedProcedure
.input(z.object({ storageId: zStorageIndexId }))
.query(async ({ input }) => {
return await getFromIdHandler({
db,
storage_id: input.storageId,
});
}),
getStorageToken: protectedProcedure.mutation(async () => {
const { success } = await ratelimit.limit("getStorageToken");
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
const token = (await getNewTokenHandler({ db }))[0];
if (!token) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error while creating token",
});
}
return token;
}),
getStorageTokenM: protectedProcedure
.input(z.object({ num: z.number() }))
.mutation(async ({ input }) => {
const { success } = await ratelimit.limit("getStorageToken");
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
return await getNewTokenHandler({ db, num: input.num });
}),
getStorageVisibility: adminProcedure
.input(z.object({ storageIds: zStorageIndexId.array() }))
.input(z.object({ storageIds: z.string().array() }))
.query(async ({ input }) => {
return await getMultipleWithRelations({
db,
ids: input.storageIds,
});
}),
getUserStorage: protectedProcedure
.input(z.object({ userId: zUserId }))
.query(async ({ input }) => {
return await getUserStorageHandler({ db, userId: input.userId });
}),
removeStorageVisibility: adminProcedure
.input(z.object({ storageId: zStorageIndexId, userId: zUserId }))
.mutation(async ({ input }) => {
await deleteUserStorageJunc({
db,
storageId: input.storageId,
userId: input.userId,
});
}),
renameFile: adminProcedure
.input(z.object({ fileId: zStorageIndexId, name: z.string() }))
.input(z.object({ fileId: z.string(), name: z.string() }))
.mutation(async ({ input }) => {
return await updateHandler({
data: { filename: input.name },
db,
fileId: input.fileId,
});
}),
resetStorageVisibility: adminProcedure
.input(z.object({ storageId: zStorageIndexId.array() }))
.mutation(async ({ input }) => {
await db
.deleteFrom("users_storage")
.where("storageid", "in", input.storageId)
.execute();
return "ok";
return await editFile(input.fileId, input.name);
}),
});

View file

@ -1,75 +1,121 @@
import { TRPCError } from "@trpc/server";
import { jsonArrayFrom } from "kysely/helpers/postgres";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import type { UsersId } from "~/schemas/public/Users";
import type { Querier } from "~/server/db";
import {
addUserStorageJunc,
deleteHandler,
deleteUserStorageJunc,
} from "~/server/services/storage.service";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
import { db, type Querier } from "~/server/db";
import { fetchFiles, retriveFilesSafely } from "../services/storage.service";
export const getAllHandler = async ({ db }: { db: Querier }) => {
return await db.selectFrom("storageindex").selectAll().execute();
};
export const getFromIdHandler = async ({
export const getMultipleWithRelations = async ({
db,
storage_id,
ids,
}: {
db: Querier;
storage_id: StorageindexId;
ids: string[];
}) => {
return await db
.selectFrom("storageindex")
.selectAll()
.where("id", "=", storage_id)
.executeTakeFirst();
const files = await fetchFiles();
const userStorage = await db
.selectFrom("users_storage")
.selectAll("users_storage")
.innerJoin("users", "users.id", "users_storage.userId")
.select("users.username")
.where(
"storageId",
"in",
files.map((f) => f.id),
)
.execute();
return ids
.map((id) => {
const file = files.find((f) => f.id === id);
if (!file) return null;
const users = userStorage
.filter((us) => us.storageId === id)
.map((us) => ({
id: us.userId,
username: us.username,
}));
return {
...file,
users,
};
})
.filter((f) => f !== null);
};
export const getUserStorageHandler = async ({
db,
userId,
}: {
db: Querier;
userId: UsersId;
}): Promise<Storageindex[]> => {
export const retrieveUserFiles = async ({ userId }: { userId: UsersId }) => {
try {
return await db
.selectFrom("storageindex")
.selectAll("storageindex")
.rightJoin("users_storage", "users_storage.storageid", "storageindex.id")
.where("users_storage.userid", "=", userId)
.orderBy("created_at", "desc")
//all files linked to the user
const files = await db
.selectFrom("users_storage")
.select(["storageId", "from_admin"])
.where("userId", "=", userId)
.execute();
} catch {
const { available, unavailable } = await retriveFilesSafely(
files.map((f) => f.storageId),
);
//remove entries in users_storage for files that no longer exist in storage
await db
.deleteFrom("users_storage")
.where("userId", "=", userId)
.where("storageId", "in", unavailable)
.execute();
//merge file metadata with users_storage info
const userFiles = files
.map((uf) => {
const file = available.find((f) => f.id === uf.storageId);
if (!file) return null;
return {
...file,
from_admin: uf.from_admin,
};
})
.filter((f) => f !== null);
return userFiles;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error while getting user storage",
message: `Error while getting user storage: ${(e as Error).message}`,
});
}
};
export const addUserStorageHandler = async ({
db,
userId,
storageId,
}: {
db: Querier;
userId: UsersId;
storageId: StorageindexId;
}) => {
export const getAllWithFromAdmin = async () => {
try {
await addUserStorageJunc({
db,
storageId,
userId,
const files = await db
.selectFrom("users_storage")
.select(["storageId", "from_admin"])
.execute();
const allFiles = await fetchFiles();
const allFilesWithFromAdmin = allFiles.map((file) => {
const userStorageEntry = files.find((f) => f.storageId === file.id);
return {
...file,
from_admin: userStorageEntry ? userStorageEntry.from_admin : true,
};
});
return "ok";
return allFilesWithFromAdmin;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while getting user storage: ${(e as Error).message}`,
});
}
};
export const addUserStorage = async (data: NewUsersStorage) => {
try {
return await db
.insertInto("users_storage")
.values(data)
.onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing())
.returningAll()
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@ -78,114 +124,58 @@ export const addUserStorageHandler = async ({
}
};
export const deleteUserStorageHandler = async ({
db,
userId,
storageId,
}: {
db: Querier;
userId: UsersId;
storageId: StorageindexId;
}) => {
export const getUserStorage = async ({ userId }: { userId: UsersId }) => {
try {
const file = await db
return await db
.selectFrom("users_storage")
.where("userId", "=", userId)
.selectAll()
.where("userid", "=", userId)
.where("storageid", "=", storageId)
.executeTakeFirst();
if (!file) {
return "no file";
}
await deleteHandler({
cb: async (fileId) => {
await db.transaction().execute(async (trx) => {
await deleteUserStorageJunc({
db: trx,
storageId: fileId,
userId,
});
await trx
.deleteFrom("storageindex")
.where("id", "=", fileId)
.execute();
});
},
db,
fileId: file.storageid,
});
return "ok";
} catch {
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error while deleting user storage",
message: `Error while getting user storage: ${(e as Error).message}`,
});
}
};
// export const deleteAllUserStorageHandler = async ({
// db,
// userId,
// }: {
// db: Querier;
// userId: UsersId;
// }) => {
// try {
// const files = await db
// .selectFrom("users_storage")
// .select("storageid")
// .where("userid", "=", userId)
// .execute();
// if (!files || files.length === 0) {
// return "no files";
// }
// await deleteFiles({
// db,
// refs: files,
// cb: async (fileIds) => {
// const ids = fileIds.map((file) => file.storageid);
// await db.transaction().execute(async (trx) => {
// await trx
// .deleteFrom("users_storage")
// .where("userid", "=", userId)
// .where("storageid", "in", ids)
// .execute();
// await trx.deleteFrom("storageindex").where("id", "in", ids).execute();
// });
// },
// });
// return "ok";
// } catch {
// throw new TRPCError({
// code: "INTERNAL_SERVER_ERROR",
// message: "Error while deleting user storage",
// });
// }
// };
export const getMultipleWithRelations = async ({
db,
ids,
export const deleteUserStorageEntry = async ({
userId,
storageId,
}: {
db: Querier;
ids: StorageindexId[];
userId: UsersId;
storageId: string;
}) => {
return await db
.selectFrom("storageindex")
.selectAll("storageindex")
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("users_storage")
.leftJoin("users", "users.id", "users_storage.userid")
.select(["users.username"])
.whereRef("users_storage.storageid", "=", "storageindex.id")
.selectAll("users_storage"),
).as("users_storages"),
])
.where("id", "in", ids)
.execute();
try {
await db
.deleteFrom("users_storage")
.where("userId", "=", userId)
.where("storageId", "=", storageId)
.execute();
return "ok";
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while deleting user storage: ${(e as Error).message}`,
});
}
};
export const purgeFileFromUserStorages = async ({
storageId,
}: {
storageId: string;
}) => {
try {
await db
.deleteFrom("users_storage")
.where("storageId", "=", storageId)
.execute();
return "ok";
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while deleting user storage: ${(e as Error).message}`,
});
}
};

View file

@ -1,146 +1,210 @@
import { TRPCError } from "@trpc/server";
import z from "zod";
import { env } from "~/env";
import type {
StorageindexId,
StorageindexUpdate,
} from "~/schemas/public/Storageindex";
import type { TempTokensToken } from "~/schemas/public/TempTokens";
import type { UsersId } from "~/schemas/public/Users";
import type { Querier } from "~/server/db";
import { deleteFile } from "../storage";
export const getNewTokenHandler = async ({
db,
num,
}: {
db: Querier;
num?: number;
}): Promise<TempTokensToken[]> => {
const tokens: TempTokensToken[] = [];
if (num && num > 1) {
for (let i = 0; i < num; i++) {
const tok = await newToken({ db });
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(),
});
tokens.push(tok.token);
}
return tokens;
}
const tok = await newToken({ db });
return [tok.token];
export type FileMetadata = z.infer<typeof FileMetadataSchema>;
export type FileMetadataWithAdmin = FileMetadata & {
from_admin: boolean;
};
const newToken = async ({ db }: { db: Querier }) => {
// Helper to fetch the list of all files
export async function fetchFiles(): Promise<FileMetadata[]> {
try {
return await db
.insertInto("temp_tokens")
.defaultValues()
.returning("token")
.executeTakeFirstOrThrow();
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error while creating token",
});
}
};
export const deleteHandler = async ({
db,
fileId,
cb,
}: {
db: Querier;
fileId: StorageindexId;
cb?: (fileId: StorageindexId) => Promise<void>;
}) => {
try {
const storage = await db
.selectFrom("storageindex")
.select(["id", "ext"])
.where("id", "=", fileId)
.executeTakeFirstOrThrow();
await deleteFile(storage.id);
if (cb) {
await cb(fileId);
const response = await fetch(
`${env.STORAGE_URL}/files?token=${env.STORAGE_TOKEN}`,
);
if (!response.ok) {
console.error("Failed to fetch file list:", response.statusText);
return [];
}
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:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Error while deleting file",
});
console.error("Error fetching file list:", error);
return [];
}
};
}
export const updateHandler = async ({
db,
fileId,
data,
}: {
db: Querier;
fileId: StorageindexId;
data: StorageindexUpdate;
}) => {
export async function getFileInfo(id: string): Promise<FileMetadata | null> {
try {
await db
.updateTable("storageindex")
.set(data)
.where("id", "=", fileId)
.execute();
return "ok";
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while updating file: ${(e as Error).message}`,
});
}
};
const response = await fetch(
`${env.STORAGE_URL}/info/${id}?token=${env.STORAGE_TOKEN}`,
);
if (!response.ok) {
console.error("Failed to fetch file list:", response.statusText);
return null;
}
const parse = FileMetadataSchema.safeParse(await response.json());
export const addUserStorageJunc = async ({
db,
userId,
storageId,
}: {
db: Querier;
userId: UsersId;
storageId: StorageindexId;
}) => {
try {
return await db
.insertInto("users_storage")
.values({ storageid: storageId, userid: userId })
.onConflict((oc) => oc.columns(["userid", "storageid"]).doNothing())
.returningAll()
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while adding user storage: ${(e as Error).message}`,
});
if (parse.success) {
return parse.data;
}
console.error("Failed to parse file metadata:", parse.error);
return null;
} catch (error) {
console.error("Error fetching file list:", error);
return null;
}
};
}
export const deleteUserStorageJunc = async ({
db,
userId,
storageId,
}: {
db: Querier;
userId: UsersId;
storageId: StorageindexId;
}) => {
try {
await db
.deleteFrom("users_storage")
.where("userid", "=", userId)
.where("storageid", "=", storageId)
.execute();
return "ok";
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while deleting user storage: ${(e as Error).message}`,
});
type EditResponse =
| {
success: true;
id: string;
}
| {
success: false;
error: string;
};
export async function editFile(
id: string,
filename?: string,
expiresAt?: string,
): Promise<EditResponse> {
const formData = new FormData();
formData.append("id", id);
if (expiresAt) {
formData.append("expires_at", expiresAt);
}
};
if (filename) {
formData.append("filename", filename);
}
try {
const response = await fetch(
`${env.STORAGE_URL}/edit?token=${env.STORAGE_TOKEN}`,
{
method: "POST",
body: formData,
},
);
if (response.ok) {
const res = await response.json();
const parsed = z
.object({
id: z.string(),
})
.parse(res);
const fileID = parsed.id;
console.log("File edited successfully.");
return { success: true, id: fileID };
}
return {
success: false,
error: `Modifica fallita con status: ${response.status}`,
};
} catch (e) {
return {
success: false,
error: `Errore Modifica file: ${(e as Error).message}`,
};
}
}
type DeleteResponse =
| {
success: true;
}
| {
success: false;
error: string;
};
// Helper to handle file deletion
export async function deleteFile(fileID: string): Promise<DeleteResponse> {
try {
// The Go server accepts POST with form data for delete
const response = await fetch(
`${env.STORAGE_URL}/delete/${fileID}?token=${env.STORAGE_TOKEN}`,
{
method: "DELETE",
},
);
// Treat redirect as success
if (response.ok || (response.status >= 300 && response.status < 400)) {
console.log(`File ${fileID} deleted successfully.`);
return { success: true };
}
return {
success: false,
error: `Eliminazione fallita con status: ${response.status}`,
};
} catch (error) {
console.error("Error during file deletion:", error);
return { success: false, error: (error as Error).message };
}
}
// export const handleDownload = async (file: FileMetadata) => {
// try {
// const response = await fetch(
// `${env.STORAGE_URL}/get/${file.id}?token=${env.STORAGE_TOKEN}&mode=download`,
// );
// if (!response.ok) {
// throw new Error("File fetch failed");
// }
// const blob = await response.blob();
// const url = window.URL.createObjectURL(blob);
// const a = document.createElement("a");
// a.href = url;
// a.download = file.originalName;
// document.body.appendChild(a);
// a.click();
// a.remove();
// window.URL.revokeObjectURL(url);
// } catch {
// throw new Error("File download failed");
// }
// };
const safeRetrive = z.object({
available: z.array(FileMetadataSchema),
unavailable: z.array(z.string()),
});
export async function retriveFilesSafely(ids: string[]) {
try {
const response = await fetch(
`${env.STORAGE_URL}/files-check?token=${env.STORAGE_TOKEN}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(ids),
},
);
if (!response.ok) {
throw new Error("File fetch failed");
}
const parse = safeRetrive.safeParse(await response.json());
if (parse.success) {
return parse.data;
}
throw new Error("File fetch parse failed");
} catch (e) {
throw new Error(`File fetch failed: ${(e as Error).message}`);
}
}

View file

@ -1,87 +0,0 @@
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/api/files`);
if (!response.ok) {
console.error("Failed to fetch file list:", response.statusText);
return [];
}
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-api/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> {
try {
// The Go server accepts POST with form data for delete
const response = await fetch(`/storage-api/delete/${fileID}`, {
method: "DELETE",
});
// 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;
}
}

View file

@ -10,7 +10,6 @@ import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { PaymentsId } from "~/schemas/public/Payments";
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { StorageindexId } from "~/schemas/public/Storageindex";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import type { UsersId } from "~/schemas/public/Users";
import type { EventDataTypes } from "~/server/controllers/event_queue.controller";
@ -45,11 +44,6 @@ export const zFlagsId = z.custom<FlagsId>(
"falied to validate FlagsId",
);
export const zStorageIndexId = z.custom<StorageindexId>(
(val) => typeof val === "string",
"falied to validate StorageIndexId",
);
export const zEtichettaId = z.custom<EtichetteIdEtichetta>(
(val) => typeof val === "number",
"falied to validate EtichettaId",