const express = require('express'); const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const session = require('express-session'); const app = express(); const port = 3001; const comicsRoot = '/www/comics'; // 修改为你的漫画主目录 app.use(require('cors')({ origin: true, credentials: true // 允许跨域携带cookie(前端用 credentials: 'include') })); app.use(express.json()); app.use(session({ secret: 'comic-viewer-2024', // 建议改成强随机字符串 resave: false, saveUninitialized: false, cookie: { httpOnly: true, maxAge: 30 * 24 * 3600 * 1000 } })); // ========================== // 用户管理 // ========================== // users.txt 格式: 每行 username:password function loadUsers() { const file = path.join(__dirname, 'users.txt'); if (!fs.existsSync(file)) return {}; const users = {}; fs.readFileSync(file, 'utf8').split(/\r?\n/).forEach(line => { if (!line.trim() || line.trim().startsWith('#')) return; const [u, p] = line.split(':'); if (u && p) users[u.trim()] = p.trim(); }); return users; } // 登录 app.post('/api/login', (req, res) => { const { username, password } = req.body; const users = loadUsers(); if (users[username] && users[username] === password) { req.session.login = true; req.session.username = username; res.json({ ok: true }); } else { res.status(401).json({ ok: false, msg: '用户名或密码错误' }); } }); // 登出 app.post('/api/logout', (req, res) => { req.session.destroy(() => res.json({ ok: true })); }); // 检查登录状态 app.get('/api/check', (req, res) => { res.json({ login: !!req.session.login, username: req.session.username }); }); // 认证中间件 function auth(req, res, next) { if (req.session.login) return next(); res.status(401).json({ ok: false, msg: '请先登录' }); } // ========================== // 业务API(所有加auth保护) // ========================== // 文件类型判定 function isImage(fn) { return /\.(jpe?g|png|webp|gif)$/i.test(fn); } function isHtml(fn) { return /\.html?$/i.test(fn); } function isVideo(fn) { return /\.(mp4|webm|mov|avi|mkv|ogg)$/i.test(fn); } // 只列出非空文件夹 app.get('/api/folders', auth, (req, res) => { fs.readdir(comicsRoot, { withFileTypes: true }, (err, files) => { if (err) return res.status(500).json([]); const dirs = files.filter(f => f.isDirectory()).map(f => f.name); Promise.all( dirs.map(d => new Promise(resolve => { fs.readdir(path.join(comicsRoot, d), (e, fls) => { const nonHidden = (fls || []).filter(f => !f.startsWith('.')); resolve(nonHidden.length > 0 ? d : null); }); }) ) ).then(results => { res.json(results.filter(Boolean)); }); }); }); // 文件夹下所有文件(图片/html/视频/其它) app.get('/api/list/:folder', auth, (req, res) => { const folder = path.basename(req.params.folder); const dir = path.join(comicsRoot, folder); fs.readdir(dir, (err, files) => { if (err) return res.status(404).json([]); const filtered = files.filter(f => !f.startsWith('.')); res.json(filtered.sort((a, b) => a.localeCompare(b, 'zh', { numeric: true }))); }); }); // 文件夹下所有图片(按文件名排序) app.get('/api/images/:folder', auth, (req, res) => { const folder = path.basename(req.params.folder); const dir = path.join(comicsRoot, folder); fs.readdir(dir, (err, files) => { if (err) return res.status(404).json([]); const imgs = files.filter(isImage).sort((a, b) => a.localeCompare(b, 'zh', { numeric: true })); res.json(imgs); }); }); // 图片/视频/其它原始文件 app.get('/api/image/:folder/:img', auth, (req, res) => { const folder = path.basename(req.params.folder); const img = path.basename(req.params.img); const file = path.join(comicsRoot, folder, img); fs.access(file, fs.constants.R_OK, (err) => { if (err) return res.status(404).end(); res.sendFile(file); }); }); // 图片缩略图(只对图片调用) app.get('/api/image-thumb/:folder/:img', auth, (req, res) => { const folder = path.basename(req.params.folder); const img = path.basename(req.params.img); const file = path.join(comicsRoot, folder, img); fs.access(file, fs.constants.R_OK, (err) => { if (err) return res.status(404).end(); sharp(file).resize({ height: 128 }).toBuffer() .then(buf => res.type('image/jpeg').send(buf)) .catch(() => res.status(404).end()); }); }); // HTML 文件(iframe方式) app.get('/api/html/:folder/:file', auth, (req, res) => { const folder = path.basename(req.params.folder); const file = path.basename(req.params.file); const filePath = path.join(comicsRoot, folder, file); fs.access(filePath, fs.constants.R_OK, (err) => { if (err) return res.status(404).end(); res.sendFile(filePath); }); }); app.listen(port, () => console.log('Comic backend running at port', port));