Files: sturdier Seafile pass-through (timeouts, retries, stop on disconnect), in-place sign-in, saved folder lists, upload name clashes, undo and recycle bin, library search, zip downloads, share link options, text viewer and editor
build-container / build (push) Successful in 20m19s
build-container / build (push) Successful in 20m19s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
004209ed4f
commit
cedd6845ce
11 files changed
+399
-64
No files matched your search
@@ -9,6 +9,7 @@ const { Readable } = require('stream');
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const SEAFILE_URL = (process.env.SEAFILE_URL || 'https://seafile.necloud.us').replace(/\/$/, '');
|
||||
const SEAFILE_TIMEOUT = Math.max(1000, parseInt(process.env.SEAFILE_TIMEOUT_MS, 10) || 30000); // (the tests shorten this)
|
||||
const ONLYOFFICE_URL = (process.env.ONLYOFFICE_URL || 'https://onlyoffice.necloud.us').replace(/\/$/, '');
|
||||
const AUDIOBOOKS_URL = (process.env.AUDIOBOOKS_URL || 'https://audiobooks.necloud.us').replace(/\/$/, '');
|
||||
const AUDIOBOOKS_SSO_URL = process.env.AUDIOBOOKS_SSO_URL || AUDIOBOOKS_URL + '/sso';
|
||||
@@ -46,43 +47,60 @@ const TYPES = {
|
||||
'.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon',
|
||||
};
|
||||
|
||||
// Seafile calls get 30 s to start answering, file transfers (seafhttp) 2 minutes; a failed read is retried twice; and when the browser goes away
|
||||
// (tab closed, download cancelled) the upstream request is stopped instead of being left running.
|
||||
async function proxySeafile(req, res) {
|
||||
const tu = new URL(SEAFILE_URL + req.url.slice('/sf'.length));
|
||||
const inline = tu.searchParams.has('hubinline'); // preview: show in the page instead of forcing a download
|
||||
tu.searchParams.delete('hubinline');
|
||||
const target = tu.toString();
|
||||
const headers = {};
|
||||
for (const h of ['authorization', 'content-type', 'content-length', 'accept', 'range']) {
|
||||
for (const h of ['authorization', 'content-type', 'content-length', 'accept', 'range', 'if-range']) {
|
||||
if (req.headers[h]) headers[h] = req.headers[h];
|
||||
}
|
||||
const hasBody = !['GET', 'HEAD'].includes(req.method);
|
||||
try {
|
||||
const upstream = await fetch(target, {
|
||||
method: req.method, headers, redirect: 'manual',
|
||||
body: hasBody ? Readable.toWeb(req) : undefined,
|
||||
duplex: hasBody ? 'half' : undefined,
|
||||
});
|
||||
const out = {};
|
||||
for (const h of ['content-type', 'content-length', 'content-disposition', 'content-range', 'accept-ranges']) {
|
||||
if (inline && h === 'content-disposition') continue;
|
||||
if (h === 'content-length' && upstream.headers.get('content-encoding')) continue; // decompressed by fetch, so the length changed
|
||||
const v = upstream.headers.get(h);
|
||||
if (v) out[h] = v;
|
||||
const hasBody = !['GET', 'HEAD'].includes(req.method), canRetry = !hasBody;
|
||||
const wait = /^\/seafhttp\//.test(tu.pathname) ? 120000 : SEAFILE_TIMEOUT;
|
||||
let gone = false, ac = null; res.on('close', () => { if (!res.writableFinished) { gone = true; if (ac) ac.abort(); } });
|
||||
let upstream, lastErr;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
if (gone) return;
|
||||
ac = new AbortController(); let timedOut = false;
|
||||
const timer = setTimeout(() => { timedOut = true; ac.abort(); }, wait);
|
||||
try {
|
||||
upstream = await fetch(target, { method: req.method, headers, redirect: 'manual', signal: ac.signal,
|
||||
body: hasBody ? Readable.toWeb(req) : undefined, duplex: hasBody ? 'half' : undefined });
|
||||
clearTimeout(timer);
|
||||
if (canRetry && [502, 503, 504].includes(upstream.status) && attempt < 2) { try { await upstream.body.cancel(); } catch {} await new Promise(r => setTimeout(r, 300 * (attempt + 1) ** 2)); continue; }
|
||||
break;
|
||||
} catch (e) {
|
||||
clearTimeout(timer); upstream = null; lastErr = timedOut ? Object.assign(new Error('Seafile took too long to answer'), { timeout: true }) : e;
|
||||
if (gone) return;
|
||||
if (canRetry && attempt < 2) { await new Promise(r => setTimeout(r, 300 * (attempt + 1) ** 2)); continue; }
|
||||
break;
|
||||
}
|
||||
// Rewrite redirects so they stay on the proxy and on the Seafile host.
|
||||
const loc = upstream.headers.get('location');
|
||||
if (loc) {
|
||||
const u = new URL(loc, SEAFILE_URL);
|
||||
if (u.origin === new URL(SEAFILE_URL).origin) out.location = '/sf' + u.pathname + u.search;
|
||||
}
|
||||
res.writeHead(upstream.status, out);
|
||||
if (upstream.body) Readable.fromWeb(upstream.body).pipe(res); else res.end();
|
||||
} catch (e) {
|
||||
res.writeHead(502, { 'content-type': 'application/json' });
|
||||
const why = e.cause ? (e.cause.code || e.cause.message) : '';
|
||||
console.error('Seafile proxy:', target.split('?')[0], e.message, why);
|
||||
res.end(JSON.stringify({ error: 'Seafile unreachable: ' + e.message + (why ? ' (' + why + ')' : '') }));
|
||||
}
|
||||
if (!upstream) {
|
||||
const e = lastErr, why = e && e.cause ? (e.cause.code || e.cause.message) : '';
|
||||
console.error('Seafile proxy:', target.split('?')[0], e && e.message, why);
|
||||
if (res.headersSent) return res.destroy();
|
||||
res.writeHead(e && e.timeout ? 504 : 502, { 'content-type': 'application/json' });
|
||||
return res.end(JSON.stringify({ error: e && e.timeout ? 'Seafile took too long to answer. Try again in a moment.' : 'Seafile unreachable: ' + (e && e.message) + (why ? ' (' + why + ')' : '') }));
|
||||
}
|
||||
const out = {};
|
||||
for (const h of ['content-type', 'content-length', 'content-disposition', 'content-range', 'accept-ranges', 'etag', 'last-modified', 'retry-after']) {
|
||||
if (inline && h === 'content-disposition') continue;
|
||||
if (h === 'content-length' && upstream.headers.get('content-encoding')) continue; // decompressed by fetch, so the length changed
|
||||
const v = upstream.headers.get(h);
|
||||
if (v) out[h] = v;
|
||||
}
|
||||
// Rewrite redirects so they stay on the proxy and on the Seafile host.
|
||||
const loc = upstream.headers.get('location');
|
||||
if (loc) {
|
||||
const u = new URL(loc, SEAFILE_URL);
|
||||
if (u.origin === new URL(SEAFILE_URL).origin) out.location = '/sf' + u.pathname + u.search;
|
||||
}
|
||||
res.writeHead(upstream.status, out);
|
||||
if (upstream.body) Readable.fromWeb(upstream.body).on('error', () => res.destroy()).pipe(res); else res.end();
|
||||
}
|
||||
|
||||
function serveStatic(req, res) {
|
||||
|
||||
Reference in new issue
Block a user