webdav: added some structure similar to `mount`

This commit is contained in:
Alex Duchesne 2024-04-06 10:24:17 -04:00
parent 7782b7c38e
commit 0cb3ef1d92
1 changed files with 98 additions and 60 deletions

View File

@ -22,7 +22,7 @@ import (
) )
var cmdWebdav = &cobra.Command{ var cmdWebdav = &cobra.Command{
Use: "webdav [flags] address:port", Use: "webdav [flags] [ip:port]",
Short: "Serve the repository via WebDAV", Short: "Serve the repository via WebDAV",
Long: ` Long: `
The "webdav" command serves the repository via WebDAV. This is a The "webdav" command serves the repository via WebDAV. This is a
@ -84,7 +84,7 @@ func init() {
cmdFlags := cmdWebdav.Flags() cmdFlags := cmdWebdav.Flags()
initMultiSnapshotFilter(cmdFlags, &webdavOptions.SnapshotFilter, true) initMultiSnapshotFilter(cmdFlags, &webdavOptions.SnapshotFilter, true)
cmdFlags.StringArrayVar(&webdavOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)") 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 { 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 // is not accessible when building on Windows and it would be ridiculous to duplicate the
// code. It should be shared, somehow. // code. It should be shared, somehow.
if len(args) == 0 { if len(args) > 1 {
return errors.Fatal("wrong number of parameters") 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 { if strings.Index(bindAddress, "http://") == 0 {
bindAddress = bindAddress[7:] bindAddress = bindAddress[7:]
} }
@ -129,26 +132,30 @@ func runWebServer(ctx context.Context, opts WebdavOptions, gopts GlobalOptions,
modTime: time.Now(), modTime: time.Now(),
children: make(map[string]*webdavFSNode), children: make(map[string]*webdavFSNode),
}, },
snapshots: make(map[string]*restic.Snapshot),
blobCache: bloblru.New(64 << 20), 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{ wd := &webdav.Handler{
FileSystem: davFS, FileSystem: davFS,
LockSystem: webdav.NewMemLS(), 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("Now serving the repository at http://%s\n", bindAddress)
Printf("Tree contains %d snapshots\n", len(davFS.root.children)) Printf("Tree contains %d snapshots\n", len(davFS.root.children))
Printf("When finished, quit with Ctrl-c here.\n") 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 { type webdavFS struct {
repo restic.Repository repo restic.Repository
root webdavFSNode root webdavFSNode
snapshots map[string]*restic.Snapshot // snapshots *restic.Snapshot
blobCache *bloblru.Cache blobCache *bloblru.Cache
} }
// Implements os.FileInfo // Implements os.FileInfo
type webdavFSNode struct { type webdavFSNode struct {
name string name string
mode os.FileMode mode os.FileMode
modTime time.Time modTime time.Time
size int64 size int64
node *restic.Node
children map[string]*webdavFSNode 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 } 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) IsDir() bool { return f.mode.IsDir() }
func (f *webdavFSNode) Sys() interface{} { return nil } func (f *webdavFSNode) Sys() interface{} { return nil }
func mountSnapshot(ctx context.Context, fs *webdavFS, mountPoint string, sn *restic.Snapshot) { func (fs *webdavFS) loadSnapshot(ctx context.Context, mountPoint string, sn *restic.Snapshot) {
Printf("Mounting snapshot %s at %s\n", sn.ID().Str(), mountPoint) Printf("Loading snapshot %s at %s\n", sn.ID().Str(), mountPoint)
// FIXME: Need a mutex here... // FIXME: Need a mutex here...
// FIXME: All this walking should be done dynamically when the client asks for a folder... // FIXME: All this walking should be done dynamically when the client asks for a folder...
walker.Walk(ctx, fs.repo, *sn.Tree, walker.WalkVisitor{ 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 { if err != nil || node == nil {
return err return err
} }
dir, wdNode, err := fs.find(mountPoint + "/" + nodepath) fs.addNode(mountPoint+"/"+nodepath, &webdavFSNode{
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, name: node.Name,
mode: node.Mode, mode: node.Mode,
modTime: node.ModTime, modTime: node.ModTime,
size: int64(node.Size), size: int64(node.Size),
node: node, node: node,
} // snapshot: sn,
dir.size = int64(len(dir.children)) })
return nil 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), "/") fullname = strings.Trim(path.Clean("/"+fullname), "/")
if fullname == "" { if fullname == "" {
return nil, &fs.root, nil return &fs.root, nil
} }
parts := strings.Split(fullname, "/") parts := strings.Split(fullname, "/")
dir := &fs.root dir := &fs.root
for dir != nil { for dir != nil {
part := parts[0] node := dir.children[parts[0]]
parts = parts[1:] parts = parts[1:]
if len(parts) == 0 { 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) { 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 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 { if err != nil {
return nil, err return nil, err
} }
if node == nil { if node == nil {
return nil, os.ErrNotExist 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -284,9 +321,10 @@ func (fs *webdavFS) Rename(ctx context.Context, oldName, newName string) error {
} }
type openFile struct { type openFile struct {
node *webdavFSNode fullpath string
fs *webdavFS node *webdavFSNode
cursor int64 fs *webdavFS
cursor int64
initialized bool initialized bool
// cumsize[i] holds the cumulative size of blobs[:i]. // 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) { 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 { if f.node.IsDir() || f.cursor < 0 {
return 0, os.ErrInvalid 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) { 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 { if !f.node.IsDir() || f.cursor < 0 {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
@ -385,8 +423,8 @@ func (f *openFile) Readdir(count int) ([]os.FileInfo, error) {
// everything and do nothing... // everything and do nothing...
if !f.initialized { if !f.initialized {
// It's a snapshot, mount it // It's a snapshot, mount it
if f.node != &f.fs.root && f.node.node == nil && len(f.children) == 0 { if f.node.snapshot != nil && f.node.children == nil {
mountSnapshot(context.TODO(), f.fs, "/"+f.node.name, f.fs.snapshots[f.node.name]) f.fs.loadSnapshot(context.TODO(), f.fullpath, f.node.snapshot)
} }
children := make([]os.FileInfo, 0, len(f.node.children)) children := make([]os.FileInfo, 0, len(f.node.children))
for _, c := range 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) { 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 { switch whence {
case io.SeekStart: case io.SeekStart:
f.cursor = offset f.cursor = offset