diff --git a/TODO.txt b/TODO.txt index 66a713c..de59588 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,2 @@ * add disk quota * add LRU cache -* add offline mode diff --git a/pkg/fs/file.go b/pkg/fs/file.go index ac39d0f..907c4ac 100644 --- a/pkg/fs/file.go +++ b/pkg/fs/file.go @@ -3,6 +3,7 @@ package fs import ( + "errors" "io" "os" @@ -10,10 +11,11 @@ import ( ) type File struct { - log logr.Logger - f *os.File - md *Metadata - offset int64 + log logr.Logger + f *os.File + md *Metadata + offset int64 + offline bool } type preload struct { @@ -38,23 +40,34 @@ func (p *preload) Read(data []byte) (n int, err error) { } func (f *File) Preload(unlock func()) { - f.log.V(2).Info("preload started") + log := f.log + if f.offline { + log.V(2).Error(errors.New("no preload in offline mode"), "error preloading file") + unlock() + return + } + log.V(2).Info("preload started") p := &preload{f: f} _, err := io.Copy(io.Discard, p) if err != nil && err != io.EOF { - f.log.Error(err, "error preloading file") + log.Error(err, "error preloading file") } - f.log.V(2).Info("preload finished", "skipped", p.skipped, "written", p.written) + log.V(2).Info("preload finished", "skipped", p.skipped, "written", p.written) unlock() } func (f *File) Read(p []byte) (n int, err error) { + log := f.log if f.hasChunk(len(p)) { n, err = f.md.ReadAt(p, f.offset) if err != nil { + log.Error(err, "error reading cache file") return } _, err = f.Seek(int64(n), io.SeekCurrent) + if err != nil { + log.Error(err, "error seeking source file") + } return } return f.readWithoutCache(p) @@ -69,8 +82,12 @@ func (f *File) hasChunk(n int) bool { } func (f *File) readWithoutCache(p []byte) (n int, err error) { + if f.offline { + return 0, io.EOF + } n, err = f.f.Read(p) if err != nil { + f.log.Error(err, "error reading source file") return } f.md.WriteAt(p, f.offset) @@ -82,6 +99,7 @@ func (f *File) readWithoutCache(p []byte) (n int, err error) { func (f *File) Seek(offset int64, whence int) (n int64, err error) { n, err = f.f.Seek(offset, whence) if err != nil { + f.log.Error(err, "error seeking source file") return } switch whence { diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go index b650cc8..932ca2d 100644 --- a/pkg/fs/fs.go +++ b/pkg/fs/fs.go @@ -73,16 +73,27 @@ func NewFS(src, dst, metadata string, log logr.Logger) (fs *FS, err error) { return fs, err } -func (fs *FS) Stat(name string) (stdfs.FileInfo, error) { - return stat(fs.src, name) +func (fs *FS) Stat(name string) (fi stdfs.FileInfo, err error) { + fi, _, err = fs.StatWithOffline(name) + return +} + +func (fs *FS) StatWithOffline(name string) (fi stdfs.FileInfo, offline bool, err error) { + f, err := fs.open(name, true) + if err != nil { + return + } + fi, err = f.Stat() + offline = f.offline + return } func (fs *FS) Open(name string) (http.File, error) { - return fs.open(name) + return fs.open(name, false) } func (fs *FS) Preload(name string) { - f, err := fs.open(name) + f, err := fs.open(name, false) if err != nil { return } @@ -95,66 +106,82 @@ func (fs *FS) Preload(name string) { } func skipLog(name string) bool { - return strings.HasSuffix(name, "index.html") + return strings.HasSuffix(name, "index.html") || strings.HasSuffix(name, "favicon.ico") } -func (fs *FS) open(name string) (f *File, err error) { +func (fs *FS) open(name string, stat bool) (f *File, err error) { log := fs.log.WithValues("file", name) - rp := filepath.Join(fs.src, filepath.FromSlash(path.Clean("/"+name))) - rf, err := os.Open(rp) + p := filepath.FromSlash(path.Clean("/" + name)) + sp := filepath.Join(fs.src, p) + dp := filepath.Join(fs.dst, p) + offline := false + sf, err := os.Open(sp) if err != nil { - if !skipLog(name) { - log.Error(err, "error opening source file") + if errors.Is(err, os.ErrNotExist) { + sf, err = os.Open(dp) + if err != nil { + if !skipLog(name) { + log.Error(err, "error opening source file") + } + return + } + offline = true + log.V(2).Info("file offline mode", "path", dp) } - return } - rfi, err := rf.Stat() + sfi, err := sf.Stat() if err != nil { log.Error(err, "error stat source file") return } - if rfi.IsDir() { - return &File{f: rf}, nil + if sfi.IsDir() || stat { + if empty, err := dirEmpty(sf.Name()); empty && err == nil { + sf, err = os.Open(dp) + offline = true + log.V(2).Info("dir offline mode", "path", dp) + } + return &File{f: sf, offline: offline}, err } - mp := filepath.Join(fs.dst, filepath.FromSlash(path.Clean("/"+name))) - var mf *os.File - i := strings.LastIndex(mp, "/") + var df *os.File + i := strings.LastIndex(dp, "/") if i > 0 { - dir := mp[:i] + 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", mp) + log.Error(err, "error creating cache dir", "dir", dp) return nil, err } } } - _, err = os.Stat(mp) + _, err = os.Stat(dp) truncate := errors.Is(err, os.ErrNotExist) if !fs.isOpen(name) { - mf, err = os.OpenFile(mp, os.O_RDWR|os.O_CREATE, 0o644) + df, err = os.OpenFile(dp, os.O_RDWR|os.O_CREATE, 0o644) if err != nil { log.Error(err, "error opening cache file") return nil, err } if truncate { - err = mf.Truncate(rfi.Size()) + err = df.Truncate(sfi.Size()) if err != nil { log.Error(err, "error truncating cache file") return nil, err } - err = mf.Sync() + err = df.Sync() if err != nil { log.Error(err, "error syncing cache file") return nil, err } } } + md := fs.metadata(name, sfi.Size(), df) return &File{ - log: log, - f: rf, - md: fs.metadata(name, rfi.Size(), mf), + log: log, + f: sf, + md: md, + offline: offline, }, nil } @@ -219,7 +246,21 @@ func (fs *FS) Close() { fs.flushMetadata() } -func stat(prefix, name string) (stdfs.FileInfo, error) { - p := filepath.Join(prefix, filepath.FromSlash(path.Clean("/"+name))) - return os.Stat(p) +func dirEmpty(name string) (bool, error) { + f, err := os.Open(name) + if err != nil { + return false, err + } + defer f.Close() + _, err = f.Readdir(1) + if err == io.EOF { + return true, nil + } + return false, err +} + +func stat(dst, name string) (fi stdfs.FileInfo, err error) { + p := filepath.FromSlash(path.Clean("/" + name)) + dp := filepath.Join(dst, p) + return os.Stat(dp) } diff --git a/pkg/srv/srv.go b/pkg/srv/srv.go index 41c5169..6f80157 100644 --- a/pkg/srv/srv.go +++ b/pkg/srv/srv.go @@ -49,7 +49,7 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.URL.Path = upath } p := path.Clean(upath) - d, err := fs.fs.Stat(p) + d, offline, err := fs.fs.StatWithOffline(p) if err != nil { msg, code := toHTTPError(err) http.Error(w, msg, code) @@ -82,7 +82,16 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } i := &responseInterceptor{w: w} + // TODO implement directory listing cache + offline = true + if offline { + r.Header.Del("If-Modified-Since") + r.Header.Del("Cache-Control") + } fs.h.ServeHTTP(i, r) + if offline { + w.Header().Del("Last-Modified") + } paths, err := i.GetPaths() if err == io.EOF { w.WriteHeader(i.Status())