Add webdav using the fuse implementation

This commit is contained in:
Alexander Weiss 2020-07-31 22:55:57 +02:00 committed by Alexander Neumann
parent 6fbb470835
commit 1de9b82850
7 changed files with 539 additions and 0 deletions

98
cmd/restic/cmd_webdav.go Normal file
View file

@ -0,0 +1,98 @@
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/spf13/cobra"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fuse"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/webdav"
)
var cmdWebDAV = &cobra.Command{
Use: "webdav [flags]",
Short: "runs a WebDAV server for the repository",
Long: `
The webdav command runs a WebDAV server for the reposiotry that you can then access via a WebDAV client.
`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runWebDAV(webdavOptions, globalOptions, args)
},
}
// WebDAVOptions collects all options for the webdav command.
type WebDAVOptions struct {
Listen string
Hosts []string
Tags restic.TagLists
Paths []string
SnapshotTemplate string
}
var webdavOptions WebDAVOptions
func init() {
cmdRoot.AddCommand(cmdWebDAV)
webdavFlags := cmdWebDAV.Flags()
webdavFlags.StringVarP(&webdavOptions.Listen, "listen", "l", "localhost:3080", "set the listen host name and `address`")
webdavFlags.StringArrayVarP(&webdavOptions.Hosts, "host", "H", nil, `only consider snapshots for this host (can be specified multiple times)`)
webdavFlags.Var(&webdavOptions.Tags, "tag", "only consider snapshots which include this `taglist`")
webdavFlags.StringArrayVar(&webdavOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`")
webdavFlags.StringVar(&webdavOptions.SnapshotTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs")
}
func runWebDAV(opts WebDAVOptions, gopts GlobalOptions, args []string) error {
if len(args) > 0 {
return errors.Fatal("this command does not accept additional arguments")
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
lock, err := lockRepo(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
err = repo.LoadIndex(gopts.ctx)
if err != nil {
return err
}
errorLogger := log.New(os.Stderr, "error log: ", log.Flags())
cfg := fuse.Config{
Hosts: opts.Hosts,
Tags: opts.Tags,
Paths: opts.Paths,
SnapshotTemplate: opts.SnapshotTemplate,
}
root := fuse.NewRoot(repo, cfg)
h, err := webdav.NewWebDAV(gopts.ctx, root)
if err != nil {
return err
}
srv := &http.Server{
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
Addr: opts.Listen,
Handler: h,
ErrorLog: errorLogger,
}
return srv.ListenAndServe()
}

103
internal/webdav/dir.go Normal file
View file

@ -0,0 +1,103 @@
package webdav
import (
"context"
"io"
"os"
"bazil.org/fuse"
"bazil.org/fuse/fs"
"github.com/restic/restic/internal/debug"
"golang.org/x/net/webdav"
)
type fuseDir interface {
fs.Node
fs.HandleReadDirAller
fs.NodeStringLookuper
}
// RepoDir implements a read-only directory
type RepoDir struct {
name string
dir fuseDir
ctx context.Context
}
// statically ensure that RepoDir implements webdav.File
var _ webdav.File = &RepoDir{}
func (f *RepoDir) Write(p []byte) (int, error) {
return 0, webdav.ErrForbidden
}
// Close closes the repo file.
func (f *RepoDir) Close() error {
debug.Log("Close %v", f.name)
return nil
}
// Read reads up to len(p) byte from the file.
func (f *RepoDir) Read(p []byte) (int, error) {
debug.Log("")
return 0, io.EOF
}
// Seek sets the offset for the next Read or Write to offset, interpreted
// according to whence: SeekStart means relative to the start of the file,
// SeekCurrent means relative to the current offset, and SeekEnd means relative
// to the end. Seek returns the new offset relative to the start of the file
// and an error, if any.
func (f *RepoDir) Seek(offset int64, whence int) (int64, error) {
debug.Log("Seek %v", f.name)
return 0, webdav.ErrForbidden
}
// Readdir reads the contents of the directory associated with file and returns
// a slice of up to n FileInfo values, as would be returned by Lstat, in
// directory order. Subsequent calls on the same file will yield further
// FileInfos.
//
// If n > 0, Readdir returns at most n FileInfo structures. In this case, if
// Readdir returns an empty slice, it will return a non-nil error explaining
// why. At the end of a directory, the error is io.EOF.
//
// If n <= 0, Readdir returns all the FileInfo from the directory in a single
// slice. In this case, if Readdir succeeds (reads all the way to the end of
// the directory), it returns the slice and a nil error. If it encounters an
// error before the end of the directory, Readdir returns the FileInfo read
// until that point and a non-nil error.
func (f *RepoDir) Readdir(count int) (entries []os.FileInfo, err error) {
debug.Log("Readdir %v, count %d", f.name, count)
dirent, err := f.dir.ReadDirAll(f.ctx)
entries = make([]os.FileInfo, 0, len(dirent))
for _, node := range dirent {
if node.Name == "." || node.Name == ".." {
continue
}
fsNode, err := f.dir.Lookup(f.ctx, node.Name)
if err != nil {
return nil, err
}
var attr fuse.Attr
err = fsNode.Attr(f.ctx, &attr)
if err != nil {
return nil, err
}
entries = append(entries, fileInfoFromAttr(node.Name, attr))
}
return entries, nil
}
// Stat returns a FileInfo describing the named file.
func (f *RepoDir) Stat() (os.FileInfo, error) {
debug.Log("Stat %v", f.name)
var attr fuse.Attr
f.dir.Attr(f.ctx, &attr)
return fileInfoFromAttr(f.name, attr), nil
}

106
internal/webdav/file.go Normal file
View file

@ -0,0 +1,106 @@
package webdav
import (
"context"
"io"
"os"
"bazil.org/fuse"
"bazil.org/fuse/fs"
"github.com/restic/restic/internal/debug"
"golang.org/x/net/webdav"
)
type fuseFile interface {
fs.Node
fs.NodeOpener
}
// RepoFile implements a read-only file
type RepoFile struct {
name string
file fuseFile
handle fs.HandleReader
seek int64
size int64
ctx context.Context
}
// statically ensure that RepoFile implements webdav.File
var _ webdav.File = &RepoFile{}
func (f *RepoFile) Write(p []byte) (int, error) {
return 0, webdav.ErrForbidden
}
// Close closes the repo file.
func (f *RepoFile) Close() error {
debug.Log("Close %v", f.name)
return nil
}
// Read reads up to len(p) byte from the file.
func (f *RepoFile) Read(p []byte) (int, error) {
debug.Log("Read %v, count %d", f.name, len(p))
var err error
if f.handle == nil {
h, err := f.file.Open(f.ctx, nil, nil)
if err != nil {
return 0, err
}
f.handle = h.(fs.HandleReader)
}
maxread := int(f.size - f.seek)
if len(p) < maxread {
maxread = len(p)
}
if maxread <= 0 {
return 0, io.EOF
}
req := &fuse.ReadRequest{Size: maxread, Offset: f.seek}
resp := &fuse.ReadResponse{Data: p}
err = f.handle.Read(f.ctx, req, resp)
if err != nil {
return 0, err
}
f.seek += int64(len(resp.Data))
return len(resp.Data), nil
}
// Seek sets the offset for the next Read or Write to offset, interpreted
// according to whence: SeekStart means relative to the start of the file,
// SeekCurrent means relative to the current offset, and SeekEnd means relative
// to the end. Seek returns the new offset relative to the start of the file
// and an error, if any.
func (f *RepoFile) Seek(offset int64, whence int) (int64, error) {
debug.Log("Seek %v, offset: %d", f.name, offset)
switch whence {
case os.SEEK_SET:
f.seek = offset
case os.SEEK_CUR:
f.seek += offset
case os.SEEK_END:
f.seek = f.size + offset
}
if f.seek < 0 || f.seek > f.size {
return 0, io.EOF
}
return f.seek, nil
}
func (f *RepoFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, io.EOF
}
// Stat returns a FileInfo describing the named file.
func (f *RepoFile) Stat() (os.FileInfo, error) {
debug.Log("Stat %v", f.name)
var attr fuse.Attr
f.file.Attr(f.ctx, &attr)
return fileInfoFromAttr(f.name, attr), nil
}

View file

@ -0,0 +1,39 @@
package webdav
import (
"os"
"time"
"bazil.org/fuse"
)
// virtFileInfo is used to construct an os.FileInfo for a server.
type virtFileInfo struct {
name string
size int64
mode os.FileMode
modtime time.Time
isdir bool
}
// statically ensure that virtFileInfo implements os.FileInfo.
var _ os.FileInfo = virtFileInfo{}
func (fi virtFileInfo) Name() string { return fi.name }
func (fi virtFileInfo) Size() int64 { return fi.size }
func (fi virtFileInfo) Mode() os.FileMode { return fi.mode }
func (fi virtFileInfo) ModTime() time.Time { return fi.modtime }
func (fi virtFileInfo) IsDir() bool { return fi.isdir }
func (fi virtFileInfo) Sys() interface{} { return nil }
func fileInfoFromAttr(name string, attr fuse.Attr) os.FileInfo {
fi := virtFileInfo{
name: name,
size: int64(attr.Size),
mode: attr.Mode,
modtime: attr.Mtime,
isdir: (attr.Mode & os.ModeDir) != 0,
}
return fi
}

102
internal/webdav/fs.go Normal file
View file

@ -0,0 +1,102 @@
package webdav
import (
"context"
"os"
"path"
"strings"
"bazil.org/fuse"
fusefs "bazil.org/fuse/fs"
"github.com/restic/restic/internal/debug"
"golang.org/x/net/webdav"
)
// RepoFileSystem implements a read-only file system on top of a repositoy.
type RepoFileSystem struct {
ctx context.Context
root fuseDir
}
// Mkdir creates a new directory, it is not available for RepoFileSystem.
func (fs *RepoFileSystem) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
return webdav.ErrForbidden
}
// RemoveAll recursively removes files and directories, it is not available for RepoFileSystem.
func (fs *RepoFileSystem) RemoveAll(ctx context.Context, name string) error {
return webdav.ErrForbidden
}
// Rename renames files or directories, it is not available for RepoFileSystem.
func (fs *RepoFileSystem) Rename(ctx context.Context, oldName, newName string) error {
return webdav.ErrForbidden
}
// open opens a file.
func (fs *RepoFileSystem) open(ctx context.Context, name string) (webdav.File, error) {
var err error
name = path.Clean(name)
parts := strings.Split(name, "/")
node := fs.root.(fusefs.Node)
for _, part := range parts {
if part == "." || part == "" {
continue
}
// if there is a part left, the actual node must be a dir
nodedir, ok := node.(fuseDir)
if !ok {
// didn't get a dir
return nil, os.ErrNotExist
}
node, err = nodedir.Lookup(fs.ctx, part)
if err != nil {
if err == fuse.ENOENT {
return nil, os.ErrNotExist
}
return nil, err
}
}
var attr fuse.Attr
err = node.Attr(fs.ctx, &attr)
if err != nil {
return nil, err
}
switch {
case attr.Mode&os.ModeDir != 0: // dir
return &RepoDir{ctx: fs.ctx, dir: node.(fuseDir), name: name}, nil
case attr.Mode&os.ModeType == 0: // file
return &RepoFile{ctx: fs.ctx, file: node.(fuseFile), name: name, size: int64(attr.Size)}, nil
}
return &RepoLink{name: name}, nil
}
// OpenFile opens a file for reading.
func (fs *RepoFileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
debug.Log("Open %v", name)
if flag != os.O_RDONLY {
return nil, webdav.ErrForbidden
}
return fs.open(ctx, name)
}
// Stat returns information on a file or directory.
func (fs *RepoFileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error) {
debug.Log("Stat %v", name)
file, err := fs.open(fs.ctx, name)
if err != nil {
return nil, err
}
return file.Stat()
}

52
internal/webdav/link.go Normal file
View file

@ -0,0 +1,52 @@
package webdav
import (
"io"
"os"
"github.com/restic/restic/internal/debug"
"golang.org/x/net/webdav"
)
// RepoFile implements a link.
// Actually no implementation; this will appear as a zero-sized file.
type RepoLink struct {
name string
}
// statically ensure that RepoFile implements webdav.File
var _ webdav.File = &RepoLink{}
func (f *RepoLink) Write(p []byte) (int, error) {
return 0, webdav.ErrForbidden
}
func (f *RepoLink) Close() error {
debug.Log("Close %v", f.name)
return nil
}
// Read reads up to len(p) byte from the file.
func (f *RepoLink) Read(p []byte) (int, error) {
debug.Log("Read %v, count %d", f.name, len(p))
return 0, io.EOF
}
func (f *RepoLink) Seek(offset int64, whence int) (int64, error) {
debug.Log("Seek %v, offset: %d", f.name, offset)
return 0, io.EOF
}
func (f *RepoLink) Readdir(count int) ([]os.FileInfo, error) {
return nil, io.EOF
}
// Stat returns a FileInfo describing the named file.
func (f *RepoLink) Stat() (os.FileInfo, error) {
debug.Log("Stat %v", f.name)
return &virtFileInfo{
name: f.name,
size: 0,
isdir: false,
}, nil
}

39
internal/webdav/webdav.go Normal file
View file

@ -0,0 +1,39 @@
package webdav
import (
"context"
"log"
"net/http"
"os"
"golang.org/x/net/webdav"
)
// WebDAV implements a WebDAV handler on the repo.
type WebDAV struct {
webdav.Handler
}
var logger = log.New(os.Stderr, "webdav log: ", log.Flags())
func logRequest(req *http.Request, err error) {
logger.Printf("req %v %v -> %v\n", req.Method, req.URL.Path, err)
}
// NewWebDAV returns a new *WebDAV which allows serving the repo via WebDAV.
func NewWebDAV(ctx context.Context, root fuseDir) (*WebDAV, error) {
fs := &RepoFileSystem{ctx: ctx, root: root}
wd := &WebDAV{
Handler: webdav.Handler{
FileSystem: fs,
LockSystem: webdav.NewMemLS(),
Logger: logRequest,
},
}
return wd, nil
}
func (srv *WebDAV) ServeHTTP(res http.ResponseWriter, req *http.Request) {
logger.Printf("handle %v %v\n", req.Method, req.URL.Path)
srv.Handler.ServeHTTP(res, req)
}