package api import ( "archive/zip" "bytes" "fmt" "net/http" "os" "path/filepath" "time" ) // BackupHandler serves GET /api/v1/backup. // It returns a zip containing config.json, users.json, miner.db, and a // backup-info.txt with the timestamp and server version. The caller must // already be authenticated via basicAuthMiddleware (registered in router.go). type BackupHandler struct { dataDir string serverVersion string } // NewBackupHandler creates a BackupHandler for the given data directory. func NewBackupHandler(dataDir, serverVersion string) *BackupHandler { return &BackupHandler{dataDir: dataDir, serverVersion: serverVersion} } func (h *BackupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } now := time.Now().UTC() dateTag := now.Format("2006-01-02") var buf bytes.Buffer zw := zip.NewWriter(&buf) // backup-info.txt info := fmt.Sprintf("AetherForge Deck Backup\nTimestamp: %s\nVersion: %s\n", now.Format(time.RFC3339), h.serverVersion) if fw, err := zw.Create("backup-info.txt"); err == nil { _, _ = fw.Write([]byte(info)) } // Helper: add a file from disk into the zip, skip gracefully if missing. addFile := func(name, diskPath string) { data, err := os.ReadFile(diskPath) if err != nil { return } fw, err := zw.Create(name) if err != nil { return } _, _ = fw.Write(data) } addFile("config.json", filepath.Join(h.dataDir, "config.json")) addFile("users.json", filepath.Join(h.dataDir, "users.json")) addFile("miner.db", filepath.Join(h.dataDir, "miner.db")) if err := zw.Close(); err != nil { http.Error(w, "Failed to create backup zip", http.StatusInternalServerError) return } filename := fmt.Sprintf("aetherforge-backup-%s.zip", dateTag) w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) w.Header().Set("Content-Length", fmt.Sprintf("%d", buf.Len())) w.WriteHeader(http.StatusOK) _, _ = w.Write(buf.Bytes()) }