package vfs import ( "context" "io/fs" "path" "path/filepath" "strings" "sync" "time" ) const Separator = "/" func IsRoot(filename string) bool { return path.Clean(filename) == Separator } func trimRelPath(p, t string) string { return strings.Trim(strings.TrimPrefix(p, t), "/") } // func clean(p string) string { // return path.Clean(Separator + strings.ReplaceAll(p, "\\", "/")) // } func RelPath(p string) string { return strings.TrimLeft(p, Separator) } func AbsPath(p string) string { if p == "" || p[0] != '/' { return Separator + p } return p } func AddTrailSlash(p string) string { if p == "" || p[len(p)-1] != '/' { return p + Separator } return p } // OnceValueWOErr returns a function that invokes f only once and returns the value // returned by f . The returned function may be called concurrently. // // If f panics, the returned function will panic with the same value on every call. func OnceValueWOErr[T any](f func() (T, error)) func() (T, error) { var ( mu sync.Mutex isExecuted bool r1 T err error ) return func() (T, error) { mu.Lock() defer mu.Unlock() if isExecuted && err == nil { return r1, nil } r1, err = f() return r1, err } } func subTimeout(ctx context.Context) (context.Context, context.CancelFunc) { if deadline, ok := ctx.Deadline(); ok { timeout := time.Until(deadline) / 2 return context.WithTimeout(ctx, timeout) } return ctx, func() {} } func Walk(ctx context.Context, vfs Filesystem, root string, walkFn filepath.WalkFunc) error { info, err := vfs.Stat(ctx, root) if err != nil { err = walkFn(root, nil, err) } else { err = walk(ctx, vfs, root, info, walkFn) } if err == filepath.SkipDir { return nil } return err } // walk recursively descends path, calling walkFn // adapted from https://golang.org/src/path/filepath/path.go func walk(ctx context.Context, vfs Filesystem, path string, info fs.FileInfo, walkFn filepath.WalkFunc) error { if !info.IsDir() { return walkFn(path, info, nil) } names, err := readdirnames(ctx, vfs, path) err1 := walkFn(path, info, err) // If err != nil, walk can't walk into this directory. // err1 != nil means walkFn want walk to skip this directory or stop walking. // Therefore, if one of err and err1 isn't nil, walk will return. if err != nil || err1 != nil { // The caller's behavior is controlled by the return value, which is decided // by walkFn. walkFn may ignore err and return nil. // If walkFn returns SkipDir, it will be handled by the caller. // So walk should return whatever walkFn returns. return err1 } for _, name := range names { filename := filepath.Join(path, name) fileInfo, err := vfs.Stat(ctx, filename) if err != nil { if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { return err } } else { err = walk(ctx, vfs, filename, fileInfo, walkFn) if err != nil { if !fileInfo.IsDir() || err != filepath.SkipDir { return err } } } } return nil } func readdirnames(ctx context.Context, vfs Filesystem, dir string) ([]string, error) { files, err := vfs.ReadDir(ctx, dir) if err != nil { return nil, err } var names []string for _, file := range files { names = append(names, file.Name()) } return names, nil }