1
0
Fork 0
mirror of https://github.com/restic/restic.git synced 2025-03-16 00:00:05 +01:00

Add WebDAV support via the restic web command.

Adds support for mounting and browsing a restic repository as a webdav endpoint.

Known Issues:
* symlinks in the source snapshot are ignored, because WebDAV does not support
  symlinking. This gives the most sensible behavior if one were copying files
  from a WebDAV mountpoint to an existing filesystem by not overwriting links
  with files.
This commit is contained in:
Will Rouesnel 2016-11-07 02:41:22 +11:00
parent bc7ea3fb7c
commit 25845f5126
6 changed files with 602 additions and 65 deletions

View file

@ -4,48 +4,44 @@
package main
import (
"os"
"net/http"
"github.com/spf13/cobra"
"restic/debug"
"restic/errors"
resticfs "restic/fs"
//resticfs "restic/fs"
"restic/fuse"
resticWebdav "restic/webdav"
systemFuse "bazil.org/fuse"
"bazil.org/fuse/fs"
"golang.org/x/net/webdav"
)
var cmdMount = &cobra.Command{
Use: "mount [flags] mountpoint",
Short: "mount the repository",
var cmdWeb = &cobra.Command{
Use: "web [flags] [[hostname]:port]",
Short: "mount the repository as a WebDAV server",
Long: `
The "mount" command mounts the repository via fuse to a directory. This is a
read-only mount.
The "web" command mounts the repository as a WebDAV server. This is a
read-only interface.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runMount(mountOptions, globalOptions, args)
return runWeb(webOptions, globalOptions, args)
},
}
// MountOptions collects all options for the mount command.
type MountOptions struct {
OwnerRoot bool
}
// WebOptions collects all options for the mount command.
type WebOptions struct{}
var mountOptions MountOptions
var webOptions WebOptions
func init() {
cmdRoot.AddCommand(cmdMount)
cmdMount.Flags().BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
cmdRoot.AddCommand(cmdWeb)
}
func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
debug.Log("start mount")
defer debug.Log("finish mount")
func web(opts WebOptions, gopts GlobalOptions, address string) error {
debug.Log("start web")
defer debug.Log("finish web")
repo, err := OpenRepository(gopts)
if err != nil {
@ -57,57 +53,22 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
return err
}
if _, err := resticfs.Stat(mountpoint); os.IsNotExist(errors.Cause(err)) {
Verbosef("Mountpoint %s doesn't exist, creating it\n", mountpoint)
err = resticfs.Mkdir(mountpoint, os.ModeDir|0700)
if err != nil {
return err
}
}
c, err := systemFuse.Mount(
mountpoint,
systemFuse.ReadOnly(),
systemFuse.FSName("restic"),
)
if err != nil {
return err
fsHandler := &webdav.Handler{
FileSystem: resticWebdav.NewWebdavFS(fuse.NewSnapshotsDir(repo, true)),
LockSystem: webdav.NewMemLS(),
Logger: nil,
}
Printf("Now serving the repository at %s\n", mountpoint)
Printf("Don't forget to umount after quitting!\n")
root := fs.Tree{}
root.Add("snapshots", fuse.NewSnapshotsDir(repo, opts.OwnerRoot))
debug.Log("serving mount at %v", mountpoint)
err = fs.Serve(c, &root)
if err != nil {
return err
}
<-c.Ready
return c.MountError
debug.Log("serving mount at %v", address)
return http.ListenAndServe(address, fsHandler)
}
func umount(mountpoint string) error {
return systemFuse.Unmount(mountpoint)
}
func runMount(opts MountOptions, gopts GlobalOptions, args []string) error {
func runWeb(opts WebOptions, gopts GlobalOptions, args []string) error {
if len(args) == 0 {
return errors.Fatalf("wrong number of parameters")
}
mountpoint := args[0]
address := args[0]
AddCleanupHandler(func() error {
debug.Log("running umount cleanup handler for mount at %v", mountpoint)
err := umount(mountpoint)
if err != nil {
Warnf("unable to umount (maybe already umounted?): %v\n", err)
}
return nil
})
return mount(opts, gopts, mountpoint)
return web(opts, gopts, address)
}

111
src/restic/webdav/dir.go Normal file
View file

@ -0,0 +1,111 @@
package webdav
import (
"os"
"restic/debug"
"bazil.org/fuse/fs"
)
// This is the type for "directory-like" nodes we get from the FUSE layer.
type fsDirType interface {
fs.HandleReadDirAller
fs.NodeStringLookuper
fs.Node
}
// Implements webdav.File for a restic snapshot directory
type dir struct {
// Name of the root snapshot directory
name string
// The root snapshots dir this file is coupled to.
dirNode fsDirType
}
func NewDir(name string, dirNode fsDirType) *dir {
if dirNode == nil {
return nil
}
return &dir{
name: name,
dirNode: dirNode,
}
}
// Read-only system never needs to clean up with restic.
func (this *dir) Close() error {
debug.Log(this.name)
return nil
}
// TODO: allow directories to be rendered as an index.
func (this *dir) Read(p []byte) (n int, err error) {
debug.Log(this.name)
return 0, nil
}
func (this *dir) Write(p []byte) (n int, err error) {
debug.Log(this.name)
return 0, os.ErrInvalid
}
func (this *dir) Seek(offset int64, whence int) (int64, error) {
debug.Log(this.name)
return 0, os.ErrInvalid
}
// Return the contents of the current directory, except for symlinks
// (there is no wide-spread support for handling symlinks in webdav).
func (this *dir) Readdir(count int) ([]os.FileInfo, error) {
debug.Log(this.name)
fileInfos := []os.FileInfo{}
dirContents, err := this.dirNode.ReadDirAll(ctx)
if err != nil {
return []os.FileInfo{}, err
}
for _, dirEnt := range dirContents {
debug.Log(dirEnt.Name)
entNode, err := this.dirNode.Lookup(ctx, dirEnt.Name)
if err != nil {
debug.Log("error while looking up directory contents: %v %v", dirEnt, err)
continue
}
if _, isLnk := entNode.(fsLinkType); isLnk {
debug.Log("skipping symlink: ", dirEnt)
continue
}
fileInfo := FileInfo{
Filename: dirEnt.Name,
}
if err := entNode.Attr(ctx, &fileInfo.Attr); err != nil {
debug.Log("error retrieving attrs for %v : %v", dirEnt.Name, err)
continue
}
fileInfos = append(fileInfos, os.FileInfo(&fileInfo))
}
return fileInfos, nil
}
func (this *dir) Stat() (os.FileInfo, error) {
debug.Log(this.name)
fileInfo := FileInfo{
Filename: this.name,
}
if err := this.dirNode.Attr(ctx, &fileInfo.Attr); err != nil {
debug.Log("error retrieving attrs for %v : %v", this.name, err)
return nil, err
}
return os.FileInfo(&fileInfo), nil
}

153
src/restic/webdav/file.go Normal file
View file

@ -0,0 +1,153 @@
package webdav
import (
"os"
"restic/debug"
"bazil.org/fuse/fs"
"bazil.org/fuse"
"io"
)
// This is the type for "file-like" nodes we get from the FUSE layer.
type fsFileType interface {
fs.HandleReader
fs.HandleReleaser
fs.Node
}
// Implements webdav.File for a restic backend
type file struct {
name string
fileNode fsFileType
// Offset into the files content.
offset int64
}
func NewFile(name string, fileNode fsFileType) *file {
if fileNode == nil {
return nil
}
return &file{
name: name,
fileNode: fileNode,
offset: 0,
}
}
// Read-only system never needs to clean up with restic.
func (this *file) Close() error {
debug.Log(this.name)
return this.fileNode.Release(ctx, nil)
}
func (this *file) Read(p []byte) (n int, err error) {
debug.Log(this.name)
// Construct a read request to the underlying restic fuse.File
req := &fuse.ReadRequest{
Dir: false,
Handle: 0,
Offset: this.offset,
Size: len(p),
Flags: 0,
LockOwner: 0,
FileFlags: 0,
}
resp := &fuse.ReadResponse{
Data: p,
}
// Stat ourselves to figure out the size below.
fi, err := this.Stat()
if err != nil {
return 0, err
}
// Do the read via the fuse wrapper
if err := this.fileNode.Read(ctx, req, resp); err != nil {
return 0, err
}
// Update the file-offset by the number of read bytes.
this.offset += int64(len(resp.Data))
// FIXME: handle if somehow we read past the end of the file?
// Check for EOF, and if so, return it.
if this.offset == fi.Size() {
err = io.EOF
}
return len(resp.Data), err
}
func (this *file) Write(p []byte) (n int, err error) {
debug.Log(this.name)
return 0, os.ErrInvalid
}
func (this *file) Seek(offset int64, whence int) (int64, error) {
debug.Log(this.name)
// Stat ourselves to figure out the size.
fi, err := this.Stat()
if err != nil {
return this.offset, err
}
// Can't be negative
if offset < 0 {
return this.offset, os.ErrInvalid
}
switch whence {
case io.SeekStart:
this.offset = offset
// If seek past end, then seek to end and return EOF
if this.offset > fi.Size() {
this.offset = fi.Size()
return this.offset, io.EOF
}
return this.offset, nil
case io.SeekEnd:
// If seek past beginning, return an invalid operation error
if offset > fi.Size() {
this.offset = 0
return this.offset, os.ErrInvalid
}
this.offset = fi.Size() - offset
return this.offset, nil
case io.SeekCurrent:
if this.offset + offset > fi.Size() {
this.offset = fi.Size()
return this.offset, io.EOF
}
return this.offset, nil
default:
return this.offset, os.ErrInvalid
}
}
func (this *file) Readdir(count int) ([]os.FileInfo, error) {
debug.Log(this.name)
debug.Log("%v", count)
return []os.FileInfo{}, os.ErrInvalid
}
func (this *file) Stat() (os.FileInfo, error) {
debug.Log(this.name)
fileInfo := FileInfo{
Filename: this.name,
}
if err := this.fileNode.Attr(ctx, &fileInfo.Attr); err != nil {
debug.Log("error retrieving attrs for %v : %v", this.name, err)
return nil, err
}
return os.FileInfo(&fileInfo), nil
}

View file

@ -0,0 +1,39 @@
package webdav
import (
"os"
"time"
"bazil.org/fuse"
)
// Implements os.FileInfo on top of fuse.Attr
type FileInfo struct {
// The file basename
Filename string
// Underlying fuse.Attr provider of the file
fuse.Attr
}
func (this FileInfo) Name() string {
return this.Filename
}
func (this FileInfo) Size() int64 {
return int64(this.Attr.Size)
}
func (this FileInfo) Mode() os.FileMode {
return this.Attr.Mode
}
func (this FileInfo) ModTime() time.Time {
return this.Attr.Mtime
}
func (this FileInfo) IsDir() bool {
return (this.Attr.Mode & os.ModeDir) != 0
}
func (this FileInfo) Sys() interface{} {
return &this.Attr
}

85
src/restic/webdav/link.go Normal file
View file

@ -0,0 +1,85 @@
package webdav
import (
"os"
"restic/debug"
"bazil.org/fuse/fs"
"golang.org/x/net/webdav"
)
// This is the type for "symlink" nodes we get from the FUSE layer. This is
// necessary because these nodes need to support file-like operations but the
// fuse-layer abstraction only returns a link interface.
type fsLinkType interface {
fs.NodeReadlinker
fs.Node
}
// Implements webdav.File for a restic backend
type link struct {
name string
linkNode fsLinkType
target webdav.File
}
func NewLink(name string, linkNode fsLinkType, target webdav.File) *link {
if linkNode == nil {
return nil
}
return &link{
name: name,
linkNode: linkNode,
target: target,
}
}
// We don't hold a persistent reference when we're created, so does that mean
// we should do so and clean up the target node here?
func (this *link) Close() error {
debug.Log(this.name)
return nil
}
func (this *link) Read(p []byte) (n int, err error) {
debug.Log(this.name)
return this.target.Read(p)
}
func (this *link) Write(p []byte) (n int, err error) {
debug.Log(this.name)
return this.target.Write(p)
}
func (this *link) Seek(offset int64, whence int) (int64, error) {
debug.Log(this.name)
return this.target.Seek(offset, whence)
}
func (this *link) Readdir(count int) ([]os.FileInfo, error) {
debug.Log(this.name)
return this.target.Readdir(count)
}
func (this *link) Stat() (os.FileInfo, error) {
debug.Log(this.name)
fileInfo := FileInfo{
Filename: this.name,
}
if err := this.linkNode.Attr(ctx, &fileInfo.Attr); err != nil {
debug.Log("error retrieving attrs for %v : %v", this.name, err)
return nil, err
}
return os.FileInfo(&fileInfo), nil
}

View file

@ -1 +1,189 @@
// +build !openbsd
// +build !windows
package webdav
import (
"os"
"strings"
"restic/errors"
resticfuse "restic/fuse"
"golang.org/x/net/webdav"
"golang.org/x/net/context"
"restic/debug"
"bazil.org/fuse/fs"
"bazil.org/fuse"
"path"
)
var ctx context.Context = context.Background()
// Implements webdav.FileSystem interface. This is a wrapper to the
// fuse.Filesystem which handles the discrepancies.
type WebdavFS struct {
root *resticfuse.SnapshotsDir
}
// Create a new webdav.FileSystem for the given repository
func NewWebdavFS(snapshotDir *resticfuse.SnapshotsDir) webdav.FileSystem {
if snapshotDir == nil {
return nil
}
return &WebdavFS{
root: snapshotDir,
}
}
func (this *WebdavFS) Mkdir(name string, perm os.FileMode) error {
debug.Log(name)
return errors.New("WebdavFS is read only. MkDir is not supported.")
}
func (this *WebdavFS) OpenFile(name string, flag int, perm os.FileMode) (webdav.File, error) {
filename, resolvedNode, err := this.resolve(name)
if err != nil {
debug.Log("resolved failed")
return nil, err
}
// Determine what type of node we landed on.
switch node := resolvedNode.(type) {
// Is is a directory type?
case fsDirType:
debug.Log("dir: %v", name)
return NewDir(filename, node), nil
case fsFileType:
debug.Log("file: %v", name)
return NewFile(filename, node), nil
case fsLinkType:
debug.Log("link: %v", name)
// We need to return not found *here* if we try to open a broken symlink.
// Of course, nothing above us can handle this anyway, but its the right
// thing to do.
target, err := this.resolveLinkNode(filename, node)
if err != nil {
return nil, err
}
return NewLink(filename, node, target), nil
default:
debug.Log("UNKNOWN: %v", name)
return nil, os.ErrInvalid
}
}
func (this *WebdavFS) RemoveAll(name string) error {
debug.Log("RemoveAll(%v)", name)
return errors.New("Unimplemented")
}
func (this *WebdavFS) Rename(oldName, newName string) error {
debug.Log("Rename(%v,%v)", oldName, newName)
return errors.New("Unimplemented")
}
func (this *WebdavFS) Stat(name string) (os.FileInfo, error) {
debug.Log(name)
filename, resolvedNode, err := this.resolve(name)
if err != nil {
return nil, err
}
// Return the file info as a fuse -> os.FileInfo mapper
fileInfo := FileInfo{
Filename: filename,
}
if err := resolvedNode.Attr(ctx, &fileInfo.Attr); err != nil {
return nil, os.ErrInvalid
}
return fileInfo, nil
}
func (this *WebdavFS) resolve(name string) (string, fs.Node, error) {
// Clean the path
name = path.Clean(name)
// Remove any leading slash, since it makes path sense logic weird
name = strings.TrimPrefix(name, "/")
// Split the path by slashes, remove 0-length tail
pathlist := strings.Split(name, "/")
if len(pathlist) == 1 && pathlist[0] == "" {
pathlist = []string{}
}
// The first node is always the snapshot root, so set it implicitly.
var filename string
var currentNode fs.Node
filename = "/"
currentNode = this.root
// Recursively resolve each path component till we arrive at the file.
// Skip the first node since it's the snapshot directory
var err error
for _, pathElem := range pathlist {
debug.Log("resolving: %v", pathElem)
// Set the current path element filename
filename = pathElem
// Check that we are still able to lookup nodes
oldNode, ok := currentNode.(fs.NodeStringLookuper)
if !ok {
return filename, nil, os.ErrNotExist
}
if currentNode, err = oldNode.Lookup(ctx, pathElem) ; err != nil {
return filename, nil, err
}
}
return filename, currentNode, err
}
// Helper function to safely resolve link nodes.
func (this *WebdavFS) resolveLinkNode(name string, node fsLinkType) (webdav.File, error) {
// Keep track of inodes we've seen before. If they start to recur, then
// we're in a look and should fail.
//inodes := make(map[uint64]interface{})
// Loop resolution until finished.
resolvedNode := fs.Node(node)
realname := name
for {
// TODO: do we need multi-level resolution (probably)
switch v := resolvedNode.(type) {
case fsDirType:
return NewDir(realname, v), nil
case fsFileType:
return NewFile(realname, v), nil
case fsLinkType:
// Still a symlink - continue resolving.
req := &fuse.ReadlinkRequest{}
nextname, err := v.Readlink(ctx, req) ;
if err != nil {
return nil, os.ErrNotExist
}
debug.Log("resolved: %s => %s", realname, nextname)
realname = nextname
// TODO: rewrite symlinks to be snapshot-relative
// Lookup the next node...
targetNode, err := this.root.Lookup(ctx, realname)
if err != nil {
return nil, os.ErrNotExist
}
resolvedNode = targetNode
default:
return nil, os.ErrInvalid
}
}
}