fix: proxy file uploads server-side via action=upload, fixes CORS for all roles
This commit is contained in:
+43
-9
@@ -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) {
|
export default async function handler(req, res) {
|
||||||
try {
|
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}` : '');
|
const authHeader = req.headers.authorization || (queryToken ? `Bearer ${queryToken}` : '');
|
||||||
if (!authHeader) return json(res, 401, { error: 'No authorization header' });
|
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 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 = [];
|
let externalProjects = [];
|
||||||
if (auth.profile.role === 'external') {
|
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 });
|
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') {
|
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' });
|
if (!folderName) return json(res, 400, { error: 'Folder name required' });
|
||||||
|
|
||||||
const resolved = toFbPath();
|
const resolved = toFbPath();
|
||||||
@@ -490,7 +524,7 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'POST' && action === 'rename') {
|
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' });
|
if (!newName) return json(res, 400, { error: 'New name required' });
|
||||||
|
|
||||||
const resolved = toFbPath();
|
const resolved = toFbPath();
|
||||||
@@ -511,8 +545,8 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
if (req.method === 'POST' && action === 'archive-move') {
|
if (req.method === 'POST' && action === 'archive-move') {
|
||||||
// Moves srcPath into dstParentPath. If destination already exists, merges contents recursively.
|
// Moves srcPath into dstParentPath. If destination already exists, merges contents recursively.
|
||||||
const srcVPath = req.body?.srcPath;
|
const srcVPath = body?.srcPath;
|
||||||
const dstParentVPath = req.body?.dstParentPath;
|
const dstParentVPath = body?.dstParentPath;
|
||||||
if (!srcVPath || !dstParentVPath) return json(res, 400, { error: 'srcPath and dstParentPath required' });
|
if (!srcVPath || !dstParentVPath) return json(res, 400, { error: 'srcPath and dstParentPath required' });
|
||||||
|
|
||||||
const resolvedSrc = toFbPath(srcVPath);
|
const resolvedSrc = toFbPath(srcVPath);
|
||||||
@@ -558,9 +592,9 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'POST' && action === 'move') {
|
if (req.method === 'POST' && action === 'move') {
|
||||||
const srcPath = req.body?.srcPath;
|
const srcPath = body?.srcPath;
|
||||||
const dstPath = req.body?.dstPath;
|
const dstPath = body?.dstPath;
|
||||||
const mode = req.body?.mode === 'copy' ? 'copy' : 'move';
|
const mode = body?.mode === 'copy' ? 'copy' : 'move';
|
||||||
if (!srcPath || !dstPath) return json(res, 400, { error: 'srcPath and dstPath required' });
|
if (!srcPath || !dstPath) return json(res, 400, { error: 'srcPath and dstPath required' });
|
||||||
|
|
||||||
const resolvedSrc = toFbPath(srcPath);
|
const resolvedSrc = toFbPath(srcPath);
|
||||||
|
|||||||
@@ -211,13 +211,15 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
|
|||||||
finally { setWorking(''); }
|
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++) {
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${url}/api/resources?source=srv&path=${encodeURIComponent(fbPath)}&override=true&auth=${encodeURIComponent(token)}`, {
|
const res = await apiFetch(`/api/filebrowser?action=upload&path=${encodeURIComponent(vPath)}`, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: file,
|
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;
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (attempt === retries) throw err;
|
if (attempt === retries) throw err;
|
||||||
@@ -231,10 +233,8 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
|
|||||||
if (!sel.length) return;
|
if (!sel.length) return;
|
||||||
setWorking('upload'); setUploadProgress(0); setError('');
|
setWorking('upload'); setUploadProgress(0); setError('');
|
||||||
try {
|
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++) {
|
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));
|
setUploadProgress(Math.round(((i + 1) / sel.length) * 100));
|
||||||
}
|
}
|
||||||
} catch (err) { setError(err.message); }
|
} catch (err) { setError(err.message); }
|
||||||
@@ -250,18 +250,16 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
|
|||||||
if (!sel.length) return;
|
if (!sel.length) return;
|
||||||
setWorking('upload'); setUploadProgress(0); setError('');
|
setWorking('upload'); setUploadProgress(0); setError('');
|
||||||
try {
|
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();
|
const dirsNeeded = new Set();
|
||||||
for (const file of sel) {
|
for (const file of sel) {
|
||||||
const parts = file.webkitRelativePath.split('/').slice(0, -1);
|
const parts = file.webkitRelativePath.split('/').slice(0, -1);
|
||||||
for (let i = 1; i <= parts.length; i++) dirsNeeded.add(parts.slice(0, i).join('/'));
|
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)) {
|
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++) {
|
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));
|
setUploadProgress(Math.round(((i + 1) / sel.length) * 100));
|
||||||
}
|
}
|
||||||
} catch (err) { setError(err.message); }
|
} catch (err) { setError(err.message); }
|
||||||
|
|||||||
@@ -207,26 +207,11 @@ async function fbqProxy(op, path, extra = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (op === 'upload') {
|
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');
|
const file = extra.body?.get?.('file');
|
||||||
if (!file) throw new Error('No file provided.');
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/octet-stream' },
|
headers: { ...authHeaders, 'Content-Type': 'application/octet-stream' },
|
||||||
body: file,
|
body: file,
|
||||||
});
|
});
|
||||||
if (!uploadResponse.ok) {
|
if (!uploadResponse.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user