diff --git a/cmd/weatherstationctl/main.go b/cmd/weatherstationctl/main.go new file mode 100644 index 0000000..9cec0df --- /dev/null +++ b/cmd/weatherstationctl/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// installPrefix is the base install directory. +// Binaries go to installPrefix/bin, assets and config go to installPrefix/. +const installPrefix = "/opt/weatherstation" + +func main() { + // Ensure target directories exist + binDir := filepath.Join(installPrefix, "bin") + if err := os.MkdirAll(binDir, 0o755); err != nil { + fatalf("创建目录失败: %s: %v", binDir, err) + } + + // Build all service-* under cmd/ + serviceDirs, err := findServiceDirs() + if err != nil { + fatalf("扫描服务目录失败: %v", err) + } + if len(serviceDirs) == 0 { + fatalf("未发现任何 service-* 微服务目录") + } + + for _, svc := range serviceDirs { + out := filepath.Join(binDir, svc) + pkg := filepath.ToSlash(filepath.Join("./cmd", svc)) + fmt.Printf("编译 %s -> %s\n", pkg, out) + if err := run("go", "build", "-o", out, pkg); err != nil { + fatalf("编译失败 %s: %v", pkg, err) + } + } + + // Copy templates, static, config.yaml to installPrefix + // Replace existing files/directories + if err := copyDirReplacing("templates", filepath.Join(installPrefix, "templates")); err != nil { + fatalf("复制 templates 失败: %v", err) + } + if err := copyDirReplacing("static", filepath.Join(installPrefix, "static")); err != nil { + fatalf("复制 static 失败: %v", err) + } + if err := copyFileReplacing("config.yaml", filepath.Join(installPrefix, "config.yaml"), 0o644); err != nil { + // 配置文件可能不存在于仓库,但按照需求尝试复制,若不存在给出提示 + if !os.IsNotExist(err) { + fatalf("复制 config.yaml 失败: %v", err) + } else { + fmt.Println("提示: 仓库根目录未找到 config.yaml,跳过复制") + } + } + + fmt.Printf("完成: 微服务安装于 %s,资源已同步到 %s\n", binDir, installPrefix) +} + +func fatalf(format string, a ...any) { + fmt.Fprintf(os.Stderr, format+"\n", a...) + os.Exit(1) +} + +// findServiceDirs returns names like service-api, service-udp under cmd/. +func findServiceDirs() ([]string, error) { + entries, err := os.ReadDir("cmd") + if err != nil { + return nil, err + } + var list []string + for _, e := range entries { + if !e.IsDir() { + continue + } + name := e.Name() + if strings.HasPrefix(name, "service-") { + // ensure main.go exists to be buildable + if _, err := os.Stat(filepath.Join("cmd", name, "main.go")); err == nil { + list = append(list, name) + } + } + } + return list, nil +} + +func run(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func copyDirReplacing(src, dst string) error { + st, err := os.Stat(src) + if err != nil { + return err + } + if !st.IsDir() { + return fmt.Errorf("%s 不是目录", src) + } + // Remove destination to ensure clean replace + if err := os.RemoveAll(dst); err != nil { + return err + } + if err := os.MkdirAll(dst, 0o755); err != nil { + return err + } + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + target := filepath.Join(dst, rel) + if d.IsDir() { + if rel == "." { + return nil + } + return os.MkdirAll(target, 0o755) + } + return copyFileReplacing(path, target, 0o644) + }) +} + +func copyFileReplacing(src, dst string, perm os.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm) + if err != nil { + return err + } + defer func() { _ = out.Close() }() + + if _, err := io.Copy(out, in); err != nil { + return err + } + return out.Sync() +} diff --git a/internal/config/config.go b/internal/config/config.go index 07f311c..084db95 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -90,14 +90,15 @@ func (c *Config) loadConfig() error { if exePath != "" { exeDir = filepath.Dir(exePath) } + // 优先顺序:可执行文件所在目录,其次其父目录;然后回退到工作目录及上级,再到系统级/用户级 configPaths := []string{ + // 可执行文件所在目录优先 + filepath.Join(exeDir, "config.yaml"), + filepath.Join(exeDir, "..", "config.yaml"), // 工作目录及其上级 "config.yaml", "../config.yaml", "../../config.yaml", - // 可执行文件所在目录(用于 /opt/weatherstation/bin 场景) - filepath.Join(exeDir, "config.yaml"), - filepath.Join(exeDir, "..", "config.yaml"), // 系统级与用户级 "/etc/weatherstation/config.yaml", filepath.Join(os.Getenv("HOME"), ".weatherstation", "config.yaml"),