github.com/creachadair/ffs@v0.17.3/fpath/fpath.go (about)

     1  // Copyright 2019 Michael J. Fromberger. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package fpath implements path traversal relative to a *file.File.  A path is
    16  // a slash-separated string, which may optionally begin with "/".
    17  package fpath
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"path"
    24  	"strings"
    25  
    26  	"github.com/creachadair/ffs/file"
    27  )
    28  
    29  var (
    30  	// ErrEmptyPath is reported by Set when given an empty path.
    31  	ErrEmptyPath = errors.New("empty path")
    32  
    33  	// ErrNilFile is reported by Set when passed a nil file.
    34  	ErrNilFile = errors.New("nil file")
    35  
    36  	// ErrSkipChildren signals to the Walk function that the children of the
    37  	// current node should not be visited.
    38  	ErrSkipChildren = errors.New("skip child files")
    39  )
    40  
    41  // Open traverses the given slash-separated path sequentially from root, and
    42  // returns the resulting file or file.ErrChildNotFound. An empty path yields
    43  // root without error.
    44  func Open(ctx context.Context, root *file.File, path string) (*file.File, error) {
    45  	fp, err := findPath(ctx, query{root: root, path: path})
    46  	return fp.target, err
    47  }
    48  
    49  // OpenPath traverses the given slash-separated path sequentially from root,
    50  // and returns a slice of all the files along the path, not including root
    51  // itself.  If any element of the path does not exist, OpenPath returns the
    52  // prefix that was found along with an file.ErrChildNotFound error.
    53  func OpenPath(ctx context.Context, root *file.File, path string) ([]*file.File, error) {
    54  	var out []*file.File
    55  	cur := root
    56  	for _, name := range parsePath(path) {
    57  		c, err := cur.Open(ctx, name)
    58  		if err != nil {
    59  			return out, err
    60  		}
    61  		out = append(out, c)
    62  		cur = c
    63  	}
    64  	return out, nil
    65  }
    66  
    67  // SetOptions control the behaviour of the Set function. A nil *SetOptions
    68  // behaves as a zero-valued options structure.
    69  type SetOptions struct {
    70  	// If true, create any path elements that do not exist along the path.
    71  	Create bool
    72  
    73  	// If not nil, this function is called for any intermediate path elements
    74  	// created along the path. It is also called for the final element if a new
    75  	// final element is not provided as File.
    76  	SetStat func(*file.Stat)
    77  
    78  	// If not nil, insert this element at the end of the path.  If nil, a new
    79  	// empty file with default options is created.
    80  	File *file.File
    81  }
    82  
    83  func (s *SetOptions) create() bool { return s != nil && s.Create }
    84  
    85  func (s *SetOptions) target() *file.File {
    86  	if s == nil {
    87  		return nil
    88  	}
    89  	return s.File
    90  }
    91  
    92  func (s *SetOptions) setStat(f *file.File) *file.File {
    93  	if s != nil && s.SetStat != nil {
    94  		fs := f.Stat()
    95  		s.SetStat(&fs)
    96  		fs.Update()
    97  	}
    98  	return f
    99  }
   100  
   101  // Set traverses the given slash-separated path sequentially from root and
   102  // inserts a file at the end of it. An empty path is an error (ErrEmptyPath).
   103  //
   104  // If opts.Create is true, any missing path entries are created; otherwise it
   105  // is an error (file.ErrChildNotFound) if any path element except the last does
   106  // not exist.
   107  //
   108  // If opts.File != nil, that file is inserted at the end of the path; otherwise
   109  // if opts.Create is true, a new empty file is inserted. If neither of these is
   110  // true, Set reports ErrNilFile.
   111  func Set(ctx context.Context, root *file.File, path string, opts *SetOptions) (*file.File, error) {
   112  	if opts.target() == nil && !opts.create() {
   113  		return nil, fmt.Errorf("set %q: %w", path, ErrNilFile)
   114  	}
   115  	dir, base := "", path
   116  	if i := strings.LastIndex(path, "/"); i >= 0 {
   117  		dir, base = path[:i], path[i+1:]
   118  	}
   119  	if base == "" {
   120  		return nil, fmt.Errorf("set %q: %w", path, ErrEmptyPath)
   121  	}
   122  	fp, err := findPath(ctx, query{
   123  		root: root,
   124  		path: dir,
   125  		ef: func(fp *foundPath, err error) error {
   126  			if errors.Is(err, file.ErrChildNotFound) && opts.create() {
   127  				c := opts.setStat(fp.target.New(&file.NewOptions{Name: fp.targetName}))
   128  				fp.target.Child().Set(fp.targetName, c)
   129  				fp.parent, fp.target = fp.target, c
   130  				return nil
   131  			}
   132  			return err
   133  		},
   134  	})
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	if last := opts.target(); last != nil {
   139  		fp.target.Child().Set(base, last)
   140  		return last, nil
   141  	}
   142  	newf := root.New(nil)
   143  	fp.target.Child().Set(base, opts.setStat(newf))
   144  	return newf, nil
   145  }
   146  
   147  // Remove removes the file at the given slash-separated path beneath root.  If
   148  // any component of the path does not exist, it returns file.ErrChildNotFound.
   149  func Remove(ctx context.Context, root *file.File, path string) error {
   150  	fp, err := findPath(ctx, query{root: root, path: path})
   151  	if err != nil {
   152  		return err
   153  	} else if fp.parent != nil {
   154  		fp.parent.Child().Remove(fp.targetName)
   155  	}
   156  	return nil
   157  }
   158  
   159  // An Entry is the argument to the visit callback for the Walk function.
   160  type Entry struct {
   161  	Path string     // the path of this entry relative to the root
   162  	File *file.File // the file for this entry (nil on error)
   163  	Err  error
   164  }
   165  
   166  // Walk walks the file tree rooted at root, depth-first, and calls visit with
   167  // an entry for each file in the tree. The entry.Path gives the path of the
   168  // file relative to the root. If an error occurred opening the file at that
   169  // path, entry.File is nil and entry.Err contains the error; otherwise
   170  // entry.File contains the file addressed by the path.
   171  //
   172  // If visit reports an error other than ErrSkipChildren, traversal stops and
   173  // that error is returned to the caller of Walk.  If it returns ErrSkipChildren
   174  // the walk continues but skips the descendant files of the current entry.
   175  func Walk(ctx context.Context, root *file.File, visit func(Entry) error) error {
   176  	q := []string{""}
   177  	for ctx.Err() == nil && len(q) != 0 {
   178  		next := q[len(q)-1]
   179  		q = q[:len(q)-1]
   180  
   181  		f, err := Open(ctx, root, next)
   182  		err = visit(Entry{
   183  			Path: next,
   184  			File: f,
   185  			Err:  err,
   186  		})
   187  		if err == nil {
   188  			if f == nil {
   189  				continue // the error was suppressed
   190  			}
   191  			kids := f.Child().Names()
   192  			for i, name := range kids {
   193  				kids[i] = path.Join(next, name)
   194  			}
   195  			for i, j := 0, len(kids)-1; i < j; i++ {
   196  				kids[i], kids[j] = kids[j], kids[i]
   197  				j--
   198  			}
   199  			q = append(q, kids...)
   200  		} else if err != ErrSkipChildren {
   201  			return err
   202  		}
   203  	}
   204  	return ctx.Err()
   205  }
   206  
   207  type errFilter = func(*foundPath, error) error
   208  
   209  func findPath(ctx context.Context, q query) (foundPath, error) {
   210  	fp := foundPath{
   211  		parent: nil,
   212  		target: q.root,
   213  	}
   214  	for _, name := range parsePath(q.path) {
   215  		fp.targetName = name
   216  		c, err := fp.target.Open(ctx, name)
   217  		if err == nil {
   218  			fp.parent, fp.target = fp.target, c
   219  		} else if q.ef == nil {
   220  			return fp, err
   221  		} else if ferr := q.ef(&fp, err); ferr != nil {
   222  			return fp, ferr
   223  		}
   224  	}
   225  	return fp, nil
   226  }
   227  
   228  type query struct {
   229  	root *file.File
   230  	path string
   231  	ef   errFilter
   232  }
   233  
   234  type foundPath struct {
   235  	parent     *file.File
   236  	target     *file.File
   237  	targetName string
   238  }
   239  
   240  func parsePath(path string) []string {
   241  	clean := strings.TrimPrefix(path, "/")
   242  	if clean == "" || path == "." {
   243  		return nil
   244  	}
   245  	return strings.Split(clean, "/")
   246  }