This commit is contained in:
Alexander Neumann 2024-03-03 14:19:22 +01:00
parent bd3022c504
commit b3f38686ee
7 changed files with 324 additions and 143 deletions

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/net/webdav"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -104,12 +105,21 @@ func runWebDAV(ctx context.Context, opts WebDAVOptions, gopts GlobalOptions, arg
// }, // },
// } // }
logRequest := func(req *http.Request, err error) {
errorLogger.Printf("req %v %v -> %v\n", req.Method, req.URL.Path, err)
}
srv := &http.Server{ srv := &http.Server{
ReadTimeout: 60 * time.Second, ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second,
Addr: opts.Listen, Addr: opts.Listen,
Handler: http.FileServer(http.FS(root)), // Handler: http.FileServer(http.FS(root)),
ErrorLog: errorLogger, Handler: &webdav.Handler{
FileSystem: rofs.WebDAVFS(root),
LockSystem: webdav.NewMemLS(),
Logger: logRequest,
},
ErrorLog: errorLogger,
} }
return srv.ListenAndServe() return srv.ListenAndServe()

View file

@ -23,7 +23,7 @@ func (d dirEntry) Info() (fs.FileInfo, error) { return d.fileInfo, nil }
// openDir represents a directory opened for reading. // openDir represents a directory opened for reading.
type openDir struct { type openDir struct {
path string path string
fileInfo FileInfo fileInfo fileInfo
entries []fs.DirEntry entries []fs.DirEntry
offset int offset int
} }

View file

@ -5,19 +5,19 @@ import (
"time" "time"
) )
// FileInfo provides information about a file or directory. // fileInfo provides information about a file or directory.
type FileInfo struct { type fileInfo struct {
name string name string
mode fs.FileMode mode fs.FileMode
modtime time.Time modtime time.Time
size int64 size int64
} }
func (fi FileInfo) Name() string { return fi.name } func (fi fileInfo) Name() string { return fi.name }
func (fi FileInfo) IsDir() bool { return fi.mode.IsDir() } func (fi fileInfo) IsDir() bool { return fi.mode.IsDir() }
func (fi FileInfo) ModTime() time.Time { return fi.modtime } func (fi fileInfo) ModTime() time.Time { return fi.modtime }
func (fi FileInfo) Mode() fs.FileMode { return fi.mode } func (fi fileInfo) Mode() fs.FileMode { return fi.mode }
func (fi FileInfo) Size() int64 { return fi.size } func (fi fileInfo) Size() int64 { return fi.size }
func (fi FileInfo) Sys() any { return nil } func (fi fileInfo) Sys() any { return nil }
var _ fs.FileInfo = FileInfo{} var _ fs.FileInfo = fileInfo{}

View file

@ -16,7 +16,7 @@ type ROFS struct {
repo restic.Repository repo restic.Repository
cfg Config cfg Config
entries map[string]rofsEntry entries map[string]rofsEntry
fileInfo FileInfo fileInfo fileInfo
} }
type rofsEntry interface { type rofsEntry interface {
@ -50,7 +50,7 @@ func New(ctx context.Context, repo restic.Repository, cfg Config) (*ROFS, error)
repo: repo, repo: repo,
cfg: cfg, cfg: cfg,
entries: make(map[string]rofsEntry), entries: make(map[string]rofsEntry),
fileInfo: FileInfo{ fileInfo: fileInfo{
name: ".", name: ".",
mode: 0755, mode: 0755,
modtime: time.Now(), modtime: time.Now(),
@ -65,6 +65,43 @@ func New(ctx context.Context, repo restic.Repository, cfg Config) (*ROFS, error)
return rofs, nil return rofs, nil
} }
func buildSnapshotEntries(ctx context.Context, repo restic.Repository, cfg Config) (map[string]rofsEntry, error) {
var snapshots restic.Snapshots
err := cfg.Filter.FindAll(ctx, repo, repo, nil, func(_ string, sn *restic.Snapshot, _ error) error {
if sn != nil {
snapshots = append(snapshots, sn)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("filter snapshots: %w", err)
}
debug.Log("found %d snapshots", len(snapshots))
list := make(map[string]rofsEntry)
list["foo"] = NewMemFile("foo", []byte("foobar content of file foo"), time.Now())
list["snapshots"] = NewMemFile("snapshots", []byte("here goes the snapshot list"), time.Now())
// list["snapshots"] = NewSnapshotsDir(cfg.PathTemplates, cfg.TimeTemplate)
return list, nil
}
func (rofs *ROFS) updateSnapshots(ctx context.Context) error {
entries, err := buildSnapshotEntries(ctx, rofs.repo, rofs.cfg)
if err != nil {
return err
}
rofs.entries = entries
return nil
}
// Open opens the named file. // Open opens the named file.
// //
// When Open returns an error, it should be of type *PathError // When Open returns an error, it should be of type *PathError
@ -90,7 +127,7 @@ func (rofs *ROFS) Open(name string) (fs.File, error) {
d := &openDir{ d := &openDir{
path: ".", path: ".",
fileInfo: FileInfo{ fileInfo: fileInfo{
name: ".", name: ".",
mode: fs.ModeDir | 0555, mode: fs.ModeDir | 0555,
modtime: time.Now(), modtime: time.Now(),
@ -115,38 +152,3 @@ func (rofs *ROFS) Open(name string) (fs.File, error) {
return entry.Open() return entry.Open()
} }
func buildSnapshotEntries(ctx context.Context, repo restic.Repository, cfg Config) (map[string]rofsEntry, error) {
var snapshots restic.Snapshots
err := cfg.Filter.FindAll(ctx, repo, repo, nil, func(_ string, sn *restic.Snapshot, _ error) error {
if sn != nil {
snapshots = append(snapshots, sn)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("filter snapshots: %w", err)
}
debug.Log("found %d snapshots", len(snapshots))
list := make(map[string]rofsEntry)
list["snapshots"] = dirEntry{}
//FIXME CONTINUE
return list, nil
}
func (rofs *ROFS) updateSnapshots(ctx context.Context) error {
entries, err := buildSnapshotEntries(ctx, rofs.repo, rofs.cfg)
if err != nil {
return err
}
rofs.entries = entries
return nil
}

View file

@ -11,7 +11,7 @@ import (
type MemFile struct { type MemFile struct {
Path string Path string
FileInfo FileInfo FileInfo fileInfo
Data []byte Data []byte
} }
@ -20,7 +20,7 @@ func NewMemFile(filename string, data []byte, modTime time.Time) MemFile {
return MemFile{ return MemFile{
Path: filename, Path: filename,
Data: data, Data: data,
FileInfo: FileInfo{ FileInfo: fileInfo{
name: path.Base(filename), name: path.Base(filename),
size: int64(len(data)), size: int64(len(data)),
mode: 0644, mode: 0644,
@ -47,7 +47,7 @@ func (f MemFile) DirEntry() fs.DirEntry {
type openMemFile struct { type openMemFile struct {
path string path string
fileInfo FileInfo fileInfo fileInfo
data []byte data []byte
offset int64 offset int64

View file

@ -1,117 +1,134 @@
package rofs package rofs
import ( // // SnapshotsDir implements a tree of snapshots in repo as a file system in various sub-directories.
"io" // type SnapshotsDir struct {
"io/fs" // lastUpdate time.Time
"slices"
"time"
"github.com/restic/restic/internal/debug" // pathTemplates []string
) // timeTemplate string
// SnapshotsDir implements a tree of snapshots in repo as a file system in various sub-directories. // // list of top-level directories
type SnapshotsDir struct { // entries []rofsEntry
modTime time.Time // }
pathTemplates []string // // ensure that the interface is implemented
timeTemplate string // var _ rofsEntry = &SnapshotsDir{}
// prepare the list of top-level directories // // NewSnapshotsDir initializes a new top-level snapshots directory.
entries []fs.DirEntry // func NewSnapshotsDir(pathTemplates []string, timeTemplate string) *SnapshotsDir {
// dir := &SnapshotsDir{
// pathTemplates: pathTemplates,
// timeTemplate: timeTemplate,
// lastUpdate: time.Now(),
// }
// used by ReadDir() with positive number of entries to return // // testnames := []string{"foo", "bar", "baz", "snapshots"}
entriesRemaining []fs.DirEntry // // for _, name := range testnames {
} // // dir.entries = append(dir.entries,
// // fs.FileInfoToDirEntry(FileInfo{
// // name: name,
// // mode: 0644,
// // modtime: time.Now(),
// // }))
// // }
// NewSnapshotsDir initializes a new top-level snapshots directory. // // slices.SortFunc(dir.entries, func(a, b fs.DirEntry) int {
func NewSnapshotsDir(pathTemplates []string, timeTemplate string) *SnapshotsDir { // // if a.Name() == b.Name() {
dir := &SnapshotsDir{ // // return 0
pathTemplates: pathTemplates, // // }
timeTemplate: timeTemplate,
modTime: time.Now(),
}
testnames := []string{"foo", "bar", "baz", "snapshots"} // // if a.Name() < b.Name() {
for _, name := range testnames { // // return 1
dir.entries = append(dir.entries, // // }
fs.FileInfoToDirEntry(FileInfo{
name: name,
mode: 0644,
modtime: time.Now(),
}))
}
slices.SortFunc(dir.entries, func(a, b fs.DirEntry) int { // // return -1
if a.Name() == b.Name() { // // })
return 0
}
if a.Name() < b.Name() { // // // prepare for readdir with positive n
return 1 // // dir.entriesRemaining = dir.entries
}
return -1 // return dir
}) // }
// prepare for readdir with positive n // // ensure that it implements all necessary interfaces.
dir.entriesRemaining = dir.entries // var _ fs.ReadDirFile = &SnapshotsDir{}
return dir // // Close closes the snapshots dir.
} // func (dir *SnapshotsDir) Close() error {
// debug.Log("Close()")
// ensure that it implements all necessary interfaces. // // reset readdir list
var _ fs.ReadDirFile = &SnapshotsDir{} // // dir.entriesRemaining = dir.entries
// Close closes the snapshots dir. // return nil
func (dir *SnapshotsDir) Close() error { // }
debug.Log("Close()")
// reset readdir list // // Read is not implemented for a dir.
dir.entriesRemaining = dir.entries // func (dir *SnapshotsDir) Read([]byte) (int, error) {
// return 0, &fs.PathError{
// Op: "read",
// Err: fs.ErrInvalid,
// }
// }
return nil // // Stat returns information about the dir.
} // func (dir *SnapshotsDir) Stat() (fs.FileInfo, error) {
// debug.Log("Stat(root)")
// Read is not implemented for a dir. // fi := FileInfo{
func (dir *SnapshotsDir) Read([]byte) (int, error) { // name: "root", // use special name, this is the root node
return 0, &fs.PathError{ // size: 0,
Op: "read", // modtime: dir.lastUpdate,
Err: fs.ErrInvalid, // mode: 0755,
} // }
}
// Stat returns information about the dir. // return fi, nil
func (dir *SnapshotsDir) Stat() (fs.FileInfo, error) { // }
debug.Log("Stat(root)")
fi := FileInfo{ // // ReadDir returns a list of entries.
name: "root", // use special name, this is the root node // func (dir *SnapshotsDir) ReadDir(n int) ([]fs.DirEntry, error) {
size: 0, // if n < 0 {
modtime: dir.modTime, // debug.Log("Readdir(root, %v), return %v entries", n, len(dir.entries))
mode: 0755, // return dir.entries, nil
} // }
return fi, nil // // complicated pointer handling
} // if n > len(dir.entriesRemaining) {
// n = len(dir.entriesRemaining)
// }
// ReadDir returns a list of entries. // if n == 0 {
func (dir *SnapshotsDir) ReadDir(n int) ([]fs.DirEntry, error) { // return nil, io.EOF
if n < 0 { // }
debug.Log("Readdir(root, %v), return %v entries", n, len(dir.entries))
return dir.entries, nil
}
// complicated pointer handling // list := dir.entriesRemaining[:n]
if n > len(dir.entriesRemaining) { // dir.entriesRemaining = dir.entriesRemaining[n:]
n = len(dir.entriesRemaining)
}
if n == 0 { // return list, nil
return nil, io.EOF // }
}
list := dir.entriesRemaining[:n] // // DirEntry returns meta data about the dir snapshots dir itself.
dir.entriesRemaining = dir.entriesRemaining[n:] // func (dir *SnapshotsDir) DirEntry() fs.DirEntry {
// return dirEntry{
// fileInfo: FileInfo{
// name: "snapshots",
// mode: fs.ModeDir | 0755,
// modtime: dir.lastUpdate,
// },
// }
// }
return list, nil // // Open opens the dir for reading.
} // func (dir *SnapshotsDir) Open() (fs.File, error) {
// d := &openDir{
// path: "snapshots",
// fileInfo: FileInfo{
// name: "snapshots",
// mode: fs.ModeDir | 0555,
// modtime: dir.lastUpdate,
// },
// entries: dirMap2DirEntry(dir.entries),
// }
// return d
// }

View file

@ -0,0 +1,152 @@
package rofs
import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path"
"golang.org/x/net/webdav"
)
// WebDAVFS returns a file system suitable for use with the WebDAV server.
func WebDAVFS(fs *ROFS) webdav.FileSystem {
return &webDAVFS{FS: fs}
}
// webDAVFS wraps an fs.FS and returns a (read-only) filesystem suitable for use with WebDAV.
type webDAVFS struct {
fs.FS
}
// ensure that WebDAVFS can be used for webdav.
var _ webdav.FileSystem = &webDAVFS{}
func (*webDAVFS) Mkdir(_ context.Context, name string, _ fs.FileMode) error {
return &fs.PathError{
Op: "Mkdir",
Path: name,
Err: fs.ErrPermission,
}
}
func (*webDAVFS) RemoveAll(_ context.Context, name string) error {
return &fs.PathError{
Op: "RemoveAll",
Path: name,
Err: fs.ErrPermission,
}
}
func (*webDAVFS) Rename(_ context.Context, from string, to string) error {
return &fs.PathError{
Op: "Rename",
Path: from,
Err: fs.ErrPermission,
}
}
func (w *webDAVFS) Open(name string) (fs.File, error) {
// use relative paths for FS
name = path.Join(".", name)
return w.FS.Open(name)
}
func (w *webDAVFS) OpenFile(ctx context.Context, name string, flag int, perm fs.FileMode) (webdav.File, error) {
// use relative paths for FS
name = path.Join(".", name)
if flag != os.O_RDONLY {
return nil, &fs.PathError{
Op: "OpenFile",
Path: name,
Err: fs.ErrPermission,
}
}
f, err := w.FS.Open(name)
if err != nil {
return nil, err
}
readdirFile, ok := f.(fs.ReadDirFile)
if !ok {
readdirFile = nil
}
seeker, ok := f.(io.Seeker)
if !ok {
seeker = nil
}
return &readOnlyFile{File: f, readDirFile: readdirFile, Seeker: seeker}, nil
}
func (w *webDAVFS) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
// use relative paths for FS
name = path.Join(".", name)
f, err := w.FS.Open(name)
if err != nil {
return nil, err
}
fi, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, err
}
err = f.Close()
if err != nil {
return nil, err
}
return fi, nil
}
type readOnlyFile struct {
fs.File
readDirFile fs.ReadDirFile
io.Seeker
}
func (f readOnlyFile) Write([]byte) (int, error) {
return 0, fs.ErrPermission
}
func (f readOnlyFile) Seek(offset int64, whence int) (int64, error) {
if f.Seeker == nil {
return 0, fs.ErrInvalid
}
return f.Seeker.Seek(offset, whence)
}
func (f readOnlyFile) Readdir(n int) ([]fs.FileInfo, error) {
if f.readDirFile == nil {
return nil, fs.ErrInvalid
}
entries, err := f.readDirFile.ReadDir(n)
if err != nil {
return nil, err
}
result := make([]fs.FileInfo, 0, len(entries))
for _, entry := range entries {
fi, err := entry.Info()
if err != nil {
return nil, fmt.Errorf("get fileinfo: %w", err)
}
result = append(result, fi)
}
return result, nil
}