mirror of
https://github.com/restic/restic.git
synced 2024-08-25 05:37:25 +02:00
283 lines
5.9 KiB
Go
283 lines
5.9 KiB
Go
package archiver
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"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, "")
|
|
}
|
|
|
|
// 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)
|
|
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))
|
|
continue
|
|
}
|
|
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
|
|
}
|