package fs import ( "fmt" "io" "os" "path/filepath" "strings" "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 { // Namee is the name of the filesystem Name string // Dir is the path to the directory to observe Dir string // Size of the filesystem in bytes Size int64 // For logging, optional Logger log.Logger } // diskFileInfo implements the FileInfo interface type diskFileInfo struct { dir string name string finfo os.FileInfo } func (fi *diskFileInfo) Name() string { return fi.name } func (fi *diskFileInfo) Size() int64 { return fi.finfo.Size() } func (fi *diskFileInfo) ModTime() time.Time { return fi.finfo.ModTime() } func (fi *diskFileInfo) IsLink() (string, bool) { mode := fi.finfo.Mode() if mode&os.ModeSymlink == 0 { return fi.name, false } path, err := os.Readlink(filepath.Join(fi.dir, fi.name)) if err != nil { return fi.name, false } path = filepath.Join(fi.dir, path) if !strings.HasPrefix(path, fi.dir) { return fi.name, false } name := strings.TrimPrefix(path, fi.dir) 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 { dir string name string 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{ dir: f.dir, name: f.name, 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 { name string dir string // Max. size of the filesystem in bytes as // given by the config maxSize int64 // 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 a disk // that implements the Filesystem interface func NewDiskFilesystem(config DiskConfig) (Filesystem, error) { fs := &diskFilesystem{ name: config.Name, maxSize: config.Size, logger: config.Logger, } if fs.logger == nil { fs.logger = log.New("") } fs.logger = fs.logger.WithFields(log.Fields{ "name": fs.name, "type": "disk", }) if err := fs.Rebase(config.Dir); err != nil { return nil, err } return fs, nil } func (fs *diskFilesystem) Name() string { return fs.name } func (fs *diskFilesystem) Base() string { return fs.dir } func (fs *diskFilesystem) Rebase(base string) error { if len(base) == 0 { return fmt.Errorf("invalid base path provided") } dir, err := filepath.Abs(base) if err != nil { return err } base = dir finfo, err := os.Stat(base) if err != nil { return fmt.Errorf("the provided base path '%s' doesn't exist", fs.dir) } if !finfo.IsDir() { return fmt.Errorf("the provided base path '%s' must be a directory", fs.dir) } fs.dir = base return nil } func (fs *diskFilesystem) Type() string { return "diskfs" } 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(func(path string, info os.FileInfo) { size += info.Size() }) fs.currentSize = size fs.lastSizeCheck = time.Now() } return fs.currentSize, fs.maxSize } func (fs *diskFilesystem) Resize(size int64) { fs.maxSize = size } func (fs *diskFilesystem) Files() int64 { var nfiles int64 = 0 fs.walk(func(path string, info os.FileInfo) { nfiles++ }) return nfiles } func (fs *diskFilesystem) Symlink(oldname, newname string) error { oldname = filepath.Join(fs.dir, filepath.Clean("/"+oldname)) if !filepath.IsAbs(newname) { return nil } newname = filepath.Join(fs.dir, filepath.Clean("/"+newname)) err := os.Symlink(oldname, newname) return err } func (fs *diskFilesystem) Open(path string) File { path = filepath.Join(fs.dir, filepath.Clean("/"+path)) f, err := os.Open(path) if err != nil { return nil } df := &diskFile{ dir: fs.dir, name: path, file: f, } return df } func (fs *diskFilesystem) Store(path string, r io.Reader) (int64, bool, error) { path = filepath.Join(fs.dir, filepath.Clean("/"+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 } size, err := f.ReadFrom(r) if err != nil { return -1, false, fmt.Errorf("reading data failed: %w", err) } return size, !replace, nil } func (fs *diskFilesystem) Delete(path string) int64 { path = filepath.Join(fs.dir, filepath.Clean("/"+path)) finfo, err := os.Stat(path) if err != nil { return -1 } size := finfo.Size() if err := os.Remove(path); err != nil { return -1 } return size } func (fs *diskFilesystem) DeleteAll() int64 { return 0 } func (fs *diskFilesystem) List(pattern string) []FileInfo { files := []FileInfo{} fs.walk(func(path string, info os.FileInfo) { if path == fs.dir { return } name := strings.TrimPrefix(path, fs.dir) if name[0] != os.PathSeparator { name = string(os.PathSeparator) + name } if info.IsDir() { name += "/" } if len(pattern) != 0 { if ok, _ := glob.Match(pattern, name, '/'); !ok { return } } files = append(files, &diskFileInfo{ dir: fs.dir, name: name, finfo: info, }) }) return files } func (fs *diskFilesystem) walk(walkfn func(path string, info os.FileInfo)) { filepath.Walk(fs.dir, 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 }) }