Add server-side image thumbnails (sharp): /uploads/thumb/<name> route, ?thumb=1 on file endpoints, disk-cached 480px WebP previews; grids now load thumbnails with lazy loading
This commit is contained in:
@@ -114,6 +114,48 @@ app.use(helmet({
|
||||
}
|
||||
}));
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
const THUMBS_DIR = path.join(__dirname, 'uploads', '.thumbs');
|
||||
const THUMB_WIDTH = 480;
|
||||
let sharp = null;
|
||||
try { sharp = require('sharp'); } catch {}
|
||||
if (sharp) {
|
||||
try { fs.mkdirSync(THUMBS_DIR, { recursive: true }); } catch {}
|
||||
}
|
||||
|
||||
function thumbFileFor(fp) {
|
||||
const base = path.basename(fp).replace(/\.[^.]+$/, '') + '.webp';
|
||||
return path.join(THUMBS_DIR, base);
|
||||
}
|
||||
|
||||
async function sendImageThumb(res, fp) {
|
||||
if (!sharp) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
return res.sendFile(fp);
|
||||
}
|
||||
const tp = thumbFileFor(fp);
|
||||
try {
|
||||
if (!fs.existsSync(tp)) {
|
||||
const tmp = tp + '.' + crypto.randomBytes(4).toString('hex') + '.tmp';
|
||||
await sharp(fp).rotate().resize({ width: THUMB_WIDTH, withoutEnlargement: true }).webp({ quality: 72 }).toFile(tmp);
|
||||
fs.renameSync(tmp, tp);
|
||||
}
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
return res.sendFile(tp);
|
||||
} catch {
|
||||
try { if (fs.existsSync(tp)) fs.unlinkSync(tp); } catch {}
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
return res.sendFile(fp);
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/uploads/thumb/:name', fileLimiter, async (req, res) => {
|
||||
const name = req.params.name;
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(name)) return res.status(400).end();
|
||||
const fp = path.join(__dirname, 'uploads', name);
|
||||
if (!fs.existsSync(fp) || !fs.statSync(fp).isFile()) return res.status(404).end();
|
||||
return sendImageThumb(res, fp);
|
||||
});
|
||||
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads'), { maxAge: '365d', immutable: true }));
|
||||
app.use('/vendor', express.static(path.join(__dirname, 'public', 'vendor'), { maxAge: '30d' }));
|
||||
app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
|
||||
@@ -1515,8 +1557,12 @@ app.get('/api/share/:shareToken/files/:fileToken', fileLimiter, async (req, res)
|
||||
const r = rows[0];
|
||||
const fp = path.join(__dirname, r.path);
|
||||
if (!fs.existsSync(fp)) return res.status(404).json({ error: 'File missing' });
|
||||
if (isImageName(r.name)) res.sendFile(fp);
|
||||
else res.download(fp, r.name);
|
||||
if (isImageName(r.name)) {
|
||||
if (req.query.thumb) return sendImageThumb(res, fp);
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
return res.sendFile(fp);
|
||||
}
|
||||
return res.download(fp, r.name);
|
||||
});
|
||||
|
||||
app.get('/s/:token', (req, res) => {
|
||||
@@ -2267,8 +2313,12 @@ app.get('/api/files/:token', fileLimiter, async (req, res) => {
|
||||
const r = rows[0];
|
||||
const fp = path.join(__dirname, r.path);
|
||||
if (!fs.existsSync(fp)) return res.status(404).json({ error: 'File missing' });
|
||||
if (isImageName(r.name)) res.sendFile(fp);
|
||||
else res.download(fp, r.name);
|
||||
if (isImageName(r.name)) {
|
||||
if (req.query.thumb) return sendImageThumb(res, fp);
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
return res.sendFile(fp);
|
||||
}
|
||||
return res.download(fp, r.name);
|
||||
});
|
||||
|
||||
app.get('/api/stats', requireAuth, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user