github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/dirtree/dirtree.go (about)

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