2025-06-09 16:19:14 +08:00

154 lines
4.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));