refactor: implement getStorageUrl utility for consistent storage URL generation

This commit is contained in:
Marco Pedone 2025-11-28 15:11:14 +01:00
parent 4229d44c2f
commit 830b89d4b1
15 changed files with 186 additions and 27 deletions

View file

@ -65,7 +65,7 @@ const OnboardingServizio = ({
alt={`Annuncio image - ${annuncio.codice}`} alt={`Annuncio image - ${annuncio.codice}`}
className="block w-[240px] rounded-[4px] object-cover object-center" className="block w-[240px] rounded-[4px] object-cover object-center"
height="140px" height="140px"
src={`${env.BASE_URL}/storage-api/get/${annuncio.immagine}?image=true`} src={`${env.BASE_URL}/storage-api/get/${annuncio.immagine}?media=image`}
width="100%" width="100%"
/> />
</Row> </Row>

View file

@ -1,6 +1,7 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { getStorageUrl } from "~/lib/storage_utils";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import type { FileMetadata } from "~/server/services/storage.service"; import type { FileMetadata } from "~/server/services/storage.service";
@ -50,7 +51,10 @@ export const AllegatoIframe = ({
} }
}; };
const requestUrl = `/storage-api/get/${allegato.id}?mode=inline`; const requestUrl = getStorageUrl({
storageId: allegato.id,
params: { mode: "inline" },
});
// For PDF on iOS, provide download link instead // For PDF on iOS, provide download link instead
if (isPDF && (isIOS || isMobile)) { if (isPDF && (isIOS || isMobile)) {
return ( return (

View file

@ -11,6 +11,7 @@ import {
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
import { handleConsegna } from "~/lib/annuncio_details"; import { handleConsegna } from "~/lib/annuncio_details";
import { getStorageUrl } from "~/lib/storage_utils";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useRicerca } from "~/providers/RicercaProvider"; import { useRicerca } from "~/providers/RicercaProvider";
@ -123,7 +124,13 @@ const SelectedComp = memo(
alt="a" alt="a"
className="size-24 rounded-md object-cover sm:size-40" className="size-24 rounded-md object-cover sm:size-40"
height={500} height={500}
src={`/storage-api/get/${selected.images[0].img}?image=true&cachekey=${selected?.toString()}`} src={getStorageUrl({
storageId: selected.images[0].img,
params: {
media: "image",
cacheKey: selected.media_updated_at?.toISOString(),
},
})}
width={500} width={500}
/> />
) : ( ) : (

View file

@ -27,6 +27,7 @@ import {
DialogTitle, DialogTitle,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { camereTesti, handleConsegna } from "~/lib/annuncio_details"; import { camereTesti, handleConsegna } from "~/lib/annuncio_details";
import { getStorageUrl } from "~/lib/storage_utils";
import { cn, formatCurrency } from "~/lib/utils"; import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { ImagesRefs } from "~/schemas/public/ImagesRefs"; import type { ImagesRefs } from "~/schemas/public/ImagesRefs";
@ -217,14 +218,26 @@ export const CardAnnuncio = ({
<CarouselItem key={`${img.img}key`}> <CarouselItem key={`${img.img}key`}>
<ImageFlbk <ImageFlbk
alt={t.card.alt_immagine} alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${img.thumb}?image=true&cachekey=${media_updated_at?.toString()}`} blurDataURL={getStorageUrl({
storageId: img.thumb,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
},
})}
className={cn( className={cn(
"h-64 w-full bg-[#e6e9ec] object-cover", "h-64 w-full bg-[#e6e9ec] object-cover",
(stato === "Trattativa" || homepage) && "rounded-[7px]", (stato === "Trattativa" || homepage) && "rounded-[7px]",
)} )}
height={1080} height={1080}
priority={idx === 0} priority={idx === 0}
src={`/storage-api/get/${img.img}?image=true&cachekey=${media_updated_at?.toString()}`} src={getStorageUrl({
storageId: img.img,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
},
})}
width={1920} width={1920}
/> />
</CarouselItem> </CarouselItem>
@ -241,9 +254,21 @@ export const CardAnnuncio = ({
> >
<VideoPlayer <VideoPlayer
className="h-64 object-cover" className="h-64 object-cover"
coverImage={`/storage-api/get/${video.thumb}?image=true&cachekey=${media_updated_at?.toString()}`} coverImage={getStorageUrl({
storageId: video.thumb,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
},
})}
key={`videoplayer-${video}`} key={`videoplayer-${video}`}
videoSrc={`/storage-api/get/${video.video}?video=true&cachekey=${media_updated_at?.toString()}`} videoSrc={getStorageUrl({
storageId: video.video,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "video",
},
})}
/> />
</CarouselItem> </CarouselItem>
); );
@ -275,11 +300,23 @@ export const CardAnnuncio = ({
{images && images.length > 0 && images[0] ? ( {images && images.length > 0 && images[0] ? (
<ImageFlbk <ImageFlbk
alt={t.card.alt_immagine} alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${images[0].thumb}?image=true&cachekey=${media_updated_at?.toString()}`} blurDataURL={getStorageUrl({
storageId: images[0].thumb,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
},
})}
className={"h-64 w-full bg-[#e6e9ec] object-cover"} className={"h-64 w-full bg-[#e6e9ec] object-cover"}
height={1080} height={1080}
priority={true} priority={true}
src={`/storage-api/get/${images[0].img}?image=true&cachekey=${media_updated_at?.toString()}`} src={getStorageUrl({
storageId: images[0].img,
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
},
})}
width={1920} width={1920}
/> />
) : ( ) : (
@ -420,7 +457,13 @@ export const CarouselAnnuncio = ({
height={400} height={400}
onClick={() => handleOpenModal(idx)} onClick={() => handleOpenModal(idx)}
priority={idx === 0} priority={idx === 0}
src={`/storage-api/get/${img.img}?image=true&cachekey=${updated_at?.toString()}`} src={getStorageUrl({
storageId: img.img,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
},
})}
width={800} width={800}
/> />
<Maximize2 <Maximize2
@ -442,9 +485,21 @@ export const CarouselAnnuncio = ({
> >
<VideoPlayer <VideoPlayer
className="aspect-square max-h-92 object-cover sm:max-h-80 xl:max-h-[25rem]" className="aspect-square max-h-92 object-cover sm:max-h-80 xl:max-h-[25rem]"
coverImage={`/storage-api/get/${video.thumb}?image=true&cachekey=${updated_at?.toString()}`} coverImage={getStorageUrl({
storageId: video.thumb,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
},
})}
key={`videoplayer-${idx}-${openModal}`} key={`videoplayer-${idx}-${openModal}`}
videoSrc={`/storage-api/get/${video.video}?video=true&cachekey=${updated_at?.toString()}`} videoSrc={getStorageUrl({
storageId: video.video,
params: {
cacheKey: updated_at?.toISOString(),
media: "video",
},
})}
/> />
<Maximize2 <Maximize2
@ -485,7 +540,13 @@ export const CarouselAnnuncio = ({
alt={`carousel-img-${idx}`} alt={`carousel-img-${idx}`}
className="mx-auto h-[90vh] w-full rounded-md bg-[#e6e9ec] object-contain sm:w-[80vw]" className="mx-auto h-[90vh] w-full rounded-md bg-[#e6e9ec] object-contain sm:w-[80vw]"
height={1080} height={1080}
src={`/storage-api/get/${img.img}?image=true&cachekey=${updated_at?.toString()}`} src={getStorageUrl({
storageId: img.img,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
},
})}
width={1920} width={1920}
/> />
</CarouselItem> </CarouselItem>
@ -498,8 +559,20 @@ export const CarouselAnnuncio = ({
> >
<VideoPlayer <VideoPlayer
className="absolute mx-auto h-[90vh] w-full max-w-fit rounded-md object-contain" className="absolute mx-auto h-[90vh] w-full max-w-fit rounded-md object-contain"
coverImage={`/storage-api/get/${video.thumb}?image=true&cachekey=${updated_at?.toString()}`} coverImage={getStorageUrl({
videoSrc={`/storage-api/get/${video.video}?video=true&cachekey=${updated_at?.toString()}`} storageId: video.thumb,
params: {
cacheKey: updated_at?.toISOString(),
media: "image",
},
})}
videoSrc={getStorageUrl({
storageId: video.video,
params: {
cacheKey: updated_at?.toISOString(),
media: "video",
},
})}
/> />
</CarouselItem> </CarouselItem>
); );

View file

@ -36,6 +36,7 @@ import {
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import { Separator } from "~/components/ui/separator"; import { Separator } from "~/components/ui/separator";
import { UploadModal } from "~/components/upload_modal"; import { UploadModal } from "~/components/upload_modal";
import { getStorageUrl } from "~/lib/storage_utils";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
@ -324,7 +325,12 @@ function FileList({
{format(new Date(file.uploadedAt), "d MMM yyyy")} {format(new Date(file.uploadedAt), "d MMM yyyy")}
</div> </div>
<div className="col-span-2 flex justify-end gap-1 md:col-span-1"> <div className="col-span-2 flex justify-end gap-1 md:col-span-1">
<Link href={`/storage-api/get/${file.id}?mode=download`}> <Link
href={getStorageUrl({
storageId: file.id,
params: { mode: "download" },
})}
>
<Button <Button
className="flex size-8 gap-1 text-sm" className="flex size-8 gap-1 text-sm"
size="icon" size="icon"

View file

@ -1,6 +1,7 @@
import { CircleCheck, ExternalLink, Trash2 } from "lucide-react"; import { CircleCheck, ExternalLink, Trash2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { getStorageUrl } from "~/lib/storage_utils";
import { useSession } from "~/providers/SessionProvider"; import { useSession } from "~/providers/SessionProvider";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
@ -149,7 +150,9 @@ export const DocumentiUploadSection = ({
alt={title} alt={title}
className="size-24 rounded-md border object-cover" className="size-24 rounded-md border object-cover"
height={96} height={96}
src={`/storage-api/get/${document.storageId}`} src={getStorageUrl({
storageId: document.storageId,
})}
width={96} width={96}
/> />
)} )}

View file

@ -6,6 +6,7 @@ import { Interactions } from "~/components/servizio/interactions";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { handleConsegna } from "~/lib/annuncio_details"; import { handleConsegna } from "~/lib/annuncio_details";
import { getStorageUrl } from "~/lib/storage_utils";
import { cn, formatCurrency } from "~/lib/utils"; import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useServizioAnnuncio } from "~/providers/ServizioProvider"; import { useServizioAnnuncio } from "~/providers/ServizioProvider";
@ -58,7 +59,13 @@ export const BasicAnnuncioCard = ({
priority priority
src={ src={
(data?.images[0] && (data?.images[0] &&
`/storage-api/get/${data.images[0].img}?image=true&cachekey=${data.media_updated_at?.toString()}`) || getStorageUrl({
storageId: data.images[0].img,
params: {
cacheKey: data.media_updated_at?.toISOString(),
media: "image",
},
})) ||
"/fallback-image.png" "/fallback-image.png"
} }
width={1920} width={1920}

View file

@ -4,6 +4,7 @@ import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { AllegatoIframe } from "~/components/allegato-iframe"; import { AllegatoIframe } from "~/components/allegato-iframe";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { getStorageUrl } from "~/lib/storage_utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
@ -40,7 +41,10 @@ export const FileSection = ({ storageId }: { storageId: string }) => {
<Link <Link
className="underline underline-offset-1 after:content-['_↗']" className="underline underline-offset-1 after:content-['_↗']"
href={`/storage-api/get/${fileInfos.id}?mode=download`} href={getStorageUrl({
storageId: fileInfos.id,
params: { mode: "download" },
})}
> >
{t.file_section.download} {t.file_section.download}
</Link> </Link>

View file

@ -0,0 +1,26 @@
import { env } from "~/env";
type Params = {
cacheKey: string;
mode: "download" | "inline";
media: "image" | "video";
};
export function getStorageUrl({
storageId,
params,
host = env.NEXT_PUBLIC_BASE_URL,
}: {
storageId: string;
params?: Partial<Params>;
host?: string;
}) {
const url = new URL(`${host}/storage-api/get/${storageId}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
return url.toString();
}

View file

@ -12,8 +12,8 @@ export const apisMiddleware = async (req: NextRequest) => {
} }
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
const isImageRequest = params.get("image") === "true"; const isImageRequest = params.get("media") === "image";
const isVideoRequest = params.get("video") === "true"; const isVideoRequest = params.get("media") === "video";
// Check if this is a Next.js Image Optimization request // Check if this is a Next.js Image Optimization request
const purpose = req.headers.get("purpose"); const purpose = req.headers.get("purpose");

View file

@ -54,6 +54,7 @@ import {
import { useMediaQuery } from "~/hooks/use-media-query"; import { useMediaQuery } from "~/hooks/use-media-query";
import { handleConsegna } from "~/lib/annuncio_details"; import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr"; import { replaceWithBr } from "~/lib/newlineToBr";
import { getStorageUrl } from "~/lib/storage_utils";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { AnnuncioContext } from "~/providers/AnnuncioProvider"; import { AnnuncioContext } from "~/providers/AnnuncioProvider";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
@ -632,9 +633,15 @@ const AnnuncioFooter = ({
return ( return (
<VideoPlayer <VideoPlayer
className="h-96 max-w-96" className="h-96 max-w-96"
coverImage={`/storage-api/get/${video.thumb}?image=true&cachekey=${updated_at?.toString()}`} coverImage={getStorageUrl({
storageId: video.thumb,
params: { cacheKey: updated_at?.toISOString(), media: "image" },
})}
key={`videoplayer-${video.video}`} // Cache busting key={`videoplayer-${video.video}`} // Cache busting
videoSrc={`/storage-api/get/${video.video}?video=true&cachekey=${updated_at?.toString()}`} videoSrc={getStorageUrl({
storageId: video.video,
params: { cacheKey: updated_at?.toISOString(), media: "video" },
})}
/> />
); );
})} })}

View file

@ -21,6 +21,7 @@ import {
DialogTrigger, DialogTrigger,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { Input } from "~/components/ui/input"; import { Input } from "~/components/ui/input";
import { getStorageUrl } from "~/lib/storage_utils";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useEnforcedSession } from "~/providers/SessionProvider"; import { useEnforcedSession } from "~/providers/SessionProvider";
@ -77,7 +78,12 @@ const AllegatoView: NextPageWithLayout<AllegatoViewProps> = ({
<span> <span>
caricato il {new Date(fileInfos.uploadedAt).toLocaleString("it")} caricato il {new Date(fileInfos.uploadedAt).toLocaleString("it")}
</span> </span>
<Link href={`/storage-api/get/${fileInfos.id}?mode=download`}> <Link
href={getStorageUrl({
storageId: fileInfos.id,
params: { mode: "download" },
})}
>
<Button className="flex gap-2"> <Button className="flex gap-2">
<Download size={16} /> <Download size={16} />
Download Download

View file

@ -5,6 +5,7 @@ import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
import { getStorageUrl } from "~/lib/storage_utils";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
@ -60,7 +61,10 @@ const FileSection = ({ storageId }: { storageId: string }) => {
<Link <Link
className="underline underline-offset-1 after:content-['_↗']" className="underline underline-offset-1 after:content-['_↗']"
href={`/storage-api/get/${fileInfos.id}?mode=download`} href={getStorageUrl({
storageId: fileInfos.id,
params: { mode: "download" },
})}
> >
{t.file_section.download} {t.file_section.download}
</Link> </Link>

View file

@ -5,6 +5,7 @@ import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
import { getStorageUrl } from "~/lib/storage_utils";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
@ -56,7 +57,10 @@ const FileSection = ({ storageId }: { storageId: string }) => {
<Link <Link
className="underline underline-offset-1 after:content-['_↗']" className="underline underline-offset-1 after:content-['_↗']"
href={`/storage-api/get/${fileInfos.id}?mode=download`} href={getStorageUrl({
storageId: fileInfos.id,
params: { mode: "download" },
})}
> >
{t.file_section.download} {t.file_section.download}
</Link> </Link>

View file

@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { env } from "~/env"; import { env } from "~/env";
import { getStorageUrl } from "~/lib/storage_utils";
import type { import type {
Annunci, Annunci,
AnnunciId, AnnunciId,
@ -115,7 +116,14 @@ export const getAnnunciMetaByCod = async ({ cod }: { cod: string }) => {
} }
const ogImage = const ogImage =
annuncio.images && annuncio.images.length > 0 && annuncio.images[0] annuncio.images && annuncio.images.length > 0 && annuncio.images[0]
? `${env.BASE_URL}/storage-api/get/${annuncio.images[0].img}?image=true&cachekey=${annuncio.media_updated_at?.toString()}` ? getStorageUrl({
storageId: annuncio.images[0].img,
params: {
cacheKey: annuncio.media_updated_at?.toISOString(),
media: "image",
},
host: env.BASE_URL,
})
: `${env.BASE_URL}/og.jpg`; : `${env.BASE_URL}/og.jpg`;
return { return {