feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- 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)
This commit is contained in:
74
server/internal/api/backup_handler.go
Normal file
74
server/internal/api/backup_handler.go
Normal file
@@ -0,0 +1,74 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user