mesh/delivery-group group feat/900-node-files checking: composed check out of date: novox/mesh-controller@210f7cf5e64b was judged against trunk 611e2275cf7c, t…
mesh/merge-gate out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
216 lines
6.1 KiB
Go
216 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// TrashKeep is how long a deleted or replaced item stays in the trash (novox/hq ADR 0394 §4).
|
|
const TrashKeep = 7 * 24 * time.Hour
|
|
|
|
// trashMeta is the small JSON beside a trashed item: what it was, who it belonged to, and who put it there.
|
|
type trashMeta struct {
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
UID int `json:"uid"`
|
|
GID int `json:"gid"`
|
|
Mode uint32 `json:"mode"`
|
|
Kind string `json:"kind"`
|
|
Caller string `json:"caller"`
|
|
Reason string `json:"reason"`
|
|
Time time.Time `json:"time"`
|
|
}
|
|
|
|
// TrashItem is one item of the trash, as trash answers it.
|
|
type TrashItem struct {
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
Kind string `json:"kind"`
|
|
Owner string `json:"owner"`
|
|
Mode string `json:"mode"`
|
|
Caller string `json:"caller"`
|
|
Reason string `json:"reason"`
|
|
Time string `json:"time"`
|
|
Until string `json:"emptied-after"`
|
|
}
|
|
|
|
// TrashAnswer is what trash answers.
|
|
type TrashAnswer struct {
|
|
Items []TrashItem `json:"items"`
|
|
}
|
|
|
|
func (f *Files) trashDir() string { return filepath.Join(f.State, "trash") }
|
|
|
|
func newID() string {
|
|
b := make([]byte, 5)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func (f *Files) id() string {
|
|
if f.NewID != nil {
|
|
return f.NewID()
|
|
}
|
|
return newID()
|
|
}
|
|
|
|
// toTrash moves a path into a folder of its own in the trash, with a note of what it was.
|
|
func (f *Files) toTrash(a Act, path string) (string, error) {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("%s cannot be put in the trash: %s", path, why(err))
|
|
}
|
|
id, err := f.ownerOf(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tid := f.id()
|
|
dir := filepath.Join(f.trashDir(), tid)
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
return "", fmt.Errorf("the trash cannot be written: %v", err)
|
|
}
|
|
if err := f.relocate(path, filepath.Join(dir, "item")); err != nil {
|
|
os.RemoveAll(dir)
|
|
return "", fmt.Errorf("%s cannot be put in the trash: %v", path, err)
|
|
}
|
|
meta, _ := json.Marshal(trashMeta{ID: tid, Path: path, UID: id.UID, GID: id.GID, Mode: permOf(info.Mode()),
|
|
Kind: typeOf(info.Mode()), Caller: a.Caller, Reason: a.Reason, Time: f.Now().UTC()})
|
|
if err := os.WriteFile(filepath.Join(dir, "meta.json"), meta, 0o600); err != nil {
|
|
return "", fmt.Errorf("%s is in the trash, but its note cannot be written: %v", path, err)
|
|
}
|
|
return tid, nil
|
|
}
|
|
|
|
func (f *Files) metaOf(id string) (trashMeta, error) {
|
|
var m trashMeta
|
|
raw, err := os.ReadFile(filepath.Join(f.trashDir(), id, "meta.json"))
|
|
if err != nil {
|
|
return m, err
|
|
}
|
|
return m, json.Unmarshal(raw, &m)
|
|
}
|
|
|
|
// entries are the ids in the trash.
|
|
func (f *Files) entries() []string {
|
|
dirs, err := os.ReadDir(f.trashDir())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var ids []string
|
|
for _, d := range dirs {
|
|
if d.IsDir() {
|
|
ids = append(ids, d.Name())
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// sweep empties what is older than TrashKeep. It runs on every write call and every trash list; a failure is
|
|
// said on stderr and tried again at the next one.
|
|
func (f *Files) sweep() {
|
|
for _, id := range f.entries() {
|
|
m, err := f.metaOf(id)
|
|
if err != nil || f.Now().Sub(m.Time) <= TrashKeep {
|
|
continue
|
|
}
|
|
if err := f.removeAll(filepath.Join(f.trashDir(), id)); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[mesh-files] the trash entry %s cannot be emptied: %v\n", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trash lists what the trash holds, oldest first.
|
|
func (f *Files) Trash() (TrashAnswer, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.sweep()
|
|
names := f.names()
|
|
ans := TrashAnswer{Items: []TrashItem{}}
|
|
var metas []trashMeta
|
|
for _, id := range f.entries() {
|
|
if m, err := f.metaOf(id); err == nil {
|
|
metas = append(metas, m)
|
|
}
|
|
}
|
|
sort.Slice(metas, func(i, j int) bool { return metas[i].Time.Before(metas[j].Time) })
|
|
for _, m := range metas {
|
|
owner := names.user(m.UID)
|
|
if owner == "" {
|
|
owner = fmt.Sprint(m.UID)
|
|
}
|
|
if g := names.group(m.GID); g != "" {
|
|
owner += ":" + g
|
|
}
|
|
ans.Items = append(ans.Items, TrashItem{ID: m.ID, Path: m.Path, Kind: m.Kind, Owner: owner,
|
|
Mode: fmt.Sprintf("%04o", m.Mode), Caller: m.Caller, Reason: m.Reason,
|
|
Time: m.Time.Format(time.RFC3339), Until: m.Time.Add(TrashKeep).Format(time.RFC3339)})
|
|
}
|
|
return ans, nil
|
|
}
|
|
|
|
var idShape = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
|
|
|
|
// restore puts an item back where it was. It refuses when something is there now.
|
|
func (f *Files) restore(id string) (trashMeta, error) {
|
|
if !idShape.MatchString(id) || strings.HasPrefix(id, ".") {
|
|
return trashMeta{}, fmt.Errorf("%q is not an id from trash", id)
|
|
}
|
|
m, err := f.metaOf(id)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return m, fmt.Errorf("the trash holds no %s (it may have been emptied after %d days)", id, int(TrashKeep.Hours()/24))
|
|
}
|
|
return m, err
|
|
}
|
|
if _, err := os.Lstat(m.Path); err == nil {
|
|
return m, fmt.Errorf("%s is there now: delete or move it first, then restore", m.Path)
|
|
}
|
|
if _, err := os.Stat(filepath.Dir(m.Path)); err != nil {
|
|
return m, fmt.Errorf("the folder of %s cannot be reached: %s", m.Path, why(err))
|
|
}
|
|
dir := filepath.Join(f.trashDir(), id)
|
|
if err := f.relocate(filepath.Join(dir, "item"), m.Path); err != nil {
|
|
return m, fmt.Errorf("%s cannot be restored: %v", m.Path, err)
|
|
}
|
|
if err := f.chownTo(m.Path, Ident{m.UID, m.GID}); err != nil {
|
|
return m, fmt.Errorf("%s is back, but its owner could not be set: %v", m.Path, err)
|
|
}
|
|
return m, f.removeAll(dir)
|
|
}
|
|
|
|
// Restore puts an item of the trash back where it was.
|
|
func (f *Files) Restore(a Act, id string) (ActAnswer, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if err := f.begin(a); err != nil {
|
|
return ActAnswer{}, err
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
var hits []hit
|
|
if idShape.MatchString(id) {
|
|
if m, err := f.metaOf(id); err == nil {
|
|
var gerr error
|
|
if hits, gerr = f.guard(a, false, m.Path); gerr != nil {
|
|
return ActAnswer{}, gerr
|
|
}
|
|
}
|
|
}
|
|
m, err := f.restore(id)
|
|
if err != nil {
|
|
return ActAnswer{}, err
|
|
}
|
|
return f.finish(a, "restore", hits, ActAnswer{Done: "restored " + m.Path, Paths: []string{m.Path}})
|
|
}
|