diff --git a/cmd/restic/cmd_webdav.go b/cmd/restic/cmd_webdav.go index 0d52b19e9..423fa58df 100644 --- a/cmd/restic/cmd_webdav.go +++ b/cmd/restic/cmd_webdav.go @@ -8,6 +8,7 @@ import ( "time" "github.com/spf13/cobra" + "golang.org/x/net/webdav" "github.com/restic/restic/internal/errors" "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{ ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, Addr: opts.Listen, - Handler: http.FileServer(http.FS(root)), - ErrorLog: errorLogger, + // Handler: http.FileServer(http.FS(root)), + Handler: &webdav.Handler{ + FileSystem: rofs.WebDAVFS(root), + LockSystem: webdav.NewMemLS(), + Logger: logRequest, + }, + ErrorLog: errorLogger, } return srv.ListenAndServe() diff --git a/internal/server/rofs/dir.go b/internal/server/rofs/dir.go index 2e504aa56..2d0ac4b67 100644 --- a/internal/server/rofs/dir.go +++ b/internal/server/rofs/dir.go @@ -23,7 +23,7 @@ func (d dirEntry) Info() (fs.FileInfo, error) { return d.fileInfo, nil } // openDir represents a directory opened for reading. type openDir struct { path string - fileInfo FileInfo + fileInfo fileInfo entries []fs.DirEntry offset int } diff --git a/internal/server/rofs/file_info.go b/internal/server/rofs/file_info.go index 963756506..dc181edd4 100644 --- a/internal/server/rofs/file_info.go +++ b/internal/server/rofs/file_info.go @@ -5,19 +5,19 @@ import ( "time" ) -// FileInfo provides information about a file or directory. -type FileInfo struct { +// fileInfo provides information about a file or directory. +type fileInfo struct { name string mode fs.FileMode modtime time.Time size int64 } -func (fi FileInfo) Name() string { return fi.name } -func (fi FileInfo) IsDir() bool { return fi.mode.IsDir() } -func (fi FileInfo) ModTime() time.Time { return fi.modtime } -func (fi FileInfo) Mode() fs.FileMode { return fi.mode } -func (fi FileInfo) Size() int64 { return fi.size } -func (fi FileInfo) Sys() any { return nil } +func (fi fileInfo) Name() string { return fi.name } +func (fi fileInfo) IsDir() bool { return fi.mode.IsDir() } +func (fi fileInfo) ModTime() time.Time { return fi.modtime } +func (fi fileInfo) Mode() fs.FileMode { return fi.mode } +func (fi fileInfo) Size() int64 { return fi.size } +func (fi fileInfo) Sys() any { return nil } -var _ fs.FileInfo = FileInfo{} +var _ fs.FileInfo = fileInfo{} diff --git a/internal/server/rofs/fs.go b/internal/server/rofs/fs.go index bb97c12e2..a1133b998 100644 --- a/internal/server/rofs/fs.go +++ b/internal/server/rofs/fs.go @@ -16,7 +16,7 @@ type ROFS struct { repo restic.Repository cfg Config entries map[string]rofsEntry - fileInfo FileInfo + fileInfo fileInfo } type rofsEntry interface { @@ -50,7 +50,7 @@ func New(ctx context.Context, repo restic.Repository, cfg Config) (*ROFS, error) repo: repo, cfg: cfg, entries: make(map[string]rofsEntry), - fileInfo: FileInfo{ + fileInfo: fileInfo{ name: ".", mode: 0755, modtime: time.Now(), @@ -65,6 +65,43 @@ func New(ctx context.Context, repo restic.Repository, cfg Config) (*ROFS, error) 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. // // 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{ path: ".", - fileInfo: FileInfo{ + fileInfo: fileInfo{ name: ".", mode: fs.ModeDir | 0555, modtime: time.Now(), @@ -115,38 +152,3 @@ func (rofs *ROFS) Open(name string) (fs.File, error) { 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 -} diff --git a/internal/server/rofs/mem_file.go b/internal/server/rofs/mem_file.go index 0bb98ffbe..f12e93cf0 100644 --- a/internal/server/rofs/mem_file.go +++ b/internal/server/rofs/mem_file.go @@ -11,7 +11,7 @@ import ( type MemFile struct { Path string - FileInfo FileInfo + FileInfo fileInfo Data []byte } @@ -20,7 +20,7 @@ func NewMemFile(filename string, data []byte, modTime time.Time) MemFile { return MemFile{ Path: filename, Data: data, - FileInfo: FileInfo{ + FileInfo: fileInfo{ name: path.Base(filename), size: int64(len(data)), mode: 0644, @@ -47,7 +47,7 @@ func (f MemFile) DirEntry() fs.DirEntry { type openMemFile struct { path string - fileInfo FileInfo + fileInfo fileInfo data []byte offset int64 diff --git a/internal/server/rofs/snapshots_dir.go b/internal/server/rofs/snapshots_dir.go index da5f5d9fb..30cd134ee 100644 --- a/internal/server/rofs/snapshots_dir.go +++ b/internal/server/rofs/snapshots_dir.go @@ -1,117 +1,134 @@ package rofs -import ( - "io" - "io/fs" - "slices" - "time" +// // SnapshotsDir implements a tree of snapshots in repo as a file system in various sub-directories. +// type SnapshotsDir struct { +// lastUpdate time.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. -type SnapshotsDir struct { - modTime time.Time +// // list of top-level directories +// entries []rofsEntry +// } - pathTemplates []string - timeTemplate string +// // ensure that the interface is implemented +// var _ rofsEntry = &SnapshotsDir{} - // prepare the list of top-level directories - entries []fs.DirEntry +// // NewSnapshotsDir initializes a new top-level snapshots directory. +// 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 - entriesRemaining []fs.DirEntry -} +// // testnames := []string{"foo", "bar", "baz", "snapshots"} +// // 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. -func NewSnapshotsDir(pathTemplates []string, timeTemplate string) *SnapshotsDir { - dir := &SnapshotsDir{ - pathTemplates: pathTemplates, - timeTemplate: timeTemplate, - modTime: time.Now(), - } +// // slices.SortFunc(dir.entries, func(a, b fs.DirEntry) int { +// // if a.Name() == b.Name() { +// // return 0 +// // } - testnames := []string{"foo", "bar", "baz", "snapshots"} - for _, name := range testnames { - dir.entries = append(dir.entries, - fs.FileInfoToDirEntry(FileInfo{ - name: name, - mode: 0644, - modtime: time.Now(), - })) - } +// // if a.Name() < b.Name() { +// // return 1 +// // } - slices.SortFunc(dir.entries, func(a, b fs.DirEntry) int { - if a.Name() == b.Name() { - return 0 - } +// // return -1 +// // }) - if a.Name() < b.Name() { - return 1 - } +// // // prepare for readdir with positive n +// // dir.entriesRemaining = dir.entries - return -1 - }) +// return dir +// } - // prepare for readdir with positive n - dir.entriesRemaining = dir.entries +// // ensure that it implements all necessary interfaces. +// 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. -var _ fs.ReadDirFile = &SnapshotsDir{} +// // reset readdir list +// // dir.entriesRemaining = dir.entries -// Close closes the snapshots dir. -func (dir *SnapshotsDir) Close() error { - debug.Log("Close()") +// return nil +// } - // reset readdir list - dir.entriesRemaining = dir.entries +// // Read is not implemented for a dir. +// 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. -func (dir *SnapshotsDir) Read([]byte) (int, error) { - return 0, &fs.PathError{ - Op: "read", - Err: fs.ErrInvalid, - } -} +// fi := FileInfo{ +// name: "root", // use special name, this is the root node +// size: 0, +// modtime: dir.lastUpdate, +// mode: 0755, +// } -// Stat returns information about the dir. -func (dir *SnapshotsDir) Stat() (fs.FileInfo, error) { - debug.Log("Stat(root)") +// return fi, nil +// } - fi := FileInfo{ - name: "root", // use special name, this is the root node - size: 0, - modtime: dir.modTime, - mode: 0755, - } +// // ReadDir returns a list of entries. +// func (dir *SnapshotsDir) ReadDir(n int) ([]fs.DirEntry, error) { +// if n < 0 { +// debug.Log("Readdir(root, %v), return %v entries", n, len(dir.entries)) +// 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. -func (dir *SnapshotsDir) ReadDir(n int) ([]fs.DirEntry, error) { - if n < 0 { - debug.Log("Readdir(root, %v), return %v entries", n, len(dir.entries)) - return dir.entries, nil - } +// if n == 0 { +// return nil, io.EOF +// } - // complicated pointer handling - if n > len(dir.entriesRemaining) { - n = len(dir.entriesRemaining) - } +// list := dir.entriesRemaining[:n] +// dir.entriesRemaining = dir.entriesRemaining[n:] - if n == 0 { - return nil, io.EOF - } +// return list, nil +// } - list := dir.entriesRemaining[:n] - dir.entriesRemaining = dir.entriesRemaining[n:] +// // DirEntry returns meta data about the dir snapshots dir itself. +// 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 +// } diff --git a/internal/server/rofs/webdav_fs.go b/internal/server/rofs/webdav_fs.go new file mode 100644 index 000000000..fd9902768 --- /dev/null +++ b/internal/server/rofs/webdav_fs.go @@ -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 +}