156 lines
3.4 KiB
Go
156 lines
3.4 KiB
Go
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
|
|
}
|
|
|
|
func RemoveTrailingSlash(p string) string {
|
|
if p == Separator {
|
|
return ""
|
|
}
|
|
return strings.TrimSuffix(p, Separator)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
names := make([]string, 0, len(files))
|
|
for _, file := range files {
|
|
if file == nil {
|
|
continue
|
|
}
|
|
|
|
names = append(names, file.Name())
|
|
}
|
|
|
|
return names, nil
|
|
}
|