Files
2026-06-21 18:11:58 +08:00

174 lines
3.6 KiB
Go

package main
import (
"archive/zip"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
var preservePaths = map[string]bool{
"config/settings.json": true,
"data": true,
}
func shouldPreserve(rel string) bool {
rel = filepath.ToSlash(rel)
for p := range preservePaths {
if rel == p || strings.HasPrefix(rel, p+"/") {
return true
}
}
return false
}
func isProcessRunning(pid int) bool {
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH")
out, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(out), fmt.Sprintf("%d", pid))
}
func main() {
pid := flag.Int("pid", 0, "PID of main app to wait for")
zipPath := flag.String("zip", "", "Path to update zip")
target := flag.String("target", "", "Install directory")
launch := flag.String("launch", "", "Executable to launch after update")
flag.Parse()
if *zipPath == "" || *target == "" {
fmt.Println("Usage: updater --pid PID --zip FILE --target DIR --launch EXE")
os.Exit(1)
}
logsDir := filepath.Join(*target, "logs")
os.MkdirAll(logsDir, 0755)
logFile, _ := os.Create(filepath.Join(logsDir, "updater.log"))
log := func(msg string, args ...interface{}) {
line := fmt.Sprintf("[%s] %s\n", time.Now().Format("15:04:05"), fmt.Sprintf(msg, args...))
fmt.Print(line)
if logFile != nil {
logFile.WriteString(line)
}
}
if *pid > 0 {
log("Waiting for process %d to exit...", *pid)
for i := 0; i < 30; i++ {
if !isProcessRunning(*pid) {
log("Process exited")
break
}
time.Sleep(500 * time.Millisecond)
}
time.Sleep(500 * time.Millisecond)
}
log("Extracting %s", *zipPath)
tmpDir := *zipPath + "_extract"
os.RemoveAll(tmpDir)
if err := extractZip(*zipPath, tmpDir); err != nil {
log("Extract failed: %v", err)
os.Exit(1)
}
// Auto-detect nested top-level directory and strip it
entries, _ := os.ReadDir(tmpDir)
if len(entries) == 1 && entries[0].IsDir() {
nested := filepath.Join(tmpDir, entries[0].Name())
log("Detected nested directory: %s, stripping", entries[0].Name())
tmpDir = nested
}
log("Installing to %s", *target)
count := 0
skipped := 0
err := filepath.Walk(tmpDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
rel, _ := filepath.Rel(tmpDir, path)
if shouldPreserve(rel) {
log(" Skip: %s", rel)
skipped++
return nil
}
dst := filepath.Join(*target, rel)
os.MkdirAll(filepath.Dir(dst), 0755)
src, err := os.Open(path)
if err != nil {
return err
}
defer src.Close()
out, err := os.Create(dst)
if err != nil {
log(" Failed to write: %s (%v)", rel, err)
return nil
}
defer out.Close()
io.Copy(out, src)
count++
return nil
})
if err != nil {
log("Install error: %v", err)
}
log("Updated %d files, skipped %d", count, skipped)
os.RemoveAll(tmpDir)
os.Remove(*zipPath)
log("Cleanup done")
if *launch != "" {
exe := filepath.Join(*target, *launch)
log("Launching %s", exe)
cmd := exec.Command(exe)
cmd.Dir = *target
cmd.Start()
}
log("Update complete")
if logFile != nil {
logFile.Close()
}
}
func extractZip(src, dst string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
path := filepath.Join(dst, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(path, 0755)
continue
}
os.MkdirAll(filepath.Dir(path), 0755)
rc, err := f.Open()
if err != nil {
return err
}
out, err := os.Create(path)
if err != nil {
rc.Close()
return err
}
io.Copy(out, rc)
out.Close()
rc.Close()
}
return nil
}