Template
170 lines
4.4 KiB
Go
170 lines
4.4 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"embed"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"regexp"
|
||
"runtime"
|
||
"strings"
|
||
)
|
||
|
||
// deploymentAssets contains everything needed to register and manage the services.
|
||
//
|
||
//go:embed .env.example install.sh start.sh stop.sh restart.sh deploy/*.service
|
||
var deploymentAssets embed.FS
|
||
|
||
var validServicePrefix = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]*$`)
|
||
|
||
var assetModes = map[string]os.FileMode{
|
||
"install.sh": 0o755,
|
||
"start.sh": 0o755,
|
||
"stop.sh": 0o755,
|
||
"restart.sh": 0o755,
|
||
".env.example": 0o644,
|
||
"deploy/server.service": 0o644,
|
||
"deploy/cli.service": 0o644,
|
||
"deploy/scheduler.service": 0o644,
|
||
}
|
||
|
||
func main() {
|
||
prefixFlag := flag.String("prefix", "", "systemd 服务名前缀;未指定时交互输入")
|
||
flag.Parse()
|
||
|
||
if runtime.GOOS != "linux" {
|
||
fatalf("初始化工具只能在 Linux 服务器上运行")
|
||
}
|
||
if err := requireRoot(); err != nil {
|
||
fatalf("%v", err)
|
||
}
|
||
|
||
appDir, err := executableDir()
|
||
if err != nil {
|
||
fatalf("获取程序目录失败: %v", err)
|
||
}
|
||
|
||
prefix, err := resolvePrefix(*prefixFlag, os.Stdin, os.Stdout)
|
||
if err != nil {
|
||
fatalf("读取项目前缀失败: %v", err)
|
||
}
|
||
|
||
if err := extractAssets(appDir); err != nil {
|
||
fatalf("释放部署文件失败: %v", err)
|
||
}
|
||
envCreated, err := initializeEnv(appDir)
|
||
if err != nil {
|
||
fatalf("初始化 .env 失败: %v", err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(appDir, ".service-prefix"), []byte(prefix+"\n"), 0o644); err != nil {
|
||
fatalf("保存项目前缀失败: %v", err)
|
||
}
|
||
|
||
fmt.Printf("部署文件已释放到 %s\n", appDir)
|
||
if envCreated {
|
||
fmt.Println("已根据 .env.example 初始化 .env,请按实际环境修改配置")
|
||
} else {
|
||
fmt.Println("检测到已有 .env,已保留现有配置")
|
||
}
|
||
fmt.Printf("正在注册服务: %s-server, %s-cli, %s-scheduler\n", prefix, prefix, prefix)
|
||
|
||
install := exec.Command("bash", filepath.Join(appDir, "install.sh"))
|
||
install.Dir = appDir
|
||
install.Stdin = os.Stdin
|
||
install.Stdout = os.Stdout
|
||
install.Stderr = os.Stderr
|
||
if err := install.Run(); err != nil {
|
||
fatalf("install.sh 执行失败: %v", err)
|
||
}
|
||
}
|
||
|
||
func requireRoot() error {
|
||
uid, err := exec.Command("id", "-u").Output()
|
||
if err != nil {
|
||
return fmt.Errorf("无法检查当前用户权限: %w", err)
|
||
}
|
||
if strings.TrimSpace(string(uid)) != "0" {
|
||
return errors.New("请使用 root 权限运行: sudo ./initializer")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func executableDir() (string, error) {
|
||
filename, err := os.Executable()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
filename, err = filepath.Abs(filename)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return filepath.Dir(filename), nil
|
||
}
|
||
|
||
func resolvePrefix(value string, in io.Reader, out io.Writer) (string, error) {
|
||
prefix := strings.TrimSpace(value)
|
||
if prefix == "" {
|
||
if _, err := fmt.Fprint(out, "请输入项目前缀(例如 myapp): "); err != nil {
|
||
return "", err
|
||
}
|
||
line, err := bufio.NewReader(in).ReadString('\n')
|
||
if err != nil && !errors.Is(err, io.EOF) {
|
||
return "", err
|
||
}
|
||
prefix = strings.TrimSpace(line)
|
||
}
|
||
|
||
if !validServicePrefix.MatchString(prefix) {
|
||
return "", fmt.Errorf("%q 不是有效前缀,只允许字母、数字、点、下划线和连字符,且必须以字母或数字开头", prefix)
|
||
}
|
||
return prefix, nil
|
||
}
|
||
|
||
func extractAssets(appDir string) error {
|
||
for name, mode := range assetModes {
|
||
content, err := deploymentAssets.ReadFile(name)
|
||
if err != nil {
|
||
return fmt.Errorf("读取 %s: %w", name, err)
|
||
}
|
||
target := filepath.Join(appDir, filepath.FromSlash(name))
|
||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||
return fmt.Errorf("创建 %s: %w", filepath.Dir(target), err)
|
||
}
|
||
if err := os.WriteFile(target, content, mode); err != nil {
|
||
return fmt.Errorf("写入 %s: %w", target, err)
|
||
}
|
||
if err := os.Chmod(target, mode); err != nil {
|
||
return fmt.Errorf("设置 %s 权限: %w", target, err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func initializeEnv(appDir string) (bool, error) {
|
||
target := filepath.Join(appDir, ".env")
|
||
if _, err := os.Stat(target); err == nil {
|
||
return false, nil
|
||
} else if !errors.Is(err, os.ErrNotExist) {
|
||
return false, err
|
||
}
|
||
|
||
content, err := deploymentAssets.ReadFile(".env.example")
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if err := os.WriteFile(target, content, 0o600); err != nil {
|
||
return false, err
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func fatalf(format string, args ...any) {
|
||
fmt.Fprintf(os.Stderr, "错误: "+format+"\n", args...)
|
||
os.Exit(1)
|
||
}
|