package fs import ( "bytes" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "sync" "time" "github.com/datarhei/core/v16/glob" "github.com/datarhei/core/v16/log" ) // DiskConfig is the config required to create a new disk filesystem. type DiskConfig struct { // For logging, optional Logger log.Logger } // RootedDiskConfig is the config required to create a new rooted disk filesystem. type RootedDiskConfig struct { // Root is the path this filesystem is rooted to Root string // For logging, optional Logger log.Logger } // diskFileInfo implements the FileInfo interface type diskFileInfo struct { root string name string mode os.FileMode finfo os.FileInfo } func (fi *diskFileInfo) Name() string { return fi.name } func (fi *diskFileInfo) Size() int64 { if fi.finfo.IsDir() { return 0 } return fi.finfo.Size() } func (fi *diskFileInfo) Mode() fs.FileMode { return fi.mode } func (fi *diskFileInfo) ModTime() time.Time { return fi.finfo.ModTime() } func (fi *diskFileInfo) IsLink() (string, bool) { if fi.mode&os.ModeSymlink == 0 { return fi.name, false } path, err := os.Readlink(filepath.Join(fi.root, fi.name)) if err != nil { return fi.name, false } if !strings.HasPrefix(path, fi.root) { return fi.name, false } name := strings.TrimPrefix(path, fi.root) if name[0] != os.PathSeparator { name = string(os.PathSeparator) + name } return name, true } func (fi *diskFileInfo) IsDir() bool { return fi.finfo.IsDir() } // diskFile implements the File interface type diskFile struct { root string name string mode os.FileMode file *os.File } func (f *diskFile) Name() string { return f.name } func (f *diskFile) Stat() (FileInfo, error) { finfo, err := f.file.Stat() if err != nil { return nil, err } dif := &diskFileInfo{ root: f.root, name: f.name, mode: f.mode, finfo: finfo, } return dif, nil } func (f *diskFile) Close() error { return f.file.Close() } func (f *diskFile) Read(p []byte) (int, error) { return f.file.Read(p) } // diskFilesystem implements the Filesystem interface type diskFilesystem struct { metadata map[string]string lock sync.RWMutex root string cwd string // Current size of the filesystem in bytes currentSize int64 lastSizeCheck time.Time // Logger from the config logger log.Logger } // NewDiskFilesystem returns a new filesystem that is backed by the disk filesystem. // The root is / and the working directory is whatever is returned by os.Getwd(). The value // of Root in the config will be ignored. func NewDiskFilesystem(config DiskConfig) (Filesystem, error) { fs := &diskFilesystem{ metadata: make(map[string]string), root: "/", cwd: "/", logger: config.Logger, } cwd, err := os.Getwd() if err != nil { return nil, err } fs.cwd = cwd if len(fs.cwd) == 0 { fs.cwd = "/" } fs.cwd = filepath.Clean(fs.cwd) if !filepath.IsAbs(fs.cwd) { return nil, fmt.Errorf("the current working directory must be an absolute path") } if fs.logger == nil { fs.logger = log.New("") } return fs, nil } // NewRootedDiskFilesystem returns a filesystem that is backed by the disk filesystem. The // root of the filesystem is defined by DiskConfig.Root. The working directory is "/". Root // must be directory. If it doesn't exist, it will be created func NewRootedDiskFilesystem(config RootedDiskConfig) (Filesystem, error) { fs := &diskFilesystem{ metadata: make(map[string]string), root: config.Root, cwd: "/", logger: config.Logger, } if len(fs.root) == 0 { fs.root = "/" } if root, err := filepath.Abs(fs.root); err != nil { return nil, err } else { fs.root = root } err := os.MkdirAll(fs.root, 0700) if err != nil { return nil, err } info, err := os.Stat(fs.root) if err != nil { return nil, err } if !info.IsDir() { return nil, fmt.Errorf("root is not a directory") } if fs.logger == nil { fs.logger = log.New("") } return fs, nil } func (fs *diskFilesystem) Name() string { return "disk" } func (fs *diskFilesystem) Type() string { return "disk" } func (fs *diskFilesystem) Metadata(key string) string { fs.lock.RLock() defer fs.lock.RUnlock() return fs.metadata[key] } func (fs *diskFilesystem) SetMetadata(key, data string) { fs.lock.Lock() defer fs.lock.Unlock() fs.metadata[key] = data } func (fs *diskFilesystem) Size() (int64, int64) { // This is to cache the size for some time in order not to // stress the underlying filesystem too much. if time.Since(fs.lastSizeCheck) >= 10*time.Second { var size int64 = 0 fs.walk(fs.root, func(path string, info os.FileInfo) { if info.IsDir() { return } size += info.Size() }) fs.currentSize = size fs.lastSizeCheck = time.Now() } return fs.currentSize, -1 } func (fs *diskFilesystem) Purge(size int64) int64 { return 0 } func (fs *diskFilesystem) Files() int64 { var nfiles int64 = 0 fs.walk(fs.root, func(path string, info os.FileInfo) { if info.IsDir() { return } nfiles++ }) return nfiles } func (fs *diskFilesystem) Symlink(oldname, newname string) error { oldname = fs.cleanPath(oldname) newname = fs.cleanPath(newname) info, err := os.Lstat(oldname) if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 { return fmt.Errorf("%s can't link to another link (%s)", newname, oldname) } if info.IsDir() { return fmt.Errorf("can't symlink directories") } return os.Symlink(oldname, newname) } func (fs *diskFilesystem) Open(path string) File { path = fs.cleanPath(path) df := &diskFile{ root: fs.root, name: strings.TrimPrefix(path, fs.root), } info, err := os.Lstat(path) if err != nil { return nil } df.mode = info.Mode() f, err := os.Open(path) if err != nil { return nil } df.file = f return df } func (fs *diskFilesystem) ReadFile(path string) ([]byte, error) { path = fs.cleanPath(path) return os.ReadFile(path) } func (fs *diskFilesystem) WriteFileReader(path string, r io.Reader) (int64, bool, error) { path = fs.cleanPath(path) replace := true dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return -1, false, fmt.Errorf("creating file failed: %w", err) } var f *os.File var err error f, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0) if err != nil { f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return -1, false, fmt.Errorf("creating file failed: %w", err) } replace = false } defer f.Close() size, err := f.ReadFrom(r) if err != nil { return -1, false, fmt.Errorf("reading data failed: %w", err) } fs.lastSizeCheck = time.Time{} return size, !replace, nil } func (fs *diskFilesystem) WriteFile(path string, data []byte) (int64, bool, error) { return fs.WriteFileReader(path, bytes.NewBuffer(data)) } func (fs *diskFilesystem) WriteFileSafe(path string, data []byte) (int64, bool, error) { path = fs.cleanPath(path) dir, filename := filepath.Split(path) tmpfile, err := os.CreateTemp(dir, filename) if err != nil { return -1, false, err } defer os.Remove(tmpfile.Name()) size, err := tmpfile.Write(data) if err != nil { return -1, false, err } if err := tmpfile.Close(); err != nil { return -1, false, err } replace := false if _, err := fs.Stat(path); err == nil { replace = true } if err := fs.rename(tmpfile.Name(), path); err != nil { return -1, false, err } fs.lastSizeCheck = time.Time{} return int64(size), !replace, nil } func (fs *diskFilesystem) Rename(src, dst string) error { src = fs.cleanPath(src) dst = fs.cleanPath(dst) return fs.rename(src, dst) } func (fs *diskFilesystem) rename(src, dst string) error { if src == dst { return nil } // First try to rename the file if err := os.Rename(src, dst); err == nil { return nil } // If renaming the file fails, copy the data if err := fs.copy(src, dst); err != nil { os.Remove(dst) return fmt.Errorf("failed to copy files: %w", err) } if err := os.Remove(src); err != nil { os.Remove(dst) return fmt.Errorf("failed to remove source file: %w", err) } return nil } func (fs *diskFilesystem) Copy(src, dst string) error { src = fs.cleanPath(src) dst = fs.cleanPath(dst) return fs.copy(src, dst) } func (fs *diskFilesystem) copy(src, dst string) error { source, err := os.Open(src) if err != nil { return fmt.Errorf("failed to open source file: %w", err) } destination, err := os.Create(dst) if err != nil { source.Close() return fmt.Errorf("failed to create destination file: %w", err) } defer destination.Close() if _, err := io.Copy(destination, source); err != nil { source.Close() os.Remove(dst) return fmt.Errorf("failed to copy data from source to destination: %w", err) } source.Close() fs.lastSizeCheck = time.Time{} return nil } func (fs *diskFilesystem) MkdirAll(path string, perm os.FileMode) error { path = fs.cleanPath(path) return os.MkdirAll(path, perm) } func (fs *diskFilesystem) Stat(path string) (FileInfo, error) { path = fs.cleanPath(path) dif := &diskFileInfo{ root: fs.root, name: strings.TrimPrefix(path, fs.root), } info, err := os.Lstat(path) if err != nil { return nil, err } dif.mode = info.Mode() if info.Mode()&os.ModeSymlink != 0 { info, err = os.Stat(path) if err != nil { return nil, err } } dif.finfo = info return dif, nil } func (fs *diskFilesystem) Remove(path string) int64 { path = fs.cleanPath(path) finfo, err := os.Stat(path) if err != nil { return -1 } size := finfo.Size() if err := os.Remove(path); err != nil { return -1 } fs.lastSizeCheck = time.Time{} return size } func (fs *diskFilesystem) RemoveAll() int64 { return 0 } func (fs *diskFilesystem) List(path, pattern string) []FileInfo { path = fs.cleanPath(path) files := []FileInfo{} fs.walk(path, func(path string, info os.FileInfo) { if path == fs.root { return } name := strings.TrimPrefix(path, fs.root) if name[0] != os.PathSeparator { name = string(os.PathSeparator) + name } if info.IsDir() { return } if len(pattern) != 0 { if ok, _ := glob.Match(pattern, name, '/'); !ok { return } } files = append(files, &diskFileInfo{ root: fs.root, name: name, finfo: info, }) }) return files } func (fs *diskFilesystem) LookPath(file string) (string, error) { if strings.Contains(file, "/") { file = fs.cleanPath(file) err := fs.findExecutable(file) if err == nil { return file, nil } return "", os.ErrNotExist } path := os.Getenv("PATH") for _, dir := range filepath.SplitList(path) { if dir == "" { // Unix shell semantics: path element "" means "." dir = "." } path := filepath.Join(dir, file) path = fs.cleanPath(path) if err := fs.findExecutable(path); err == nil { if !filepath.IsAbs(path) { return path, os.ErrNotExist } return path, nil } } return "", os.ErrNotExist } func (fs *diskFilesystem) findExecutable(file string) error { d, err := fs.Stat(file) if err != nil { return err } m := d.Mode() if m.IsDir() { return fmt.Errorf("is a directory") } if m&0111 != 0 { return nil } return os.ErrPermission } func (fs *diskFilesystem) walk(path string, walkfn func(path string, info os.FileInfo)) { filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if info.IsDir() { walkfn(path, info) return nil } mode := info.Mode() if !mode.IsRegular() && mode&os.ModeSymlink == 0 { return nil } walkfn(path, info) return nil }) } func (fs *diskFilesystem) cleanPath(path string) string { if !filepath.IsAbs(path) { path = filepath.Join(fs.cwd, path) } return filepath.Join(fs.root, filepath.Clean(path)) }