- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help - Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete - Sigil scramble post-forge uniquification and Dispense Reveal ceremony - Full system check, desktop push, BITS/host-binary persistence, Path Tracer - Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav - README documents alerts, sigil scramble, and pack-usb workflow - USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
75 lines
2.1 KiB
Go
75 lines
2.1 KiB
Go
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())
|
|
}
|