154 lines
4.9 KiB
JavaScript
154 lines
4.9 KiB
JavaScript
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));
|