diff --git a/package.json b/package.json index 69b9722..76a209f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.0.38", + "version": "2.0.39", "type": "module", "scripts": { "dev": "vite", diff --git a/server/routes/admin-assets.js b/server/routes/admin-assets.js index 8319dd9..222e51a 100644 --- a/server/routes/admin-assets.js +++ b/server/routes/admin-assets.js @@ -37,10 +37,12 @@ export function register(app) { const filename = typeof req.body?.filename === 'string' ? req.body.filename : '' const dataUrl = typeof req.body?.dataUrl === 'string' ? req.body.dataUrl : '' const rawFolder = typeof req.body?.folder === 'string' ? req.body.folder : 'uploads' + const rawStorage = typeof req.body?.storage === 'string' ? req.body.storage : 'r2' const ext = inferImageExtensionFromDataUrl(dataUrl) const ALLOWED_FOLDERS = ['uploads', 'images', 'guides', 'finished'] const folder = ALLOWED_FOLDERS.includes(rawFolder) ? rawFolder : 'uploads' + const useLocal = rawStorage === 'local' || !isR2Configured() if (!ext) { res.status(400).json({ message: 'Upload must be a PNG, JPG, WEBP, GIF, PDF, DOC, or DOCX data URL.' }) @@ -58,20 +60,20 @@ export function register(app) { const finalName = `${baseName}-${Date.now()}${ext}` let url - if (isR2Configured()) { - const contentType = dataUrl.match(/^data:([^;]+);base64,/)?.[1] ?? 'application/octet-stream' - url = await uploadToR2(buffer, `${folder}/${finalName}`, contentType) - } else { + if (useLocal) { await mkdir(UPLOADS_DIR, { recursive: true }) await writeFile(path.join(UPLOADS_DIR, finalName), buffer) url = `/uploads/${finalName}` + } else { + const contentType = dataUrl.match(/^data:([^;]+);base64,/)?.[1] ?? 'application/octet-stream' + url = await uploadToR2(buffer, `${folder}/${finalName}`, contentType) } const metadata = await readUploadsMetadata() - metadata[finalName] = { tags: [], url, folder } + metadata[finalName] = { tags: [], url, folder: useLocal ? 'local' : folder } await writeUploadsMetadata(metadata) - res.json({ ok: true, asset: { filename: finalName, url, folder } }) + res.json({ ok: true, asset: { filename: finalName, url, folder: useLocal ? 'local' : folder, storage: useLocal ? 'local' : 'r2' } }) } catch (err) { console.error('[admin-assets] upload failed:', err) res.status(500).json({ message: 'Upload failed.' }) diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index 1465ee7..681567e 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1553,6 +1553,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) { const [assetTagEdits, setAssetTagEdits] = useState>({}) const [opsStatus, setOpsStatus] = useState(null) const [assetUploadPending, setAssetUploadPending] = useState(false) + const [assetUploadDest, setAssetUploadDest] = useState('r2:uploads') const [endSeriesOpen, setEndSeriesOpen] = useState(false) const [endSeriesNewTitle, setEndSeriesNewTitle] = useState('') @@ -2196,10 +2197,13 @@ async function handleAssetUpload(event: ChangeEvent) { reader.readAsDataURL(file) }) + const [storage, folder] = assetUploadDest === 'local' + ? ['local', 'uploads'] + : ['r2', assetUploadDest.replace('r2:', '')] const res = await fetch('/api/admin-assets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename: file.name, dataUrl }), + body: JSON.stringify({ filename: file.name, dataUrl, storage, folder }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) @@ -5781,6 +5785,19 @@ async function handleAssetUpload(event: ChangeEvent) {

Upload, tag, and reuse hosted images from this domain.

+