Replace backing storage (#374)

* Centralize random suffix generation

* Introduce ••• menu in admin photos table

* Finalize re-upload behavior

* Finalize re-upload locales

* Honor resize config when re-uploading files
This commit is contained in:
Sam Becker 2026-02-10 21:17:57 -06:00 committed by GitHub
parent f962074af9
commit 351f8869a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 168 additions and 88 deletions

View File

@ -6,7 +6,6 @@ import {
} from '@/photo'; } from '@/photo';
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'; import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { isUploadPathnameValid } from '@/photo/storage';
export async function POST(request: Request): Promise<NextResponse> { export async function POST(request: Request): Promise<NextResponse> {
const body: HandleUploadBody = await request.json(); const body: HandleUploadBody = await request.json();
@ -15,18 +14,13 @@ export async function POST(request: Request): Promise<NextResponse> {
const jsonResponse = await handleUpload({ const jsonResponse = await handleUpload({
body, body,
request, request,
onBeforeGenerateToken: async (pathname) => { onBeforeGenerateToken: async () => {
const session = await auth(); const session = await auth();
if (session?.user) { if (session?.user) {
if (isUploadPathnameValid(pathname)) {
return { return {
maximumSizeInBytes: MAX_PHOTO_UPLOAD_SIZE_IN_BYTES, maximumSizeInBytes: MAX_PHOTO_UPLOAD_SIZE_IN_BYTES,
allowedContentTypes: ACCEPTED_PHOTO_FILE_TYPES, allowedContentTypes: ACCEPTED_PHOTO_FILE_TYPES,
addRandomSuffix: true,
}; };
} else {
throw new Error('Invalid upload');
}
} else { } else {
throw new Error('Unauthenticated upload'); throw new Error('Unauthenticated upload');
} }

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { ComponentProps, useMemo } from 'react'; import { ComponentProps, useMemo, useRef } from 'react';
import { import {
getPathComponents, getPathComponents,
PATH_ROOT, PATH_ROOT,
@ -9,6 +9,7 @@ import {
} from '@/app/path'; } from '@/app/path';
import { import {
deletePhotoAction, deletePhotoAction,
replacePhotoStorageAction,
syncPhotoAction, syncPhotoAction,
toggleFavoritePhotoAction, toggleFavoritePhotoAction,
togglePrivatePhotoAction, togglePrivatePhotoAction,
@ -17,6 +18,7 @@ import {
Photo, Photo,
deleteConfirmationTextForPhoto, deleteConfirmationTextForPhoto,
downloadFileNameForPhoto, downloadFileNameForPhoto,
titleForPhoto,
} from '@/photo'; } from '@/photo';
import { isPathFavs, isPhotoFav, TAG_PRIVATE } from '@/tag'; import { isPathFavs, isPhotoFav, TAG_PRIVATE } from '@/tag';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
@ -34,23 +36,32 @@ import { KEY_COMMANDS } from '@/photo/key-commands';
import { useAppText } from '@/i18n/state/client'; import { useAppText } from '@/i18n/state/client';
import IconLock from '@/components/icons/IconLock'; import IconLock from '@/components/icons/IconLock';
import IconTrash from '@/components/icons/IconTrash'; import IconTrash from '@/components/icons/IconTrash';
import IconUpload from '@/components/icons/IconUpload';
import { uploadPhotoFromClient } from '@/photo/storage';
import ImageInput from '@/components/ImageInput';
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
export default function AdminPhotoMenu({ export default function AdminPhotoMenu({
photo, photo,
revalidatePhoto, revalidatePhoto,
includeFavorite = true, includeFavorite = true,
showKeyCommands, showKeyCommands,
alwaysVisible,
...props ...props
}: Omit<ComponentProps<typeof MoreMenu>, 'sections'> & { }: Omit<ComponentProps<typeof MoreMenu>, 'sections' | 'ariaLabel'> & {
photo: Photo photo: Photo
revalidatePhoto?: RevalidatePhoto revalidatePhoto?: RevalidatePhoto
includeFavorite?: boolean includeFavorite?: boolean
showKeyCommands?: boolean showKeyCommands?: boolean
alwaysVisible?: boolean
}) { }) {
const { isUserSignedIn, registerAdminUpdate } = useAppState(); const { isUserSignedIn, registerAdminUpdate } = useAppState();
const appText = useAppText(); const appText = useAppText();
const inputRef = useRef<HTMLInputElement>(null);
const onUploadFinishRef = useRef<() => void>(null);
const path = usePathname(); const path = usePathname();
const pathComponents = getPathComponents(path); const pathComponents = getPathComponents(path);
const isOnPhotoDetail = pathComponents.photoId === photo.id; const isOnPhotoDetail = pathComponents.photoId === photo.id;
@ -68,7 +79,7 @@ export default function AdminPhotoMenu({
label: appText.admin.edit, label: appText.admin.edit,
icon: <IconEdit icon: <IconEdit
size={14} size={14}
className="translate-x-[0.5px] translate-y-[0.5px]" className="translate-x-[1px] translate-y-[-0.5px]"
/>, />,
href: pathForAdminPhotoEdit(photo.id), href: pathForAdminPhotoEdit(photo.id),
...showKeyCommands && { keyCommand: KEY_COMMANDS.edit }, ...showKeyCommands && { keyCommand: KEY_COMMANDS.edit },
@ -112,8 +123,8 @@ export default function AdminPhotoMenu({
items.push({ items.push({
label: appText.admin.download, label: appText.admin.download,
icon: <MdOutlineFileDownload icon: <MdOutlineFileDownload
size={17} size={18}
className="translate-x-[-1px]" className="translate-x-[-1.5px]"
/>, />,
href: photo.url, href: photo.url,
hrefDownloadName: downloadFileNameForPhoto(photo), hrefDownloadName: downloadFileNameForPhoto(photo),
@ -137,6 +148,17 @@ export default function AdminPhotoMenu({
.then(() => revalidatePhoto?.(photo.id)), .then(() => revalidatePhoto?.(photo.id)),
...showKeyCommands && { keyCommand: KEY_COMMANDS.sync }, ...showKeyCommands && { keyCommand: KEY_COMMANDS.sync },
}); });
items.push({
label: appText.admin.reUpload,
icon: <IconUpload
size={16}
className="translate-x-[-1px] translate-y-px"
/>,
action: () => new Promise(resolve => {
onUploadFinishRef.current = resolve;
inputRef.current?.click();
}),
});
return { items }; return { items };
}, [ }, [
@ -189,11 +211,24 @@ export default function AdminPhotoMenu({
, [sectionMain, sectionDelete]); , [sectionMain, sectionDelete]);
return ( return (
isUserSignedIn isUserSignedIn || alwaysVisible
? <MoreMenu {...{ ? <>
<MoreMenu {...{
...props, ...props,
sections, sections,
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
}}/> }}/>
<ImageInput
ref={inputRef}
onBlobReady={async ({ blob, extension }) =>
uploadPhotoFromClient(blob, extension)
.then(updatedStorageUrl =>
replacePhotoStorageAction(photo.id, updatedStorageUrl))
.then(() => revalidatePhoto?.(photo.id))
.finally(onUploadFinishRef.current)}
shouldResize={!PRESERVE_ORIGINAL_UPLOADS}
/>
</>
: null : null
); );
} }

View File

@ -12,7 +12,6 @@ import EditButton from './EditButton';
import { useAppState } from '@/app/AppState'; import { useAppState } from '@/app/AppState';
import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll'; import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll';
import PhotoSyncButton from './PhotoSyncButton'; import PhotoSyncButton from './PhotoSyncButton';
import DeletePhotoButton from './DeletePhotoButton';
import { Timezone } from '@/utility/timezone'; import { Timezone } from '@/utility/timezone';
import { photoNeedsToBeUpdated } from '@/photo/update'; import { photoNeedsToBeUpdated } from '@/photo/update';
import PhotoVisibilityIcon from '@/photo/visibility/PhotoVisibilityIcon'; import PhotoVisibilityIcon from '@/photo/visibility/PhotoVisibilityIcon';
@ -20,6 +19,7 @@ import { doesPhotoHaveDefaultVisibility } from '@/photo/visibility';
import UpdateTooltip from '@/photo/update/UpdateTooltip'; import UpdateTooltip from '@/photo/update/UpdateTooltip';
import PhotoColors from '@/photo/color/PhotoColors'; import PhotoColors from '@/photo/color/PhotoColors';
import SyncColorButton from '@/photo/color/SyncColorButton'; import SyncColorButton from '@/photo/color/SyncColorButton';
import AdminPhotoMenu from './AdminPhotoMenu';
export default function AdminPhotosTable({ export default function AdminPhotosTable({
photos, photos,
@ -142,11 +142,12 @@ export default function AdminPhotosTable({
/> />
{debugColorData && {debugColorData &&
<SyncColorButton photoId={photo.id} />} <SyncColorButton photoId={photo.id} />}
{canDelete && <AdminPhotoMenu
<DeletePhotoButton
photo={photo} photo={photo}
onDelete={() => revalidatePhoto?.(photo.id, true)} revalidatePhoto={revalidatePhoto}
/>} disabled={!canEdit || !canDelete}
alwaysVisible
/>
</div> </div>
</Fragment>)} </Fragment>)}
</AdminTable> </AdminTable>

View File

@ -30,6 +30,7 @@ export default function MoreMenu({
isOpen: isOpenProp, isOpen: isOpenProp,
setIsOpen: setIsOpenProp, setIsOpen: setIsOpenProp,
onOpen, onOpen,
disabled,
...props ...props
}: { }: {
sections: MoreMenuSection[] sections: MoreMenuSection[]
@ -42,6 +43,7 @@ export default function MoreMenu({
isOpen?: boolean isOpen?: boolean
setIsOpen?: (isOpen: boolean) => void setIsOpen?: (isOpen: boolean) => void
onOpen?: () => void onOpen?: () => void
disabled?: boolean
} & ComponentProps<typeof DropdownMenu.Content>){ } & ComponentProps<typeof DropdownMenu.Content>){
const [isOpenInternal, setIsOpenInternal] = useState(isOpenProp ?? false); const [isOpenInternal, setIsOpenInternal] = useState(isOpenProp ?? false);
@ -62,7 +64,7 @@ export default function MoreMenu({
open={isOpen} open={isOpen}
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
> >
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild {...{ disabled }}>
<button <button
type="button" type="button"
className={clsx( className={clsx(

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: 'সর্বজনীন করুন', public: 'সর্বজনীন করুন',
download: 'ডাউনলোড', download: 'ডাউনলোড',
sync: 'সিঙ্ক', sync: 'সিঙ্ক',
reUpload: 'পুনরায় আপলোড করুন',
delete: 'ডিলিট', delete: 'ডিলিট',
deleteConfirm: 'আপনি কি "{{photoTitle}}" মুছে ফেলতে চান?', deleteConfirm: 'আপনি কি "{{photoTitle}}" মুছে ফেলতে চান?',
}, },

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: 'Make Public', public: 'Make Public',
download: 'Download', download: 'Download',
sync: 'Sync', sync: 'Sync',
reUpload: 'Re-upload',
delete: 'Delete', delete: 'Delete',
deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"', deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"',
}, },

View File

@ -134,6 +134,7 @@ export const TEXT = {
public: 'Make Public', public: 'Make Public',
download: 'Download', download: 'Download',
sync: 'Sync', sync: 'Sync',
reUpload: 'Re-upload',
delete: 'Delete', delete: 'Delete',
deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"', deleteConfirm: 'Are you sure you want to delete "{{photoTitle}}?"',
}, },

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: 'सार्वजनिक बनाएं', public: 'सार्वजनिक बनाएं',
download: 'डाउनलोड करें', download: 'डाउनलोड करें',
sync: 'सिंक करें', sync: 'सिंक करें',
reUpload: 'पुनः अपलोड करें',
delete: 'हटाएं', delete: 'हटाएं',
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
deleteConfirm: 'क्या आप सुनिश्चित हैं कि "{{photoTitle}}" को हटाना चाहते हैं?', deleteConfirm: 'क्या आप सुनिश्चित हैं कि "{{photoTitle}}" को हटाना चाहते हैं?',

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: 'Buat Publik', public: 'Buat Publik',
download: 'Unduh', download: 'Unduh',
sync: 'Sinkronkan', sync: 'Sinkronkan',
reUpload: 'Unggah ulang',
delete: 'Hapus', delete: 'Hapus',
deleteConfirm: 'Apakah Anda yakin ingin menghapus "{{photoTitle}}"?', deleteConfirm: 'Apakah Anda yakin ingin menghapus "{{photoTitle}}"?',
}, },

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: 'Tornar Público', public: 'Tornar Público',
download: 'Baixar', download: 'Baixar',
sync: 'Sincronizar', sync: 'Sincronizar',
reUpload: 'Enviar novamente',
delete: 'Excluir', delete: 'Excluir',
deleteConfirm: 'Tem certeza de que deseja excluir "{{photoTitle}}"?', deleteConfirm: 'Tem certeza de que deseja excluir "{{photoTitle}}"?',
}, },

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: 'Tornar Público', public: 'Tornar Público',
download: 'Descarregar', download: 'Descarregar',
sync: 'Sincronizar', sync: 'Sincronizar',
reUpload: 'Carregar novamente',
delete: 'Excluir', delete: 'Excluir',
deleteConfirm: 'Tens certeza de que deseja excluir "{{photoTitle}}"?', deleteConfirm: 'Tens certeza de que deseja excluir "{{photoTitle}}"?',
}, },

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: 'Herkese Açık Yap', public: 'Herkese Açık Yap',
download: 'İndir', download: 'İndir',
sync: 'Senkronize Et', sync: 'Senkronize Et',
reUpload: 'Yeniden Yükle',
delete: 'Sil', delete: 'Sil',
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
deleteConfirm: '"{{photoTitle}}" adlı fotoğrafı silmek istediğinize emin misiniz?', deleteConfirm: '"{{photoTitle}}" adlı fotoğrafı silmek istediğinize emin misiniz?',

View File

@ -135,6 +135,7 @@ export const TEXT: I18N = {
public: '设为公开', public: '设为公开',
download: '下载', download: '下载',
sync: '同步', sync: '同步',
reUpload: '重新上传',
delete: '删除', delete: '删除',
deleteConfirm: '确定要删除 "{{photoTitle}}" 吗?', deleteConfirm: '确定要删除 "{{photoTitle}}" 吗?',
}, },

View File

@ -9,7 +9,6 @@ import {
shouldShowFilmDataForPhoto, shouldShowFilmDataForPhoto,
shouldShowLensDataForPhoto, shouldShowLensDataForPhoto,
shouldShowRecipeDataForPhoto, shouldShowRecipeDataForPhoto,
titleForPhoto,
} from '.'; } from '.';
import AppGrid from '@/components/AppGrid'; import AppGrid from '@/components/AppGrid';
import ImageLarge from '@/components/image/ImageLarge'; import ImageLarge from '@/components/image/ImageLarge';
@ -260,7 +259,6 @@ export default function PhotoLarge({
photo, photo,
revalidatePhoto, revalidatePhoto,
includeFavorite: includeFavoriteInAdminMenu, includeFavorite: includeFavoriteInAdminMenu,
ariaLabel: `Admin menu for '${titleForPhoto(photo)}' photo`,
showKeyCommands: showAdminKeyCommands, showKeyCommands: showAdminKeyCommands,
}} />; }} />;

View File

@ -9,7 +9,7 @@ import { RefObject, useTransition, useRef, useEffect } from 'react';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
import ResponsiveText from '@/components/primitives/ResponsiveText'; import ResponsiveText from '@/components/primitives/ResponsiveText';
import { useAppText } from '@/i18n/state/client'; import { useAppText } from '@/i18n/state/client';
import { uploadPhotoFromClient } from './storage'; import { uploadTempPhotoFromClient } from './storage';
export default function PhotoUploadWithStatus({ export default function PhotoUploadWithStatus({
inputRef, inputRef,
@ -119,7 +119,7 @@ export default function PhotoUploadWithStatus({
}, },
}); });
} else { } else {
return uploadPhotoFromClient( return uploadTempPhotoFromClient(
blob, blob,
extension, extension,
) )

View File

@ -22,7 +22,10 @@ import {
convertPhotoToFormData, convertPhotoToFormData,
} from './form'; } from './form';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { deleteFile } from '@/platforms/storage'; import {
deleteFile,
getFileNamePartsFromStorageUrl,
} from '@/platforms/storage';
import { import {
revalidateAdminPaths, revalidateAdminPaths,
revalidateAllKeysAndPaths, revalidateAllKeysAndPaths,
@ -58,7 +61,10 @@ import {
} from '@/app/config'; } from '@/app/config';
import { generateAiImageQueries } from './ai/server'; import { generateAiImageQueries } from './ai/server';
import { createStreamableValue } from '@ai-sdk/rsc'; import { createStreamableValue } from '@ai-sdk/rsc';
import { convertUploadToPhoto } from './storage/server'; import {
convertUploadToPhoto,
storeOptimizedPhotosForUrl,
} from './storage/server';
import { UrlAddStatus } from '@/admin/AdminUploadsClient'; import { UrlAddStatus } from '@/admin/AdminUploadsClient';
import { convertStringToArray } from '@/utility/string'; import { convertStringToArray } from '@/utility/string';
import { after } from 'next/server'; import { after } from 'next/server';
@ -74,6 +80,7 @@ import {
upgradeTagToAlbum, upgradeTagToAlbum,
} from '@/album/server'; } from '@/album/server';
import { addPhotoAlbumIds } from '@/album/query'; import { addPhotoAlbumIds } from '@/album/query';
import { getStorageUrlsForPhoto } from './storage';
// Private actions // Private actions
@ -512,6 +519,29 @@ export const renamePhotoRecipeGloballyAction = async (formData: FormData) =>
} }
}); });
export const replacePhotoStorageAction = async (
photoId: string,
updatedStorageUrl: string,
) =>
runAuthenticatedAdminServerAction(async () => {
const photo = await getPhoto(photoId, true);
if (photo) {
const {
fileExtension: extension,
} = getFileNamePartsFromStorageUrl(updatedStorageUrl);
await updatePhoto(convertPhotoToPhotoDbInsert({
...photo,
url: updatedStorageUrl,
extension,
}));
await storeOptimizedPhotosForUrl(updatedStorageUrl);
const existingStorageUrls = await getStorageUrlsForPhoto(photo)
.then(urls => urls.map(({ url }) => url));
await Promise.all(existingStorageUrls.map(deleteFile));
revalidatePhoto(photo.id);
}
});
export const deleteUploadsAction = async (urls: string[]) => export const deleteUploadsAction = async (urls: string[]) =>
runAuthenticatedAdminServerAction(async () => { runAuthenticatedAdminServerAction(async () => {
await Promise.all(urls.map(url => deleteFile(url))); await Promise.all(urls.map(url => deleteFile(url)));

View File

@ -72,7 +72,7 @@ const THUMBNAIL_SIZE = 300;
export default function PhotoForm({ export default function PhotoForm({
type = 'create', type = 'create',
initialPhotoForm, initialPhotoForm,
photoStorageUrls, photoStorageUrls = [],
updatedExifData, updatedExifData,
updatedBlurData, updatedBlurData,
photoAlbumTitles = [], photoAlbumTitles = [],
@ -291,7 +291,11 @@ export default function PhotoForm({
const footerForField = (key: keyof PhotoFormData) => { const footerForField = (key: keyof PhotoFormData) => {
switch (key) { switch (key) {
case 'url': case 'url':
return photoStorageUrls && photoStorageUrls.length > 1 return photoStorageUrls.length === 0
? <span className="text-error">
No storage found for photo
</span>
: photoStorageUrls.length > 1
? <SmallDisclosure label="Optimized file set"> ? <SmallDisclosure label="Optimized file set">
<div className="space-y-1"> <div className="space-y-1">
{photoStorageUrls.map(({ url, size }) => { {photoStorageUrls.map(({ url, size }) => {
@ -320,7 +324,7 @@ export default function PhotoForm({
})} })}
</div> </div>
</SmallDisclosure> </SmallDisclosure>
: undefined; : null;
} }
}; };

View File

@ -71,9 +71,6 @@ export const getOptimizedUrlsFromPhotoUrl = (url: string) => {
`${urlBase}/${fileName}`); `${urlBase}/${fileName}`);
}; };
export const isUploadPathnameValid = (pathname?: string) =>
pathname?.match(new RegExp(`(?:${PREFIX_UPLOAD})\.[a-z]{1,4}`, 'i'));
export const generateRandomFileNameForPhoto = () => export const generateRandomFileNameForPhoto = () =>
generateFileNameWithId(PREFIX_PHOTO); generateFileNameWithId(PREFIX_PHOTO);
@ -83,12 +80,18 @@ export const getStorageUploadUrls = () =>
export const getStoragePhotoUrls = () => export const getStoragePhotoUrls = () =>
getStorageUrlsForPrefix(`${PREFIX_PHOTO}-`); getStorageUrlsForPrefix(`${PREFIX_PHOTO}-`);
export const uploadPhotoFromClient = ( export const uploadTempPhotoFromClient = (
file: File | Blob, file: File | Blob,
extension = EXTENSION_DEFAULT, extension = EXTENSION_DEFAULT,
) => ) =>
uploadFileFromClient(file, PREFIX_UPLOAD, extension); uploadFileFromClient(file, PREFIX_UPLOAD, extension);
export const uploadPhotoFromClient = (
file: File | Blob,
extension = EXTENSION_DEFAULT,
) =>
uploadFileFromClient(file, PREFIX_PHOTO, extension);
const getSuffixFromNextImageSize = (nextSize: NextImageSize) => const getSuffixFromNextImageSize = (nextSize: NextImageSize) =>
OPTIMIZED_FILE_SIZES.find(({ size }) => size === nextSize)?.suffix OPTIMIZED_FILE_SIZES.find(({ size }) => size === nextSize)?.suffix
?? OPTIMIZED_SUFFIX_DEFAULT; ?? OPTIMIZED_SUFFIX_DEFAULT;

View File

@ -11,10 +11,13 @@ import {
getOptimizedPhotoFileMeta, getOptimizedPhotoFileMeta,
} from '.'; } from '.';
export const storeOptimizedPhotos = async ( export const storeOptimizedPhotosForUrl = async (
url: string, url: string,
fileBytes: ArrayBuffer, _fileBytes?: ArrayBuffer,
) => { ) => {
const fileBytes = _fileBytes
? _fileBytes
: await fetch(url).then(res => res.arrayBuffer());
const { fileNameBase } = getFileNamePartsFromStorageUrl(url); const { fileNameBase } = getFileNamePartsFromStorageUrl(url);
const optimizedPhotoFileMeta = getOptimizedPhotoFileMeta(fileNameBase); const optimizedPhotoFileMeta = getOptimizedPhotoFileMeta(fileNameBase);
for (const { fileName, size, quality } of optimizedPhotoFileMeta) { for (const { fileName, size, quality } of optimizedPhotoFileMeta) {
@ -55,7 +58,7 @@ export const convertUploadToPhoto = async ({
} }
// Store optimized photos after original photo is copied/moved // Store optimized photos after original photo is copied/moved
const updatedUrl = await promise const updatedUrl = await promise
.then(async url => storeOptimizedPhotos(url, fileBytes)); .then(async url => storeOptimizedPhotosForUrl(url, fileBytes));
return updatedUrl; return updatedUrl;
}; };

View File

@ -114,32 +114,33 @@ export const storageTypeFromUrl = (url: string): StorageType => {
export const uploadFromClientViaPresignedUrl = async ( export const uploadFromClientViaPresignedUrl = async (
file: File | Blob, file: File | Blob,
fileNameBase: string, fileName: string,
extension: string,
addRandomSuffix?: boolean,
) => { ) => {
const key = addRandomSuffix const url = await fetch(`${PATH_API_PRESIGNED_URL}/${fileName}`)
? `${fileNameBase}-${generateStorageId()}.${extension}`
: `${fileNameBase}.${extension}`;
const url = await fetch(`${PATH_API_PRESIGNED_URL}/${key}`)
.then((response) => response.text()); .then((response) => response.text());
return fetch(url, { method: 'PUT', body: file }) return fetch(url, { method: 'PUT', body: file })
.then(() => `${baseUrlForStorage(CURRENT_STORAGE)}/${key}`); .then(() => `${baseUrlForStorage(CURRENT_STORAGE)}/${fileName}`);
}; };
export const uploadFileFromClient = async ( export const uploadFileFromClient = async (
file: File | Blob, file: File | Blob,
fileNameBase: string, _fileName: string,
extension: string, extension: string,
) => ( addRandomSuffix = true,
) => {
const fileName = addRandomSuffix
? `${_fileName}-${generateStorageId()}.${extension}`
: `${_fileName}.${extension}`;
return (
CURRENT_STORAGE === 'cloudflare-r2' || CURRENT_STORAGE === 'cloudflare-r2' ||
CURRENT_STORAGE === 'aws-s3' || CURRENT_STORAGE === 'aws-s3' ||
CURRENT_STORAGE === 'minio' CURRENT_STORAGE === 'minio'
) )
? uploadFromClientViaPresignedUrl(file, fileNameBase, extension, true) ? uploadFromClientViaPresignedUrl(file, fileName)
: vercelBlobUploadFromClient(file, `${fileNameBase}.${extension}`); : vercelBlobUploadFromClient(file, fileName);
};
export const putFile = ( export const putFile = (
file: Buffer, file: Buffer,