Initial Commit

This commit is contained in:
ldy
2025-06-09 16:19:14 +08:00
parent 5cae6fec49
commit 6a032196e7
12 changed files with 569 additions and 0 deletions

15
backend/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "comicviewer-backend",
"version": "1.1.0",
"main": "server.js",
"type": "commonjs",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"sharp": "^0.33.4"
}
}

153
backend/server.js Normal file
View File

@@ -0,0 +1,153 @@
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));

1
backend/users.txt Normal file
View File

@@ -0,0 +1 @@
username:password