mirror of https://github.com/restic/restic.git
Merge 166e94d82e
into faffd15d13
This commit is contained in:
commit
e8d86f7e98
|
@ -0,0 +1,469 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/net/webdav"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/bloblru"
|
||||||
|
"github.com/restic/restic/internal/debug"
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/walker"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdWebdav = &cobra.Command{
|
||||||
|
Use: "webdav [flags] [ip:port]",
|
||||||
|
Short: "Serve the repository via WebDAV",
|
||||||
|
Long: `
|
||||||
|
The "webdav" command serves the repository via WebDAV. This is a
|
||||||
|
read-only mount.
|
||||||
|
|
||||||
|
Snapshot Directories
|
||||||
|
====================
|
||||||
|
|
||||||
|
If you need a different template for directories that contain snapshots,
|
||||||
|
you can pass a time template via --time-template and path templates via
|
||||||
|
--path-template.
|
||||||
|
|
||||||
|
Example time template without colons:
|
||||||
|
|
||||||
|
--time-template "2006-01-02_15-04-05"
|
||||||
|
|
||||||
|
You need to specify a sample format for exactly the following timestamp:
|
||||||
|
|
||||||
|
Mon Jan 2 15:04:05 -0700 MST 2006
|
||||||
|
|
||||||
|
For details please see the documentation for time.Format() at:
|
||||||
|
https://godoc.org/time#Time.Format
|
||||||
|
|
||||||
|
For path templates, you can use the following patterns which will be replaced:
|
||||||
|
%i by short snapshot ID
|
||||||
|
%I by long snapshot ID
|
||||||
|
%u by username
|
||||||
|
%h by hostname
|
||||||
|
%t by tags
|
||||||
|
%T by timestamp as specified by --time-template
|
||||||
|
|
||||||
|
The default path templates are:
|
||||||
|
"ids/%i"
|
||||||
|
"snapshots/%T"
|
||||||
|
"hosts/%h/%T"
|
||||||
|
"tags/%t/%T"
|
||||||
|
|
||||||
|
EXIT STATUS
|
||||||
|
===========
|
||||||
|
|
||||||
|
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||||
|
`,
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runWebServer(cmd.Context(), webdavOptions, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebdavOptions struct {
|
||||||
|
restic.SnapshotFilter
|
||||||
|
TimeTemplate string
|
||||||
|
PathTemplates []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var webdavOptions WebdavOptions
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdRoot.AddCommand(cmdWebdav)
|
||||||
|
cmdFlags := cmdWebdav.Flags()
|
||||||
|
initMultiSnapshotFilter(cmdFlags, &webdavOptions.SnapshotFilter, true)
|
||||||
|
cmdFlags.StringArrayVar(&webdavOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)")
|
||||||
|
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 {
|
||||||
|
if len(args) > 1 {
|
||||||
|
return errors.Fatal("wrong number of parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Proper validation, also add support for IPv6
|
||||||
|
bindAddress := "127.0.0.1:3080"
|
||||||
|
if len(args) == 1 {
|
||||||
|
bindAddress = strings.ToLower(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Index(bindAddress, "http://") == 0 {
|
||||||
|
bindAddress = bindAddress[7:]
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
snapshotLister, err := restic.MemorizeList(ctx, repo, restic.SnapshotFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||||
|
err = repo.LoadIndex(ctx, bar)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
davFS := &webdavFS{
|
||||||
|
repo: repo,
|
||||||
|
root: webdavFSNode{
|
||||||
|
name: "",
|
||||||
|
mode: 0555 | os.ModeDir,
|
||||||
|
modTime: time.Now(),
|
||||||
|
children: make(map[string]*webdavFSNode),
|
||||||
|
},
|
||||||
|
blobCache: bloblru.New(64 << 20),
|
||||||
|
}
|
||||||
|
|
||||||
|
wd := &webdav.Handler{
|
||||||
|
FileSystem: davFS,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
// Ignore PathTemplates for now because `fuse.snapshots_dir(struct)` is not accessible when building
|
||||||
|
// on Windows and it would be ridiculous to duplicate the code. It should be shared, somehow!
|
||||||
|
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("Tree contains %d snapshots\n", len(davFS.root.children))
|
||||||
|
Printf("When finished, quit with Ctrl-c here.\n")
|
||||||
|
|
||||||
|
// FIXME: Remove before PR, this is handy for testing but likely undesirable :)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
browseURL := "\\\\" + strings.Replace(bindAddress, ":", "@", 1) + "\\DavWWWRoot"
|
||||||
|
exec.Command("explorer", browseURL).Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ListenAndServe(bindAddress, wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements webdav.FileSystem
|
||||||
|
type webdavFS struct {
|
||||||
|
repo restic.Repository
|
||||||
|
root webdavFSNode
|
||||||
|
// snapshots *restic.Snapshot
|
||||||
|
blobCache *bloblru.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements os.FileInfo
|
||||||
|
type webdavFSNode struct {
|
||||||
|
name string
|
||||||
|
mode os.FileMode
|
||||||
|
modTime time.Time
|
||||||
|
size int64
|
||||||
|
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) Size() int64 { return f.size }
|
||||||
|
func (f *webdavFSNode) Mode() os.FileMode { return f.mode }
|
||||||
|
func (f *webdavFSNode) ModTime() time.Time { return f.modTime }
|
||||||
|
func (f *webdavFSNode) IsDir() bool { return f.mode.IsDir() }
|
||||||
|
func (f *webdavFSNode) Sys() interface{} { return nil }
|
||||||
|
|
||||||
|
func (fs *webdavFS) loadSnapshot(ctx context.Context, mountPoint string, sn *restic.Snapshot) {
|
||||||
|
Printf("Loading snapshot %s at %s\n", sn.ID().Str(), mountPoint)
|
||||||
|
// FIXME: Need a mutex here...
|
||||||
|
// FIXME: All this walking should be done dynamically when the client asks for a folder...
|
||||||
|
walker.Walk(ctx, fs.repo, *sn.Tree, walker.WalkVisitor{
|
||||||
|
ProcessNode: func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) error {
|
||||||
|
if err != nil || node == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fs.addNode(mountPoint+"/"+nodepath, &webdavFSNode{
|
||||||
|
name: node.Name,
|
||||||
|
mode: node.Mode,
|
||||||
|
modTime: node.ModTime,
|
||||||
|
size: int64(node.Size),
|
||||||
|
node: node,
|
||||||
|
// snapshot: sn,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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), "/")
|
||||||
|
if fullname == "" {
|
||||||
|
return &fs.root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(fullname, "/")
|
||||||
|
dir := &fs.root
|
||||||
|
|
||||||
|
for dir != nil {
|
||||||
|
node := dir.children[parts[0]]
|
||||||
|
parts = parts[1:]
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
dir = node
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||||
|
debug.Log("OpenFile %s", name)
|
||||||
|
|
||||||
|
// Client can only read
|
||||||
|
if flag&(os.O_WRONLY|os.O_RDWR) != 0 {
|
||||||
|
return nil, os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &openFile{fullpath: path.Clean("/" + name), node: node, fs: fs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *webdavFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||||
|
node, err := fs.findNode(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *webdavFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||||
|
return os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *webdavFS) RemoveAll(ctx context.Context, name string) error {
|
||||||
|
return os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *webdavFS) Rename(ctx context.Context, oldName, newName string) error {
|
||||||
|
return os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
type openFile struct {
|
||||||
|
fullpath string
|
||||||
|
node *webdavFSNode
|
||||||
|
fs *webdavFS
|
||||||
|
cursor int64
|
||||||
|
children []os.FileInfo
|
||||||
|
// cumsize[i] holds the cumulative size of blobs[:i].
|
||||||
|
cumsize []uint64
|
||||||
|
|
||||||
|
initialized bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) getBlobAt(ctx context.Context, i int) (blob []byte, err error) {
|
||||||
|
blob, ok := f.fs.blobCache.Get(f.node.node.Content[i])
|
||||||
|
if ok {
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err = f.fs.repo.LoadBlob(ctx, restic.DataBlob, f.node.node.Content[i], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.fs.blobCache.Add(f.node.node.Content[i], blob)
|
||||||
|
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Read(p []byte) (int, error) {
|
||||||
|
debug.Log("Read %s %d %d", f.fullpath, f.cursor, len(p))
|
||||||
|
if f.node.IsDir() || f.cursor < 0 {
|
||||||
|
return 0, os.ErrInvalid
|
||||||
|
}
|
||||||
|
if f.cursor >= f.node.Size() {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wait until the first read before we do anything because WebDAV clients tend to open
|
||||||
|
// everything and do nothing...
|
||||||
|
if !f.initialized {
|
||||||
|
var bytes uint64
|
||||||
|
cumsize := make([]uint64, 1+len(f.node.node.Content))
|
||||||
|
for i, id := range f.node.node.Content {
|
||||||
|
size, found := f.fs.repo.LookupBlobSize(id, restic.DataBlob)
|
||||||
|
if !found {
|
||||||
|
return 0, errors.Errorf("id %v not found in repository", id)
|
||||||
|
}
|
||||||
|
bytes += uint64(size)
|
||||||
|
cumsize[i+1] = bytes
|
||||||
|
}
|
||||||
|
if bytes != f.node.node.Size {
|
||||||
|
Printf("sizes do not match: node.Size %d != size %d", bytes, f.node.Size())
|
||||||
|
}
|
||||||
|
f.cumsize = cumsize
|
||||||
|
f.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := uint64(f.cursor)
|
||||||
|
remainingBytes := uint64(len(p))
|
||||||
|
readBytes := 0
|
||||||
|
|
||||||
|
if offset+remainingBytes > uint64(f.node.Size()) {
|
||||||
|
remainingBytes = uint64(f.node.Size()) - remainingBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip blobs before the offset
|
||||||
|
startContent := -1 + sort.Search(len(f.cumsize), func(i int) bool {
|
||||||
|
return f.cumsize[i] > offset
|
||||||
|
})
|
||||||
|
offset -= f.cumsize[startContent]
|
||||||
|
|
||||||
|
for i := startContent; remainingBytes > 0 && i < len(f.cumsize)-1; i++ {
|
||||||
|
blob, err := f.getBlobAt(context.TODO(), i)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset > 0 {
|
||||||
|
blob = blob[offset:]
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
copied := copy(p, blob)
|
||||||
|
remainingBytes -= uint64(copied)
|
||||||
|
readBytes += copied
|
||||||
|
|
||||||
|
p = p[copied:]
|
||||||
|
}
|
||||||
|
|
||||||
|
f.cursor += int64(readBytes)
|
||||||
|
return readBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
|
debug.Log("Readdir %s %d %d", f.fullpath, f.cursor, count)
|
||||||
|
if !f.node.IsDir() || f.cursor < 0 {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wait until the first read before we do anything because WebDAV clients tend to open
|
||||||
|
// everything and do nothing...
|
||||||
|
if !f.initialized {
|
||||||
|
// It's a snapshot, mount it
|
||||||
|
if f.node.snapshot != nil && f.node.children == nil {
|
||||||
|
f.fs.loadSnapshot(context.TODO(), f.fullpath, f.node.snapshot)
|
||||||
|
}
|
||||||
|
children := make([]os.FileInfo, 0, len(f.node.children))
|
||||||
|
for _, c := range f.node.children {
|
||||||
|
children = append(children, c)
|
||||||
|
}
|
||||||
|
f.children = children
|
||||||
|
f.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if count <= 0 {
|
||||||
|
return f.children, nil
|
||||||
|
}
|
||||||
|
if f.cursor >= f.node.Size() {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
start := f.cursor
|
||||||
|
f.cursor += int64(count)
|
||||||
|
if f.cursor > f.node.Size() {
|
||||||
|
f.cursor = f.node.Size()
|
||||||
|
}
|
||||||
|
return f.children[start:f.cursor], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
debug.Log("Seek %s %d %d", f.fullpath, offset, whence)
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
f.cursor = offset
|
||||||
|
case io.SeekCurrent:
|
||||||
|
f.cursor += offset
|
||||||
|
case io.SeekEnd:
|
||||||
|
f.cursor = f.node.Size() - offset
|
||||||
|
default:
|
||||||
|
return 0, os.ErrInvalid
|
||||||
|
}
|
||||||
|
return f.cursor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Stat() (os.FileInfo, error) {
|
||||||
|
return f.node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Write(p []byte) (int, error) {
|
||||||
|
return 0, os.ErrPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *openFile) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue