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 ": " 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 }