mirror of
https://github.com/kopia/kopia.git
synced 2026-04-04 14:23:21 -04:00
beginnings of WebDAV mounting support, not really working yet, need to do have better caching
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
// +build !windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bazil.org/fuse"
|
||||
"fmt"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/fs/loggingfs"
|
||||
"github.com/kopia/kopia/fs/repofs"
|
||||
|
||||
fusefs "bazil.org/fuse/fs"
|
||||
kopiafuse "github.com/kopia/kopia/fuse"
|
||||
"github.com/kopia/kopia/internal/fscache"
|
||||
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
@@ -24,29 +22,8 @@
|
||||
mountMaxCachedDirectories = mountCommand.Flag("max-cached-dirs", "Limit the number of cached directories").Default("100").Int()
|
||||
)
|
||||
|
||||
type root struct {
|
||||
fusefs.Node
|
||||
}
|
||||
|
||||
func (r *root) Root() (fusefs.Node, error) {
|
||||
return r.Node, nil
|
||||
}
|
||||
|
||||
func runMountCommand(context *kingpin.ParseContext) error {
|
||||
rep := mustOpenRepository(nil)
|
||||
|
||||
fuseConnection, err := fuse.Mount(
|
||||
*mountPoint,
|
||||
fuse.ReadOnly(),
|
||||
fuse.FSName("kopia"),
|
||||
fuse.Subtype("kopia"),
|
||||
fuse.VolumeName("Kopia"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var entry fs.Directory
|
||||
|
||||
if *mountObjectID == "all" {
|
||||
@@ -63,15 +40,19 @@ func runMountCommand(context *kingpin.ParseContext) error {
|
||||
entry = loggingfs.Wrap(entry).(fs.Directory)
|
||||
}
|
||||
|
||||
cache := kopiafuse.NewCache(
|
||||
kopiafuse.MaxCachedDirectories(*mountMaxCachedDirectories),
|
||||
kopiafuse.MaxCachedDirectoryEntries(*mountMaxCachedEntries),
|
||||
cache := fscache.NewCache(
|
||||
fscache.MaxCachedDirectories(*mountMaxCachedDirectories),
|
||||
fscache.MaxCachedDirectoryEntries(*mountMaxCachedEntries),
|
||||
)
|
||||
rootNode := kopiafuse.NewDirectoryNode(entry, cache)
|
||||
|
||||
fusefs.Serve(fuseConnection, &root{rootNode})
|
||||
|
||||
return nil
|
||||
switch *mountMode {
|
||||
case "FUSE":
|
||||
return mountDirectoryFUSE(entry, *mountPoint, cache)
|
||||
case "WEBDAV":
|
||||
return mountDirectoryWebDAV(entry, *mountPoint, cache)
|
||||
default:
|
||||
return fmt.Errorf("unsupported mode: %q", *mountMode)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
41
cli/command_mount_fuse.go
Normal file
41
cli/command_mount_fuse.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// +build !windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bazil.org/fuse"
|
||||
fusefs "bazil.org/fuse/fs"
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/internal/fscache"
|
||||
"github.com/kopia/kopia/internal/fusemount"
|
||||
)
|
||||
|
||||
type root struct {
|
||||
fusefs.Node
|
||||
}
|
||||
|
||||
func (r *root) Root() (fusefs.Node, error) {
|
||||
return r.Node, nil
|
||||
}
|
||||
|
||||
var (
|
||||
mountMode = mountCommand.Flag("mode", "Mount mode").Default("FUSE").Enum("WEBDAV", "FUSE")
|
||||
)
|
||||
|
||||
func mountDirectoryFUSE(entry fs.Directory, mountPoint string, cache *fscache.Cache) error {
|
||||
rootNode := fusemount.NewDirectoryNode(entry, cache)
|
||||
|
||||
fuseConnection, err := fuse.Mount(
|
||||
mountPoint,
|
||||
fuse.ReadOnly(),
|
||||
fuse.FSName("kopia"),
|
||||
fuse.Subtype("kopia"),
|
||||
fuse.VolumeName("Kopia"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fusefs.Serve(fuseConnection, &root{rootNode})
|
||||
}
|
||||
18
cli/command_mount_nofuse.go
Normal file
18
cli/command_mount_nofuse.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// +build windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/internal/fscache"
|
||||
)
|
||||
|
||||
var (
|
||||
mountMode = mountCommand.Flag("mode", "Mount mode").Default(defaultMountMode()).Enum("WEBDAV")
|
||||
)
|
||||
|
||||
func mountDirectoryFUSE(entry fs.Directory, mountPoint string, cache *fscache.Cache) error {
|
||||
return fmt.Errorf("FUSE is not supported")
|
||||
}
|
||||
51
cli/command_mount_webdav.go
Normal file
51
cli/command_mount_webdav.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/internal/fscache"
|
||||
"github.com/kopia/kopia/internal/webdavmount"
|
||||
)
|
||||
|
||||
func mountDirectoryWebDAV(entry fs.Directory, mountPoint string, cache *fscache.Cache) error {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", &webdav.Handler{
|
||||
FileSystem: webdavmount.WebDAVFS(entry, cache),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
Logger: func(r *http.Request, err error) {
|
||||
if err != nil {
|
||||
log.Printf("%v %v err: %v", r.Method, r.URL.RequestURI(), err)
|
||||
} else {
|
||||
log.Printf("%v %v OK", r.Method, r.URL.RequestURI())
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
s := http.Server{
|
||||
Addr: "127.0.0.1:9998",
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
onCtrlC(func() {
|
||||
s.Shutdown(context.Background())
|
||||
})
|
||||
|
||||
fmt.Printf("Server listening at http://%v/ Press Ctrl-C to shut down.\n", s.Addr)
|
||||
|
||||
err := s.ListenAndServe()
|
||||
if err == http.ErrServerClosed {
|
||||
fmt.Println("Server shut down.")
|
||||
|
||||
}
|
||||
if err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
// Package fuse implements FUSE filesystem nodes for mounting contents of filesystem stored in repository.
|
||||
//
|
||||
// The FUSE implementation used is from bazil.org/fuse
|
||||
package fuse
|
||||
// Package fscache implements in-memory cache of filesystem entries.
|
||||
package fscache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -35,7 +31,7 @@ type Cache struct {
|
||||
tail *cacheEntry
|
||||
}
|
||||
|
||||
func (c *Cache) allocateID() int64 {
|
||||
func (c *Cache) AllocateID() int64 {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
@@ -80,7 +76,7 @@ func (c *Cache) remove(e *cacheEntry) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) getEntries(id int64, cb func() (fs.Entries, error)) (fs.Entries, error) {
|
||||
func (c *Cache) GetEntries(id int64, cb func() (fs.Entries, error)) (fs.Entries, error) {
|
||||
if c == nil {
|
||||
return cb()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package fuse
|
||||
package fscache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -144,57 +144,57 @@ func TestCache(t *testing.T) {
|
||||
cv.verifyCacheOrdering(t)
|
||||
|
||||
// fetch id1
|
||||
c.getEntries(id1, cs.get(id1))
|
||||
c.GetEntries(id1, cs.get(id1))
|
||||
cv.verifyCacheMiss(t, id1)
|
||||
cv.verifyCacheOrdering(t, id1)
|
||||
|
||||
// fetch id1 again - cache hit, no change
|
||||
c.getEntries(id1, cs.get(id1))
|
||||
c.GetEntries(id1, cs.get(id1))
|
||||
cv.verifyCacheHit(t, id1)
|
||||
cv.verifyCacheOrdering(t, id1)
|
||||
|
||||
// fetch id2
|
||||
c.getEntries(id2, cs.get(id2))
|
||||
c.GetEntries(id2, cs.get(id2))
|
||||
cv.verifyCacheMiss(t, id2)
|
||||
cv.verifyCacheOrdering(t, id2, id1)
|
||||
|
||||
// fetch id1 again - cache hit, id1 moved to the top of the LRU list
|
||||
c.getEntries(id1, cs.get(id1))
|
||||
c.GetEntries(id1, cs.get(id1))
|
||||
cv.verifyCacheHit(t, id1)
|
||||
cv.verifyCacheOrdering(t, id1, id2)
|
||||
|
||||
// fetch id2 again
|
||||
c.getEntries(id2, cs.get(id2))
|
||||
c.GetEntries(id2, cs.get(id2))
|
||||
cv.verifyCacheHit(t, id2)
|
||||
cv.verifyCacheOrdering(t, id2, id1)
|
||||
|
||||
// fetch id3
|
||||
c.getEntries(id3, cs.get(id3))
|
||||
c.GetEntries(id3, cs.get(id3))
|
||||
cv.verifyCacheMiss(t, id3)
|
||||
cv.verifyCacheOrdering(t, id3, id2, id1)
|
||||
|
||||
// fetch id4
|
||||
c.getEntries(id4, cs.get(id4))
|
||||
c.GetEntries(id4, cs.get(id4))
|
||||
cv.verifyCacheMiss(t, id4)
|
||||
cv.verifyCacheOrdering(t, id4, id3)
|
||||
|
||||
// fetch id1 again
|
||||
c.getEntries(id1, cs.get(id1))
|
||||
c.GetEntries(id1, cs.get(id1))
|
||||
cv.verifyCacheMiss(t, id1)
|
||||
cv.verifyCacheOrdering(t, id1, id4)
|
||||
|
||||
// fetch id5, it's a big one that expels all but one
|
||||
c.getEntries(id5, cs.get(id5))
|
||||
c.GetEntries(id5, cs.get(id5))
|
||||
cv.verifyCacheMiss(t, id5)
|
||||
cv.verifyCacheOrdering(t, id5, id1)
|
||||
|
||||
// fetch id6
|
||||
c.getEntries(id6, cs.get(id6))
|
||||
c.GetEntries(id6, cs.get(id6))
|
||||
cv.verifyCacheMiss(t, id6)
|
||||
cv.verifyCacheOrdering(t, id6)
|
||||
|
||||
// fetch id7
|
||||
c.getEntries(id7, cs.get(id7))
|
||||
c.GetEntries(id7, cs.get(id7))
|
||||
cv.verifyCacheMiss(t, id7)
|
||||
cv.verifyCacheOrdering(t, id6)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// +build !windows
|
||||
|
||||
// Package fuse implements FUSE filesystem nodes for mounting contents of filesystem stored in repository.
|
||||
// Package fusemount implements FUSE filesystem nodes for mounting contents of filesystem stored in repository.
|
||||
//
|
||||
// The FUSE implementation used is from bazil.org/fuse
|
||||
package fuse
|
||||
package fusemount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,6 +12,7 @@
|
||||
"sort"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/internal/fscache"
|
||||
|
||||
"bazil.org/fuse"
|
||||
fusefs "bazil.org/fuse/fs"
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
type fuseNode struct {
|
||||
entry fs.Entry
|
||||
cache *Cache
|
||||
cache *fscache.Cache
|
||||
}
|
||||
|
||||
func (n *fuseNode) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||
@@ -76,7 +77,7 @@ func (dir *fuseDirectoryNode) Lookup(ctx context.Context, fileName string) (fuse
|
||||
}
|
||||
|
||||
func (dir *fuseDirectoryNode) readPossiblyCachedReaddir() (fs.Entries, error) {
|
||||
return dir.cache.getEntries(dir.cacheID, func() (fs.Entries, error) {
|
||||
return dir.cache.GetEntries(dir.cacheID, func() (fs.Entries, error) {
|
||||
entries, err := dir.directory().Readdir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -127,7 +128,7 @@ func (sl *fuseSymlinkNode) Readlink(ctx context.Context, req *fuse.ReadlinkReque
|
||||
return sl.entry.(fs.Symlink).Readlink()
|
||||
}
|
||||
|
||||
func newFuseNode(e fs.Entry, cache *Cache) (fusefs.Node, error) {
|
||||
func newFuseNode(e fs.Entry, cache *fscache.Cache) (fusefs.Node, error) {
|
||||
switch e := e.(type) {
|
||||
case fs.Directory:
|
||||
return newDirectoryNode(e, cache), nil
|
||||
@@ -140,11 +141,11 @@ func newFuseNode(e fs.Entry, cache *Cache) (fusefs.Node, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func newDirectoryNode(dir fs.Directory, cache *Cache) fusefs.Node {
|
||||
return &fuseDirectoryNode{fuseNode{dir, cache}, cache.allocateID()}
|
||||
func newDirectoryNode(dir fs.Directory, cache *fscache.Cache) fusefs.Node {
|
||||
return &fuseDirectoryNode{fuseNode{dir, cache}, cache.AllocateID()}
|
||||
}
|
||||
|
||||
// NewDirectoryNode returns FUSE Node for a given fs.Directory
|
||||
func NewDirectoryNode(dir fs.Directory, cache *Cache) fusefs.Node {
|
||||
func NewDirectoryNode(dir fs.Directory, cache *fscache.Cache) fusefs.Node {
|
||||
return newDirectoryNode(dir, cache)
|
||||
}
|
||||
198
internal/webdavmount/webdavmount.go
Normal file
198
internal/webdavmount/webdavmount.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package webdavmount
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/repo"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/internal/fscache"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
var _ os.FileInfo = webdavFileInfo{}
|
||||
var _ webdav.File = (*webdavFile)(nil)
|
||||
var _ webdav.File = (*webdavDir)(nil)
|
||||
|
||||
type webdavFile struct {
|
||||
repo.ObjectReader
|
||||
entry fs.File
|
||||
}
|
||||
|
||||
func (f *webdavFile) Readdir(n int) ([]os.FileInfo, error) {
|
||||
return nil, errors.New("not a directory")
|
||||
}
|
||||
|
||||
func (f *webdavFile) Stat() (os.FileInfo, error) {
|
||||
return webdavFileInfo{f.entry.Metadata()}, nil
|
||||
}
|
||||
|
||||
func (f *webdavFile) Write(b []byte) (int, error) {
|
||||
return 0, errors.New("read-only filesystem")
|
||||
}
|
||||
|
||||
type webdavDir struct {
|
||||
entry fs.Directory
|
||||
}
|
||||
|
||||
func (d *webdavDir) Readdir(n int) ([]os.FileInfo, error) {
|
||||
log.Printf("ReadDir(%v)", d.entry.Metadata().Name)
|
||||
entries, err := d.entry.Readdir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n > 0 && n < len(entries) {
|
||||
entries = entries[0:n]
|
||||
}
|
||||
|
||||
var fis []os.FileInfo
|
||||
for _, e := range entries {
|
||||
fis = append(fis, &webdavFileInfo{e.Metadata()})
|
||||
}
|
||||
return fis, nil
|
||||
}
|
||||
|
||||
func (d *webdavDir) Stat() (os.FileInfo, error) {
|
||||
return webdavFileInfo{d.entry.Metadata()}, nil
|
||||
}
|
||||
|
||||
func (d *webdavDir) Write(b []byte) (int, error) {
|
||||
return 0, errors.New("read-only filesystem")
|
||||
}
|
||||
|
||||
func (d *webdavDir) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *webdavDir) Read(b []byte) (int, error) {
|
||||
return 0, errors.New("not supported")
|
||||
}
|
||||
|
||||
func (d *webdavDir) Seek(int64, int) (int64, error) {
|
||||
return 0, errors.New("not supported")
|
||||
}
|
||||
|
||||
type webdavFileInfo struct {
|
||||
md *fs.EntryMetadata
|
||||
}
|
||||
|
||||
func (i webdavFileInfo) IsDir() bool {
|
||||
return (i.md.FileMode() & os.ModeDir) != 0
|
||||
}
|
||||
|
||||
func (i webdavFileInfo) ModTime() time.Time {
|
||||
return i.md.ModTime
|
||||
}
|
||||
|
||||
func (i webdavFileInfo) Mode() os.FileMode {
|
||||
return i.md.FileMode()
|
||||
}
|
||||
|
||||
func (i webdavFileInfo) Name() string {
|
||||
return i.md.Name
|
||||
}
|
||||
|
||||
func (i webdavFileInfo) Size() int64 {
|
||||
return i.md.FileSize
|
||||
}
|
||||
|
||||
func (i webdavFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
type webdavFS struct {
|
||||
dir fs.Directory
|
||||
cache *fscache.Cache
|
||||
}
|
||||
|
||||
func (w *webdavFS) Mkdir(ctx context.Context, path string, mode os.FileMode) error {
|
||||
return fmt.Errorf("can't create %q: read-only filesystem", path)
|
||||
}
|
||||
|
||||
func (w *webdavFS) RemoveAll(ctx context.Context, path string) error {
|
||||
return fmt.Errorf("can't remove %q: read-only filesystem", path)
|
||||
}
|
||||
|
||||
func (w *webdavFS) Rename(ctx context.Context, oldPath, newPath string) error {
|
||||
return fmt.Errorf("can't rename %q to %q: read-only filesystem", oldPath, newPath)
|
||||
}
|
||||
|
||||
func (w *webdavFS) OpenFile(ctx context.Context, path string, flags int, mode os.FileMode) (webdav.File, error) {
|
||||
f, err := w.findEntry(path)
|
||||
if err != nil {
|
||||
log.Printf("OpenFile(%q) failed with %v", path, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch f := f.(type) {
|
||||
case fs.Directory:
|
||||
log.Printf("OpenFile(%q) succeeded with directory: %v", path, f.Metadata())
|
||||
return &webdavDir{f}, nil
|
||||
case fs.File:
|
||||
log.Printf("OpenFile(%q) succeeded with file: %v", path, f.Metadata())
|
||||
return &webdavFile{nil, f}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("can't open %q: not implemented", path)
|
||||
}
|
||||
|
||||
func (w *webdavFS) Stat(ctx context.Context, path string) (os.FileInfo, error) {
|
||||
e, err := w.findEntry(path)
|
||||
if err != nil {
|
||||
log.Printf("Stat(%q) failed with %v", path, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("Stat(%q) success with %v", path, e.Metadata())
|
||||
return webdavFileInfo{e.Metadata()}, nil
|
||||
}
|
||||
|
||||
func (w *webdavFS) findEntry(path string) (fs.Entry, error) {
|
||||
parts := removeEmpty(strings.Split(path, "/"))
|
||||
var e fs.Entry = w.dir
|
||||
for i, p := range parts {
|
||||
d, ok := e.(fs.Directory)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q not found in %q (not a directory)", p, strings.Join(parts[0:i], "/"))
|
||||
}
|
||||
|
||||
entries, err := d.Readdir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
log.Printf("%+v", e.Metadata())
|
||||
}
|
||||
|
||||
e = entries.FindByName(p)
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("%q not found in %q (not found)", p, strings.Join(parts[0:i], "/"))
|
||||
}
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func removeEmpty(s []string) []string {
|
||||
result := s[:0]
|
||||
for _, e := range s {
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, e)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// WebDAVFS returns a webdav.FileSystem implementation for a given directory.
|
||||
func WebDAVFS(entry fs.Directory, cache *fscache.Cache) webdav.FileSystem {
|
||||
return &webdavFS{entry, cache}
|
||||
}
|
||||
Reference in New Issue
Block a user