diff --git a/config.go b/config.go index ab875dd..f06d786 100644 --- a/config.go +++ b/config.go @@ -51,11 +51,12 @@ type mysqlConfig struct { } type webConfig struct { - Enabled bool `yaml:"enabled"` - Host string `yaml:"host"` - Port int `yaml:"port"` - StaticDir string `yaml:"static_dir"` - Admin webAdminConfig `yaml:"admin"` + Enabled bool `yaml:"enabled"` + Host string `yaml:"host"` + Port int `yaml:"port"` + SocketPath string `yaml:"socket_path"` + StaticDir string `yaml:"static_dir"` + Admin webAdminConfig `yaml:"admin"` } type webAdminConfig struct { @@ -103,11 +104,12 @@ type rawMySQLConfig struct { } type rawWebConfig struct { - Enabled *bool `yaml:"enabled"` - Host *string `yaml:"host"` - Port *int `yaml:"port"` - StaticDir *string `yaml:"static_dir"` - Admin *rawWebAdminConfig `yaml:"admin"` + Enabled *bool `yaml:"enabled"` + Host *string `yaml:"host"` + Port *int `yaml:"port"` + SocketPath *string `yaml:"socket_path"` + StaticDir *string `yaml:"static_dir"` + Admin *rawWebAdminConfig `yaml:"admin"` } type rawWebAdminConfig struct { @@ -138,10 +140,11 @@ func defaultConfig() *config { MySQL: mysqlConfig{DSN: ""}, }, Web: webConfig{ - Enabled: true, - Host: "0.0.0.0", - Port: 8080, - StaticDir: "./dist", + Enabled: true, + Host: "0.0.0.0", + Port: 8080, + SocketPath: defaultWebSocketPath(), + StaticDir: "./dist", Admin: webAdminConfig{ Username: "admin", Password: "admin", @@ -169,6 +172,17 @@ func defaultSQLitePath() string { return defaultSQLitePathForGOOS(runtime.GOOS) } +func defaultWebSocketPath() string { + return defaultWebSocketPathForGOOS(runtime.GOOS) +} + +func defaultWebSocketPathForGOOS(goos string) string { + if goos == "windows" { + return filepath.Join(".", "win", "opt", "mesh_mqtt_go", "web.sock") + } + return filepath.Join(string(filepath.Separator), "opt", "mesh_mqtt_go", "web.sock") +} + func defaultSQLitePathForGOOS(goos string) string { if goos == "windows" { return filepath.Join(".", "win", "etc", "mesh_mqtt_go", "mesh_mqtt_go.db") @@ -307,6 +321,11 @@ func normalizeConfig(raw rawConfig) (*config, bool) { } else { cfg.Web.Port = *raw.Web.Port } + if raw.Web.SocketPath == nil { + changed = true + } else { + cfg.Web.SocketPath = *raw.Web.SocketPath + } if raw.Web.StaticDir == nil { changed = true } else { @@ -358,7 +377,7 @@ func validateConfig(cfg *config) error { return fmt.Errorf("invalid database.driver %q: must be sqlite or mysql", cfg.Database.Driver) } if cfg.Web.Enabled { - if cfg.Web.Port <= 0 || cfg.Web.Port > 65535 { + if cfg.Web.SocketPath == "" && (cfg.Web.Port <= 0 || cfg.Web.Port > 65535) { return fmt.Errorf("invalid web port %d: must be 1-65535", cfg.Web.Port) } if cfg.Web.StaticDir == "" { diff --git a/config_test.go b/config_test.go index e82a620..013ad38 100644 --- a/config_test.go +++ b/config_test.go @@ -38,6 +38,9 @@ func TestLoadConfigCreatesDefaultFile(t *testing.T) { if cfg.Web.Port != 8080 { t.Fatalf("web port = %d, want 8080", cfg.Web.Port) } + if cfg.Web.SocketPath != defaultWebSocketPath() { + t.Fatalf("web socket path = %q, want %q", cfg.Web.SocketPath, defaultWebSocketPath()) + } if cfg.Web.StaticDir != "./dist" { t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir) } @@ -77,7 +80,7 @@ func TestLoadConfigFillsMissingFields(t *testing.T) { t.Fatal(err) } text := string(data) - for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port:", "static_dir:"} { + for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port:", "socket_path:", "static_dir:"} { if !strings.Contains(text, want) { t.Fatalf("completed config missing %q in:\n%s", want, text) } @@ -187,11 +190,25 @@ func TestValidateConfigDatabase(t *testing.T) { func TestValidateConfigWeb(t *testing.T) { cfg := defaultConfig() + cfg.Web.SocketPath = "" cfg.Web.Port = 0 if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web port") { t.Fatalf("invalid web port error = %v, want web port error", err) } + cfg = defaultConfig() + cfg.Web.Port = 0 + if err := validateConfig(cfg); err != nil { + t.Fatalf("web socket with invalid port error = %v, want nil", err) + } + + cfg = defaultConfig() + cfg.Web.SocketPath = "" + cfg.Web.Port = 0 + if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web port") { + t.Fatalf("invalid web port without socket error = %v, want web port error", err) + } + cfg = defaultConfig() cfg.Web.StaticDir = "" if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web.static_dir") { diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..d4905e8 --- /dev/null +++ b/install.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +SERVICE_NAME="mesh_mqtt_go" +SERVICE_USER="mesh_mqtt_go" +CONFIG_DIR="/etc/${SERVICE_NAME}" +DATA_DIR="/srv/${SERVICE_NAME}" +INSTALL_DIR="/opt/${SERVICE_NAME}" +SOCKET_PATH="${INSTALL_DIR}/web.sock" +FRONTEND_DIR="meshmap_frontend" +BINARY_NAME="${SERVICE_NAME}" +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + +if [[ "${EUID}" -ne 0 ]]; then + echo "请使用 root 权限运行: sudo $0" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +echo "拉取最新代码..." +git pull + +echo "编译前端..." +cd "${SCRIPT_DIR}/${FRONTEND_DIR}" +if [[ -f package-lock.json ]]; then + npm ci +else + npm install +fi +npm run build + +echo "编译 Go 程序..." +cd "${SCRIPT_DIR}" +go build -o "${BINARY_NAME}" . + +echo "检查系统用户..." +if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then + useradd --system --home-dir "${DATA_DIR}" --shell /usr/sbin/nologin "${SERVICE_USER}" +fi + +echo "创建目录..." +install -d -m 0750 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${CONFIG_DIR}" "${DATA_DIR}" +install -d -m 0755 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${INSTALL_DIR}" + +echo "安装程序和前端文件..." +install -m 0755 -o root -g root "${SCRIPT_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}" +rm -rf "${INSTALL_DIR}/dist" +cp -a "${SCRIPT_DIR}/${FRONTEND_DIR}/dist" "${INSTALL_DIR}/dist" +chown root:root "${INSTALL_DIR}/${BINARY_NAME}" +chown -R root:root "${INSTALL_DIR}/dist" +chown "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}" +chmod 0755 "${INSTALL_DIR}" +find "${INSTALL_DIR}/dist" -type d -exec chmod 0755 {} \; +find "${INSTALL_DIR}/dist" -type f -exec chmod 0644 {} \; + +if [[ ! -f "${CONFIG_DIR}/config.yaml" ]]; then + cat > "${CONFIG_DIR}/config.yaml" < "${SERVICE_FILE}" <