feat: sync full workspace including web modules, docs, and configurations to Gitea
Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
99
axhub-make/vite-plugins/exportImageProxyPlugin.ts
Normal file
99
axhub-make/vite-plugins/exportImageProxyPlugin.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
import { getRequestPathname, serializeErrorForLog } from './utils/httpUtils';
|
||||
import { isAllowedProxyImageUrl } from './utils/proxyUtils';
|
||||
|
||||
export function exportImageProxyPlugin(): Plugin {
|
||||
return {
|
||||
name: 'export-image-proxy-plugin',
|
||||
configureServer(server: any) {
|
||||
server.middlewares.use(async (req: any, res: any, next: any) => {
|
||||
const pathname = getRequestPathname(req);
|
||||
if (req.method !== 'GET' || pathname !== '/api/export/image-proxy') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
||||
const targetUrl = String(requestUrl.searchParams.get('url') || '').trim();
|
||||
|
||||
if (!targetUrl) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({ error: 'Missing url query parameter' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAllowedProxyImageUrl(targetUrl)) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({ error: 'Unsupported proxy target url' }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const upstreamResponse = await fetch(targetUrl, {
|
||||
method: 'GET',
|
||||
redirect: 'follow',
|
||||
headers: {
|
||||
Accept: 'image/*,*/*;q=0.8',
|
||||
'User-Agent': 'AxhubMakeExportProxy/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
if (!upstreamResponse.ok) {
|
||||
res.statusCode = upstreamResponse.status;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({
|
||||
error: `Upstream responded with ${upstreamResponse.status}`,
|
||||
targetUrl,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = String(upstreamResponse.headers.get('content-type') || '').toLowerCase();
|
||||
if (contentType && !contentType.startsWith('image/')) {
|
||||
res.statusCode = 415;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({
|
||||
error: `Unsupported upstream content-type: ${contentType}`,
|
||||
targetUrl,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const body = Buffer.from(await upstreamResponse.arrayBuffer());
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cache-Control', upstreamResponse.headers.get('cache-control') || 'public, max-age=600');
|
||||
res.setHeader('Content-Type', contentType || 'application/octet-stream');
|
||||
res.setHeader('Content-Length', String(body.byteLength));
|
||||
|
||||
const etag = upstreamResponse.headers.get('etag');
|
||||
if (etag) {
|
||||
res.setHeader('ETag', etag);
|
||||
}
|
||||
|
||||
const lastModified = upstreamResponse.headers.get('last-modified');
|
||||
if (lastModified) {
|
||||
res.setHeader('Last-Modified', lastModified);
|
||||
}
|
||||
|
||||
res.end(body);
|
||||
} catch (error: any) {
|
||||
console.error('[export-image-proxy] request failed', {
|
||||
targetUrl,
|
||||
error: serializeErrorForLog(error),
|
||||
});
|
||||
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({
|
||||
error: error?.message || 'Failed to fetch target image',
|
||||
targetUrl,
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user