Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 498b593b28 | |||
| c57af012b1 | |||
| 86d79e550b | |||
| 07d66ac666 | |||
| 42d2b5bf1c | |||
| d7c2a9cfad | |||
| 8da99d6861 | |||
| 6f20c662eb | |||
| 48d6b642e0 |
56
source_clean/deploy.sh
Executable file
56
source_clean/deploy.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# 如果 docker-compose.yml 不存在,自动下载
|
||||||
|
if [ ! -f docker-compose.yml ]; then
|
||||||
|
echo "📥 下载 docker-compose.yml..."
|
||||||
|
wget -q https://gitea.timxx.cn/admin/CloudSearch/raw/branch/master/source_clean/docker-compose.yml
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔍 检测 Redis..."
|
||||||
|
|
||||||
|
EXISTING_REDIS=$(docker ps --format '{{.Names}}' | grep -i redis | head -1)
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_REDIS" ]; then
|
||||||
|
if docker network inspect cloudsearch-net --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | grep -qw "$EXISTING_REDIS"; then
|
||||||
|
echo "✅ 已有 Redis: $EXISTING_REDIS (已加入 cloudsearch-net),跳过创建"
|
||||||
|
else
|
||||||
|
echo "✅ 已有 Redis: $EXISTING_REDIS,正在加入网络..."
|
||||||
|
docker network connect cloudsearch-net "$EXISTING_REDIS" 2>/dev/null || true
|
||||||
|
echo " ✅ 已加入 cloudsearch-net"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检测 Redis 密码
|
||||||
|
REDIS_PASS=$(docker inspect "$EXISTING_REDIS" --format '{{range .Config.Cmd}}{{println .}}{{end}}' 2>/dev/null | grep -A1 'requirepass' | tail -1 || true)
|
||||||
|
if [ -n "$REDIS_PASS" ]; then
|
||||||
|
REDIS_URL="redis://:${REDIS_PASS}@${EXISTING_REDIS}:6379"
|
||||||
|
echo " 🔑 检测到 Redis 密码,已自动配置"
|
||||||
|
else
|
||||||
|
REDIS_URL="redis://${EXISTING_REDIS}:6379"
|
||||||
|
fi
|
||||||
|
PROFILE=""
|
||||||
|
else
|
||||||
|
echo "📦 未检测到 Redis,将自动创建..."
|
||||||
|
REDIS_URL="redis://CloudSearch_Redis:6379"
|
||||||
|
PROFILE="--profile full"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 生成 .env
|
||||||
|
cat > .env <<EOF
|
||||||
|
REDIS_URL=${REDIS_URL}
|
||||||
|
CORS_ORIGIN=https://zy.hk.timxx.cn
|
||||||
|
JWT_SECRET=cloudsearch-jwt-secret-2024
|
||||||
|
ADMIN_PASSWORD=0nL5kLhMIJ1121PYmQb25A
|
||||||
|
LOG_LEVEL=info
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🚀 启动服务..."
|
||||||
|
docker compose $PROFILE up -d
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ 部署完成"
|
||||||
|
docker compose ps
|
||||||
42
source_clean/docker-compose.yml
Normal file
42
source_clean/docker-compose.yml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: cloudsearch
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.timxx.cn/admin/cloudsearch:latest
|
||||||
|
container_name: CloudSearch_App
|
||||||
|
restart: unless-stopped
|
||||||
|
ports: ["9527:9527"]
|
||||||
|
environment:
|
||||||
|
REDIS_URL: ${REDIS_URL}
|
||||||
|
PANSOU_URL: http://pansou:8888
|
||||||
|
CORS_ORIGIN: ${CORS_ORIGIN:-https://zy.hk.timxx.cn}
|
||||||
|
JWT_SECRET: ${JWT_SECRET:-cloudsearch-jwt-secret-2024}
|
||||||
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-0nL5kLhMIJ1121PYmQb25A}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||||
|
volumes: ["cloudsearch-data:/app/data"]
|
||||||
|
depends_on: [pansou]
|
||||||
|
networks: [cloudsearch-net]
|
||||||
|
|
||||||
|
pansou:
|
||||||
|
image: ghcr.io/fish2018/pansou-web:latest
|
||||||
|
container_name: CloudSearch_PanSou
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
cloudsearch-net: { aliases: [pansou] }
|
||||||
|
|
||||||
|
# Redis — 仅当系统没有现成 Redis 时才启动
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: CloudSearch_Redis
|
||||||
|
restart: unless-stopped
|
||||||
|
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||||
|
volumes: ["redis-data:/data"]
|
||||||
|
networks: [cloudsearch-net]
|
||||||
|
profiles: [full]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cloudsearch-data:
|
||||||
|
redis-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
cloudsearch-net:
|
||||||
|
driver: bridge
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
nodeEnv: string;
|
nodeEnv: string;
|
||||||
@@ -23,6 +27,50 @@ export interface Config {
|
|||||||
dbPath: string;
|
dbPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_JWT_SECRETS = ['', 'cloudsearch-jwt-secret-dev', 'cloudsearch-jwt-secret-2024', 'your-super-secret-jwt-key-change-me'];
|
||||||
|
const DEFAULT_PASSWORDS = ['', 'admin123', 'admin', 'password', '123456', '0nL5kLhMIJ1121PYmQb25A'];
|
||||||
|
|
||||||
|
function loadOrGenerateSecret(key: string, envKey: string, defaultVal: string, isDefault: (v: string) => boolean, byteLen: number): string {
|
||||||
|
const envVal = process.env[envKey];
|
||||||
|
|
||||||
|
// If env var is set and NOT a known default → use it directly
|
||||||
|
if (envVal && !isDefault(envVal)) {
|
||||||
|
return envVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load from persisted secrets file
|
||||||
|
const secretsPath = path.join(process.env.DATA_DIR || '/app/data', 'secrets.json');
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(secretsPath)) {
|
||||||
|
const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8'));
|
||||||
|
if (secrets[key]) return secrets[key];
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// If env var is set but it's a default → still use it (admin set it explicitly, maybe for dev)
|
||||||
|
if (envVal) return envVal;
|
||||||
|
|
||||||
|
// No env var, no persisted file → auto-generate
|
||||||
|
const generated = byteLen === 16
|
||||||
|
? crypto.randomBytes(8).toString('hex') // 16-char hex for passwords
|
||||||
|
: crypto.randomBytes(byteLen).toString('hex'); // 64-char hex for JWT
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dir = path.dirname(secretsPath);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
let existing: Record<string, string> = {};
|
||||||
|
if (fs.existsSync(secretsPath)) {
|
||||||
|
try { existing = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); } catch (_) {}
|
||||||
|
}
|
||||||
|
existing[key] = generated;
|
||||||
|
fs.writeFileSync(secretsPath, JSON.stringify(existing, null, 2));
|
||||||
|
console.log(`[Config] Auto-generated ${envKey} → saved to secrets.json`);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`[Config] Could not persist ${envKey}:`, e.message);
|
||||||
|
}
|
||||||
|
return generated;
|
||||||
|
}
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
port: parseInt(process.env.PORT || '9527', 10),
|
port: parseInt(process.env.PORT || '9527', 10),
|
||||||
nodeEnv: process.env.NODE_ENV || 'development',
|
nodeEnv: process.env.NODE_ENV || 'development',
|
||||||
@@ -30,9 +78,9 @@ const config: Config = {
|
|||||||
pansouUrl: process.env.PANSOU_URL || 'http://localhost:8888',
|
pansouUrl: process.env.PANSOU_URL || 'http://localhost:8888',
|
||||||
pansouAuthToken: process.env.PANSOU_AUTH_TOKEN || '',
|
pansouAuthToken: process.env.PANSOU_AUTH_TOKEN || '',
|
||||||
videoParserUrl: process.env.VIDEO_PARSER_URL || 'http://localhost:3001',
|
videoParserUrl: process.env.VIDEO_PARSER_URL || 'http://localhost:3001',
|
||||||
jwtSecret: process.env.JWT_SECRET || 'cloudsearch-jwt-secret-dev',
|
jwtSecret: loadOrGenerateSecret('jwt_secret', 'JWT_SECRET', 'cloudsearch-jwt-secret-dev', (v) => DEFAULT_JWT_SECRETS.includes(v), 32),
|
||||||
adminUsername: process.env.ADMIN_USERNAME || 'admin',
|
adminUsername: process.env.ADMIN_USERNAME || 'admin',
|
||||||
adminPassword: process.env.ADMIN_PASSWORD || 'admin123',
|
adminPassword: loadOrGenerateSecret('admin_password', 'ADMIN_PASSWORD', 'admin123', (v) => DEFAULT_PASSWORDS.includes(v), 16),
|
||||||
validation: {
|
validation: {
|
||||||
concurrency: parseInt(process.env.VALIDATION_CONCURRENCY || '10', 10),
|
concurrency: parseInt(process.env.VALIDATION_CONCURRENCY || '10', 10),
|
||||||
timeout: parseInt(process.env.VALIDATION_TIMEOUT || '5000', 10),
|
timeout: parseInt(process.env.VALIDATION_TIMEOUT || '5000', 10),
|
||||||
|
|||||||
@@ -42,14 +42,7 @@ export function validateConfig(): ValidationError[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── Cookie Encryption ───
|
// ─── Cookie Encryption ───
|
||||||
if (!process.env.COOKIE_ENCRYPTION_KEY) {
|
// Key is auto-generated and persisted to encryption.key if COOKIE_ENCRYPTION_KEY is not set
|
||||||
errors.push({
|
|
||||||
key: 'COOKIE_ENCRYPTION_KEY',
|
|
||||||
message: '未设置网盘 Cookie 加密密钥!Cookie 将以明文存储。生产环境强烈建议设置。\n' +
|
|
||||||
'生成: openssl rand -hex 32',
|
|
||||||
severity: 'warn',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Port conflict check (best-effort) ───
|
// ─── Port conflict check (best-effort) ───
|
||||||
if (config.port < 1024 && (process as any).getuid?.() !== 0) {
|
if (config.port < 1024 && (process as any).getuid?.() !== 0) {
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ export function getDb(): Database.Database {
|
|||||||
db.pragma('journal_mode = WAL');
|
db.pragma('journal_mode = WAL');
|
||||||
db.pragma('foreign_keys = ON');
|
db.pragma('foreign_keys = ON');
|
||||||
|
|
||||||
// Performance indexes (IF NOT EXISTS ensures idempotent)
|
|
||||||
db.exec(`
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_cc_type_active ON cloud_configs(cloud_type, is_active);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_cc_uid ON cloud_configs(cookie_uid);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_cc_verification ON cloud_configs(verification_status);
|
|
||||||
`);
|
|
||||||
|
|
||||||
runMigrations(db);
|
runMigrations(db);
|
||||||
seedAdmin(db);
|
seedAdmin(db);
|
||||||
|
|
||||||
@@ -46,6 +39,7 @@ function runMigrations(db: Database.Database): void {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
cloud_type TEXT NOT NULL,
|
cloud_type TEXT NOT NULL,
|
||||||
cookie TEXT,
|
cookie TEXT,
|
||||||
|
cloud_type_uid TEXT DEFAULT NULL,
|
||||||
nickname TEXT,
|
nickname TEXT,
|
||||||
is_active INTEGER NOT NULL DEFAULT 1,
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
storage_used TEXT,
|
storage_used TEXT,
|
||||||
@@ -131,6 +125,14 @@ function runMigrations(db: Database.Database): void {
|
|||||||
migrateSaveRecords(db);
|
migrateSaveRecords(db);
|
||||||
migrateContentCache(db);
|
migrateContentCache(db);
|
||||||
migrateCloudConfigs(db);
|
migrateCloudConfigs(db);
|
||||||
|
|
||||||
|
// Performance indexes on cloud_configs (after all columns exist)
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_type_active ON cloud_configs(cloud_type, is_active);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_uid ON cloud_configs(cookie_uid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cc_verification ON cloud_configs(verification_status);
|
||||||
|
`);
|
||||||
|
|
||||||
cleanupOldSaveRecords(db);
|
cleanupOldSaveRecords(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +205,7 @@ function migrateCloudConfigs(db: Database.Database): void {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
cloud_type TEXT NOT NULL,
|
cloud_type TEXT NOT NULL,
|
||||||
cookie TEXT,
|
cookie TEXT,
|
||||||
|
cloud_type_uid TEXT DEFAULT NULL,
|
||||||
nickname TEXT,
|
nickname TEXT,
|
||||||
is_active INTEGER NOT NULL DEFAULT 1,
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
storage_used TEXT,
|
storage_used TEXT,
|
||||||
@@ -250,6 +253,13 @@ function migrateCloudConfigs(db: Database.Database): void {
|
|||||||
db.exec("ALTER TABLE cloud_configs ADD COLUMN notify_config TEXT DEFAULT NULL");
|
db.exec("ALTER TABLE cloud_configs ADD COLUMN notify_config TEXT DEFAULT NULL");
|
||||||
console.log('[DB] cloud_configs migration: notify_config column added');
|
console.log('[DB] cloud_configs migration: notify_config column added');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration 6: Add cloud_type_uid column
|
||||||
|
const hasCloudTypeUid = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%cloud_type_uid%'").get();
|
||||||
|
if (!hasCloudTypeUid) {
|
||||||
|
db.exec("ALTER TABLE cloud_configs ADD COLUMN cloud_type_uid TEXT DEFAULT NULL");
|
||||||
|
console.log('[DB] cloud_configs migration: cloud_type_uid column added');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
* Production MUST set COOKIE_ENCRYPTION_KEY!
|
* Production MUST set COOKIE_ENCRYPTION_KEY!
|
||||||
*/
|
*/
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
const ALGORITHM = 'aes-256-gcm';
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
const IV_LENGTH = 12; // 96-bit nonce for GCM
|
const IV_LENGTH = 12; // 96-bit nonce for GCM
|
||||||
@@ -25,9 +27,30 @@ function getKey(): Buffer {
|
|||||||
ENCRYPTION_KEY = crypto.createHash('sha256').update(envKey).digest();
|
ENCRYPTION_KEY = crypto.createHash('sha256').update(envKey).digest();
|
||||||
console.log('[Crypto] Cookie encryption enabled (key from COOKIE_ENCRYPTION_KEY, SHA-256 derived)');
|
console.log('[Crypto] Cookie encryption enabled (key from COOKIE_ENCRYPTION_KEY, SHA-256 derived)');
|
||||||
} else {
|
} else {
|
||||||
// Default stable key (not ephemeral) — data survives container restart
|
// Auto-generate a random key and persist to file
|
||||||
ENCRYPTION_KEY = crypto.createHash('sha256').update('cloudsearch-cookie-key-v1').digest();
|
const keyFile = path.join(process.env.DATA_DIR || '/app/data', 'encryption.key');
|
||||||
console.log('[Crypto] Cookie encryption enabled (built-in default key — set COOKIE_ENCRYPTION_KEY in .env for extra security)');
|
try {
|
||||||
|
if (fs.existsSync(keyFile)) {
|
||||||
|
const savedKey = fs.readFileSync(keyFile, 'utf8').trim();
|
||||||
|
if (savedKey.length >= 32) {
|
||||||
|
ENCRYPTION_KEY = crypto.createHash('sha256').update(savedKey).digest();
|
||||||
|
console.log('[Crypto] Cookie encryption enabled (loaded from encryption.key)');
|
||||||
|
return ENCRYPTION_KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) { /* file read failed, will generate new key */ }
|
||||||
|
|
||||||
|
const autoKey = crypto.randomBytes(32).toString('hex');
|
||||||
|
try {
|
||||||
|
const dir = path.dirname(keyFile);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.writeFileSync(keyFile, autoKey);
|
||||||
|
console.log('[Crypto] Auto-generated encryption key saved to encryption.key');
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log('[Crypto] Could not persist key, using ephemeral:', e.message);
|
||||||
|
}
|
||||||
|
ENCRYPTION_KEY = crypto.createHash('sha256').update(autoKey).digest();
|
||||||
|
console.log('[Crypto] Cookie encryption enabled (auto-generated key)');
|
||||||
}
|
}
|
||||||
return ENCRYPTION_KEY;
|
return ENCRYPTION_KEY;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user