package restic import ( "bytes" "context" "encoding/json" "fmt" "path" "sort" "strings" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/debug" ) // Tree is an ordered list of nodes. type Tree struct { Nodes []*Node `json:"nodes"` } // NewTree creates a new tree object with the given initial capacity. func NewTree(capacity int) *Tree { return &Tree{ Nodes: make([]*Node, 0, capacity), } } func (t *Tree) String() string { return fmt.Sprintf("Tree<%d nodes>", len(t.Nodes)) } // Equals returns true if t and other have exactly the same nodes. func (t *Tree) Equals(other *Tree) bool { if len(t.Nodes) != len(other.Nodes) { debug.Log("tree.Equals(): trees have different number of nodes") return false } for i := 0; i < len(t.Nodes); i++ { if !t.Nodes[i].Equals(*other.Nodes[i]) { debug.Log("tree.Equals(): node %d is different:", i) debug.Log(" %#v", t.Nodes[i]) debug.Log(" %#v", other.Nodes[i]) return false } } return true } // Insert adds a new node at the correct place in the tree. func (t *Tree) Insert(node *Node) error { pos, found := t.find(node.Name) if found != nil { return errors.Errorf("node %q already present", node.Name) } // https://github.com/golang/go/wiki/SliceTricks t.Nodes = append(t.Nodes, nil) copy(t.Nodes[pos+1:], t.Nodes[pos:]) t.Nodes[pos] = node return nil } func (t *Tree) find(name string) (int, *Node) { pos := sort.Search(len(t.Nodes), func(i int) bool { return t.Nodes[i].Name >= name }) if pos < len(t.Nodes) && t.Nodes[pos].Name == name { return pos, t.Nodes[pos] } return pos, nil } // Find returns a node with the given name, or nil if none could be found. func (t *Tree) Find(name string) *Node { if t == nil { return nil } _, node := t.find(name) return node } // Sort sorts the nodes by name. func (t *Tree) Sort() { list := Nodes(t.Nodes) sort.Sort(list) t.Nodes = list } // Subtrees returns a slice of all subtree IDs of the tree. func (t *Tree) Subtrees() (trees IDs) { for _, node := range t.Nodes { if node.Type == "dir" && node.Subtree != nil { trees = append(trees, *node.Subtree) } } return trees } type BlobLoader interface { LoadBlob(context.Context, BlobType, ID, []byte) ([]byte, error) } // LoadTree loads a tree from the repository. func LoadTree(ctx context.Context, r BlobLoader, id ID) (*Tree, error) { debug.Log("load tree %v", id) buf, err := r.LoadBlob(ctx, TreeBlob, id, nil) if err != nil { return nil, err } t := &Tree{} err = json.Unmarshal(buf, t) if err != nil { return nil, err } return t, nil } type BlobSaver interface { SaveBlob(context.Context, BlobType, []byte, ID, bool) (ID, bool, int, error) } // SaveTree stores a tree into the repository and returns the ID. The ID is // checked against the index. The tree is only stored when the index does not // contain the ID. func SaveTree(ctx context.Context, r BlobSaver, t *Tree) (ID, error) { buf, err := json.Marshal(t) if err != nil { return ID{}, errors.Wrap(err, "MarshalJSON") } // append a newline so that the data is always consistent (json.Encoder // adds a newline after each object) buf = append(buf, '\n') id, _, _, err := r.SaveBlob(ctx, TreeBlob, buf, ID{}, false) return id, err } var ErrTreeNotOrdered = errors.New("nodes are not ordered or duplicate") type TreeJSONBuilder struct { buf bytes.Buffer lastName string } func NewTreeJSONBuilder() *TreeJSONBuilder { tb := &TreeJSONBuilder{} _, _ = tb.buf.WriteString(`{"nodes":[`) return tb } func (builder *TreeJSONBuilder) AddNode(node *Node) error { if node.Name <= builder.lastName { return fmt.Errorf("node %q, last%q: %w", node.Name, builder.lastName, ErrTreeNotOrdered) } if builder.lastName != "" { _ = builder.buf.WriteByte(',') } builder.lastName = node.Name val, err := json.Marshal(node) if err != nil { return err } _, _ = builder.buf.Write(val) return nil } func (builder *TreeJSONBuilder) Finalize() ([]byte, error) { // append a newline so that the data is always consistent (json.Encoder // adds a newline after each object) _, _ = builder.buf.WriteString("]}\n") buf := builder.buf.Bytes() // drop reference to buffer builder.buf = bytes.Buffer{} return buf, nil } func FindTreeDirectory(ctx context.Context, repo BlobLoader, id *ID, dir string) (*ID, error) { if id == nil { return nil, errors.New("tree id is null") } dirs := strings.Split(path.Clean(dir), "/") subfolder := "" for _, name := range dirs { if name == "" || name == "." { continue } subfolder = path.Join(subfolder, name) tree, err := LoadTree(ctx, repo, *id) if err != nil { return nil, fmt.Errorf("path %s: %w", subfolder, err) } node := tree.Find(name) if node == nil { return nil, fmt.Errorf("path %s: not found", subfolder) } if node.Type != "dir" || node.Subtree == nil { return nil, fmt.Errorf("path %s: not a directory", subfolder) } id = node.Subtree } return id, nil }