diff --git a/components/admin/upload/livephoto-file-upload.tsx b/components/admin/upload/livephoto-file-upload.tsx index 3004239e..067588cf 100644 --- a/components/admin/upload/livephoto-file-upload.tsx +++ b/components/admin/upload/livephoto-file-upload.tsx @@ -52,6 +52,7 @@ export default function LivephotoFileUpload() { const [exif, setExif] = useState({} as ExifType) const [title, setTitle] = useState('') const [url, setUrl] = useState('') + const [imageName, setImageName] = useState('') const [previewUrl, setPreviewUrl] = useState('') const [videoUrl, setVideoUrl] = useState('') const [loading, setLoading] = useState(false) @@ -124,6 +125,7 @@ export default function LivephotoFileUpload() { album: album, url: url, title: title, + image_name: imageName, preview_url: previewUrl, blurhash: hash, video_url: videoUrl, @@ -167,6 +169,8 @@ export default function LivephotoFileUpload() { await loadExif(processedFile) setHash(await encodeBrowserThumbHash(processedFile)) setUrl(res?.data?.url) + // Preserve the original uploaded filename for the download default name. + setImageName(processedFile.name) } async function onRequestVideoUpload(file: File) { @@ -181,6 +185,7 @@ export default function LivephotoFileUpload() { async function onBeforeUpload(type: number) { if (type === 1) { setTitle('') + setImageName('') setPreviewUrl('') setVideoUrl('') setImageLabels([]) @@ -192,6 +197,7 @@ export default function LivephotoFileUpload() { setExif({} as ExifType) setHash('') setUrl('') + setImageName('') setTitle('') setDetail('') setWidth(0) diff --git a/components/admin/upload/multiple-file-upload.tsx b/components/admin/upload/multiple-file-upload.tsx index 2e3debde..d9e5466b 100644 --- a/components/admin/upload/multiple-file-upload.tsx +++ b/components/admin/upload/multiple-file-upload.tsx @@ -78,6 +78,11 @@ export default function MultipleFileUpload() { album: album, url: url, title: '', + // Preserve the original uploaded filename so downloads default to it + // instead of the randomized storage key. `file` here is the + // (post-HEIC-conversion) uploaded file, so its name matches the + // stored bytes (e.g. `foo.jpg` for a converted `foo.heic`). + image_name: file.name, preview_url: previewUrl, blurhash: hash, exif: exifObj, diff --git a/hono/images.ts b/hono/images.ts index 14c9eb86..3076fbde 100644 --- a/hono/images.ts +++ b/hono/images.ts @@ -39,6 +39,14 @@ app.post('/', async (c) => { if (body?.exif?.dateTime && !dayjs(body?.exif?.dateTime, 'YYYY:MM:DD HH:mm:ss', true).isValid()) { body.exif.dateTime = '' } + // Reduce the preserved original filename to a bare basename (defense in + // depth — `File.name` from the client is already a basename, but strip any + // path separators so it stays safe as the download Content-Disposition + // value). Empty/whitespace falls back to null → download derives the name + // from the URL, i.e. the prior behaviour. + if (typeof body.image_name === 'string') { + body.image_name = body.image_name.split(/[\\/]/).pop()?.trim() || null + } // New images are stored with variants_ready=false; the background // preprocess ticker (see instrumentation.ts) picks them up asynchronously // and generates variants via a tracked /admin/tasks run — no inline work