Initial Commit
This commit is contained in:
15
backend/package.json
Normal file
15
backend/package.json
Normal 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
153
backend/server.js
Normal 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
1
backend/users.txt
Normal file
@@ -0,0 +1 @@
|
||||
username:password
|
||||
Reference in New Issue
Block a user