restic/internal/archiver/tree.go
2024-07-05 21:03:34 +02:00

300 lines
6.4 KiB
Go

package archiver
import (
"fmt"
"sort"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
)
// Tree recursively defines how a snapshot should look like when
// archived.
//
// When `Path` is set, this is a leaf node and the contents of `Path` should be
// inserted at this point in the tree.
//
// The attribute `Root` is used to distinguish between files/dirs which have
// the same name, but live in a separate directory on the local file system.
//
// `FileInfoPath` is used to extract metadata for intermediate (=non-leaf)
// trees.
type Tree struct {
Nodes map[string]Tree
Path string // where the files/dirs to be saved are found
FileInfoPath string // where the dir can be found that is not included itself, but its subdirs
Root string // parent directory of the tree
}
// pathComponents returns all path components of p. If a virtual directory
// (volume name on Windows) is added, virtualPrefix is set to true. See the
// tests for examples.
func pathComponents(fs fs.FS, p string, includeRelative bool) (components []string, virtualPrefix bool) {
volume := fs.VolumeName(p)
if !fs.IsAbs(p) {
if !includeRelative {
p = fs.Join(fs.Separator(), p)
}
}
p = fs.Clean(p)
for {
dir, file := fs.Dir(p), fs.Base(p)
if p == dir {
break
}
components = append(components, file)
p = dir
}
// reverse components
for i := len(components)/2 - 1; i >= 0; i-- {
opp := len(components) - 1 - i
components[i], components[opp] = components[opp], components[i]
}
if volume != "" {
// strip colon
if len(volume) == 2 && volume[1] == ':' {
volume = volume[:1]
}
components = append([]string{volume}, components...)
virtualPrefix = true
}
return components, virtualPrefix
}
// rootDirectory returns the directory which contains the first element of target.
func rootDirectory(fs fs.FS, target string) string {
if target == "" {
return ""
}
if fs.IsAbs(target) {
return fs.Join(fs.VolumeName(target), fs.Separator())
}
target = fs.Clean(target)
pc, _ := pathComponents(fs, target, true)
rel := "."
for _, c := range pc {
if c == ".." {
rel = fs.Join(rel, c)
}
}
return rel
}
// Add adds a new file or directory to the tree.
func (t *Tree) Add(fs fs.FS, path string) error {
if path == "" {
panic("invalid path (empty string)")
}
if t.Nodes == nil {
t.Nodes = make(map[string]Tree)
}
pc, virtualPrefix := pathComponents(fs, path, false)
if len(pc) == 0 {
return errors.New("invalid path (no path components)")
}
name := pc[0]
root := rootDirectory(fs, path)
tree := Tree{Root: root}
origName := name
i := 0
for {
other, ok := t.Nodes[name]
if !ok {
break
}
i++
if other.Root == root {
tree = other
break
}
// resolve conflict and try again
name = fmt.Sprintf("%s-%d", origName, i)
continue
}
if len(pc) > 1 {
subroot := fs.Join(root, origName)
if virtualPrefix {
// use the original root dir if this is a virtual directory (volume name on Windows)
subroot = root
}
err := tree.add(fs, path, subroot, pc[1:])
if err != nil {
return err
}
tree.FileInfoPath = subroot
} else {
tree.Path = path
}
t.Nodes[name] = tree
return nil
}
// add adds a new target path into the tree.
func (t *Tree) add(fs fs.FS, target, root string, pc []string) error {
if len(pc) == 0 {
return errors.Errorf("invalid path %q", target)
}
if t.Nodes == nil {
t.Nodes = make(map[string]Tree)
}
name := pc[0]
if len(pc) == 1 {
tree, ok := t.Nodes[name]
if !ok {
t.Nodes[name] = Tree{Path: target}
return nil
}
if tree.Path != "" {
return errors.Errorf("path is already set for target %v", target)
}
tree.Path = target
t.Nodes[name] = tree
return nil
}
tree := Tree{}
if other, ok := t.Nodes[name]; ok {
tree = other
}
subroot := fs.Join(root, name)
tree.FileInfoPath = subroot
err := tree.add(fs, target, subroot, pc[1:])
if err != nil {
return err
}
t.Nodes[name] = tree
return nil
}
func (t Tree) String() string {
return formatTree(t, "")
}
// Leaf returns true if this is a leaf node, which means Path is set to a
// non-empty string and the contents of Path should be inserted at this point
// in the tree.
func (t Tree) Leaf() bool {
return t.Path != ""
}
// NodeNames returns the sorted list of subtree names.
func (t Tree) NodeNames() []string {
// iterate over the nodes of atree in lexicographic (=deterministic) order
names := make([]string, 0, len(t.Nodes))
for name := range t.Nodes {
names = append(names, name)
}
sort.Strings(names)
return names
}
// formatTree returns a text representation of the tree t.
func formatTree(t Tree, indent string) (s string) {
for name, node := range t.Nodes {
s += fmt.Sprintf("%v/%v, root %q, path %q, meta %q\n", indent, name, node.Root, node.Path, node.FileInfoPath)
s += formatTree(node, indent+" ")
}
return s
}
// unrollTree unrolls the tree so that only leaf nodes have Path set.
func unrollTree(f fs.FS, t *Tree) error {
// if the current tree is a leaf node (Path is set) and has additional
// nodes, add the contents of Path to the nodes.
if t.Path != "" && len(t.Nodes) > 0 {
debug.Log("resolve path %v", t.Path)
entries, err := fs.Readdirnames(f, t.Path, 0)
if err != nil {
return err
}
for _, entry := range entries {
if node, ok := t.Nodes[entry]; ok {
if node.Path == "" {
node.Path = f.Join(t.Path, entry)
t.Nodes[entry] = node
continue
}
if node.Path == f.Join(t.Path, entry) {
continue
}
return errors.Errorf("tree unrollTree: collision on path, node %#v, path %q", node, f.Join(t.Path, entry))
}
t.Nodes[entry] = Tree{Path: f.Join(t.Path, entry)}
}
t.Path = ""
}
for i, subtree := range t.Nodes {
err := unrollTree(f, &subtree)
if err != nil {
return err
}
t.Nodes[i] = subtree
}
return nil
}
// NewTree creates a Tree from the target files/directories.
func NewTree(fs fs.FS, targets []string) (*Tree, error) {
debug.Log("targets: %v", targets)
tree := &Tree{}
seen := make(map[string]struct{})
for _, target := range targets {
target = fs.Clean(target)
// skip duplicate targets
if _, ok := seen[target]; ok {
continue
}
seen[target] = struct{}{}
err := tree.Add(fs, target)
if err != nil {
return nil, err
}
}
debug.Log("before unroll:\n%v", tree)
err := unrollTree(fs, tree)
if err != nil {
return nil, err
}
debug.Log("result:\n%v", tree)
return tree, nil
}