feat: Enhance installation script and download functionality

- Add support for a force installation mode in the install script, allowing users to overwrite existing configurations and databases.
- Improve database setup logic to ensure existing users and databases are only dropped during a force installation.
- Introduce a new API endpoint to list available download files, enhancing the user experience on the download page by only displaying existing files.
- Update HTML templates to reflect the availability of download files dynamically.
This commit is contained in:
2026-02-26 21:22:39 +08:00
parent 900cc5fa09
commit 0f183b61a4
3 changed files with 105 additions and 40 deletions

View File

@@ -1,7 +1,9 @@
#!/bin/bash
#
# FunMC 一键部署脚本
# 用法: curl -fsSL https://fc.funmc.cn/install.sh | bash
# 用法: bash install.sh [ -force ]
# - 无参数: 更新安装保留数据库与现有配置server.env / relay.env / credentials.txt
# - -force: 强制覆盖安装,清空数据库并重写所有配置
#
set -e
@@ -30,6 +32,12 @@ echo "║ 魔幻方开发 ║"
echo "║ ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo -e "${NC}"
if [ "$FORCE_INSTALL" -eq 1 ]; then
echo -e "${YELLOW}运行模式: 强制覆盖安装(将清空数据库并重写配置)${NC}"
else
echo -e "${GREEN}运行模式: 更新安装(保留数据库与现有配置)${NC}"
fi
echo ""
# 检查 root 权限
if [ "$EUID" -ne 0 ]; then
@@ -38,6 +46,15 @@ if [ "$EUID" -ne 0 ]; then
exit 1
fi
# 解析参数:-force 或 --force 为强制覆盖安装,否则为更新(不覆盖数据)
FORCE_INSTALL=0
for arg in "$@"; do
if [ "$arg" = "-force" ] || [ "$arg" = "--force" ]; then
FORCE_INSTALL=1
break
fi
done
# 检测系统
detect_os() {
if [ -f /etc/os-release ]; then
@@ -99,32 +116,37 @@ install_nodejs() {
fi
}
# 配置数据库(固定密码 12345678强制删除旧库与用户并重建
# 配置数据库(-force 时强制删除并重建,否则仅确保库存在且不覆盖数据
setup_database() {
echo -e "${YELLOW}[4/7] 配置数据库...${NC}"
systemctl enable postgresql
systemctl start postgresql
# 强制断开对 funmc 库的所有连接,再删除库和用户(避免 role already exists / 无法删除库)
if [ "$FORCE_INSTALL" -eq 1 ]; then
# 强制断开对 funmc 库的所有连接,再删除库和用户
sudo -u postgres psql -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'funmc' AND pid <> pg_backend_pid();" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "DROP DATABASE IF EXISTS funmc;" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "DROP USER IF EXISTS funmc;" 2>/dev/null || true
# 创建用户和数据库
sudo -u postgres psql -d postgres -c "CREATE USER funmc WITH PASSWORD '12345678';"
sudo -u postgres psql -d postgres -c "CREATE DATABASE funmc OWNER funmc;"
sudo -u postgres psql -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE funmc TO funmc;"
echo -e "${GREEN}✓ 数据库已强制重建(密码 12345678${NC}"
else
# 更新模式:不删除,仅确保用户和库存在(若已存在则跳过)
sudo -u postgres psql -d postgres -c "CREATE USER funmc WITH PASSWORD '12345678';" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "CREATE DATABASE funmc OWNER funmc;" 2>/dev/null || true
sudo -u postgres psql -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE funmc TO funmc;" 2>/dev/null || true
echo -e "${GREEN}✓ 数据库检查完成(未覆盖现有数据)${NC}"
fi
# 配置 pg_hba.conf
# 配置 pg_hba.conf(仅追加缺失项)
PG_HBA=$(sudo -u postgres psql -t -c "SHOW hba_file;" | xargs)
if ! grep -q "funmc" "$PG_HBA"; then
echo "local funmc funmc md5" >> "$PG_HBA"
echo "host funmc funmc 127.0.0.1/32 md5" >> "$PG_HBA"
systemctl reload postgresql
fi
echo -e "${GREEN}✓ 数据库配置完成(密码 12345678已强制覆盖旧数据${NC}"
}
# 下载并编译 FunMC
@@ -160,18 +182,19 @@ build_funmc() {
echo -e "${GREEN}✓ FunMC 编译完成${NC}"
}
# 配置服务
# 配置服务-force 时重写配置;否则若已有配置则保留仅做迁移与重启)
configure_services() {
echo -e "${YELLOW}[6/7] 配置服务...${NC}"
WROTE_CONFIG=0
if [ "$FORCE_INSTALL" -eq 1 ] || [ ! -f "$CONFIG_DIR/server.env" ]; then
# 强制安装或首次安装:生成新配置
WROTE_CONFIG=1
DB_PASSWORD="12345678"
JWT_SECRET=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 48)
ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 12)
# 获取服务器 IP
SERVER_IP=$(curl -s ifconfig.me || curl -s ipinfo.io/ip || hostname -I | awk '{print $1}')
# 创建主配置文件
cat > $CONFIG_DIR/server.env << EOF
# FunMC 服务端配置
DATABASE_URL=postgres://funmc:${DB_PASSWORD}@localhost/funmc
@@ -196,14 +219,21 @@ CLIENT_VERSION=${FUNMC_VERSION}
DOWNLOADS_DIR=$INSTALL_DIR/downloads
EOF
# 创建中继配置
cat > $CONFIG_DIR/relay.env << EOF
RELAY_PORT=7900
JWT_SECRET=${JWT_SECRET}
RUST_LOG=info
EOF
else
# 更新模式:保留现有配置,仅确保 DB_PASSWORD 等变量存在供后续迁移使用
DB_PASSWORD=$(grep DATABASE_URL "$CONFIG_DIR/server.env" 2>/dev/null | sed -n 's/.*:\/\/funmc:\([^@]*\)@.*/\1/p')
if [ -z "$DB_PASSWORD" ]; then
DB_PASSWORD="12345678"
fi
echo -e "${GREEN}✓ 保留现有配置(未覆盖 server.env / relay.env${NC}"
fi
# 创建 systemd 服务文件
# 创建 systemd 服务文件(始终更新以便安装路径等变更生效)
cat > /etc/systemd/system/funmc-server.service << EOF
[Unit]
Description=FunMC API Server
@@ -256,7 +286,8 @@ EOF
echo -e "${GREEN}✓ 服务配置完成${NC}"
# 保存凭据
# 仅强制/首次安装时写入凭据文件,更新模式不覆盖
if [ "$WROTE_CONFIG" -eq 1 ]; then
cat > $CONFIG_DIR/credentials.txt << EOF
======================================
FunMC 服务端安装信息
@@ -277,6 +308,7 @@ JWT 密钥: ${JWT_SECRET}
======================================
EOF
chmod 600 $CONFIG_DIR/credentials.txt
fi
}
# 配置防火墙

View File

@@ -32,6 +32,24 @@ pub struct ClientBuild {
pub status: String,
}
/// 列出 downloads 目录下可用的文件名(供下载页仅对存在的文件显示「下载」)
pub async fn list_download_files() -> Json<Vec<String>> {
let downloads_dir = std::env::var("DOWNLOADS_DIR").unwrap_or_else(|_| "./downloads".to_string());
let mut names = Vec::new();
if let Ok(mut rd) = tokio::fs::read_dir(&downloads_dir).await {
while let Ok(Some(entry)) = rd.next_entry().await {
if let Ok(meta) = entry.metadata().await {
if meta.is_file() {
if let Ok(name) = entry.file_name().into_string() {
names.push(name);
}
}
}
}
}
Json(names)
}
pub async fn get_client_config(State(state): State<Arc<AppState>>) -> Json<ClientConfig> {
let config = state.server_config.read().unwrap();
@@ -103,8 +121,8 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<div class="text-5xl mb-4">🪟</div>
<h3 class="text-xl font-semibold mb-2">Windows</h3>
<p class="text-gray-500 text-sm mb-4">Windows 10/11</p>
<a href="{server_url}/api/v1/download/FunMC-{version}-windows-x64.exe"
class="inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
<a href="{server_url}/api/v1/download/FunMC-{version}-windows-x64.exe" data-download-file="FunMC-{version}-windows-x64.exe"
class="dl-link inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
下载 .exe
</a>
</div>
@@ -117,12 +135,12 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<h3 class="text-xl font-semibold mb-2">macOS</h3>
<p class="text-gray-500 text-sm mb-4">macOS 11+</p>
<div class="space-y-2">
<a href="{server_url}/api/v1/download/FunMC-{version}-macos-arm64.dmg"
class="inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
<a href="{server_url}/api/v1/download/FunMC-{version}-macos-arm64.dmg" data-download-file="FunMC-{version}-macos-arm64.dmg"
class="dl-link inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
Apple Silicon
</a>
<a href="{server_url}/api/v1/download/FunMC-{version}-macos-x64.dmg"
class="inline-block w-full py-2 px-4 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm">
<a href="{server_url}/api/v1/download/FunMC-{version}-macos-x64.dmg" data-download-file="FunMC-{version}-macos-x64.dmg"
class="dl-link inline-block w-full py-2 px-4 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm">
Intel Mac
</a>
</div>
@@ -135,8 +153,8 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<div class="text-5xl mb-4">🐧</div>
<h3 class="text-xl font-semibold mb-2">Linux</h3>
<p class="text-gray-500 text-sm mb-4">Ubuntu/Debian/Fedora</p>
<a href="{server_url}/api/v1/download/FunMC-{version}-linux-x64.AppImage"
class="inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
<a href="{server_url}/api/v1/download/FunMC-{version}-linux-x64.AppImage" data-download-file="FunMC-{version}-linux-x64.AppImage"
class="dl-link inline-block w-full py-3 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium">
下载 AppImage
</a>
</div>
@@ -151,8 +169,8 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
<div class="text-center">
<div class="text-4xl mb-3">🤖</div>
<h4 class="font-semibold mb-2">Android</h4>
<a href="{server_url}/api/v1/download/FunMC-{version}-android.apk"
class="inline-block w-full py-2 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
<a href="{server_url}/api/v1/download/FunMC-{version}-android.apk" data-download-file="FunMC-{version}-android.apk"
class="dl-link inline-block w-full py-2 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
下载 APK
</a>
</div>
@@ -186,6 +204,20 @@ pub async fn download_page(State(state): State<Arc<AppState>>) -> Html<String> {
魔幻方开发 · FunMC
</div>
</div>
<script>
(function(){{
var apiBase = window.location.origin + '/api/v1';
fetch(apiBase + '/download/list').then(function(r){{ return r.json(); }}).then(function(list){{
var set = new Set(list || []);
document.querySelectorAll('a.dl-link[data-download-file]').forEach(function(a){{
var name = a.getAttribute('data-download-file');
if (!set.has(name)) {{
a.outerHTML = '<span class="inline-block w-full py-3 px-4 bg-gray-300 text-gray-500 rounded-lg cursor-not-allowed">暂无</span>';
}}
}});
}}).catch(function(){{}});
}})();
</script>
</body>
</html>"##,
server_name = config.server_name,

View File

@@ -59,6 +59,7 @@ pub fn router(_state: Arc<AppState>) -> Router<Arc<AppState>> {
.route("/admin/builds/trigger", post(download::trigger_build))
// Download
.route("/client-config", get(download::get_client_config))
.route("/download/list", get(download::list_download_files))
.route("/download/:filename", get(download::download_file))
// WebSocket signaling
.route("/ws", get(ws_handler))