github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/fs/dirtree/dirtree.go (about)

     1  // Package dirtree contains the DirTree type which is used for
     2  // building filesystem heirachies in memory.
     3  package dirtree
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"path"
     9  	"sort"
    10  	"time"
    11  
    12  	"github.com/rclone/rclone/fs"
    13  	"github.com/rclone/rclone/lib/errors"
    14  )
    15  
    16  // DirTree is a map of directories to entries
    17  type DirTree map[string]fs.DirEntries
    18  
    19  // New returns a fresh DirTree
    20  func New() DirTree {
    21  	return make(DirTree)
    22  }
    23  
    24  // parentDir finds the parent directory of path
    25  func parentDir(entryPath string) string {
    26  	dirPath := path.Dir(entryPath)
    27  	if dirPath == "." {
    28  		dirPath = ""
    29  	}
    30  	return dirPath
    31  }
    32  
    33  // Add an entry to the tree
    34  // it doesn't create parents
    35  func (dt DirTree) Add(entry fs.DirEntry) {
    36  	dirPath := parentDir(entry.Remote())
    37  	dt[dirPath] = append(dt[dirPath], entry)
    38  }
    39  
    40  // AddDir adds a directory entry to the tree
    41  // this creates the directory itself if required
    42  // it doesn't create parents
    43  func (dt DirTree) AddDir(entry fs.DirEntry) {
    44  	dt.Add(entry)
    45  	// create the directory itself if it doesn't exist already
    46  	dirPath := entry.Remote()
    47  	if _, ok := dt[dirPath]; !ok {
    48  		dt[dirPath] = nil
    49  	}
    50  }
    51  
    52  // AddEntry adds the entry and creates the parents for it regardless
    53  // of whether it is a file or a directory.
    54  func (dt DirTree) AddEntry(entry fs.DirEntry) {
    55  	switch entry.(type) {
    56  	case fs.Directory:
    57  		dt.AddDir(entry)
    58  	case fs.Object:
    59  		dt.Add(entry)
    60  	default:
    61  		panic("unknown entry type")
    62  	}
    63  	remoteParent := parentDir(entry.Remote())
    64  	dt.CheckParent("", remoteParent)
    65  }
    66  
    67  // Find returns the DirEntry for filePath or nil if not found
    68  func (dt DirTree) Find(filePath string) (parentPath string, entry fs.DirEntry) {
    69  	parentPath = parentDir(filePath)
    70  	for _, entry := range dt[parentPath] {
    71  		if entry.Remote() == filePath {
    72  			return parentPath, entry
    73  		}
    74  	}
    75  	return parentPath, nil
    76  }
    77  
    78  // CheckParent checks that dirPath has a *Dir in its parent
    79  func (dt DirTree) CheckParent(root, dirPath string) {
    80  	if dirPath == root {
    81  		return
    82  	}
    83  	parentPath, entry := dt.Find(dirPath)
    84  	if entry != nil {
    85  		return
    86  	}
    87  	dt[parentPath] = append(dt[parentPath], fs.NewDir(dirPath, time.Now()))
    88  	dt.CheckParent(root, parentPath)
    89  }
    90  
    91  // CheckParents checks every directory in the tree has *Dir in its parent
    92  func (dt DirTree) CheckParents(root string) {
    93  	for dirPath := range dt {
    94  		dt.CheckParent(root, dirPath)
    95  	}
    96  }
    97  
    98  // Sort sorts all the Entries
    99  func (dt DirTree) Sort() {
   100  	for _, entries := range dt {
   101  		sort.Stable(entries)
   102  	}
   103  }
   104  
   105  // Dirs returns the directories in sorted order
   106  func (dt DirTree) Dirs() (dirNames []string) {
   107  	for dirPath := range dt {
   108  		dirNames = append(dirNames, dirPath)
   109  	}
   110  	sort.Strings(dirNames)
   111  	return dirNames
   112  }
   113  
   114  // Prune remove directories from a directory tree. dirNames contains
   115  // all directories to remove as keys, with true as values. dirNames
   116  // will be modified in the function.
   117  func (dt DirTree) Prune(dirNames map[string]bool) error {
   118  	// We use map[string]bool to avoid recursion (and potential
   119  	// stack exhaustion).
   120  
   121  	// First we need delete directories from their parents.
   122  	for dName, remove := range dirNames {
   123  		if !remove {
   124  			// Currently all values should be
   125  			// true, therefore this should not
   126  			// happen. But this makes function
   127  			// more predictable.
   128  			fs.Infof(dName, "Directory in the map for prune, but the value is false")
   129  			continue
   130  		}
   131  		if dName == "" {
   132  			// if dName is root, do nothing (no parent exist)
   133  			continue
   134  		}
   135  		parent := parentDir(dName)
   136  		// It may happen that dt does not have a dName key,
   137  		// since directory was excluded based on a filter. In
   138  		// such case the loop will be skipped.
   139  		for i, entry := range dt[parent] {
   140  			switch x := entry.(type) {
   141  			case fs.Directory:
   142  				if x.Remote() == dName {
   143  					// the slice is not sorted yet
   144  					// to delete item
   145  					// a) replace it with the last one
   146  					dt[parent][i] = dt[parent][len(dt[parent])-1]
   147  					// b) remove last
   148  					dt[parent] = dt[parent][:len(dt[parent])-1]
   149  					// we modify a slice within a loop, but we stop
   150  					// iterating immediately
   151  					break
   152  				}
   153  			case fs.Object:
   154  				// do nothing
   155  			default:
   156  				return errors.Errorf("unknown object type %T", entry)
   157  
   158  			}
   159  		}
   160  	}
   161  
   162  	for len(dirNames) > 0 {
   163  		// According to golang specs, if new keys were added
   164  		// during range iteration, they may be skipped.
   165  		for dName, remove := range dirNames {
   166  			if !remove {
   167  				fs.Infof(dName, "Directory in the map for prune, but the value is false")
   168  				continue
   169  			}
   170  			// First, add all subdirectories to dirNames.
   171  
   172  			// It may happen that dt[dName] does not exist.
   173  			// If so, the loop will be skipped.
   174  			for _, entry := range dt[dName] {
   175  				switch x := entry.(type) {
   176  				case fs.Directory:
   177  					excludeDir := x.Remote()
   178  					dirNames[excludeDir] = true
   179  				case fs.Object:
   180  					// do nothing
   181  				default:
   182  					return errors.Errorf("unknown object type %T", entry)
   183  
   184  				}
   185  			}
   186  			// Then remove current directory from DirTree
   187  			delete(dt, dName)
   188  			// and from dirNames
   189  			delete(dirNames, dName)
   190  		}
   191  	}
   192  	return nil
   193  }
   194  
   195  // String emits a simple representation of the DirTree
   196  func (dt DirTree) String() string {
   197  	out := new(bytes.Buffer)
   198  	for _, dir := range dt.Dirs() {
   199  		_, _ = fmt.Fprintf(out, "%s/\n", dir)
   200  		for _, entry := range dt[dir] {
   201  			flag := ""
   202  			if _, ok := entry.(fs.Directory); ok {
   203  				flag = "/"
   204  			}
   205  			_, _ = fmt.Fprintf(out, "  %s%s\n", path.Base(entry.Remote()), flag)
   206  		}
   207  	}
   208  	return out.String()
   209  }