diff --git a/api/filebrowser.js b/api/filebrowser.js index 0080da3..3aa10d6 100644 --- a/api/filebrowser.js +++ b/api/filebrowser.js @@ -363,9 +363,28 @@ function toListResponse(vPath, entries, { readOnly = false } = {}) { }; } +export const config = { api: { bodyParser: false, sizeLimit: '50mb' } }; + +async function readBody(req) { + return new Promise((resolve, reject) => { + const chunks = []; + req.on('data', chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); + req.on('end', () => resolve(Buffer.concat(chunks))); + req.on('error', reject); + }); +} + export default async function handler(req, res) { try { - const queryToken = String(req.query?.sb_access_token || req.body?.sb_access_token || '').trim(); + const rawBody = await readBody(req); + const body = (() => { + if (!rawBody.length) return {}; + const ct = (req.headers['content-type'] || '').split(';')[0].trim(); + if (ct === 'application/json') { try { return JSON.parse(rawBody.toString()); } catch { return {}; } } + return {}; + })(); + + const queryToken = String(req.query?.sb_access_token || body?.sb_access_token || '').trim(); const authHeader = req.headers.authorization || (queryToken ? `Bearer ${queryToken}` : ''); if (!authHeader) return json(res, 401, { error: 'No authorization header' }); @@ -382,7 +401,7 @@ export default async function handler(req, res) { } const action = req.query.action || (req.method === 'GET' ? 'list' : ''); - const requestedPath = req.query.path || req.body?.path || '/'; + const requestedPath = req.query.path || body?.path || '/'; let externalProjects = []; if (auth.profile.role === 'external') { @@ -467,8 +486,23 @@ export default async function handler(req, res) { return json(res, 200, { token, url: config.url, fbPath: resolved.fbPath }); } + if (req.method === 'POST' && action === 'upload') { + const resolved = toFbPath(); + if (resolved.virtual) return json(res, 400, { error: 'Cannot upload to virtual directory' }); + const contentType = (req.headers['content-type'] || 'application/octet-stream').split(';')[0].trim(); + const uploadUrl = `${config.url}/api/resources?source=${FB_SOURCE}&path=${encodeURIComponent(resolved.fbPath)}&override=true`; + let token = await getToken(config); + let upRes = await fetch(uploadUrl, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': contentType }, body: rawBody }); + if (upRes.status === 401) { + token = await getToken(config, true); + upRes = await fetch(uploadUrl, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': contentType }, body: rawBody }); + } + if (!upRes.ok) { const t = await upRes.text(); return json(res, upRes.status, { error: t || `Upload failed (${upRes.status})` }); } + return json(res, 200, { success: true }); + } + if (req.method === 'POST' && action === 'mkdir') { - const folderName = safeName(req.body?.name, ''); + const folderName = safeName(body?.name, ''); if (!folderName) return json(res, 400, { error: 'Folder name required' }); const resolved = toFbPath(); @@ -490,7 +524,7 @@ export default async function handler(req, res) { } if (req.method === 'POST' && action === 'rename') { - const newName = safeName(req.body?.name, ''); + const newName = safeName(body?.name, ''); if (!newName) return json(res, 400, { error: 'New name required' }); const resolved = toFbPath(); @@ -511,8 +545,8 @@ export default async function handler(req, res) { if (req.method === 'POST' && action === 'archive-move') { // Moves srcPath into dstParentPath. If destination already exists, merges contents recursively. - const srcVPath = req.body?.srcPath; - const dstParentVPath = req.body?.dstParentPath; + const srcVPath = body?.srcPath; + const dstParentVPath = body?.dstParentPath; if (!srcVPath || !dstParentVPath) return json(res, 400, { error: 'srcPath and dstParentPath required' }); const resolvedSrc = toFbPath(srcVPath); @@ -558,9 +592,9 @@ export default async function handler(req, res) { } if (req.method === 'POST' && action === 'move') { - const srcPath = req.body?.srcPath; - const dstPath = req.body?.dstPath; - const mode = req.body?.mode === 'copy' ? 'copy' : 'move'; + const srcPath = body?.srcPath; + const dstPath = body?.dstPath; + const mode = body?.mode === 'copy' ? 'copy' : 'move'; if (!srcPath || !dstPath) return json(res, 400, { error: 'srcPath and dstPath required' }); const resolvedSrc = toFbPath(srcPath); diff --git a/src/components/FileBrowser.jsx b/src/components/FileBrowser.jsx index 45aa475..77ce220 100644 --- a/src/components/FileBrowser.jsx +++ b/src/components/FileBrowser.jsx @@ -211,13 +211,15 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) { finally { setWorking(''); } }; - async function uploadOneFile(url, token, fbPath, file, retries = 3) { + async function uploadOneFile(vPath, file, retries = 3) { for (let attempt = 1; attempt <= retries; attempt++) { try { - const res = await fetch(`${url}/api/resources?source=srv&path=${encodeURIComponent(fbPath)}&override=true&auth=${encodeURIComponent(token)}`, { - method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: file, + const res = await apiFetch(`/api/filebrowser?action=upload&path=${encodeURIComponent(vPath)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/octet-stream' }, + body: file, }); - if (!res.ok) { const t = await res.text(); throw new Error(t || res.status); } + if (!res) return; return; } catch (err) { if (attempt === retries) throw err; @@ -231,10 +233,8 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) { if (!sel.length) return; setWorking('upload'); setUploadProgress(0); setError(''); try { - const tokenData = await apiFetch('/api/filebrowser?action=upload-token', { method: 'POST', body: JSON.stringify({ path: currentPath }) }); - const { token, url, fbPath } = tokenData; for (let i = 0; i < sel.length; i++) { - await uploadOneFile(url, token, joinVirtualPath(fbPath, sel[i].name), sel[i]); + await uploadOneFile(joinVirtualPath(currentPath, sel[i].name), sel[i]); setUploadProgress(Math.round(((i + 1) / sel.length) * 100)); } } catch (err) { setError(err.message); } @@ -250,18 +250,16 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) { if (!sel.length) return; setWorking('upload'); setUploadProgress(0); setError(''); try { - const tokenData = await apiFetch('/api/filebrowser?action=upload-token', { method: 'POST', body: JSON.stringify({ path: currentPath }) }); - const { token, url, fbPath } = tokenData; const dirsNeeded = new Set(); for (const file of sel) { const parts = file.webkitRelativePath.split('/').slice(0, -1); for (let i = 1; i <= parts.length; i++) dirsNeeded.add(parts.slice(0, i).join('/')); } for (const dir of [...dirsNeeded].sort((a, b) => a.split('/').length - b.split('/').length)) { - await fetch(`${url}/api/resources?source=srv&path=${encodeURIComponent(joinVirtualPath(fbPath, dir))}&isDir=true&auth=${encodeURIComponent(token)}`, { method: 'POST' }).catch(() => {}); + await apiFetch(`/api/filebrowser?action=mkdir`, { method: 'POST', body: JSON.stringify({ path: currentPath, name: dir.split('/').pop() }) }).catch(() => {}); } for (let i = 0; i < sel.length; i++) { - await uploadOneFile(url, token, joinVirtualPath(fbPath, sel[i].webkitRelativePath), sel[i]); + await uploadOneFile(joinVirtualPath(currentPath, sel[i].webkitRelativePath), sel[i]); setUploadProgress(Math.round(((i + 1) / sel.length) * 100)); } } catch (err) { setError(err.message); } diff --git a/src/pages/FileSharing.jsx b/src/pages/FileSharing.jsx index 2cb16d8..ae980cc 100644 --- a/src/pages/FileSharing.jsx +++ b/src/pages/FileSharing.jsx @@ -207,26 +207,11 @@ async function fbqProxy(op, path, extra = {}) { } if (op === 'upload') { - const normalized = String(path || '/'); - const parts = normalized.split('/').filter(Boolean); - const fileName = parts.pop() || ''; - const parentPath = `/${parts.join('/')}` || '/'; - const tokenResponse = await fetch(`/api/filebrowser?action=upload-token&sb_access_token=${encodeURIComponent(accessToken)}`, { - method: 'POST', - headers: { ...authHeaders, 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: parentPath }), - }); - if (!tokenResponse.ok) { - const text = await tokenResponse.text(); - throw new Error(text || `Error ${tokenResponse.status}`); - } - const { token, url, fbPath } = await tokenResponse.json(); - if (!token || !url || !fbPath) throw new Error('Upload token unavailable.'); const file = extra.body?.get?.('file'); if (!file) throw new Error('No file provided.'); - const uploadResponse = await fetch(`${url}/api/resources?source=srv&path=${encodeURIComponent(`${fbPath}/${fileName}`)}&override=true&auth=${encodeURIComponent(token)}`, { + const uploadResponse = await fetch(`/api/filebrowser?action=upload&path=${encodeURIComponent(path)}&sb_access_token=${encodeURIComponent(accessToken)}`, { method: 'POST', - headers: { 'Content-Type': 'application/octet-stream' }, + headers: { ...authHeaders, 'Content-Type': 'application/octet-stream' }, body: file, }); if (!uploadResponse.ok) {