fix(backup): 手动备份按服冷却、每服保留上限与独立保留期,容量驱逐不再删除回收世界的唯一副本

This commit is contained in:
Lemon-miaow committed 2026-09-24 19:58:59 +08:00
1 parent f69cdec9d4
commit 9bda3a52fa
30 files changed
+801 -42

No files matched your search

+41
View File
@@ -1,9 +1,11 @@
package api
import (
"context"
"errors"
"net/http"
"strings"
"time"
"felis.lolicon.best/internal/apis/felis/v1alpha1"
"felis.lolicon.best/internal/maintenance"
@@ -255,10 +257,49 @@ func (a *API) handleBackupNow(w http.ResponseWriter, r *http.Request) {
writeError(w, r, errForbidden)
return
}
if !p.IsAdmin() {
if err := a.backupAllowance(r.Context(), name); err != nil {
writeError(w, r, err)
return
}
}
a.enqueueBackup(w, r, name, rec, auditActor(p), "external")
}
// backupAllowance rations an owner's on-demand backups (data-durability-9):
// one per BackupCooldown per server, and none while the present backups fill
// BackupStoreCap. The owner's older backups are pruned by the Job itself
// ([archive] manual_keep), so these two gates bound the rate and the total.
func (a *API) backupAllowance(ctx context.Context, name string) error {
if a.BackupCooldown > 0 {
now := a.now()
last, err := a.Repo.LastBackupRequest(ctx, name, now.Add(-a.BackupCooldown))
if err != nil {
return err
}
if !last.IsZero() {
wait := last.Add(a.BackupCooldown).Sub(now)
if wait > 0 {
return newError(http.StatusTooManyRequests, "backup_cooldown",
"a backup of this server was started %s ago; the next one can start in %s",
now.Sub(last).Round(time.Second), wait.Round(time.Second)).retryAfter(wait)
}
}
}
if a.BackupStoreCap > 0 {
used, err := a.Repo.BackupStoreBytes(ctx)
if err != nil {
return err
}
if used >= a.BackupStoreCap {
return newError(http.StatusInsufficientStorage, "backup_store_full",
"the backup store is full; ask an administrator to free space")
}
}
return nil
}
// handleInternalBackup is the internal-face backup trigger. The break-glass console
// (root on the node, holding the service token) POSTs here to snapshot a stopped
// world while felis-api is alive — it goes through the API rather than direct-to-CRD