From ff7bfc534f59e0d8adf76526f6933b446da27b39 Mon Sep 17 00:00:00 2001 From: Alex Duchesne Date: Fri, 5 Apr 2024 23:05:12 -0400 Subject: [PATCH 1/4] Implemented WebDAV server to browse repositories --- cmd/restic/cmd_webdav.go | 429 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 cmd/restic/cmd_webdav.go diff --git a/cmd/restic/cmd_webdav.go b/cmd/restic/cmd_webdav.go new file mode 100644 index 000000000..7628a2be5 --- /dev/null +++ b/cmd/restic/cmd_webdav.go @@ -0,0 +1,429 @@ +package main + +import ( + "context" + "io" + "net/http" + "os" + "path" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + "golang.org/x/net/webdav" + + "github.com/restic/restic/internal/bloblru" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/walker" +) + +var cmdWebdav = &cobra.Command{ + Use: "webdav [flags] address:port", + Short: "Serve the repository via WebDAV", + Long: ` +The "webdav" command serves the repository via WebDAV. This is a +read-only mount. + +Snapshot Directories +==================== + +If you need a different template for directories that contain snapshots, +you can pass a time template via --time-template and path templates via +--path-template. + +Example time template without colons: + + --time-template "2006-01-02_15-04-05" + +You need to specify a sample format for exactly the following timestamp: + + Mon Jan 2 15:04:05 -0700 MST 2006 + +For details please see the documentation for time.Format() at: + https://godoc.org/time#Time.Format + +For path templates, you can use the following patterns which will be replaced: + %i by short snapshot ID + %I by long snapshot ID + %u by username + %h by hostname + %t by tags + %T by timestamp as specified by --time-template + +The default path templates are: + "ids/%i" + "snapshots/%T" + "hosts/%h/%T" + "tags/%t/%T" + +EXIT STATUS +=========== + +Exit status is 0 if the command was successful, and non-zero if there was any error. +`, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runWebServer(cmd.Context(), webdavOptions, globalOptions, args) + }, +} + +type WebdavOptions struct { + restic.SnapshotFilter + TimeTemplate string + PathTemplates []string +} + +var webdavOptions WebdavOptions + +func init() { + cmdRoot.AddCommand(cmdWebdav) + cmdFlags := cmdWebdav.Flags() + initMultiSnapshotFilter(cmdFlags, &webdavOptions.SnapshotFilter, true) + cmdFlags.StringArrayVar(&webdavOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)") + cmdFlags.StringVar(&webdavOptions.TimeTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs") +} + +func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, args []string) error { + + if len(args) == 0 { + return errors.Fatal("wrong number of parameters") + } + + bindAddress := args[0] + + if strings.Index(bindAddress, "http://") == 0 { + bindAddress = bindAddress[7:] + } + + // PathTemplates and TimeTemplate are ignored for now because `fuse.snapshots_dir(struct)` + // is not accessible when building on Windows and it would be ridiculous to duplicate the + // code. It should be shared, somehow. + + ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) + if err != nil { + return err + } + defer unlock() + + snapshotLister, err := restic.MemorizeList(ctx, repo, restic.SnapshotFile) + if err != nil { + return err + } + + bar := newIndexProgress(gopts.Quiet, gopts.JSON) + err = repo.LoadIndex(ctx, bar) + if err != nil { + return err + } + + davFS := &webdavFS{ + repo: repo, + root: webdavFSNode{ + name: "", + mode: 0555 | os.ModeDir, + modTime: time.Now(), + children: make(map[string]*webdavFSNode), + }, + snapshots: make(map[string]*restic.Snapshot), + blobCache: bloblru.New(64 << 20), + } + + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &webdavOptions.SnapshotFilter, args[1:]) { + snID := sn.ID().Str() + davFS.root.children[snID] = &webdavFSNode{ + name: snID, + mode: 0555 | os.ModeDir, + modTime: sn.Time, + children: make(map[string]*webdavFSNode), + } + davFS.snapshots[snID] = sn + } + + wd := &webdav.Handler{ + FileSystem: davFS, + LockSystem: webdav.NewMemLS(), + } + + Printf("Now serving the repository at http://%s\n", bindAddress) + Printf("Tree contains %d snapshots\n", len(davFS.root.children)) + Printf("When finished, quit with Ctrl-c here.\n") + + return http.ListenAndServe(bindAddress, wd) +} + +type webdavFS struct { + repo restic.Repository + root webdavFSNode + snapshots map[string]*restic.Snapshot + blobCache *bloblru.Cache +} + +// Implements os.FileInfo +type webdavFSNode struct { + name string + mode os.FileMode + modTime time.Time + size int64 + + node *restic.Node + children map[string]*webdavFSNode +} + +func (f *webdavFSNode) Name() string { return f.name } +func (f *webdavFSNode) Size() int64 { return f.size } +func (f *webdavFSNode) Mode() os.FileMode { return f.mode } +func (f *webdavFSNode) ModTime() time.Time { return f.modTime } +func (f *webdavFSNode) IsDir() bool { return f.mode.IsDir() } +func (f *webdavFSNode) Sys() interface{} { return nil } + +func mountSnapshot(ctx context.Context, fs *webdavFS, mountPoint string, sn *restic.Snapshot) { + Printf("Mounting snapshot %s at %s\n", sn.ID().Str(), mountPoint) + // FIXME: Need a mutex here... + // FIXME: All this walking should be done dynamically when the client asks for a folder... + walker.Walk(ctx, fs.repo, *sn.Tree, walker.WalkVisitor{ + ProcessNode: func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) error { + if err != nil || node == nil { + return err + } + dir, wdNode, err := fs.find(mountPoint + "/" + nodepath) + if err != nil || dir == nil { + Printf("Found a leaf before the branch was created (parent not found %s)!\n", nodepath) + return walker.ErrSkipNode + } + if wdNode != nil { + Printf("Walked through a file that already exists in the tree!!! (%s: %s)\n", node.Type, nodepath) + return walker.ErrSkipNode + } + if dir.children == nil { + dir.children = make(map[string]*webdavFSNode) + } + dir.children[node.Name] = &webdavFSNode{ + name: node.Name, + mode: node.Mode, + modTime: node.ModTime, + size: int64(node.Size), + node: node, + } + dir.size = int64(len(dir.children)) + return nil + }, + }) +} + +func (fs *webdavFS) find(fullname string) (*webdavFSNode, *webdavFSNode, error) { + fullname = strings.Trim(path.Clean("/"+fullname), "/") + if fullname == "" { + return nil, &fs.root, nil + } + + parts := strings.Split(fullname, "/") + dir := &fs.root + + for dir != nil { + part := parts[0] + parts = parts[1:] + if len(parts) == 0 { + return dir, dir.children[part], nil + } + dir = dir.children[part] + } + + return nil, nil, os.ErrNotExist +} + +func (fs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { + Printf("OpenFile %s\n", name) + + // Client can only read + if flag&(os.O_WRONLY|os.O_RDWR) != 0 { + return nil, os.ErrPermission + } + + _, node, err := fs.find(name) + if err != nil { + return nil, err + } + if node == nil { + return nil, os.ErrNotExist + } + return &openFile{node: node, fs: fs}, nil +} + +func (fs *webdavFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { + _, node, err := fs.find(name) + if err != nil { + return nil, err + } + if node == nil { + return nil, os.ErrNotExist + } + return node, nil +} + +func (fs *webdavFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error { + return os.ErrPermission +} + +func (fs *webdavFS) RemoveAll(ctx context.Context, name string) error { + return os.ErrPermission +} + +func (fs *webdavFS) Rename(ctx context.Context, oldName, newName string) error { + return os.ErrPermission +} + +type openFile struct { + node *webdavFSNode + fs *webdavFS + cursor int64 + + initialized bool + // cumsize[i] holds the cumulative size of blobs[:i]. + children []os.FileInfo + cumsize []uint64 +} + +func (f *openFile) getBlobAt(ctx context.Context, i int) (blob []byte, err error) { + blob, ok := f.fs.blobCache.Get(f.node.node.Content[i]) + if ok { + return blob, nil + } + + blob, err = f.fs.repo.LoadBlob(ctx, restic.DataBlob, f.node.node.Content[i], nil) + if err != nil { + return nil, err + } + + f.fs.blobCache.Add(f.node.node.Content[i], blob) + + return blob, nil +} + +func (f *openFile) Read(p []byte) (int, error) { + Printf("Read %s %d %d\n", f.node.name, f.cursor, len(p)) + if f.node.IsDir() || f.cursor < 0 { + return 0, os.ErrInvalid + } + if f.cursor >= f.node.Size() { + return 0, io.EOF + } + + // We wait until the first read before we do anything because WebDAV clients tend to open + // everything and do nothing... + if !f.initialized { + var bytes uint64 + cumsize := make([]uint64, 1+len(f.node.node.Content)) + for i, id := range f.node.node.Content { + size, found := f.fs.repo.LookupBlobSize(id, restic.DataBlob) + if !found { + return 0, errors.Errorf("id %v not found in repository", id) + } + bytes += uint64(size) + cumsize[i+1] = bytes + } + if bytes != f.node.node.Size { + Printf("sizes do not match: node.Size %d != size %d", bytes, f.node.Size()) + } + f.cumsize = cumsize + f.initialized = true + } + + offset := uint64(f.cursor) + remainingBytes := uint64(len(p)) + readBytes := 0 + + if offset+remainingBytes > uint64(f.node.Size()) { + remainingBytes = uint64(f.node.Size()) - remainingBytes + } + + // Skip blobs before the offset + startContent := -1 + sort.Search(len(f.cumsize), func(i int) bool { + return f.cumsize[i] > offset + }) + offset -= f.cumsize[startContent] + + for i := startContent; remainingBytes > 0 && i < len(f.cumsize)-1; i++ { + blob, err := f.getBlobAt(context.TODO(), i) + if err != nil { + return 0, err + } + + if offset > 0 { + blob = blob[offset:] + offset = 0 + } + + copied := copy(p, blob) + remainingBytes -= uint64(copied) + readBytes += copied + + p = p[copied:] + } + + f.cursor += int64(readBytes) + return readBytes, nil +} + +func (f *openFile) Readdir(count int) ([]os.FileInfo, error) { + Printf("Readdir %s %d %d\n", f.node.name, f.cursor, count) + if !f.node.IsDir() || f.cursor < 0 { + return nil, os.ErrInvalid + } + + // We wait until the first read before we do anything because WebDAV clients tend to open + // everything and do nothing... + if !f.initialized { + // It's a snapshot, mount it + if f.node != &f.fs.root && f.node.node == nil && len(f.children) == 0 { + mountSnapshot(context.TODO(), f.fs, "/"+f.node.name, f.fs.snapshots[f.node.name]) + } + children := make([]os.FileInfo, 0, len(f.node.children)) + for _, c := range f.node.children { + children = append(children, c) + } + f.children = children + f.initialized = true + } + + if count <= 0 { + return f.children, nil + } + if f.cursor >= f.node.Size() { + return nil, io.EOF + } + start := f.cursor + f.cursor += int64(count) + if f.cursor > f.node.Size() { + f.cursor = f.node.Size() + } + return f.children[start:f.cursor], nil +} + +func (f *openFile) Seek(offset int64, whence int) (int64, error) { + Printf("Seek %s %d %d\n", f.node.name, offset, whence) + switch whence { + case io.SeekStart: + f.cursor = offset + case io.SeekCurrent: + f.cursor += offset + case io.SeekEnd: + f.cursor = f.node.Size() - offset + default: + return 0, os.ErrInvalid + } + return f.cursor, nil +} + +func (f *openFile) Stat() (os.FileInfo, error) { + return f.node, nil +} + +func (f *openFile) Write(p []byte) (int, error) { + return 0, os.ErrPermission +} + +func (f *openFile) Close() error { + return nil +} From 7782b7c38ebaf0a24e83b9b64297df189d150d42 Mon Sep 17 00:00:00 2001 From: Alex Duchesne Date: Sat, 6 Apr 2024 10:23:59 -0400 Subject: [PATCH 2/4] webdav: open Windows Explorer when starting --- cmd/restic/cmd_webdav.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cmd/restic/cmd_webdav.go b/cmd/restic/cmd_webdav.go index 7628a2be5..bca6858f9 100644 --- a/cmd/restic/cmd_webdav.go +++ b/cmd/restic/cmd_webdav.go @@ -5,7 +5,9 @@ import ( "io" "net/http" "os" + "os/exec" "path" + "runtime" "sort" "strings" "time" @@ -87,20 +89,21 @@ func init() { func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, args []string) error { + // PathTemplates and TimeTemplate are ignored for now because `fuse.snapshots_dir(struct)` + // is not accessible when building on Windows and it would be ridiculous to duplicate the + // code. It should be shared, somehow. + if len(args) == 0 { return errors.Fatal("wrong number of parameters") } bindAddress := args[0] + // FIXME: Proper validation, also check for IPv6 if strings.Index(bindAddress, "http://") == 0 { bindAddress = bindAddress[7:] } - // PathTemplates and TimeTemplate are ignored for now because `fuse.snapshots_dir(struct)` - // is not accessible when building on Windows and it would be ridiculous to duplicate the - // code. It should be shared, somehow. - ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock) if err != nil { return err @@ -150,6 +153,12 @@ func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, Printf("Tree contains %d snapshots\n", len(davFS.root.children)) Printf("When finished, quit with Ctrl-c here.\n") + // FIXME: Remove before PR, this is handy for testing but likely undesirable :) + if runtime.GOOS == "windows" { + browseURL := "\\\\" + strings.Replace(bindAddress, ":", "@", 1) + "\\DavWWWRoot" + exec.Command("explorer", browseURL).Start() + } + return http.ListenAndServe(bindAddress, wd) } From 0cb3ef1d92fc3881ebad7af263973406373345ab Mon Sep 17 00:00:00 2001 From: Alex Duchesne Date: Sat, 6 Apr 2024 10:24:17 -0400 Subject: [PATCH 3/4] webdav: added some structure similar to `mount` --- cmd/restic/cmd_webdav.go | 158 ++++++++++++++++++++++++--------------- 1 file changed, 98 insertions(+), 60 deletions(-) diff --git a/cmd/restic/cmd_webdav.go b/cmd/restic/cmd_webdav.go index bca6858f9..edfa3320b 100644 --- a/cmd/restic/cmd_webdav.go +++ b/cmd/restic/cmd_webdav.go @@ -22,7 +22,7 @@ import ( ) var cmdWebdav = &cobra.Command{ - Use: "webdav [flags] address:port", + Use: "webdav [flags] [ip:port]", Short: "Serve the repository via WebDAV", Long: ` The "webdav" command serves the repository via WebDAV. This is a @@ -84,7 +84,7 @@ func init() { cmdFlags := cmdWebdav.Flags() initMultiSnapshotFilter(cmdFlags, &webdavOptions.SnapshotFilter, true) cmdFlags.StringArrayVar(&webdavOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)") - cmdFlags.StringVar(&webdavOptions.TimeTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs") + cmdFlags.StringVar(&webdavOptions.TimeTemplate, "snapshot-template", "2006-01-02_15-04-05", "set `template` to use for snapshot dirs") } func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, args []string) error { @@ -93,13 +93,16 @@ func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, // is not accessible when building on Windows and it would be ridiculous to duplicate the // code. It should be shared, somehow. - if len(args) == 0 { + if len(args) > 1 { return errors.Fatal("wrong number of parameters") } - bindAddress := args[0] + // FIXME: Proper validation, also add support for IPv6 + bindAddress := "127.0.0.1:3080" + if len(args) == 1 { + bindAddress = strings.ToLower(args[0]) + } - // FIXME: Proper validation, also check for IPv6 if strings.Index(bindAddress, "http://") == 0 { bindAddress = bindAddress[7:] } @@ -129,26 +132,30 @@ func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, modTime: time.Now(), children: make(map[string]*webdavFSNode), }, - snapshots: make(map[string]*restic.Snapshot), blobCache: bloblru.New(64 << 20), } - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &webdavOptions.SnapshotFilter, args[1:]) { - snID := sn.ID().Str() - davFS.root.children[snID] = &webdavFSNode{ - name: snID, - mode: 0555 | os.ModeDir, - modTime: sn.Time, - children: make(map[string]*webdavFSNode), - } - davFS.snapshots[snID] = sn - } - wd := &webdav.Handler{ FileSystem: davFS, LockSystem: webdav.NewMemLS(), } + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &webdavOptions.SnapshotFilter, nil) { + node := &webdavFSNode{ + name: sn.ID().Str(), + mode: 0555 | os.ModeDir, + modTime: sn.Time, + children: nil, + snapshot: sn, + } + davFS.addNode("/ids/"+node.name, node) + davFS.addNode("/hosts/"+sn.Hostname+"/"+node.name, node) + davFS.addNode("/snapshots/"+sn.Time.Format(opts.TimeTemplate)+"/"+node.name, node) + for _, tag := range sn.Tags { + davFS.addNode("/tags/"+tag+"/"+node.name, node) + } + } + Printf("Now serving the repository at http://%s\n", bindAddress) Printf("Tree contains %d snapshots\n", len(davFS.root.children)) Printf("When finished, quit with Ctrl-c here.\n") @@ -163,21 +170,23 @@ func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, } type webdavFS struct { - repo restic.Repository - root webdavFSNode - snapshots map[string]*restic.Snapshot + repo restic.Repository + root webdavFSNode + // snapshots *restic.Snapshot blobCache *bloblru.Cache } // Implements os.FileInfo type webdavFSNode struct { - name string - mode os.FileMode - modTime time.Time - size int64 - - node *restic.Node + name string + mode os.FileMode + modTime time.Time + size int64 children map[string]*webdavFSNode + + // Should be an interface to save on memory? + node *restic.Node + snapshot *restic.Snapshot } func (f *webdavFSNode) Name() string { return f.name } @@ -187,8 +196,8 @@ func (f *webdavFSNode) ModTime() time.Time { return f.modTime } func (f *webdavFSNode) IsDir() bool { return f.mode.IsDir() } func (f *webdavFSNode) Sys() interface{} { return nil } -func mountSnapshot(ctx context.Context, fs *webdavFS, mountPoint string, sn *restic.Snapshot) { - Printf("Mounting snapshot %s at %s\n", sn.ID().Str(), mountPoint) +func (fs *webdavFS) loadSnapshot(ctx context.Context, mountPoint string, sn *restic.Snapshot) { + Printf("Loading snapshot %s at %s\n", sn.ID().Str(), mountPoint) // FIXME: Need a mutex here... // FIXME: All this walking should be done dynamically when the client asks for a folder... walker.Walk(ctx, fs.repo, *sn.Tree, walker.WalkVisitor{ @@ -196,50 +205,75 @@ func mountSnapshot(ctx context.Context, fs *webdavFS, mountPoint string, sn *res if err != nil || node == nil { return err } - dir, wdNode, err := fs.find(mountPoint + "/" + nodepath) - if err != nil || dir == nil { - Printf("Found a leaf before the branch was created (parent not found %s)!\n", nodepath) - return walker.ErrSkipNode - } - if wdNode != nil { - Printf("Walked through a file that already exists in the tree!!! (%s: %s)\n", node.Type, nodepath) - return walker.ErrSkipNode - } - if dir.children == nil { - dir.children = make(map[string]*webdavFSNode) - } - dir.children[node.Name] = &webdavFSNode{ + fs.addNode(mountPoint+"/"+nodepath, &webdavFSNode{ name: node.Name, mode: node.Mode, modTime: node.ModTime, size: int64(node.Size), node: node, - } - dir.size = int64(len(dir.children)) + // snapshot: sn, + }) return nil }, }) } -func (fs *webdavFS) find(fullname string) (*webdavFSNode, *webdavFSNode, error) { +func (fs *webdavFS) addNode(fullpath string, node *webdavFSNode) error { + fullpath = strings.Trim(path.Clean("/"+fullpath), "/") + if fullpath == "" { + return os.ErrInvalid + } + + parts := strings.Split(fullpath, "/") + dir := &fs.root + + for len(parts) > 0 { + part := parts[0] + parts = parts[1:] + if !dir.IsDir() { + return os.ErrInvalid + } + if dir.children == nil { + dir.children = make(map[string]*webdavFSNode) + } + if len(parts) == 0 { + dir.children[part] = node + dir.size = int64(len(dir.children)) + return nil + } + if dir.children[part] == nil { + dir.children[part] = &webdavFSNode{ + name: part, + mode: 0555 | os.ModeDir, + modTime: dir.modTime, + children: nil, + } + } + dir = dir.children[part] + } + + return os.ErrInvalid +} + +func (fs *webdavFS) findNode(fullname string) (*webdavFSNode, error) { fullname = strings.Trim(path.Clean("/"+fullname), "/") if fullname == "" { - return nil, &fs.root, nil + return &fs.root, nil } parts := strings.Split(fullname, "/") dir := &fs.root for dir != nil { - part := parts[0] + node := dir.children[parts[0]] parts = parts[1:] if len(parts) == 0 { - return dir, dir.children[part], nil + return node, nil } - dir = dir.children[part] + dir = node } - return nil, nil, os.ErrNotExist + return nil, os.ErrNotExist } func (fs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { @@ -250,18 +284,21 @@ func (fs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os return nil, os.ErrPermission } - _, node, err := fs.find(name) + node, err := fs.findNode(name) + if err == os.ErrNotExist { + // FIXME: Walk up the tree to make sure the snapshot (if any) is loaded + } if err != nil { return nil, err } if node == nil { return nil, os.ErrNotExist } - return &openFile{node: node, fs: fs}, nil + return &openFile{fullpath: path.Clean("/" + name), node: node, fs: fs}, nil } func (fs *webdavFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { - _, node, err := fs.find(name) + node, err := fs.findNode(name) if err != nil { return nil, err } @@ -284,9 +321,10 @@ func (fs *webdavFS) Rename(ctx context.Context, oldName, newName string) error { } type openFile struct { - node *webdavFSNode - fs *webdavFS - cursor int64 + fullpath string + node *webdavFSNode + fs *webdavFS + cursor int64 initialized bool // cumsize[i] holds the cumulative size of blobs[:i]. @@ -311,7 +349,7 @@ func (f *openFile) getBlobAt(ctx context.Context, i int) (blob []byte, err error } func (f *openFile) Read(p []byte) (int, error) { - Printf("Read %s %d %d\n", f.node.name, f.cursor, len(p)) + Printf("Read %s %d %d\n", f.fullpath, f.cursor, len(p)) if f.node.IsDir() || f.cursor < 0 { return 0, os.ErrInvalid } @@ -376,7 +414,7 @@ func (f *openFile) Read(p []byte) (int, error) { } func (f *openFile) Readdir(count int) ([]os.FileInfo, error) { - Printf("Readdir %s %d %d\n", f.node.name, f.cursor, count) + Printf("Readdir %s %d %d\n", f.fullpath, f.cursor, count) if !f.node.IsDir() || f.cursor < 0 { return nil, os.ErrInvalid } @@ -385,8 +423,8 @@ func (f *openFile) Readdir(count int) ([]os.FileInfo, error) { // everything and do nothing... if !f.initialized { // It's a snapshot, mount it - if f.node != &f.fs.root && f.node.node == nil && len(f.children) == 0 { - mountSnapshot(context.TODO(), f.fs, "/"+f.node.name, f.fs.snapshots[f.node.name]) + if f.node.snapshot != nil && f.node.children == nil { + f.fs.loadSnapshot(context.TODO(), f.fullpath, f.node.snapshot) } children := make([]os.FileInfo, 0, len(f.node.children)) for _, c := range f.node.children { @@ -411,7 +449,7 @@ func (f *openFile) Readdir(count int) ([]os.FileInfo, error) { } func (f *openFile) Seek(offset int64, whence int) (int64, error) { - Printf("Seek %s %d %d\n", f.node.name, offset, whence) + Printf("Seek %s %d %d\n", f.fullpath, offset, whence) switch whence { case io.SeekStart: f.cursor = offset From 166e94d82e6ca8d137206ec6cfe28f50a21f15c6 Mon Sep 17 00:00:00 2001 From: Alex Duchesne Date: Sun, 7 Apr 2024 17:32:30 -0400 Subject: [PATCH 4/4] webdav: removed unnecessary error checks --- cmd/restic/cmd_webdav.go | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/cmd/restic/cmd_webdav.go b/cmd/restic/cmd_webdav.go index edfa3320b..96866d1e1 100644 --- a/cmd/restic/cmd_webdav.go +++ b/cmd/restic/cmd_webdav.go @@ -16,6 +16,7 @@ import ( "golang.org/x/net/webdav" "github.com/restic/restic/internal/bloblru" + "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/walker" @@ -88,11 +89,6 @@ func init() { } func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, args []string) error { - - // PathTemplates and TimeTemplate are ignored for now because `fuse.snapshots_dir(struct)` - // is not accessible when building on Windows and it would be ridiculous to duplicate the - // code. It should be shared, somehow. - if len(args) > 1 { return errors.Fatal("wrong number of parameters") } @@ -148,6 +144,8 @@ func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, children: nil, snapshot: sn, } + // Ignore PathTemplates for now because `fuse.snapshots_dir(struct)` is not accessible when building + // on Windows and it would be ridiculous to duplicate the code. It should be shared, somehow! davFS.addNode("/ids/"+node.name, node) davFS.addNode("/hosts/"+sn.Hostname+"/"+node.name, node) davFS.addNode("/snapshots/"+sn.Time.Format(opts.TimeTemplate)+"/"+node.name, node) @@ -169,6 +167,7 @@ func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions, return http.ListenAndServe(bindAddress, wd) } +// Implements webdav.FileSystem type webdavFS struct { repo restic.Repository root webdavFSNode @@ -277,7 +276,7 @@ func (fs *webdavFS) findNode(fullname string) (*webdavFSNode, error) { } func (fs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { - Printf("OpenFile %s\n", name) + debug.Log("OpenFile %s", name) // Client can only read if flag&(os.O_WRONLY|os.O_RDWR) != 0 { @@ -291,9 +290,6 @@ func (fs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os if err != nil { return nil, err } - if node == nil { - return nil, os.ErrNotExist - } return &openFile{fullpath: path.Clean("/" + name), node: node, fs: fs}, nil } @@ -302,9 +298,6 @@ func (fs *webdavFS) Stat(ctx context.Context, name string) (os.FileInfo, error) if err != nil { return nil, err } - if node == nil { - return nil, os.ErrNotExist - } return node, nil } @@ -325,11 +318,11 @@ type openFile struct { node *webdavFSNode fs *webdavFS cursor int64 + children []os.FileInfo + // cumsize[i] holds the cumulative size of blobs[:i]. + cumsize []uint64 initialized bool - // cumsize[i] holds the cumulative size of blobs[:i]. - children []os.FileInfo - cumsize []uint64 } func (f *openFile) getBlobAt(ctx context.Context, i int) (blob []byte, err error) { @@ -349,7 +342,7 @@ func (f *openFile) getBlobAt(ctx context.Context, i int) (blob []byte, err error } func (f *openFile) Read(p []byte) (int, error) { - Printf("Read %s %d %d\n", f.fullpath, f.cursor, len(p)) + debug.Log("Read %s %d %d", f.fullpath, f.cursor, len(p)) if f.node.IsDir() || f.cursor < 0 { return 0, os.ErrInvalid } @@ -414,7 +407,7 @@ func (f *openFile) Read(p []byte) (int, error) { } func (f *openFile) Readdir(count int) ([]os.FileInfo, error) { - Printf("Readdir %s %d %d\n", f.fullpath, f.cursor, count) + debug.Log("Readdir %s %d %d", f.fullpath, f.cursor, count) if !f.node.IsDir() || f.cursor < 0 { return nil, os.ErrInvalid } @@ -449,7 +442,7 @@ func (f *openFile) Readdir(count int) ([]os.FileInfo, error) { } func (f *openFile) Seek(offset int64, whence int) (int64, error) { - Printf("Seek %s %d %d\n", f.fullpath, offset, whence) + debug.Log("Seek %s %d %d", f.fullpath, offset, whence) switch whence { case io.SeekStart: f.cursor = offset