package apply import ( "fmt" "os" "path/filepath" "time" "git.novox.be/novox/mesh-sdk/go/declaration" ) // ObjectKeep is the node-engine's own keep of the content of files given by digest (novox/hq ADR 0486 §1, issue 924): one // file per object, named as the object is, beside the node's state. **What a node once had it never asks the bus for // again**, so neither the controller's sweep of the bucket (an object is removed a day after no composition of the // controller names it) nor a bus that does not answer can refuse a composition the node already ran. The node's account // may only read its bucket (ADR 0486 §2), so the keep is the node's, not the bucket's: nothing is written back. // // An object unused for KeptObjectsFor is let go when another is kept, so the keep holds what the node applied lately and // not every export it ever ran. Every failure to keep is silent and costs only a fetch: the keep is a copy. type ObjectKeep struct { Dir string // Now is the clock; nil is time.Now. Now func() time.Time } // KeptObjectsFor is how long an object of the keep is held after it was last used. const KeptObjectsFor = 14 * 24 * time.Hour // KeepObjectsIn is the keep at dir; an empty dir keeps nothing. func KeepObjectsIn(dir string) *ObjectKeep { if dir == "" { return nil } return &ObjectKeep{Dir: dir} } func (k *ObjectKeep) now() time.Time { if k.Now != nil { return k.Now() } return time.Now() } // get is the kept content of the object name when it is what f names, and marks it used. func (k *ObjectKeep) get(name string, f *declaration.File) ([]byte, bool) { if k == nil { return nil, false } path := filepath.Join(k.Dir, name) body, err := os.ReadFile(path) if err != nil || int64(len(body)) != f.Size || f.Verify(body) != nil { return nil, false } now := k.now() _ = os.Chtimes(path, now, now) return body, true } // put keeps body under name, unless it is kept already (then it is marked used), and lets go of what was unused too long. func (k *ObjectKeep) put(name string, body []byte) { if k == nil { return } path := filepath.Join(k.Dir, name) now := k.now() if info, err := os.Stat(path); err == nil && info.Size() == int64(len(body)) { _ = os.Chtimes(path, now, now) return } if err := os.MkdirAll(k.Dir, 0o700); err != nil { return } tmp, err := os.CreateTemp(k.Dir, ".put-*") if err != nil { return } _, werr := tmp.Write(body) cerr := tmp.Close() if werr != nil || cerr != nil || os.Chmod(tmp.Name(), 0o600) != nil || os.Rename(tmp.Name(), path) != nil { _ = os.Remove(tmp.Name()) return } _ = os.Chtimes(path, now, now) k.prune(now) } // prune lets go of every object unused for KeptObjectsFor. func (k *ObjectKeep) prune(now time.Time) { entries, err := os.ReadDir(k.Dir) if err != nil { return } for _, e := range entries { info, err := e.Info() if err != nil || !info.Mode().IsRegular() { continue } if now.Sub(info.ModTime()) >= KeptObjectsFor { _ = os.Remove(filepath.Join(k.Dir, e.Name())) } } } // never says, in a failed fetch's error before its cause (a transient one's detail keeps the tail), that the node's own // keep did not hold the object either. func (k *ObjectKeep) never() string { if k == nil { return "" } return fmt.Sprintf(" (this node's own keep of objects, %s, never held it)", k.Dir) }