sqlite支持

This commit is contained in:
mtvpls
2026-04-11 11:28:04 +08:00
parent 0d00fafcb5
commit c7879748f9
6 changed files with 217 additions and 42 deletions
+7 -1
View File
@@ -45,11 +45,14 @@ ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
ENV DOCKER_ENV=true
ENV SQLITE_DB_PATH=/app/.data/moontv.db
# 从构建器中复制 standalone 输出
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# 从构建器中复制 scripts 目录
COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts
# 从构建器中复制 migrations 目录
COPY --from=builder --chown=nextjs:nodejs /app/migrations ./migrations
# 从构建器中复制 start.js
COPY --from=builder --chown=nextjs:nodejs /app/start.js ./start.js
# 从构建器中复制自定义 server.js(包含 Socket.IO 支持)
@@ -58,9 +61,12 @@ COPY --from=builder --chown=nextjs:nodejs /app/server.js ./server.js
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 从构建器中复制生产依赖(包含 Socket.IO)
# 从构建器中复制生产依赖(包含 Socket.IO / better-sqlite3
COPY --from=builder --chown=nextjs:nodejs /tmp/prod-deps/node_modules ./node_modules
# 准备 SQLite 数据目录
RUN mkdir -p /app/.data && chown -R nextjs:nodejs /app/.data
# 切换到非特权用户
USER nextjs
+21 -1
View File
@@ -232,6 +232,26 @@ volumes:
kvrocks-data:
```
### SQLite 存储
```yml
services:
moontv-core:
image: ghcr.io/mtvpls/moontvplus:latest
container_name: moontv-core
restart: on-failure
ports:
- '3000:3000'
environment:
- USERNAME=admin
- PASSWORD=admin_password
- NEXT_PUBLIC_STORAGE_TYPE=d1
- SQLITE_DB_PATH=/app/.data/moontv.db
volumes:
- ./data:/app/.data
```
### Redis 存储(有一定的丢数据风险)
```yml
@@ -289,7 +309,7 @@ services:
#### Lite 镜像说明
`ghcr.io/mtvpls/moontvplus-lite:latest` 为更小的镜像,但不支持启动内置观影室服务。
`ghcr.io/mtvpls/moontvplus-lite:latest` 为更小的镜像,但不支持启动内置观影室服务,也不支持 SQLite`NEXT_PUBLIC_STORAGE_TYPE=d1`)自动初始化方案
示例:
+9
View File
@@ -0,0 +1,9 @@
ignoredBuiltDependencies:
- bufferutil
- esbuild
- sharp
- unrs-resolver
- workerd
onlyBuiltDependencies:
- better-sqlite3
+149 -33
View File
@@ -3,60 +3,176 @@ const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const MIGRATIONS_DIR = path.join(__dirname, '../migrations');
// SHA-256 加密密码(与 Redis 保持一致)
function hashPassword(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
// 确保 .data 目录存在
const dataDir = path.join(__dirname, '../.data');
function getSqliteDbPath() {
return process.env.SQLITE_DB_PATH || path.join(process.cwd(), '.data', 'moontv.db');
}
function ensureDataDir(dbPath) {
const dataDir = path.dirname(dbPath);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// 创建数据库
const dbPath = path.join(dataDir, 'moontv.db');
const db = new Database(dbPath);
console.log('📦 Initializing SQLite database for development...');
console.log('📍 Database location:', dbPath);
// 读取迁移脚本
const migrationPath = path.join(__dirname, '../migrations/001_initial_schema.sql');
if (!fs.existsSync(migrationPath)) {
console.error('❌ Migration file not found:', migrationPath);
process.exit(1);
}
function configureDatabase(db) {
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.pragma('busy_timeout = 5000');
}
function ensureMigrationTable(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL UNIQUE,
applied_at INTEGER NOT NULL
)
`);
}
function getMigrationFiles() {
if (!fs.existsSync(MIGRATIONS_DIR)) {
throw new Error(`Migrations directory not found: ${MIGRATIONS_DIR}`);
}
return fs
.readdirSync(MIGRATIONS_DIR)
.filter((file) => file.endsWith('.sql'))
.sort((a, b) => a.localeCompare(b));
}
function columnExists(db, tableName, columnName) {
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
return columns.some((column) => column.name === columnName);
}
function migrationAlreadySatisfied(db, file) {
if (file === '003_add_new_episodes_to_play_records.sql') {
return columnExists(db, 'play_records', 'new_episodes');
}
if (file === '004_add_tvbox_subscribe_token.sql') {
return columnExists(db, 'users', 'tvbox_subscribe_token');
}
return false;
}
function markMigrationApplied(db, file) {
db.prepare(
'INSERT OR IGNORE INTO schema_migrations (filename, applied_at) VALUES (?, ?)'
).run(file, Date.now());
}
function runMigrations(db) {
ensureMigrationTable(db);
const applied = new Set(
db.prepare('SELECT filename FROM schema_migrations ORDER BY filename ASC')
.all()
.map((row) => row.filename)
);
const migrationFiles = getMigrationFiles();
for (const file of migrationFiles) {
if (applied.has(file)) {
continue;
}
if (migrationAlreadySatisfied(db, file)) {
console.log(`⏭️ Migration already satisfied, marking as applied: ${file}`);
markMigrationApplied(db, file);
continue;
}
const migrationPath = path.join(MIGRATIONS_DIR, file);
const sql = fs.readFileSync(migrationPath, 'utf8');
// 执行迁移
try {
db.exec(sql);
console.log('✅ Database schema created successfully!');
console.log(`▶️ Applying migration: ${file}`);
// 创建默认管理员用户(可选)
const transaction = db.transaction(() => {
db.exec(sql);
markMigrationApplied(db, file);
});
transaction();
console.log(`✅ Migration applied: ${file}`);
}
}
function ensureDefaultAdmin(db) {
const username = process.env.USERNAME || 'admin';
const password = process.env.PASSWORD || '123456789';
const passwordHash = hashPassword(password);
const stmt = db.prepare(`
INSERT OR IGNORE INTO users (username, password_hash, role, created_at, playrecord_migrated, favorite_migrated, skip_migrated)
const existingUser = db
.prepare('SELECT username FROM users WHERE username = ? LIMIT 1')
.get(username);
if (existingUser) {
console.log(`️ Admin user already exists: ${username}`);
return;
}
db.prepare(`
INSERT INTO users (
username, password_hash, role, created_at,
playrecord_migrated, favorite_migrated, skip_migrated
)
VALUES (?, ?, 'owner', ?, 1, 1, 1)
`);
`).run(username, passwordHash, Date.now());
stmt.run(username, passwordHash, Date.now());
console.log(`✅ Default admin user created: ${username}`);
} catch (err) {
console.error('❌ Migration failed:', err);
process.exit(1);
}
function initSQLiteDatabase() {
const dbPath = getSqliteDbPath();
ensureDataDir(dbPath);
let db;
try {
db = new Database(dbPath);
} catch (error) {
if (error && typeof error.message === 'string' && error.message.includes('Could not locate the bindings file')) {
console.error('❌ better-sqlite3 native binding is missing or incompatible with current Node.js runtime.');
console.error('💡 Please run: pnpm rebuild better-sqlite3');
console.error('💡 If you recently changed Node.js version, reinstall dependencies or rebuild native modules.');
}
throw error;
}
configureDatabase(db);
console.log('📦 Initializing SQLite database...');
console.log('📍 Database location:', dbPath);
try {
runMigrations(db);
ensureDefaultAdmin(db);
} finally {
db.close();
}
console.log('');
console.log('🎉 SQLite database initialized successfully!');
console.log('');
console.log('Next steps:');
console.log('1. Set NEXT_PUBLIC_STORAGE_TYPE=d1 in .env');
console.log('2. Run: npm run dev');
console.log('🎉 SQLite database is ready!');
}
module.exports = {
initSQLiteDatabase,
getSqliteDbPath,
};
if (require.main === module) {
try {
initSQLiteDatabase();
} catch (err) {
console.error('❌ SQLite initialization failed:', err);
process.exit(1);
}
}
+21
View File
@@ -4,6 +4,27 @@ const { parse } = require('url');
const next = require('next');
const { Server } = require('socket.io');
function shouldInitSQLite() {
const isCloudflare = process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare';
return process.env.NEXT_PUBLIC_STORAGE_TYPE === 'd1' && !isCloudflare && process.env.MOONTV_LITE !== 'true';
}
function ensureSQLiteReady() {
if (!shouldInitSQLite()) {
return;
}
try {
const { initSQLiteDatabase } = require('./scripts/init-sqlite.js');
initSQLiteDatabase();
} catch (error) {
console.error('❌ Error initializing SQLite database:', error);
throw error;
}
}
ensureSQLiteReady();
const dev = process.env.NODE_ENV !== 'production';
const hostname = process.env.HOSTNAME || '0.0.0.0';
const port = parseInt(process.env.PORT || '3000', 10);
+5 -2
View File
@@ -110,12 +110,15 @@ function getD1Adapter(): any {
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.join(process.cwd(), '.data', 'moontv.db');
const dbPath =
process.env.SQLITE_DB_PATH || path.join(process.cwd(), '.data', 'moontv.db');
const db = new Database(dbPath);
db.pragma('journal_mode = WAL'); // 启用 WAL 模式提升性能
db.pragma('foreign_keys = ON'); // 与 D1 保持一致,启用外键约束
db.pragma('busy_timeout = 5000'); // 避免启动阶段或并发写入时立即锁失败
console.log('Using SQLite database (development mode)');
console.log('Using SQLite database (non-Cloudflare mode)');
console.log('Database location:', dbPath);
return new SQLiteAdapter(db);