Files
core/io/fs/disk.go
2023-01-31 14:45:58 +01:00

362 lines
6.3 KiB
Go

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
})
}