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:
parent
bc7ea3fb7c
commit
25845f5126
6 changed files with 602 additions and 65 deletions
|
@ -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
111
src/restic/webdav/dir.go
Normal 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
153
src/restic/webdav/file.go
Normal 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
|
||||
}
|
39
src/restic/webdav/fileinfo.go
Normal file
39
src/restic/webdav/fileinfo.go
Normal 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
85
src/restic/webdav/link.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue