打包构建

This commit is contained in:
Yeomai committed 2026-10-18 16:17:01 +08:00
1 parent a6e45e0cb1
commit ee8772c1b7
36 files changed
+940 -21

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
+148
View File
@@ -0,0 +1,148 @@
import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs'
import { join } from 'node:path'
import { randomBytes, scryptSync } from 'node:crypto'
import { DatabaseSync } from 'node:sqlite'
const USERNAME_PATTERN = /^[a-zA-Z0-9_]{3,32}$/
const MIN_PASSWORD_LENGTH = 8
const SETTINGS_ALLOW_REGISTRATION = 'allow_registration'
const dataDir = process.env.DATA_DIR || '/data'
const configPath = join(dataDir, '.fnos-setup.json')
const donePath = join(dataDir, '.fnos-setup.done')
function log(message) {
console.log(`[PicHost fnos-bootstrap] ${message}`)
}
function fail(message) {
console.error(`[PicHost fnos-bootstrap] ${message}`)
process.exit(1)
}
function hashPassword(password) {
const salt = randomBytes(16).toString('hex')
const derived = scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1 })
return `scrypt$16384$8$1$${salt}$${derived.toString('hex')}`
}
// 与 server/utils/db.ts 初始建表一致;完整迁移由应用启动后的 getDb() 负责。
function initDb(database) {
database.exec(`
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS activity_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT NOT NULL,
key TEXT NOT NULL,
original_name TEXT NOT NULL,
size INTEGER NOT NULL DEFAULT 0,
content_type TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at
ON activity_logs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_activity_logs_action_created
ON activity_logs(action, created_at DESC);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'user')),
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
`)
const columns = database.prepare('PRAGMA table_info(users)').all()
if (!columns.some(column => column.name === 'api_token')) {
database.exec('ALTER TABLE users ADD COLUMN api_token TEXT')
database.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS idx_users_api_token ON users(api_token) WHERE api_token IS NOT NULL'
)
}
if (!columns.some(column => column.name === 'auto_delete_days')) {
database.exec('ALTER TABLE users ADD COLUMN auto_delete_days INTEGER NOT NULL DEFAULT 0')
}
const userColumns = database.prepare('PRAGMA table_info(users)').all()
if (!userColumns.some(column => column.name === 'auto_delete_enabled_at')) {
database.exec('ALTER TABLE users ADD COLUMN auto_delete_enabled_at TEXT')
}
}
function countUsers(database) {
const row = database.prepare('SELECT COUNT(*) AS count FROM users').get()
return Number(row?.count ?? 0)
}
function setSetting(database, key, value) {
const updatedAt = new Date().toISOString()
database.prepare(`
INSERT INTO settings (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at
`).run(key, value, updatedAt)
}
mkdirSync(dataDir, { recursive: true })
const dbPath = join(dataDir, 'pichost.db')
const db = new DatabaseSync(dbPath)
initDb(db)
if (countUsers(db) > 0) {
if (existsSync(configPath)) {
renameSync(configPath, donePath)
}
log('already initialized')
process.exit(0)
}
if (!existsSync(configPath)) {
log('no setup file, waiting for install wizard')
process.exit(0)
}
let setup
try {
setup = JSON.parse(readFileSync(configPath, 'utf8'))
} catch {
fail('invalid .fnos-setup.json')
}
const username = String(setup.username ?? '').trim()
const password = String(setup.password ?? '')
const allowRegistration = Boolean(setup.allowRegistration)
if (!USERNAME_PATTERN.test(username)) {
fail('invalid admin username in setup file')
}
if (password.length < MIN_PASSWORD_LENGTH) {
fail('invalid admin password in setup file')
}
const createdAt = new Date().toISOString()
db.prepare(`
INSERT INTO users (username, password_hash, role, created_at)
VALUES (?, ?, 'admin', ?)
`).run(username, hashPassword(password), createdAt)
setSetting(db, SETTINGS_ALLOW_REGISTRATION, allowRegistration ? 'true' : 'false')
db.close()
renameSync(configPath, donePath)
log(`admin "${username}" created`)
+96
View File
@@ -0,0 +1,96 @@
#!/bin/bash
# 飞牛在此脚本阶段显示约 80%。只写文件并立刻返回,不要等待容器。
DATA_DIR="/${TRIM_APPDEST_VOL}/@appshare/pichost/data"
SETUP_FILE="${DATA_DIR}/.fnos-setup.json"
RESTORE_PENDING="${DATA_DIR}/.fnos-restore.pending"
BOOTSTRAP_SRC="$(cd "$(dirname "$0")" && pwd)/fnos-bootstrap.mjs"
SETUP_MODE="${wizard_setup_mode:-init}"
USERNAME="${wizard_admin_username:-admin}"
PASSWORD="${wizard_admin_password:-}"
ALLOW_REGISTRATION="${wizard_allow_registration:-false}"
BACKUP_PATH="${wizard_backup_path:-}"
PORT="${wizard_port:-6892}"
log_fail() {
echo "$1" > "${TRIM_TEMP_LOGFILE:-/dev/stderr}"
}
case "${PORT}" in
''|*[!0-9]*)
log_fail "服务端口必须是数字。"
exit 1
;;
esac
if [ "${PORT}" -lt 1024 ] || [ "${PORT}" -gt 65535 ]; then
log_fail "服务端口需在 1024–65535 之间。"
exit 1
fi
mkdir -p "${DATA_DIR}"
rm -f "${SETUP_FILE}" "${RESTORE_PENDING}" "${DATA_DIR}/.fnos-restore.failed" 2>/dev/null || true
case "${SETUP_MODE}" in
restore)
BACKUP_PATH="$(printf '%s' "${BACKUP_PATH}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
if [ -z "${BACKUP_PATH}" ]; then
log_fail "请填写备份文件的 NAS 绝对路径。"
exit 1
fi
if [ ! -f "${BACKUP_PATH}" ]; then
log_fail "找不到备份文件:${BACKUP_PATH}"
exit 1
fi
case "${BACKUP_PATH}" in
*.phost.tar.gz|*.tar.gz) ;;
*)
log_fail "备份文件须为 .phost.tar.gz 或 .tar.gz 格式。"
exit 1
;;
esac
mkdir -p "${DATA_DIR}/backups"
RESTORE_ARCHIVE="${DATA_DIR}/backups/fnos-setup-restore.phost.tar.gz"
cp "${BACKUP_PATH}" "${RESTORE_ARCHIVE}"
printf '/data/backups/fnos-setup-restore.phost.tar.gz\n' > "${RESTORE_PENDING}"
rm -f "${DATA_DIR}/fnos-bootstrap.mjs" 2>/dev/null || true
;;
init|*)
if [ -z "${PASSWORD}" ]; then
log_fail "请设置管理员密码。"
exit 1
fi
case "${USERNAME}" in
'')
log_fail "请设置管理员用户名。"
exit 1
;;
esac
if command -v python3 >/dev/null 2>&1; then
USERNAME="${USERNAME}" PASSWORD="${PASSWORD}" ALLOW_REGISTRATION="${ALLOW_REGISTRATION}" python3 - <<'PY' > "${SETUP_FILE}"
import json, os
print(json.dumps({
"username": os.environ["USERNAME"],
"password": os.environ["PASSWORD"],
"allowRegistration": os.environ.get("ALLOW_REGISTRATION", "false").lower() == "true",
}))
PY
else
case "${ALLOW_REGISTRATION}" in
true|TRUE|1|yes|on) ALLOW_JSON=true ;;
*) ALLOW_JSON=false ;;
esac
printf '{"username":"%s","password":"%s","allowRegistration":%s}\n' \
"${USERNAME}" "${PASSWORD}" "${ALLOW_JSON}" > "${SETUP_FILE}"
fi
if [ -f "${BOOTSTRAP_SRC}" ]; then
cp "${BOOTSTRAP_SRC}" "${DATA_DIR}/fnos-bootstrap.mjs"
fi
;;
esac
docker restart -t 2 pichost >/dev/null 2>&1 || true
exit 0
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
DATA_DIR="/${TRIM_APPDEST_VOL}/@appshare/pichost/data"
mkdir -p "${DATA_DIR}" 2>/dev/null || true
exit 0
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
CONTAINER_NAME="pichost"
is_running() {
docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -q '"Status": "running"'
}
case "$1" in
start)
exit 0
;;
stop)
exit 0
;;
status)
if is_running; then
exit 0
fi
exit 3
;;
*)
echo "Unknown command: $1" > "${TRIM_TEMP_LOGFILE:-/dev/stderr}"
exit 1
;;
esac
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
ACTION="${wizard_data_action:-keep}"
if [ "$ACTION" = "delete" ]; then
DATA_DIR="/${TRIM_APPDEST_VOL}/@appshare/pichost/data"
if [ -d "${DATA_DIR}" ]; then
rm -rf "${DATA_DIR:?}"/*
fi
if [ -n "${TRIM_PKGVAR:-}" ] && [ -d "$TRIM_PKGVAR" ]; then
rm -rf "${TRIM_PKGVAR:?}"/*
fi
if [ -n "${TRIM_PKGETC:-}" ] && [ -d "$TRIM_PKGETC" ]; then
rm -rf "${TRIM_PKGETC:?}"/*
fi
fi
exit 0
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# 升级 fpk 后:使用飞牛中已导入的本地镜像重建容器(pichost/data 数据目录不变)。
COMPOSE_DIR="${TRIM_APPDEST}/docker"
COMPOSE_FILE="${COMPOSE_DIR}/docker-compose.yaml"
CONTAINER_NAME="pichost"
log_fail() {
echo "$1" > "${TRIM_TEMP_LOGFILE:-/dev/stderr}"
}
if [ ! -f "${COMPOSE_FILE}" ]; then
log_fail "找不到 docker-compose.yaml"
exit 1
fi
IMAGE="$(sed -n 's/^[[:space:]]*image:[[:space:]]*//p' "${COMPOSE_FILE}" | head -1 | tr -d ' \r')"
if [ -z "${IMAGE}" ]; then
log_fail "无法从 compose 解析镜像名"
exit 1
fi
# 升级向导不含 port 字段,尽量沿用旧容器端口
if [ -z "${wizard_port:-}" ] && docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
wizard_port="$(docker inspect "${CONTAINER_NAME}" --format '{{with index .HostConfig.PortBindings "6892/tcp"}}{{with index . 0}}{{.HostPort}}{{end}}{{end}}' 2>/dev/null || true)"
fi
export wizard_port="${wizard_port:-6892}"
if ! docker compose version >/dev/null 2>&1; then
if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
docker restart -t 10 "${CONTAINER_NAME}" >/dev/null 2>&1 || {
log_fail "docker compose 不可用,且容器重启失败"
exit 1
}
exit 0
fi
log_fail "docker compose 不可用,且未找到 ${CONTAINER_NAME} 容器"
exit 1
fi
if ! (cd "${COMPOSE_DIR}" && docker compose up -d --force-recreate); then
log_fail "重建容器失败,请检查 Docker 日志(镜像:${IMAGE})"
exit 1
fi
exit 0
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
exit 0