266 lines
5.1 KiB
Go
266 lines
5.1 KiB
Go
// Copyright (C) 2022 Marius Schellenberger
|
|
|
|
package fs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
stdfs "io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-logr/logr"
|
|
)
|
|
|
|
type FS struct {
|
|
log logr.Logger
|
|
cancel func()
|
|
NoCache http.Handler
|
|
src string
|
|
dst string
|
|
dc *DirCache
|
|
sc *StatCache
|
|
|
|
q *Quota
|
|
mh *MetadataHandler
|
|
ph *PreloadHandler
|
|
}
|
|
|
|
func NewFS(quota int64, max int, src, dst, metadata string, log logr.Logger) (fs *FS, err error) {
|
|
if !filepath.IsAbs(src) {
|
|
return nil, errors.New("src path is not absolute")
|
|
}
|
|
if !filepath.IsAbs(dst) {
|
|
return nil, errors.New("dst path is not absolute")
|
|
}
|
|
if src == dst {
|
|
return nil, errors.New("src and dst path can not be equal")
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
fs = &FS{
|
|
log: log,
|
|
cancel: cancel,
|
|
NoCache: http.FileServer(http.Dir(src)),
|
|
src: src,
|
|
dst: dst,
|
|
dc: NewDirCache(),
|
|
}
|
|
|
|
fs.mh, err = NewMetadataHandler(ctx, fs, metadata, dst, log.WithName("metadata"))
|
|
if err != nil {
|
|
return
|
|
}
|
|
fs.q, err = NewQuota(fs.mh.Size(), quota, fs, log.WithName("quota"))
|
|
if err != nil {
|
|
return
|
|
}
|
|
fs.ph = NewPreloadHandler(ctx, fs, max, log.WithName("preload"))
|
|
fs.sc = NewStatCache(ctx)
|
|
return
|
|
}
|
|
|
|
func (fs *FS) Stat(name string) (fi stdfs.FileInfo, err error) {
|
|
sp, dp := fs.paths(name)
|
|
fi, err = fs.sc.Get(name)
|
|
if err == nil {
|
|
return
|
|
}
|
|
fi, err = os.Stat(sp)
|
|
if err == nil {
|
|
fs.sc.Set(name, fi)
|
|
return
|
|
}
|
|
fi, err = os.Stat(dp)
|
|
return
|
|
}
|
|
|
|
func (fs *FS) RemoveDst(name string) error {
|
|
_, dp := fs.paths(name)
|
|
return os.Remove(dp)
|
|
}
|
|
|
|
func (fs *FS) StatDst(name string) (fi stdfs.FileInfo, err error) {
|
|
fi, err = fs.sc.Get(name)
|
|
if err == nil {
|
|
return
|
|
}
|
|
return fs.statDst(name)
|
|
}
|
|
|
|
func (fs *FS) statDst(name string) (stdfs.FileInfo, error) {
|
|
_, dp := fs.paths(name)
|
|
return os.Stat(dp)
|
|
}
|
|
|
|
func (fs *FS) OpenDst(name string) (f http.File, err error) {
|
|
log := fs.log.WithValues("file", name)
|
|
_, dp := fs.paths(name)
|
|
fi, err := fs.StatDst(name)
|
|
if err != nil {
|
|
if !skipLog(name) {
|
|
log.Error(err, "error stat cache file")
|
|
}
|
|
return
|
|
}
|
|
file, err := os.Open(dp)
|
|
if err != nil {
|
|
if !skipLog(name) {
|
|
log.Error(err, "error opening cache file")
|
|
}
|
|
return
|
|
}
|
|
if fi.IsDir() {
|
|
f = &Dir{log: log.WithName("dir"), f: file, dc: fs.dc}
|
|
return
|
|
}
|
|
md := fs.mh.Metadata(name, fi.Size())
|
|
f = &File{
|
|
log: log.WithName("file"),
|
|
f: file,
|
|
md: md,
|
|
offline: true,
|
|
}
|
|
return
|
|
}
|
|
|
|
func (fs *FS) QuotaUsage() (cur, max int64) {
|
|
return fs.q.Usage()
|
|
}
|
|
|
|
func (fs *FS) CancelPreload(name string) {
|
|
fs.ph.CancelPreload(name)
|
|
}
|
|
|
|
func (fs *FS) Preloads() Preloads {
|
|
return fs.ph.Preloads()
|
|
}
|
|
|
|
func (fs *FS) Preload(name string) {
|
|
fs.ph.Preload(name)
|
|
}
|
|
|
|
func skipLog(name string) bool {
|
|
return strings.HasSuffix(name, "index.html") || strings.HasSuffix(name, "favicon.ico")
|
|
}
|
|
|
|
func (fs *FS) paths(name string) (sp string, dp string) {
|
|
p := filepath.FromSlash(path.Clean("/" + name))
|
|
sp = filepath.Join(fs.src, p)
|
|
dp = filepath.Join(fs.dst, p)
|
|
return
|
|
}
|
|
|
|
func (fs *FS) open(name, sp, dp string) (f *os.File, fi os.FileInfo, offline bool, err error) {
|
|
f, err = os.Open(sp)
|
|
if err == nil {
|
|
fi, err = f.Stat()
|
|
if err == nil {
|
|
return
|
|
}
|
|
f.Close()
|
|
}
|
|
f, err = os.Open(dp)
|
|
if err != nil {
|
|
return
|
|
}
|
|
fi, err = f.Stat()
|
|
if err != nil {
|
|
f.Close()
|
|
}
|
|
offline = true
|
|
return
|
|
}
|
|
|
|
func (fs *FS) Open(name string) (f http.File, err error) {
|
|
log := fs.log.WithValues("file", name)
|
|
sp, dp := fs.paths(name)
|
|
file, fi, offline, err := fs.open(name, sp, dp)
|
|
if err != nil {
|
|
if !skipLog(name) {
|
|
log.Error(err, "error opening source file")
|
|
}
|
|
return
|
|
}
|
|
if fi.IsDir() {
|
|
if offline {
|
|
log.V(2).Info("dir offline mode", "path", dp)
|
|
}
|
|
f = &Dir{log: log.WithName("dir"), f: file, dc: fs.dc}
|
|
return
|
|
}
|
|
if offline {
|
|
log.V(2).Info("file offline mode", "path", dp)
|
|
}
|
|
md := fs.mh.Metadata(name, fi.Size())
|
|
f = &File{
|
|
log: log.WithName("file"),
|
|
f: file,
|
|
md: md,
|
|
offline: offline,
|
|
}
|
|
return
|
|
}
|
|
|
|
func (fs *FS) openCacheFile(name string, size int64) (df *os.File, err error) {
|
|
_, dp := fs.paths(name)
|
|
log := fs.log
|
|
if i := strings.LastIndex(dp, "/"); i > 0 {
|
|
dir := dp[:i]
|
|
_, err = os.Stat(dir)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
err = os.MkdirAll(dir, 0o755)
|
|
if err != nil {
|
|
log.Error(err, "error creating cache dir", "dir", dp)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
_, err = os.Stat(dp)
|
|
truncate := errors.Is(err, os.ErrNotExist)
|
|
df, err = os.OpenFile(dp, os.O_RDWR|os.O_CREATE, 0o644)
|
|
if err != nil {
|
|
log.Error(err, "error opening cache file")
|
|
return
|
|
}
|
|
if truncate {
|
|
err = df.Truncate(size)
|
|
if err != nil {
|
|
log.Error(err, "error truncating cache file")
|
|
return
|
|
}
|
|
err = df.Sync()
|
|
if err != nil {
|
|
log.Error(err, "error syncing cache file")
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (fs *FS) CacheStatus(name string) int {
|
|
return fs.mh.CacheStatus(name)
|
|
}
|
|
|
|
func (fs *FS) Close() {
|
|
fs.cancel()
|
|
}
|
|
|
|
func dirEmpty(name string) (bool, error) {
|
|
f, err := os.Open(name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
_, err = f.ReadDir(1)
|
|
if err == io.EOF {
|
|
f.Close()
|
|
return true, nil
|
|
}
|
|
f.Close()
|
|
return false, err
|
|
}
|