Files
felhom-controller/controller/internal/settings/kept_copies.go
T

82 lines
2.8 KiB
Go

package settings
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
// KeptCopy is one second-drive copy whose source is gone (D25, operator ruling 2026-10-21). The record is what the
// backups page lists and what „Eltávolítás" removes — by Path, identity-checked against Dev/Ino, never by size or a
// name pattern (07 §8.2 as amended by D25).
type KeptCopy struct {
ID string `json:"id"` // stable: sha256 of Path, first 16 hex
Kind string `json:"kind"` // "share" | "app"
Source string `json:"source"` // the share's name, or "<app>: <folder>"
Drive string `json:"drive"` // the SOURCE drive's label (or its path when the drive is no longer registered)
Reason string `json:"reason"` // "drive_away" | "drive_gone" | "folder_missing"
DestBase string `json:"dest_base"` // the copy tree it lives in; Path must stay inside it
Path string `json:"path"` // the copy's absolute directory
Dev uint64 `json:"dev"` // identity at record time
Ino uint64 `json:"ino"` // identity at record time
SizeBytes int64 `json:"size_bytes"` // at the latest run
Since string `json:"since"` // RFC3339, first time it was kept
}
// KeptCopyID is the record's id for a copy path.
func KeptCopyID(path string) string {
h := sha256.Sum256([]byte(path))
return hex.EncodeToString(h[:8])
}
// GetKeptCopies returns a copy of the kept-copy records.
func (s *Settings) GetKeptCopies() []KeptCopy {
s.mu.RLock()
defer s.mu.RUnlock()
return append([]KeptCopy(nil), s.KeptCopies...)
}
// UpsertKeptCopy records a kept copy (or refreshes its size and identity), keeping the first Since.
func (s *Settings) UpsertKeptCopy(k KeptCopy) error {
s.mu.Lock()
defer s.mu.Unlock()
if k.Path == "" || k.DestBase == "" {
return fmt.Errorf("kept copy: empty path or dest base")
}
k.ID = KeptCopyID(k.Path)
for i := range s.KeptCopies {
if s.KeptCopies[i].ID == k.ID {
since := s.KeptCopies[i].Since
k.Since = since
if s.KeptCopies[i] == k {
return nil // nothing changed — no write
}
s.KeptCopies[i] = k
return s.save()
}
}
if k.Since == "" {
k.Since = time.Now().UTC().Format(time.RFC3339)
}
s.KeptCopies = append(s.KeptCopies, k)
if s.log != nil {
s.log.Printf("[INFO] [settings] kept copy recorded: kind=%s source=%q reason=%s path=%s", k.Kind, k.Source, k.Reason, k.Path)
}
return s.save()
}
// DropKeptCopy forgets the record of a copy path (the source came back, or the household removed the copy). The
// copy's data is not touched here.
func (s *Settings) DropKeptCopy(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
for i := range s.KeptCopies {
if s.KeptCopies[i].ID == id {
s.KeptCopies = append(s.KeptCopies[:i], s.KeptCopies[i+1:]...)
return s.save()
}
}
return nil
}