utils/filesystem/filepath.go
package filesystem
import (
"fmt"
"io/fs"
"path/filepath"
"strings"
"syscall"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/reflection"
)
// FilepathStem returns the final path component, without its suffix.
func FilepathStem(fp string) string {
return strings.TrimSuffix(filepath.Base(fp), filepath.Ext(fp))
}
// FileTreeDepth returns the depth of a file in a tree starting from root
func FileTreeDepth(fs FS, root, filePath string) (depth int64, err error) {
if reflection.IsEmpty(filePath) {
return
}
rel, err := fs.ConvertToRelativePath(root, filePath)
if err != nil {
return
}
diff := rel[0]
if reflection.IsEmpty(diff) {
return
}
diff = strings.ReplaceAll(diff, string(fs.PathSeparator()), "/")
depth = int64(len(strings.Split(diff, "/")) - 1)
return
}
// EndsWithPathSeparator states whether a path is ending with a path separator of not
func EndsWithPathSeparator(fs FS, filePath string) bool {
return strings.HasSuffix(filePath, "/") || strings.HasSuffix(filePath, string(fs.PathSeparator()))
}
// NewPathValidationRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid not empty path.
// `when` describes whether the rule is enforced or not
func NewPathValidationRule(filesystem FS, when bool) validation.Rule {
return &pathValidationRule{condition: when, filesystem: filesystem}
}
// NewOSPathValidationRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid path for the Operating System's filesystem.
// `when` describes whether the rule is enforced or not
func NewOSPathValidationRule(when bool) validation.Rule {
return NewPathValidationRule(GetGlobalFileSystem(), when)
}
type pathValidationRule struct {
condition bool
filesystem FS
}
func (r *pathValidationRule) Validate(value interface{}) error {
err := validation.Required.When(r.condition).Validate(value)
if err != nil {
return fmt.Errorf("%w: path [%v] is required: %v", commonerrors.ErrUndefined, value, err.Error())
}
if !r.condition {
return nil
}
pathString, err := validation.EnsureString(value)
if err != nil {
return fmt.Errorf("%w: path [%v] must be a string: %v", commonerrors.ErrInvalid, value, err.Error())
}
pathString = strings.TrimSpace(pathString)
// This check is here because it validates the path on any platform (it is a cross-platform check)
// Indeed if the path exists, then it can only be valid.
if r.filesystem.Exists(pathString) {
return nil
}
// Inspired from https://github.com/go-playground/validator/blob/84254aeb5a59e615ec0b66ab53b988bc0677f55e/baked_in.go#L1604 and https://stackoverflow.com/questions/35231846/golang-check-if-string-is-valid-path
if pathString == "" {
return fmt.Errorf("%w: the path [%v] is empty", commonerrors.ErrUndefined, value)
}
// This check is to catch errors on Linux. It does not work as well on Windows.
if _, err := r.filesystem.Stat(pathString); err != nil {
switch t := err.(type) {
case *fs.PathError:
if t.Err == syscall.EINVAL {
return fmt.Errorf("%w: the path [%v] has invalid characters: %v", commonerrors.ErrInvalid, value, err.Error())
}
default:
// make the linter happy
}
}
// The following case is not caught on Windows by the check above.
if strings.Contains(pathString, "\n") {
return fmt.Errorf("%w: the path [%v] has carriage returns characters", commonerrors.ErrInvalid, value)
}
// TODO add platform validation checks: e.g. https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN on windows
return nil
}
// NewPathExistRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid not empty path and actually exists.
// `when` describes whether the rule is enforced or not.
func NewPathExistRule(filesystem FS, when bool) validation.Rule {
return &pathExistValidationRule{filesystem: filesystem, condition: when}
}
// NewOSPathExistRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid path for the Operating system's filesystem and actually exists.
// `when` describes whether the rule is enforced or not.
func NewOSPathExistRule(when bool) validation.Rule {
return NewPathExistRule(GetGlobalFileSystem(), when)
}
type pathExistValidationRule struct {
condition bool
filesystem FS
}
func (r *pathExistValidationRule) Validate(value interface{}) error {
err := NewPathValidationRule(r.filesystem, r.condition).Validate(value)
if err != nil {
return err
}
if !r.condition {
return nil
}
path, err := validation.EnsureString(value)
if err != nil {
return fmt.Errorf("%w: path [%v] must be a string: %v", commonerrors.ErrInvalid, value, err.Error())
}
if !r.filesystem.Exists(path) {
err = fmt.Errorf("%w: path [%v] does not exist", commonerrors.ErrNotFound, path)
}
return err
}