feat(server): ✨ Serve files locally instead of proxying from the CDN
This commit is contained in:
parent
2d90e1e2b3
commit
1791831f8f
|
@ -1,13 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "test-repo",
|
"name": "selfh-st_icons",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2"
|
||||||
"node-fetch": "^3.3.0"
|
}
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,89 +1,88 @@
|
||||||
import express from 'express'
|
import fs from 'fs'
|
||||||
import fetch from 'node-fetch'
|
import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
const PORT = 4050
|
const PORT = 4050
|
||||||
const CDN_ROOT = 'https://cdn.jsdelivr.net/gh/selfhst/icons'
|
|
||||||
const CDN_PATH = 'svg'
|
const APP_DIR = process.cwd()
|
||||||
|
|
||||||
async function fileExists(url) {
|
async function readFile(filePath, res) {
|
||||||
try {
|
const readStream = fs.createReadStream(filePath);
|
||||||
const resp = await fetch(url, { method: 'HEAD' });
|
|
||||||
return resp.ok;
|
readStream.on("open", () => {
|
||||||
} catch {
|
res.type(path.extname(filePath).slice(1));
|
||||||
return false;
|
readStream.pipe(res);
|
||||||
}
|
});
|
||||||
}
|
readStream.on("error", () => {
|
||||||
|
res.set("Content-Type", "text/plain");
|
||||||
async function fetchAndPipe(url, res) {
|
res.status(404).end("Not found");
|
||||||
const response = await fetch(url);
|
});
|
||||||
if (!response.ok) return res.status(404).send('File not found');
|
}
|
||||||
res.type(path.extname(url).slice(1));
|
|
||||||
response.body.pipe(res);
|
app.get('/', (_req, res) => {
|
||||||
}
|
res.send('Self-hosted icon server');
|
||||||
|
});
|
||||||
app.get('/*', async (req, res) => {
|
|
||||||
const urlPath = req.path;
|
app.get('/*', async (req, res) => {
|
||||||
const extMatch = urlPath.match(/\.(\w+)$/);
|
const urlPath = req.path;
|
||||||
if (!extMatch)
|
const extMatch = urlPath.match(/\.(\w+)$/);
|
||||||
return res.status(404).send('File extension missing');
|
if (!extMatch)
|
||||||
|
return res.status(404).send('File extension missing');
|
||||||
const ext = extMatch[1].toLowerCase();
|
|
||||||
if (!['png', 'webp', 'svg'].includes(ext))
|
const ext = extMatch[1].toLowerCase();
|
||||||
return res.status(404).send('Format not supported');
|
if (!['png', 'webp', 'svg'].includes(ext))
|
||||||
|
return res.status(404).send('Format not supported');
|
||||||
const filename = urlPath.slice(1);
|
|
||||||
const lowerFilename = filename.toLowerCase();
|
const filename = urlPath.slice(1);
|
||||||
|
const lowerFilename = filename.toLowerCase();
|
||||||
const isSuffix = lowerFilename.endsWith('-light.svg') || lowerFilename.endsWith('-dark.svg');
|
|
||||||
|
const isSuffix = lowerFilename.endsWith('-light.svg') || lowerFilename.endsWith('-dark.svg');
|
||||||
if (isSuffix) {
|
|
||||||
return fetchAndPipe(`${CDN_ROOT}/${CDN_PATH}/${filename}`, res);
|
if (isSuffix) {
|
||||||
}
|
return readFile(`${APP_DIR}/svg/${filename}`, res);
|
||||||
|
}
|
||||||
let mainUrl;
|
|
||||||
if (ext === 'png') {
|
let filePath;
|
||||||
mainUrl = `${CDN_ROOT}/png/${filename}`;
|
if (ext === 'png') {
|
||||||
} else if (ext === 'webp') {
|
filePath = `${APP_DIR}/png/${filename}`;
|
||||||
mainUrl = `${CDN_ROOT}/webp/${filename}`;
|
} else if (ext === 'webp') {
|
||||||
} else if (ext === 'svg') {
|
filePath = `${APP_DIR}/webp/${filename}`;
|
||||||
mainUrl = `${CDN_ROOT}/svg/${filename}`;
|
} else if (ext === 'svg') {
|
||||||
} else {
|
filePath = `${APP_DIR}/svg/${filename}`;
|
||||||
mainUrl = null;
|
} else {
|
||||||
}
|
filePath = null;
|
||||||
|
}
|
||||||
const hasColor = !!req.query['color'] && req.query['color'].trim() !== '';
|
|
||||||
|
const hasColor = !!req.query['color'] && req.query['color'].trim() !== '';
|
||||||
if (ext === 'svg') {
|
|
||||||
if (hasColor) {
|
if (ext === 'svg') {
|
||||||
const baseName = filename.replace(/\.(png|webp|svg)$/, '');
|
if (hasColor) {
|
||||||
const suffixUrl = `${CDN_ROOT}/${CDN_PATH}/${baseName}-light.svg`;
|
const baseName = filename.replace(/\.(png|webp|svg)$/, '');
|
||||||
if (await fileExists(suffixUrl)) {
|
const baseFile = `${APP_DIR}/svg/${baseName}-light.svg`;
|
||||||
let svgContent = await fetch(suffixUrl).then(r => r.text());
|
|
||||||
const color = req.query['color'].startsWith('#') ? req.query['color'] : `#${req.query['color']}`;
|
if (fs.existsSync(baseFile)) {
|
||||||
svgContent = svgContent
|
let svgContent = fs.readFileSync(baseFile, { encoding: 'utf-8'});
|
||||||
.replace(/style="[^"]*fill:\s*#fff[^"]*"/gi, (match) => {
|
const color = req.query['color'].startsWith('#') ? req.query['color'] : `#${req.query['color']}`;
|
||||||
console.log('Replacing style fill:', match);
|
svgContent = svgContent
|
||||||
return match.replace(/fill:\s*#fff/gi, `fill:${color}`);
|
.replace(/style="[^"]*fill:\s*#fff[^"]*"/gi, (match) => {
|
||||||
})
|
console.log('Replacing style fill:', match);
|
||||||
.replace(/fill="#fff"/gi, `fill="${color}"`);
|
return match.replace(/fill:\s*#fff/gi, `fill:${color}`);
|
||||||
return res.type('image/svg+xml').send(svgContent);
|
})
|
||||||
} else {
|
.replace(/fill="#fff"/gi, `fill="${color}"`);
|
||||||
return fetchAndPipe(mainUrl, res);
|
return res.type('image/svg+xml').send(svgContent);
|
||||||
}
|
} else {
|
||||||
} else {
|
return res.status(404).end("Not found");
|
||||||
return fetchAndPipe(mainUrl, res);
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
return readFile(filePath, res);
|
||||||
// PNG/WebP: serve directly
|
}
|
||||||
return fetchAndPipe(mainUrl, res);
|
} else {
|
||||||
}
|
// PNG/WebP: serve directly
|
||||||
});
|
return readFile(filePath, res);
|
||||||
|
}
|
||||||
app.get('/', (req, res) => {
|
});
|
||||||
res.send('Self-hosted icon server');
|
|
||||||
});
|
app.listen(PORT, () => {
|
||||||
app.listen(PORT, () => {
|
console.log(`Listening on port ${PORT}`);
|
||||||
console.log(`Listening on port ${PORT}`);
|
|
||||||
});
|
});
|
Loading…
Reference in New Issue