package fs import ( "bytes" "fmt" "io" "io/fs" "os" "gitlab.com/golang-utils/fs/path" ) type FileSystem interface { Open(name path.Path) (fs.File, error) // from fs.FS ReadDir(name path.Path) ([]fs.DirEntry, error) // from fs.ReadDirFS ReadFile(name path.Path) ([]byte, error) // from fs.ReadFileFS Stat(name path.Path) (fs.FileInfo, error) // WriteFile reads from the given reader and writes all the content // to the file of the given path. If the file already exists, an error is returned. // Otherwise the file is newly created WriteFile(name path.Path, rd io.Reader, perm fs.FileMode) error Mkdir(name path.Path, recursive bool, perm fs.FileMode) error Rm(name path.Path, recursive bool) error // RenameFile renames a file. It is not allowed for directories // and will also not move files, if the target is a directory. // Renaming will fail, if there exists already a file under this name. // Renaming will also fail, if the new file path is in a different directory RenameFile(old path.Path, newName string) error Err() error } type FileSystemMover interface { Move(src, target path.Path) error MountPoint(p path.Path) (string, error) } /* from io/fs.ValidPath // ValidPath reports whether the given path name // is valid for use in a call to Open. // // Path names passed to open are UTF-8-encoded, // unrooted, slash-separated sequences of path elements, like “x/y/z”. // Path names must not contain an element that is “.” or “..” or the empty string, // except for the special case that the root directory is named “.”. // Paths must not start or end with a slash: “/x” and “x/” are invalid. // // Note that paths are slash-separated on all systems, even Windows. // Paths containing other characters such as backslash and colon // are accepted as valid, but those characters must never be // interpreted by an FS implementation as path element separators. */ // ValidRoot returns, if the path can be used as a root for a filesystem (e.g. for use with fs.Dir). // This is only the case, if it does not go upwards (with two dots syntax). func validFSPath(f path.Path) bool { if f.IsAbs() { return false } return fs.ValidPath(f.Rel()) } func FileExits(fsys FileSystem, f path.Path) bool { if !validFSPath(f) { return false } _, err := fsys.Stat(f) if err == os.ErrNotExist { return false } if err != nil { //panic(err.Error()) return false } return true } func IsDir(fsys FileSystem, p path.Path) bool { if !validFSPath(p) { return false } f, err := fsys.Stat(p) if err == os.ErrNotExist { return false } if err != nil { return false } return f.IsDir() } // TODO copy files reads all the content into memory and therefor might fail for very large files. // we should allow to copy them too, by copying byte for byte. // CopyFile copies the content of the given srcFile to the given targetFile. If targetFile already // exists, it is overwritten. If createTargetDir is true, the directory of targetFile is created, if it // does not exist. func CopyFile(fsys FileSystem, srcFile, targetFile path.Path, createTargetDir bool) error { bt, err := fsys.ReadFile(srcFile) if err != nil { return fmt.Errorf("could not read from %q: %v", srcFile, err) } if !IsDir(fsys, targetFile.Dir()) { if createTargetDir { err = fsys.Mkdir(targetFile.Dir(), true, 0755) if err != nil { return fmt.Errorf("could not create target dir %q: %v", targetFile.Dir(), err) } } else { return fmt.Errorf("target dir %q does not exist", targetFile.Dir()) } } rd := bytes.NewReader(bt) err = fsys.WriteFile(targetFile, rd, 0644) if err != nil { return fmt.Errorf("could not write to %q: %v", targetFile, err) } return nil } /* RenameFile file means, the same file is going to another place and can also change its name. In every case we want the target to be a file name and not a directory. If the target is a directory and exists, an error is returned If the src is a directory, an error is returned If the target is a file that exists, an error is returned if overwrite is false, otherwise the target file is overwritten both paths must be absolute, if not, an error is written For the local filesystem an optimization is done, in case that source and target are on the same mountpoint (drive letter in Windows). then the os Move operation will be used. In every other case, the sourcefile will be copied to the target path and if the copy was successfull, the original file will be removed. if it was not sucessfull, an error is returned and the original file will not be removed, however the target file is removed in that case if FS implements the FileSystemMover interface, this optimization will be used for the same mountpoint */ func RenameFile(fsys FileSystem, src, target path.Path, overwrite bool) error { if !FileExits(fsys, src) { return fmt.Errorf("source %s does not exist", src.Local()) } if IsDir(fsys, src) { return fmt.Errorf("source %s is a directory", src.Local()) } if !overwrite && !FileExits(fsys, target) { return fmt.Errorf("target %s already exists", target.Local()) } if FileExits(fsys, target) && IsDir(fsys, target) { return fmt.Errorf("target %s is a directory", target.Local()) } if mover, isMover := fsys.(FileSystemMover); isMover { mpSrc, errSrc := mover.MountPoint(src) mpTrgt, errTrgt := mover.MountPoint(target) if errSrc == nil && errTrgt == nil && mpSrc == mpTrgt { return mover.Move(src, target) } } /* TODO before copying, we should check, if there is enough disk space */ err := CopyFile(fsys, src, target, true) if err != nil { return err } return fsys.Rm(src, false) } /* same as RenameFile, only that src and target are directories. that means that all files within are moved and if everything went fine, the directory is considered moved. if they are on the same mountpoint, the optimization is used and the the Move operation of the fs is used. if FS implements the FileSystemMover interface, this optimization will be used for the same mountpoint */ func RenameDir(fsys FileSystem, src, target path.Path, overwrite bool) error { if !FileExits(fsys, src) { return fmt.Errorf("source %s does not exist", src.Local()) } if !IsDir(fsys, src) { return fmt.Errorf("source %s is no directory", src.Local()) } if !overwrite && !FileExits(fsys, target) { return fmt.Errorf("target %s already exists", target.Local()) } if FileExits(fsys, target) && !IsDir(fsys, target) { return fmt.Errorf("target %s is no directory", target.Local()) } if mover, isMover := fsys.(FileSystemMover); isMover { mpSrc, errSrc := mover.MountPoint(src) mpTrgt, errTrgt := mover.MountPoint(target) if errSrc == nil && errTrgt == nil && mpSrc == mpTrgt { return mover.Move(src, target) } } /* TODO before copying, we should check, if there is enough disk space */ /* TODO: copy "by hand" all the files and dirs in src to target */ return nil } /* MoveTo is moving the source into the directory that is given as a target (src does not change its name) src might be a file or a directory. the same optimizations apply if FS implements the FileSystemMover interface, this optimization will be used for the same mountpoint */ func MoveTo(fsys FileSystem, src, targetDir path.Path) error { if !FileExits(fsys, src) { return fmt.Errorf("source %s does not exist", src.Local()) } if !FileExits(fsys, targetDir) { return fmt.Errorf("targetDir %s does not exists", targetDir.Local()) } if !IsDir(fsys, targetDir) { return fmt.Errorf("target %s is no directory", targetDir.Local()) } if mover, isMover := fsys.(FileSystemMover); isMover { mpSrc, errSrc := mover.MountPoint(src) mpTrgt, errTrgt := mover.MountPoint(targetDir) if errSrc == nil && errTrgt == nil && mpSrc == mpTrgt { return mover.Move(src, targetDir) } } /* TODO before copying, we should check, if there is enough disk space */ /* TODO: optimization, if src is a file */ /* TODO: copy "by hand" */ return nil }