mirror of
https://github.com/nianzhibai/91.git
synced 2026-06-15 08:45:41 +08:00
feat: add prebuilt installer workflow
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.x"
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
|
||||
- name: Build release packages
|
||||
run: scripts/build-release.sh
|
||||
|
||||
- name: Upload release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
gh release upload "$TAG" release/*.tar.gz --clobber
|
||||
else
|
||||
gh release create "$TAG" release/*.tar.gz --title "$TAG" --notes "Prebuilt Linux release packages."
|
||||
fi
|
||||
@@ -23,6 +23,7 @@ tools/
|
||||
|
||||
# 编译产物
|
||||
backend/server
|
||||
release/
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# 91 爬虫脚本独立运行时的默认输出文件(backend 跑时会显式 --output 到 backend/data/spider91/,所以不会落在这里)
|
||||
|
||||
@@ -71,6 +71,68 @@ npm install
|
||||
FRONTEND_MODE=dev ./start.sh --restart
|
||||
```
|
||||
|
||||
## 新服务器一键安装
|
||||
|
||||
如果你只是想在一台 Ubuntu / Debian 服务器上尽快跑起来,推荐使用预编译安装脚本。普通用户不需要安装 Go、Node.js,也不需要自己编译;脚本会按服务器 CPU 架构下载 GitHub Release 里的预编译包,安装运行依赖,写入 systemd 服务并启动。
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y curl ca-certificates
|
||||
curl -fsSL https://raw.githubusercontent.com/nianzhibai/91/main/install.sh -o install.sh
|
||||
sudo bash install.sh
|
||||
```
|
||||
|
||||
部署完成后访问:
|
||||
|
||||
- 前台:`http://服务器IP:9191/`
|
||||
- 后台:`http://服务器IP:9191/admin`
|
||||
|
||||
第一次打开后台会要求设置管理员用户名和密码。常用维护命令:
|
||||
|
||||
```bash
|
||||
sudo bash install.sh status
|
||||
sudo bash install.sh logs
|
||||
sudo bash install.sh update
|
||||
sudo bash install.sh restart
|
||||
sudo bash install.sh stop
|
||||
```
|
||||
|
||||
安装后会自动创建 `91` 指令,和 OpenList 的管理指令类似:
|
||||
|
||||
```bash
|
||||
91 # 打开管理菜单
|
||||
91 status # 查看状态
|
||||
91 logs # 查看日志
|
||||
91 update # 更新
|
||||
91 restart # 重启
|
||||
91 stop # 停止
|
||||
```
|
||||
|
||||
同时也保留 `video-site-91` 作为同等别名。
|
||||
|
||||
想换端口:
|
||||
|
||||
```bash
|
||||
FRONTEND_PORT=8080 sudo -E bash install.sh
|
||||
```
|
||||
|
||||
如果服务器还有云厂商安全组,请记得放行对应端口,默认是 `9191/tcp`。
|
||||
|
||||
如果你是项目维护者,要预先编译发布包:
|
||||
|
||||
```bash
|
||||
scripts/build-release.sh
|
||||
```
|
||||
|
||||
它会生成:
|
||||
|
||||
- `release/video-site-91-linux-amd64.tar.gz`
|
||||
- `release/video-site-91-linux-arm64.tar.gz`
|
||||
|
||||
把这两个文件上传到 GitHub Release 后,`install.sh` 就能自动下载。仓库也带了 GitHub Actions:推送 `v*` 标签时会自动构建并上传这两个 Release 包。
|
||||
|
||||
源码部署仍然保留在 `deploy.sh`,适合你想在服务器上直接 clone、编译和调试时使用。
|
||||
|
||||
## 第一次使用
|
||||
|
||||
1. 打开 `http://127.0.0.1:9191/`,先完成管理员账号设置。
|
||||
|
||||
@@ -165,6 +165,14 @@ go test ./... -count=1
|
||||
|
||||
## 部署到 Linux
|
||||
|
||||
推荐先使用根目录的预编译安装脚本:
|
||||
|
||||
```bash
|
||||
sudo bash install.sh
|
||||
```
|
||||
|
||||
它会从 GitHub Release 下载预编译包,安装运行依赖、写入 systemd 服务并启动。下面是手动部署方式,适合你想自己接管构建和服务管理时使用。
|
||||
|
||||
```bash
|
||||
# 交叉编译
|
||||
GOOS=linux GOARCH=amd64 go build -o video-server ./cmd/server
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFrontendHandlerServesStaticAsset(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
assets := filepath.Join(dir, "assets")
|
||||
if err := os.MkdirAll(assets, 0o755); err != nil {
|
||||
t.Fatalf("mkdir assets: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<html>app</html>"), 0o644); err != nil {
|
||||
t.Fatalf("write index: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(assets, "app.js"), []byte("console.log('ok')"), 0o644); err != nil {
|
||||
t.Fatalf("write asset: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/assets/app.js", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
frontendHandler(dir).ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "console.log") {
|
||||
t.Fatalf("body = %q, want asset content", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontendHandlerFallsBackToIndexForSPARoute(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<html>app</html>"), 0o644); err != nil {
|
||||
t.Fatalf("write index: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
frontendHandler(dir).ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
if rr.Body.String() != "<html>app</html>" {
|
||||
t.Fatalf("body = %q, want index", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontendHandlerDoesNotSwallowBackendRoutes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<html>app</html>"), 0o644); err != nil {
|
||||
t.Fatalf("write index: %v", err)
|
||||
}
|
||||
|
||||
for _, target := range []string{"/api/missing", "/admin/api/missing", "/p/missing"} {
|
||||
req := httptest.NewRequest(http.MethodGet, target, nil)
|
||||
rr := httptest.NewRecorder()
|
||||
frontendHandler(dir).ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("%s status = %d, want 404", target, rr.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -212,6 +213,7 @@ func main() {
|
||||
|
||||
apiServer.RegisterRoutes(r, authr)
|
||||
adminServer.Register(r)
|
||||
mountFrontend(r)
|
||||
|
||||
// 凌晨流水线:每天 cron_hour 触发一次,串行跑
|
||||
// Phase 1 扫所有非 spider91 / localupload 网盘 + 删除检测 + 入队封面/teaser
|
||||
@@ -1441,6 +1443,65 @@ func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func mountFrontend(r chi.Router) {
|
||||
dir := strings.TrimSpace(os.Getenv("VIDEO_FRONTEND_DIR"))
|
||||
if dir == "" {
|
||||
dir = "./dist"
|
||||
}
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return
|
||||
}
|
||||
indexPath := filepath.Join(dir, "index.html")
|
||||
if st, err := os.Stat(indexPath); err != nil || st.IsDir() {
|
||||
return
|
||||
}
|
||||
log.Printf("serving frontend from %s", dir)
|
||||
r.NotFound(frontendHandler(dir))
|
||||
}
|
||||
|
||||
func frontendHandler(dir string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if isBackendRoute(r.URL.Path) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
cleanPath := path.Clean("/" + r.URL.Path)
|
||||
rel := strings.TrimPrefix(cleanPath, "/")
|
||||
if rel != "" && rel != "." {
|
||||
name := filepath.FromSlash(rel)
|
||||
f, err := os.Open(filepath.Join(dir, name))
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
if st, statErr := f.Stat(); statErr == nil && !st.IsDir() {
|
||||
http.ServeContent(w, r, st.Name(), st.ModTime(), f)
|
||||
return
|
||||
}
|
||||
}
|
||||
if filepath.Ext(name) != "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, filepath.Join(dir, "index.html"))
|
||||
}
|
||||
}
|
||||
|
||||
func isBackendRoute(p string) bool {
|
||||
return p == "/api" ||
|
||||
strings.HasPrefix(p, "/api/") ||
|
||||
p == "/admin/api" ||
|
||||
strings.HasPrefix(p, "/admin/api/") ||
|
||||
p == "/p" ||
|
||||
strings.HasPrefix(p, "/p/")
|
||||
}
|
||||
|
||||
func parseBoolDefault(raw string, def bool) bool {
|
||||
if raw == "" {
|
||||
return def
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
SELF_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
APP_NAME="${APP_NAME:-video-site-91}"
|
||||
BACKEND_SERVICE="${BACKEND_SERVICE:-video-site-backend}"
|
||||
FRONTEND_SERVICE="${FRONTEND_SERVICE:-video-site-frontend}"
|
||||
FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}"
|
||||
FRONTEND_PORT="${FRONTEND_PORT:-9191}"
|
||||
BACKEND_LISTEN="${BACKEND_LISTEN:-127.0.0.1:9192}"
|
||||
GO_VERSION="${GO_VERSION:-1.23.12}"
|
||||
INSTALL_DEPS="${INSTALL_DEPS:-1}"
|
||||
CONFIGURE_UFW="${CONFIGURE_UFW:-1}"
|
||||
|
||||
export PATH="/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
||||
|
||||
log() {
|
||||
printf '\033[1;34m[deploy]\033[0m %s\n' "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '\033[1;33m[deploy]\033[0m %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: sudo bash deploy.sh [install|update|restart|stop|status|logs|uninstall]
|
||||
|
||||
Default action:
|
||||
install Install dependencies, build, create systemd services, and start.
|
||||
|
||||
Actions:
|
||||
install First deployment or full repair
|
||||
update Rebuild current code and restart services
|
||||
restart Restart systemd services
|
||||
stop Stop systemd services
|
||||
status Show service status
|
||||
logs Follow backend and frontend logs
|
||||
uninstall Remove systemd services only; keep repo, config, and data
|
||||
|
||||
Common overrides:
|
||||
FRONTEND_PORT=9191 Public web port
|
||||
FRONTEND_HOST=0.0.0.0 Public web bind address
|
||||
GO_VERSION=1.23.12
|
||||
INSTALL_DEPS=0 Do not install missing Node/Go/ffmpeg
|
||||
CONFIGURE_UFW=0 Do not open UFW port automatically
|
||||
DEPLOY_USER=<user> Service user; defaults to sudo user or root
|
||||
|
||||
Examples:
|
||||
sudo bash deploy.sh
|
||||
FRONTEND_PORT=8080 sudo -E bash deploy.sh
|
||||
sudo bash deploy.sh update
|
||||
sudo bash deploy.sh logs
|
||||
EOF
|
||||
}
|
||||
|
||||
need_root() {
|
||||
if [[ "${EUID}" -eq 0 ]]; then
|
||||
return
|
||||
fi
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
die "this action needs root. Re-run as root or install sudo."
|
||||
fi
|
||||
log "root permission required; re-running with sudo"
|
||||
exec sudo -E bash "$SELF_PATH" "$@"
|
||||
}
|
||||
|
||||
detect_deploy_user() {
|
||||
DEPLOY_USER="${DEPLOY_USER:-${SUDO_USER:-$(id -un)}}"
|
||||
if [[ "$REPO_DIR" == /root/* && "$DEPLOY_USER" != "root" ]]; then
|
||||
warn "repo is under /root; using root as service user so systemd can read it"
|
||||
DEPLOY_USER="root"
|
||||
fi
|
||||
if ! id "$DEPLOY_USER" >/dev/null 2>&1; then
|
||||
die "DEPLOY_USER does not exist: $DEPLOY_USER"
|
||||
fi
|
||||
DEPLOY_GROUP="${DEPLOY_GROUP:-$(id -gn "$DEPLOY_USER")}"
|
||||
DEPLOY_HOME="$(getent passwd "$DEPLOY_USER" | cut -d: -f6)"
|
||||
if [[ -z "$DEPLOY_HOME" ]]; then
|
||||
DEPLOY_HOME="/root"
|
||||
fi
|
||||
}
|
||||
|
||||
as_deploy_user() {
|
||||
if [[ "$DEPLOY_USER" == "root" ]]; then
|
||||
HOME="$DEPLOY_HOME" PATH="$PATH" "$@"
|
||||
return
|
||||
fi
|
||||
runuser -u "$DEPLOY_USER" -- env HOME="$DEPLOY_HOME" PATH="$PATH" "$@"
|
||||
}
|
||||
|
||||
require_repo() {
|
||||
[[ -f "$REPO_DIR/package.json" ]] || die "package.json not found; run this script from the project root"
|
||||
[[ -d "$REPO_DIR/backend" ]] || die "backend directory not found; run this script from the project root"
|
||||
}
|
||||
|
||||
version_ge() {
|
||||
[[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]]
|
||||
}
|
||||
|
||||
node_ok() {
|
||||
command -v node >/dev/null 2>&1 || return 1
|
||||
command -v npm >/dev/null 2>&1 || return 1
|
||||
local major
|
||||
major="$(node -v | sed -E 's/^v([0-9]+).*/\1/')"
|
||||
[[ "$major" =~ ^[0-9]+$ ]] && (( major >= 18 ))
|
||||
}
|
||||
|
||||
go_ok() {
|
||||
command -v go >/dev/null 2>&1 || return 1
|
||||
local version
|
||||
version="$(go env GOVERSION 2>/dev/null || true)"
|
||||
if [[ -z "$version" ]]; then
|
||||
version="$(go version | awk '{print $3}')"
|
||||
fi
|
||||
version="${version#go}"
|
||||
version_ge "$version" "1.23"
|
||||
}
|
||||
|
||||
apt_install() {
|
||||
[[ "$INSTALL_DEPS" == "1" ]] || die "missing dependencies and INSTALL_DEPS=0"
|
||||
command -v apt-get >/dev/null 2>&1 || die "automatic install currently supports Debian/Ubuntu with apt-get"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
log "installing base packages"
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl git ffmpeg openssl iproute2 build-essential
|
||||
}
|
||||
|
||||
install_node() {
|
||||
if node_ok; then
|
||||
log "Node $(node -v) and npm $(npm -v) are ready"
|
||||
return
|
||||
fi
|
||||
[[ "$INSTALL_DEPS" == "1" ]] || die "Node.js 18+ and npm are required"
|
||||
command -v apt-get >/dev/null 2>&1 || die "install Node.js 18+ manually, then re-run"
|
||||
log "installing Node.js 20 from NodeSource"
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/video-site-nodesource.sh
|
||||
bash /tmp/video-site-nodesource.sh
|
||||
apt-get install -y nodejs
|
||||
node_ok || die "Node.js install finished, but node/npm version check still failed"
|
||||
log "Node $(node -v) and npm $(npm -v) are ready"
|
||||
}
|
||||
|
||||
install_go() {
|
||||
if go_ok; then
|
||||
log "Go $(go env GOVERSION 2>/dev/null || go version | awk '{print $3}') is ready"
|
||||
return
|
||||
fi
|
||||
[[ "$INSTALL_DEPS" == "1" ]] || die "Go 1.23+ is required"
|
||||
|
||||
local arch go_arch tmp url
|
||||
arch="$(uname -m)"
|
||||
case "$arch" in
|
||||
x86_64|amd64) go_arch="amd64" ;;
|
||||
aarch64|arm64) go_arch="arm64" ;;
|
||||
*) die "unsupported CPU architecture for automatic Go install: $arch" ;;
|
||||
esac
|
||||
|
||||
url="https://go.dev/dl/go${GO_VERSION}.linux-${go_arch}.tar.gz"
|
||||
tmp="$(mktemp -d)"
|
||||
log "installing Go ${GO_VERSION} from ${url}"
|
||||
curl -fL "$url" -o "$tmp/go.tgz"
|
||||
rm -rf /usr/local/go
|
||||
tar -C /usr/local -xzf "$tmp/go.tgz"
|
||||
rm -rf "$tmp"
|
||||
go_ok || die "Go install finished, but go version check still failed"
|
||||
log "Go $(go env GOVERSION) is ready"
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
if [[ "$INSTALL_DEPS" == "1" ]]; then
|
||||
apt_install
|
||||
fi
|
||||
install_node
|
||||
install_go
|
||||
command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg is required"
|
||||
command -v ffprobe >/dev/null 2>&1 || die "ffprobe is required"
|
||||
}
|
||||
|
||||
ensure_ownership() {
|
||||
local paths=()
|
||||
[[ -e "$REPO_DIR/backend/config.yaml" ]] && paths+=("$REPO_DIR/backend/config.yaml")
|
||||
[[ -d "$REPO_DIR/backend/data" ]] && paths+=("$REPO_DIR/backend/data")
|
||||
[[ -d "$REPO_DIR/dist" ]] && paths+=("$REPO_DIR/dist")
|
||||
[[ -d "$REPO_DIR/node_modules" ]] && paths+=("$REPO_DIR/node_modules")
|
||||
[[ -e "$REPO_DIR/backend/server" ]] && paths+=("$REPO_DIR/backend/server")
|
||||
if (( ${#paths[@]} > 0 )); then
|
||||
chown -R "$DEPLOY_USER:$DEPLOY_GROUP" "${paths[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_config() {
|
||||
local cfg="$REPO_DIR/backend/config.yaml"
|
||||
local example="$REPO_DIR/backend/config.example.yaml"
|
||||
mkdir -p "$REPO_DIR/backend/data"
|
||||
|
||||
if [[ ! -f "$cfg" ]]; then
|
||||
log "creating backend/config.yaml from example"
|
||||
cp "$example" "$cfg"
|
||||
sed -i -E "s#listen: \".*\"#listen: \"$BACKEND_LISTEN\"#" "$cfg"
|
||||
else
|
||||
log "backend/config.yaml already exists; keeping it"
|
||||
fi
|
||||
|
||||
if grep -q 'session_secret: "change-me-to-a-random-string"' "$cfg"; then
|
||||
local secret
|
||||
secret="$(openssl rand -hex 32)"
|
||||
sed -i -E "s#session_secret: \".*\"#session_secret: \"$secret\"#" "$cfg"
|
||||
log "generated a random session_secret"
|
||||
fi
|
||||
|
||||
ensure_ownership
|
||||
}
|
||||
|
||||
install_frontend() {
|
||||
log "installing frontend dependencies"
|
||||
if [[ -f "$REPO_DIR/package-lock.json" ]]; then
|
||||
as_deploy_user bash -lc "cd '$REPO_DIR' && npm ci"
|
||||
else
|
||||
as_deploy_user bash -lc "cd '$REPO_DIR' && npm install"
|
||||
fi
|
||||
|
||||
log "building frontend"
|
||||
as_deploy_user bash -lc "cd '$REPO_DIR' && npm run build"
|
||||
}
|
||||
|
||||
build_backend() {
|
||||
log "building backend binary"
|
||||
as_deploy_user bash -lc "cd '$REPO_DIR/backend' && go build -o server ./cmd/server"
|
||||
}
|
||||
|
||||
systemd_env_lines() {
|
||||
local lines=""
|
||||
if [[ -n "${HTTP_PROXY:-}" ]]; then
|
||||
lines+="Environment=HTTP_PROXY=${HTTP_PROXY}"$'\n'
|
||||
fi
|
||||
if [[ -n "${HTTPS_PROXY:-}" ]]; then
|
||||
lines+="Environment=HTTPS_PROXY=${HTTPS_PROXY}"$'\n'
|
||||
fi
|
||||
if [[ -n "${NO_PROXY:-}" ]]; then
|
||||
lines+="Environment=NO_PROXY=${NO_PROXY}"$'\n'
|
||||
fi
|
||||
printf '%s' "$lines"
|
||||
}
|
||||
|
||||
write_systemd_units() {
|
||||
local npm_bin backend_unit frontend_unit env_lines
|
||||
npm_bin="$(command -v npm)"
|
||||
backend_unit="/etc/systemd/system/${BACKEND_SERVICE}.service"
|
||||
frontend_unit="/etc/systemd/system/${FRONTEND_SERVICE}.service"
|
||||
env_lines="$(systemd_env_lines)"
|
||||
|
||||
log "writing systemd unit: $backend_unit"
|
||||
cat >"$backend_unit" <<EOF
|
||||
[Unit]
|
||||
Description=Video Site Backend
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${DEPLOY_USER}
|
||||
Group=${DEPLOY_GROUP}
|
||||
WorkingDirectory=${REPO_DIR}/backend
|
||||
ExecStart=${REPO_DIR}/backend/server
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=20
|
||||
Environment=HOME=${DEPLOY_HOME}
|
||||
Environment=PATH=/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
${env_lines}LimitNOFILE=65536
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=${BACKEND_SERVICE}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
log "writing systemd unit: $frontend_unit"
|
||||
cat >"$frontend_unit" <<EOF
|
||||
[Unit]
|
||||
Description=Video Site Frontend
|
||||
After=network-online.target ${BACKEND_SERVICE}.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${DEPLOY_USER}
|
||||
Group=${DEPLOY_GROUP}
|
||||
WorkingDirectory=${REPO_DIR}
|
||||
ExecStart=${npm_bin} run preview -- --host ${FRONTEND_HOST} --port ${FRONTEND_PORT}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=HOME=${DEPLOY_HOME}
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=${FRONTEND_SERVICE}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${BACKEND_SERVICE}.service" "${FRONTEND_SERVICE}.service" >/dev/null
|
||||
}
|
||||
|
||||
open_firewall_port() {
|
||||
[[ "$CONFIGURE_UFW" == "1" ]] || return
|
||||
command -v ufw >/dev/null 2>&1 || return
|
||||
if ufw status 2>/dev/null | grep -qi "Status: active"; then
|
||||
log "UFW is active; allowing ${FRONTEND_PORT}/tcp"
|
||||
ufw allow "${FRONTEND_PORT}/tcp"
|
||||
fi
|
||||
}
|
||||
|
||||
restart_services() {
|
||||
log "starting services"
|
||||
systemctl restart "${BACKEND_SERVICE}.service"
|
||||
systemctl restart "${FRONTEND_SERVICE}.service"
|
||||
}
|
||||
|
||||
show_summary() {
|
||||
echo
|
||||
log "deployment finished"
|
||||
echo " frontend: http://<server-ip>:${FRONTEND_PORT}/"
|
||||
echo " admin: http://<server-ip>:${FRONTEND_PORT}/admin"
|
||||
echo " backend: 127.0.0.1:9192"
|
||||
echo
|
||||
echo "First visit will ask you to create the admin username and password."
|
||||
echo "Useful commands:"
|
||||
echo " sudo bash deploy.sh status"
|
||||
echo " sudo bash deploy.sh logs"
|
||||
echo " sudo bash deploy.sh update"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
systemctl --no-pager --full status "${BACKEND_SERVICE}.service" "${FRONTEND_SERVICE}.service" || true
|
||||
}
|
||||
|
||||
install_or_update() {
|
||||
local mode="$1"
|
||||
require_repo
|
||||
detect_deploy_user
|
||||
install_dependencies
|
||||
prepare_config
|
||||
install_frontend
|
||||
build_backend
|
||||
write_systemd_units
|
||||
open_firewall_port
|
||||
restart_services
|
||||
show_status
|
||||
[[ "$mode" == "install" ]] && show_summary
|
||||
}
|
||||
|
||||
uninstall_services() {
|
||||
systemctl disable --now "${FRONTEND_SERVICE}.service" "${BACKEND_SERVICE}.service" 2>/dev/null || true
|
||||
rm -f "/etc/systemd/system/${FRONTEND_SERVICE}.service" "/etc/systemd/system/${BACKEND_SERVICE}.service"
|
||||
systemctl daemon-reload
|
||||
log "removed systemd services; repo, config, and data were kept"
|
||||
}
|
||||
|
||||
main() {
|
||||
local action="${1:-install}"
|
||||
case "$action" in
|
||||
install|deploy)
|
||||
need_root "$@"
|
||||
install_or_update "install"
|
||||
;;
|
||||
update)
|
||||
need_root "$@"
|
||||
install_or_update "update"
|
||||
;;
|
||||
restart)
|
||||
need_root "$@"
|
||||
restart_services
|
||||
show_status
|
||||
;;
|
||||
stop)
|
||||
need_root "$@"
|
||||
systemctl stop "${FRONTEND_SERVICE}.service" "${BACKEND_SERVICE}.service"
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
logs)
|
||||
journalctl -u "${BACKEND_SERVICE}.service" -u "${FRONTEND_SERVICE}.service" -f
|
||||
;;
|
||||
uninstall)
|
||||
need_root "$@"
|
||||
uninstall_services
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+462
@@ -0,0 +1,462 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
APP_NAME="${APP_NAME:-video-site-91}"
|
||||
GITHUB_REPO="${GITHUB_REPO:-nianzhibai/91}"
|
||||
INSTALL_PATH="${INSTALL_PATH:-/opt/video-site-91}"
|
||||
SERVICE_NAME="${SERVICE_NAME:-video-site-91}"
|
||||
FRONTEND_PORT_WAS_SET="${FRONTEND_PORT+x}"
|
||||
FRONTEND_PORT="${FRONTEND_PORT:-9191}"
|
||||
VERSION="${VERSION:-latest}"
|
||||
GH_PROXY="${GH_PROXY:-}"
|
||||
CONFIGURE_UFW="${CONFIGURE_UFW:-1}"
|
||||
INSTALL_DEPS="${INSTALL_DEPS:-1}"
|
||||
VERSION_FILE="$INSTALL_PATH/.version"
|
||||
MANAGER_PATH="/usr/local/sbin/${APP_NAME}-manager"
|
||||
COMMAND_LINK="/usr/local/bin/91"
|
||||
APP_COMMAND_LINK="/usr/local/bin/${APP_NAME}"
|
||||
|
||||
RED='\033[1;31m'
|
||||
GREEN='\033[1;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[1;34m'
|
||||
RESET='\033[0m'
|
||||
|
||||
log() {
|
||||
printf "${BLUE}[install]${RESET} %s\n" "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf "${YELLOW}[install]${RESET} %s\n" "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf "${RED}[install]${RESET} %s\n" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
sudo bash install.sh [install]
|
||||
91 [update|restart|stop|status|logs|uninstall]
|
||||
|
||||
Default action:
|
||||
install.sh with no args downloads the prebuilt release package and starts the service.
|
||||
91 with no args opens the management menu.
|
||||
|
||||
Actions:
|
||||
install Install to $INSTALL_PATH
|
||||
update Download latest release and replace program files, keeping config/data
|
||||
restart Restart service
|
||||
stop Stop service
|
||||
status Show service status
|
||||
logs Follow service logs
|
||||
uninstall Remove service and optionally delete installed files
|
||||
|
||||
Options via environment:
|
||||
GITHUB_REPO=$GITHUB_REPO
|
||||
VERSION=$VERSION latest or a release tag such as v0.1.0
|
||||
INSTALL_PATH=$INSTALL_PATH
|
||||
FRONTEND_PORT=$FRONTEND_PORT
|
||||
GH_PROXY=$GH_PROXY
|
||||
INSTALL_DEPS=$INSTALL_DEPS
|
||||
CONFIGURE_UFW=$CONFIGURE_UFW
|
||||
|
||||
Examples:
|
||||
sudo bash install.sh
|
||||
FRONTEND_PORT=8080 sudo -E bash install.sh
|
||||
91
|
||||
91 update
|
||||
91 logs
|
||||
EOF
|
||||
}
|
||||
|
||||
is_manager_invocation() {
|
||||
local name
|
||||
name="$(basename "$0")"
|
||||
[[ "$name" == "91" || "$name" == "$APP_NAME" || "$name" == "$(basename "$MANAGER_PATH")" ]]
|
||||
}
|
||||
|
||||
need_root() {
|
||||
if [[ "$(id -u)" == "0" ]]; then
|
||||
return
|
||||
fi
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
exec sudo -E bash "$0" "$@"
|
||||
fi
|
||||
die "please run as root"
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
local machine
|
||||
machine="$(uname -m)"
|
||||
case "$machine" in
|
||||
x86_64|amd64) ARCH="amd64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
*) die "unsupported architecture: $machine" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_base_url() {
|
||||
if [[ "$VERSION" == "latest" ]]; then
|
||||
printf '%shttps://github.com/%s/releases/latest/download' "$GH_PROXY" "$GITHUB_REPO"
|
||||
else
|
||||
printf '%shttps://github.com/%s/releases/download/%s' "$GH_PROXY" "$GITHUB_REPO" "$VERSION"
|
||||
fi
|
||||
}
|
||||
|
||||
asset_name() {
|
||||
printf '%s-linux-%s.tar.gz' "$APP_NAME" "$ARCH"
|
||||
}
|
||||
|
||||
install_deps() {
|
||||
if [[ "$INSTALL_DEPS" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
log "installing runtime dependencies"
|
||||
apt-get update
|
||||
apt-get install -y ca-certificates curl tar ffmpeg openssl iproute2 python3 python3-requests python3-bs4 python3-lxml
|
||||
return
|
||||
fi
|
||||
|
||||
for cmd in curl tar ffmpeg ffprobe openssl; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || die "missing command: $cmd"
|
||||
done
|
||||
}
|
||||
|
||||
check_system() {
|
||||
[[ "$(uname -s)" == "Linux" ]] || die "Linux is required"
|
||||
command -v systemctl >/dev/null 2>&1 || die "systemd is required"
|
||||
detect_arch
|
||||
}
|
||||
|
||||
check_disk_space() {
|
||||
local parent avail
|
||||
parent="$(dirname "$INSTALL_PATH")"
|
||||
mkdir -p "$parent"
|
||||
avail="$(df -Pm "$parent" | awk 'NR==2 {print $4}')"
|
||||
if [[ "$avail" =~ ^[0-9]+$ ]] && (( avail < 512 )); then
|
||||
die "not enough free space under $parent, need at least 512 MB"
|
||||
fi
|
||||
}
|
||||
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
local retry=0
|
||||
while (( retry < 3 )); do
|
||||
if curl -fL --connect-timeout 15 --retry 2 --retry-delay 2 "$url" -o "$output"; then
|
||||
[[ -s "$output" ]] && return 0
|
||||
fi
|
||||
retry=$((retry + 1))
|
||||
warn "download failed, retry $retry/3"
|
||||
sleep $((retry * 2))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare_config() {
|
||||
local cfg="$INSTALL_PATH/config.yaml"
|
||||
local example="$INSTALL_PATH/config.example.yaml"
|
||||
mkdir -p "$INSTALL_PATH/data"
|
||||
|
||||
if [[ ! -f "$cfg" ]]; then
|
||||
cp "$example" "$cfg"
|
||||
sed -i -E "s#listen: \".*\"#listen: \"0.0.0.0:${FRONTEND_PORT}\"#" "$cfg"
|
||||
chmod 600 "$cfg"
|
||||
log "created $cfg"
|
||||
else
|
||||
log "keeping existing $cfg"
|
||||
if [[ -n "$FRONTEND_PORT_WAS_SET" ]]; then
|
||||
sed -i -E "s#listen: \".*\"#listen: \"0.0.0.0:${FRONTEND_PORT}\"#" "$cfg"
|
||||
log "updated listen port to ${FRONTEND_PORT}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -q 'session_secret: "change-me-to-a-random-string"' "$cfg"; then
|
||||
local secret
|
||||
secret="$(openssl rand -hex 32)"
|
||||
sed -i -E "s#session_secret: \".*\"#session_secret: \"$secret\"#" "$cfg"
|
||||
log "generated random session_secret"
|
||||
fi
|
||||
}
|
||||
|
||||
write_service() {
|
||||
cat >"/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
|
||||
[Unit]
|
||||
Description=Video Site 91
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${INSTALL_PATH}
|
||||
ExecStart=${INSTALL_PATH}/server
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=20
|
||||
Environment=VIDEO_CONFIG=${INSTALL_PATH}/config.yaml
|
||||
Environment=VIDEO_FRONTEND_DIR=${INSTALL_PATH}/dist
|
||||
Environment=HOME=/root
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
LimitNOFILE=65536
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=${SERVICE_NAME}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${SERVICE_NAME}.service" >/dev/null
|
||||
}
|
||||
|
||||
install_cli() {
|
||||
local src
|
||||
src="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
if [[ -f "$src" ]]; then
|
||||
cp "$src" "$MANAGER_PATH"
|
||||
chmod 755 "$MANAGER_PATH"
|
||||
ln -sf "$MANAGER_PATH" "$COMMAND_LINK"
|
||||
ln -sf "$MANAGER_PATH" "$APP_COMMAND_LINK"
|
||||
fi
|
||||
}
|
||||
|
||||
open_firewall_port() {
|
||||
[[ "$CONFIGURE_UFW" == "1" ]] || return
|
||||
command -v ufw >/dev/null 2>&1 || return
|
||||
if ufw status 2>/dev/null | grep -qi "Status: active"; then
|
||||
log "allowing ${FRONTEND_PORT}/tcp in UFW"
|
||||
ufw allow "${FRONTEND_PORT}/tcp"
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_and_unpack() {
|
||||
local tmp archive url root
|
||||
tmp="$(mktemp -d)"
|
||||
archive="$tmp/$(asset_name)"
|
||||
url="$(download_base_url)/$(asset_name)"
|
||||
log "downloading $url"
|
||||
if ! download_file "$url" "$archive"; then
|
||||
warn "download failed: $url"
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! tar -xzf "$archive" -C "$tmp"; then
|
||||
warn "extract failed"
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
fi
|
||||
root="$tmp/${APP_NAME}-linux-${ARCH}"
|
||||
if [[ ! -f "$root/server" || ! -d "$root/dist" || ! -f "$root/config.example.yaml" ]]; then
|
||||
warn "release package layout is invalid"
|
||||
rm -rf "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$INSTALL_PATH"
|
||||
cp "$root/server" "$INSTALL_PATH/server"
|
||||
rm -rf "$INSTALL_PATH/dist"
|
||||
cp -R "$root/dist" "$INSTALL_PATH/dist"
|
||||
cp "$root/config.example.yaml" "$INSTALL_PATH/config.example.yaml"
|
||||
if [[ -d "$root/91VideoSpider" ]]; then
|
||||
rm -rf "$INSTALL_PATH/91VideoSpider"
|
||||
cp -R "$root/91VideoSpider" "$INSTALL_PATH/91VideoSpider"
|
||||
fi
|
||||
chmod +x "$INSTALL_PATH/server"
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
current_version_from_github() {
|
||||
if [[ "$VERSION" != "latest" ]]; then
|
||||
printf '%s' "$VERSION"
|
||||
return
|
||||
fi
|
||||
curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" \
|
||||
| sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' \
|
||||
| head -n1
|
||||
}
|
||||
|
||||
record_version() {
|
||||
local version
|
||||
version="$(current_version_from_github || true)"
|
||||
[[ -n "$version" ]] || version="$VERSION"
|
||||
{
|
||||
echo "$version"
|
||||
date '+%Y-%m-%d %H:%M:%S'
|
||||
} >"$VERSION_FILE"
|
||||
}
|
||||
|
||||
show_success() {
|
||||
local local_ip public_ip version
|
||||
local_ip="$(ip addr show 2>/dev/null | awk '/inet / && $2 !~ /^127/ {sub(/\/.*/, "", $2); print $2; exit}')"
|
||||
public_ip="$(curl -s4 --connect-timeout 5 ip.sb 2>/dev/null || true)"
|
||||
version="$(head -n1 "$VERSION_FILE" 2>/dev/null || echo unknown)"
|
||||
|
||||
echo
|
||||
printf "${GREEN}安装完成${RESET}\n"
|
||||
echo "版本:$version"
|
||||
[[ -n "$local_ip" ]] && echo "局域网:http://${local_ip}:${FRONTEND_PORT}/"
|
||||
[[ -n "$public_ip" ]] && echo "公网: http://${public_ip}:${FRONTEND_PORT}/"
|
||||
echo "后台: http://服务器IP:${FRONTEND_PORT}/admin"
|
||||
echo "数据: $INSTALL_PATH/data"
|
||||
echo
|
||||
echo "首次访问后台时会要求设置管理员用户名和密码。"
|
||||
echo "管理命令:91 或 91 status | logs | update | restart | stop"
|
||||
}
|
||||
|
||||
install_app() {
|
||||
check_system
|
||||
check_disk_space
|
||||
install_deps
|
||||
systemctl stop "${SERVICE_NAME}.service" 2>/dev/null || true
|
||||
fetch_and_unpack || die "install failed"
|
||||
prepare_config
|
||||
write_service
|
||||
install_cli
|
||||
open_firewall_port
|
||||
record_version
|
||||
systemctl restart "${SERVICE_NAME}.service"
|
||||
show_success
|
||||
}
|
||||
|
||||
update_app() {
|
||||
check_system
|
||||
check_disk_space
|
||||
install_deps
|
||||
[[ -f "$INSTALL_PATH/server" ]] || die "not installed at $INSTALL_PATH"
|
||||
|
||||
local backup
|
||||
backup="$(mktemp -d)"
|
||||
cp "$INSTALL_PATH/server" "$backup/server"
|
||||
[[ -d "$INSTALL_PATH/dist" ]] && cp -R "$INSTALL_PATH/dist" "$backup/dist"
|
||||
|
||||
systemctl stop "${SERVICE_NAME}.service" 2>/dev/null || true
|
||||
if ! fetch_and_unpack; then
|
||||
warn "update failed; restoring previous files"
|
||||
cp "$backup/server" "$INSTALL_PATH/server"
|
||||
rm -rf "$INSTALL_PATH/dist"
|
||||
[[ -d "$backup/dist" ]] && cp -R "$backup/dist" "$INSTALL_PATH/dist"
|
||||
systemctl start "${SERVICE_NAME}.service" 2>/dev/null || true
|
||||
rm -rf "$backup"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
prepare_config
|
||||
write_service
|
||||
install_cli
|
||||
record_version
|
||||
systemctl restart "${SERVICE_NAME}.service"
|
||||
rm -rf "$backup"
|
||||
log "updated"
|
||||
}
|
||||
|
||||
uninstall_app() {
|
||||
systemctl disable --now "${SERVICE_NAME}.service" 2>/dev/null || true
|
||||
rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
systemctl daemon-reload
|
||||
rm -f "$COMMAND_LINK" "$APP_COMMAND_LINK" "$MANAGER_PATH"
|
||||
|
||||
if [[ -t 0 ]]; then
|
||||
read -r -p "删除 $INSTALL_PATH 里的程序、配置和数据吗?[y/N]: " confirm
|
||||
case "$confirm" in
|
||||
[yY]) rm -rf "$INSTALL_PATH" ;;
|
||||
*) log "kept $INSTALL_PATH" ;;
|
||||
esac
|
||||
else
|
||||
log "removed service; kept $INSTALL_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
if [[ ! -t 0 ]]; then
|
||||
usage
|
||||
return 0
|
||||
fi
|
||||
|
||||
while true; do
|
||||
clear
|
||||
echo "欢迎使用 91 管理脚本"
|
||||
echo
|
||||
echo "基础功能:"
|
||||
echo "1、查看状态"
|
||||
echo "2、查看日志"
|
||||
echo "3、更新 91"
|
||||
echo "4、重启 91"
|
||||
echo "5、停止 91"
|
||||
echo "6、卸载 91"
|
||||
echo "0、退出"
|
||||
echo
|
||||
read -r -p "请输入选项 [0-6]: " choice
|
||||
|
||||
case "$choice" in
|
||||
1) main status ;;
|
||||
2) main logs ;;
|
||||
3) main update ;;
|
||||
4) main restart ;;
|
||||
5) main stop ;;
|
||||
6) main uninstall ;;
|
||||
0) exit 0 ;;
|
||||
*) echo "无效的选项" ;;
|
||||
esac
|
||||
|
||||
echo
|
||||
read -r -n1 -s -p "按任意键继续 ..."
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
local action="${1:-}"
|
||||
if [[ -z "$action" ]]; then
|
||||
if is_manager_invocation; then
|
||||
show_menu
|
||||
return
|
||||
fi
|
||||
action="install"
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
install)
|
||||
need_root "$@"
|
||||
install_app
|
||||
;;
|
||||
update)
|
||||
need_root "$@"
|
||||
update_app
|
||||
;;
|
||||
restart)
|
||||
need_root "$@"
|
||||
systemctl restart "${SERVICE_NAME}.service"
|
||||
;;
|
||||
stop)
|
||||
need_root "$@"
|
||||
systemctl stop "${SERVICE_NAME}.service"
|
||||
;;
|
||||
status)
|
||||
systemctl --no-pager --full status "${SERVICE_NAME}.service" || true
|
||||
;;
|
||||
logs)
|
||||
journalctl -u "${SERVICE_NAME}.service" -f
|
||||
;;
|
||||
menu)
|
||||
show_menu
|
||||
;;
|
||||
uninstall)
|
||||
need_root "$@"
|
||||
uninstall_app
|
||||
;;
|
||||
-h|--help|help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUT_DIR="${OUT_DIR:-$ROOT_DIR/release}"
|
||||
APP_NAME="${APP_NAME:-video-site-91}"
|
||||
VERSION="${VERSION:-$(git -C "$ROOT_DIR" describe --tags --always --dirty 2>/dev/null || date +%Y%m%d%H%M%S)}"
|
||||
|
||||
log() {
|
||||
printf '[release] %s\n' "$*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: scripts/build-release.sh
|
||||
|
||||
Builds precompiled release packages:
|
||||
release/video-site-91-linux-amd64.tar.gz
|
||||
release/video-site-91-linux-arm64.tar.gz
|
||||
|
||||
Environment overrides:
|
||||
OUT_DIR=$OUT_DIR
|
||||
APP_NAME=$APP_NAME
|
||||
VERSION=$VERSION
|
||||
EOF
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
need_cmd npm
|
||||
log "installing frontend dependencies"
|
||||
if [[ -f "$ROOT_DIR/package-lock.json" ]]; then
|
||||
npm --prefix "$ROOT_DIR" ci
|
||||
else
|
||||
npm --prefix "$ROOT_DIR" install
|
||||
fi
|
||||
|
||||
log "building frontend"
|
||||
npm --prefix "$ROOT_DIR" run build
|
||||
}
|
||||
|
||||
build_package() {
|
||||
local goos="$1"
|
||||
local goarch="$2"
|
||||
local artifact="$APP_NAME-$goos-$goarch"
|
||||
local work="$OUT_DIR/.work/$artifact"
|
||||
|
||||
rm -rf "$work"
|
||||
mkdir -p "$work"
|
||||
|
||||
log "building backend for $goos/$goarch"
|
||||
(
|
||||
cd "$ROOT_DIR/backend"
|
||||
CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" go build -trimpath -ldflags="-s -w" -o "$work/server" ./cmd/server
|
||||
)
|
||||
|
||||
cp "$ROOT_DIR/backend/config.example.yaml" "$work/config.example.yaml"
|
||||
cp -R "$ROOT_DIR/dist" "$work/dist"
|
||||
mkdir -p "$work/91VideoSpider"
|
||||
cp "$ROOT_DIR/91VideoSpider/spider_91porn.py" "$work/91VideoSpider/spider_91porn.py"
|
||||
|
||||
cat >"$work/README.txt" <<EOF
|
||||
$APP_NAME $VERSION
|
||||
|
||||
This is a prebuilt release package.
|
||||
Use install.sh from the repository to install it on a Linux server.
|
||||
EOF
|
||||
|
||||
chmod +x "$work/server"
|
||||
tar -C "$OUT_DIR/.work" -czf "$OUT_DIR/$artifact.tar.gz" "$artifact"
|
||||
log "wrote $OUT_DIR/$artifact.tar.gz"
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
-h|--help|help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
"")
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
need_cmd go
|
||||
need_cmd tar
|
||||
mkdir -p "$OUT_DIR/.work"
|
||||
build_frontend
|
||||
build_package linux amd64
|
||||
build_package linux arm64
|
||||
rm -rf "$OUT_DIR/.work"
|
||||
log "done"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user