forked from TrueCloudLab/restic
Compare commits
5 commits
master
...
add-webdav
Author | SHA1 | Date | |
---|---|---|---|
|
9da6e7c329 | ||
|
b3f38686ee | ||
|
bd3022c504 | ||
|
057b56a1b6 | ||
|
1de9b82850 |
16 changed files with 1299 additions and 4 deletions
126
cmd/restic/cmd_webdav.go
Normal file
126
cmd/restic/cmd_webdav.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/webdav"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/server/rofs"
|
||||
)
|
||||
|
||||
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(cmd.Context(), webdavOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// WebDAVOptions collects all options for the webdav command.
|
||||
type WebDAVOptions struct {
|
||||
Listen string
|
||||
|
||||
restic.SnapshotFilter
|
||||
TimeTemplate string
|
||||
PathTemplates []string
|
||||
}
|
||||
|
||||
var webdavOptions WebDAVOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdWebDAV)
|
||||
|
||||
fs := cmdWebDAV.Flags()
|
||||
fs.StringVarP(&webdavOptions.Listen, "listen", "l", "localhost:3080", "set the listen host name and `address`")
|
||||
|
||||
initMultiSnapshotFilter(fs, &webdavOptions.SnapshotFilter, true)
|
||||
|
||||
fs.StringArrayVar(&webdavOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)")
|
||||
fs.StringVar(&webdavOptions.TimeTemplate, "time-template", time.RFC3339, "set `template` to use for times")
|
||||
}
|
||||
|
||||
func runWebDAV(ctx context.Context, opts WebDAVOptions, gopts GlobalOptions, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.Fatal("this command does not accept additional arguments")
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(ctx, gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, ctx, err := lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
err = repo.LoadIndex(ctx, bar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errorLogger := log.New(os.Stderr, "error log: ", log.Flags())
|
||||
|
||||
cfg := rofs.Config{
|
||||
Filter: opts.SnapshotFilter,
|
||||
TimeTemplate: opts.TimeTemplate,
|
||||
PathTemplates: opts.PathTemplates,
|
||||
}
|
||||
|
||||
root, err := rofs.New(ctx, repo, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// root := os.DirFS(".")
|
||||
|
||||
// h, err := webdav.NewWebDAV(ctx, root)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// root := fstest.MapFS{
|
||||
// "foobar": &fstest.MapFile{
|
||||
// Data: []byte("foobar test content"),
|
||||
// Mode: 0644,
|
||||
// ModTime: time.Now(),
|
||||
// },
|
||||
// "test.txt": &fstest.MapFile{
|
||||
// Data: []byte("other file"),
|
||||
// Mode: 0640,
|
||||
// ModTime: time.Now(),
|
||||
// },
|
||||
// }
|
||||
|
||||
logRequest := func(req *http.Request, err error) {
|
||||
errorLogger.Printf("req %v %v -> %v\n", req.Method, req.URL.Path, err)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
ReadTimeout: 60 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
Addr: opts.Listen,
|
||||
// Handler: http.FileServer(http.FS(root)),
|
||||
Handler: &webdav.Handler{
|
||||
FileSystem: rofs.WebDAVFS(root),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
Logger: logRequest,
|
||||
},
|
||||
ErrorLog: errorLogger,
|
||||
}
|
||||
|
||||
return srv.ListenAndServe()
|
||||
}
|
8
go.mod
8
go.mod
|
@ -25,12 +25,12 @@ require (
|
|||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.16.0
|
||||
golang.org/x/term v0.16.0
|
||||
golang.org/x/sys v0.17.0
|
||||
golang.org/x/term v0.17.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.157.0
|
||||
|
|
8
go.sum
8
go.sum
|
@ -205,6 +205,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
@ -224,6 +226,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
|
@ -251,11 +255,15 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
|
92
internal/server/rofs/dir.go
Normal file
92
internal/server/rofs/dir.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package rofs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"slices"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
)
|
||||
|
||||
// dirEntry holds data for a file or directory.
|
||||
type dirEntry struct {
|
||||
fileInfo fs.FileInfo
|
||||
}
|
||||
|
||||
var _ fs.DirEntry = dirEntry{}
|
||||
|
||||
func (d dirEntry) Name() string { return d.fileInfo.Name() }
|
||||
func (d dirEntry) IsDir() bool { return d.fileInfo.IsDir() }
|
||||
func (d dirEntry) Type() fs.FileMode { return d.fileInfo.Mode().Type() }
|
||||
func (d dirEntry) Info() (fs.FileInfo, error) { return d.fileInfo, nil }
|
||||
|
||||
// openDir represents a directory opened for reading.
|
||||
type openDir struct {
|
||||
path string
|
||||
fileInfo fileInfo
|
||||
entries []fs.DirEntry
|
||||
offset int
|
||||
}
|
||||
|
||||
var _ fs.ReadDirFile = &openDir{}
|
||||
|
||||
func (d *openDir) Close() error {
|
||||
debug.Log("Close(%v)", d.path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *openDir) Stat() (fs.FileInfo, error) {
|
||||
debug.Log("Stat(%v)", d.path)
|
||||
return d.fileInfo, nil
|
||||
}
|
||||
|
||||
func (d *openDir) Read([]byte) (int, error) {
|
||||
return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid}
|
||||
}
|
||||
|
||||
func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
|
||||
n := len(d.entries) - d.offset
|
||||
|
||||
if n == 0 && count > 0 {
|
||||
debug.Log("ReadDir(%v, %v) -> EOF", d.path, count)
|
||||
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if count > 0 && n > count {
|
||||
n = count
|
||||
}
|
||||
|
||||
list := make([]fs.DirEntry, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
list = append(list, d.entries[d.offset+i])
|
||||
}
|
||||
|
||||
d.offset += n
|
||||
|
||||
debug.Log("ReadDir(%v, %v) -> %v entries", d.path, count, len(list))
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func dirMap2DirEntry(m map[string]rofsEntry) []fs.DirEntry {
|
||||
list := make([]fs.DirEntry, 0, len(m))
|
||||
|
||||
for _, entry := range m {
|
||||
list = append(list, entry.DirEntry())
|
||||
}
|
||||
|
||||
slices.SortFunc(list, func(a, b fs.DirEntry) int {
|
||||
if a.Name() == b.Name() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if a.Name() < b.Name() {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 1
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
23
internal/server/rofs/file_info.go
Normal file
23
internal/server/rofs/file_info.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package rofs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fileInfo provides information about a file or directory.
|
||||
type fileInfo struct {
|
||||
name string
|
||||
mode fs.FileMode
|
||||
modtime time.Time
|
||||
size int64
|
||||
}
|
||||
|
||||
func (fi fileInfo) Name() string { return fi.name }
|
||||
func (fi fileInfo) IsDir() bool { return fi.mode.IsDir() }
|
||||
func (fi fileInfo) ModTime() time.Time { return fi.modtime }
|
||||
func (fi fileInfo) Mode() fs.FileMode { return fi.mode }
|
||||
func (fi fileInfo) Size() int64 { return fi.size }
|
||||
func (fi fileInfo) Sys() any { return nil }
|
||||
|
||||
var _ fs.FileInfo = fileInfo{}
|
153
internal/server/rofs/fs.go
Normal file
153
internal/server/rofs/fs.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
// Package rofs implements a read-only file system on top of a restic repository.
|
||||
package rofs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
// ROFS implements a read-only filesystem on top of a repo.
|
||||
type ROFS struct {
|
||||
repo restic.Repository
|
||||
cfg Config
|
||||
entries map[string]rofsEntry
|
||||
fileInfo fileInfo
|
||||
}
|
||||
|
||||
type rofsEntry interface {
|
||||
Open() (fs.File, error)
|
||||
DirEntry() fs.DirEntry
|
||||
}
|
||||
|
||||
// statically ensure that *FS implements fs.FS
|
||||
var _ fs.FS = &ROFS{}
|
||||
|
||||
// Config holds settings for a filesystem.
|
||||
type Config struct {
|
||||
Filter restic.SnapshotFilter
|
||||
TimeTemplate string
|
||||
PathTemplates []string
|
||||
}
|
||||
|
||||
// New returns a new filesystem for the repo.
|
||||
func New(ctx context.Context, repo restic.Repository, cfg Config) (*ROFS, error) {
|
||||
// set defaults, if PathTemplates is not set
|
||||
if len(cfg.PathTemplates) == 0 {
|
||||
cfg.PathTemplates = []string{
|
||||
"ids/%i",
|
||||
"snapshots/%T",
|
||||
"hosts/%h/%T",
|
||||
"tags/%t/%T",
|
||||
}
|
||||
}
|
||||
|
||||
rofs := &ROFS{
|
||||
repo: repo,
|
||||
cfg: cfg,
|
||||
entries: make(map[string]rofsEntry),
|
||||
fileInfo: fileInfo{
|
||||
name: ".",
|
||||
mode: 0755,
|
||||
modtime: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
err := rofs.updateSnapshots(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rofs, nil
|
||||
}
|
||||
|
||||
func (rofs *ROFS) updateSnapshots(ctx context.Context) error {
|
||||
|
||||
entries, err := buildSnapshotEntries(ctx, rofs.repo, rofs.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rofs.entries = entries
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildSnapshotEntries(ctx context.Context, repo restic.Repository, cfg Config) (map[string]rofsEntry, error) {
|
||||
var snapshots restic.Snapshots
|
||||
err := cfg.Filter.FindAll(ctx, repo, repo, nil, func(_ string, sn *restic.Snapshot, _ error) error {
|
||||
if sn != nil {
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("filter snapshots: %w", err)
|
||||
}
|
||||
|
||||
debug.Log("found %d snapshots", len(snapshots))
|
||||
|
||||
list := make(map[string]rofsEntry)
|
||||
list["foo"] = NewMemFile("foo", []byte("foobar content of file foo"), time.Now())
|
||||
|
||||
list["snapshots"] = NewSnapshotsDir(ctx, repo, cfg.PathTemplates, cfg.TimeTemplate)
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Open opens the named file.
|
||||
//
|
||||
// When Open returns an error, it should be of type *PathError
|
||||
// with the Op field set to "open", the Path field set to name,
|
||||
// and the Err field describing the problem.
|
||||
//
|
||||
// Open should reject attempts to open names that do not satisfy
|
||||
// ValidPath(name), returning a *PathError with Err set to
|
||||
// ErrInvalid or ErrNotExist.
|
||||
func (rofs *ROFS) Open(name string) (fs.File, error) {
|
||||
if !fs.ValidPath(name) {
|
||||
debug.Log("Open(%v), invalid path name", name)
|
||||
|
||||
return nil, &fs.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: fs.ErrInvalid,
|
||||
}
|
||||
}
|
||||
|
||||
if name == "." {
|
||||
debug.Log("Open(%v) (root)", name)
|
||||
|
||||
d := &openDir{
|
||||
path: ".",
|
||||
fileInfo: fileInfo{
|
||||
name: ".",
|
||||
mode: fs.ModeDir | 0555,
|
||||
modtime: time.Now(),
|
||||
},
|
||||
entries: dirMap2DirEntry(rofs.entries),
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
entry, ok := rofs.entries[name]
|
||||
if !ok {
|
||||
debug.Log("Open(%v) -> does not exist", name)
|
||||
return nil, &fs.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
debug.Log("Open(%v)", name)
|
||||
|
||||
return entry.Open()
|
||||
}
|
35
internal/server/rofs/fs_test.go
Normal file
35
internal/server/rofs/fs_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package rofs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
func TestROFs(t *testing.T) {
|
||||
repo := repository.TestRepository(t)
|
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, "2024-02-25T17:21:56+01:00")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
restic.TestCreateSnapshot(t, repo, timestamp, 2)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
root, err := New(ctx, repo, Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = fstest.TestFS(root, "snapshots")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
117
internal/server/rofs/mem_file.go
Normal file
117
internal/server/rofs/mem_file.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package rofs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
)
|
||||
|
||||
type MemFile struct {
|
||||
Path string
|
||||
FileInfo fileInfo
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// NewMemFile returns a new file.
|
||||
func NewMemFile(filename string, data []byte, modTime time.Time) MemFile {
|
||||
return MemFile{
|
||||
Path: filename,
|
||||
Data: data,
|
||||
FileInfo: fileInfo{
|
||||
name: path.Base(filename),
|
||||
size: int64(len(data)),
|
||||
mode: 0644,
|
||||
modtime: modTime,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (f MemFile) Open() (fs.File, error) {
|
||||
return &openMemFile{
|
||||
path: f.Path,
|
||||
fileInfo: f.FileInfo,
|
||||
data: f.Data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f MemFile) DirEntry() fs.DirEntry {
|
||||
return dirEntry{
|
||||
fileInfo: f.FileInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// openMemFile is a file that is currently open.
|
||||
type openMemFile struct {
|
||||
path string
|
||||
|
||||
fileInfo fileInfo
|
||||
data []byte
|
||||
|
||||
offset int64
|
||||
}
|
||||
|
||||
// make sure it implements all the necessary interfaces
|
||||
var _ fs.File = &openMemFile{}
|
||||
var _ io.Seeker = &openMemFile{}
|
||||
|
||||
func (f *openMemFile) Close() error {
|
||||
debug.Log("Close(%v)", f.path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *openMemFile) Stat() (fs.FileInfo, error) {
|
||||
debug.Log("Stat(%v)", f.path)
|
||||
|
||||
return f.fileInfo, nil
|
||||
}
|
||||
|
||||
func (f *openMemFile) Read(p []byte) (int, error) {
|
||||
if f.offset >= int64(len(f.data)) {
|
||||
debug.Log("Read(%v, %v) -> EOF", f.path, len(p))
|
||||
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if f.offset < 0 {
|
||||
debug.Log("Read(%v, %v) -> offset negative", f.path, len(p))
|
||||
|
||||
return 0, &fs.PathError{
|
||||
Op: "read",
|
||||
Path: f.path,
|
||||
Err: fs.ErrInvalid,
|
||||
}
|
||||
}
|
||||
|
||||
n := copy(p, f.data[f.offset:])
|
||||
f.offset += int64(n)
|
||||
|
||||
debug.Log("Read(%v, %v) -> %v bytes", f.path, len(p), n)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *openMemFile) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
case io.SeekCurrent:
|
||||
offset += f.offset
|
||||
case io.SeekEnd:
|
||||
offset += int64(len(f.data))
|
||||
}
|
||||
|
||||
if offset < 0 || offset > int64(len(f.data)) {
|
||||
debug.Log("Seek(%v, %v, %v) -> error invalid offset %v", f.path, offset, whence, offset)
|
||||
|
||||
return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid}
|
||||
}
|
||||
|
||||
debug.Log("Seek(%v, %v, %v), new offset %v", f.path, offset, whence, offset)
|
||||
|
||||
f.offset = offset
|
||||
|
||||
return offset, nil
|
||||
}
|
148
internal/server/rofs/snapshots_dir.go
Normal file
148
internal/server/rofs/snapshots_dir.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package rofs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
// SnapshotsDir implements a tree of snapshots in repo as a file system in various sub-directories.
|
||||
type SnapshotsDir struct {
|
||||
lastUpdate time.Time
|
||||
|
||||
pathTemplates []string
|
||||
timeTemplate string
|
||||
|
||||
// list of top-level directories
|
||||
entries []rofsEntry
|
||||
|
||||
repo restic.Repository
|
||||
}
|
||||
|
||||
// ensure that the interface is implemented
|
||||
var _ rofsEntry = &SnapshotsDir{}
|
||||
|
||||
// NewSnapshotsDir initializes a new top-level snapshots directory.
|
||||
func NewSnapshotsDir(ctx context.Context, repo restic.Repository, pathTemplates []string, timeTemplate string) *SnapshotsDir {
|
||||
dir := &SnapshotsDir{
|
||||
pathTemplates: pathTemplates,
|
||||
timeTemplate: timeTemplate,
|
||||
lastUpdate: time.Now(),
|
||||
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
// testnames := []string{"foo", "bar", "baz", "snapshots"}
|
||||
// for _, name := range testnames {
|
||||
// dir.entries = append(dir.entries,
|
||||
// fs.FileInfoToDirEntry(FileInfo{
|
||||
// name: name,
|
||||
// mode: 0644,
|
||||
// modtime: time.Now(),
|
||||
// }))
|
||||
// }
|
||||
|
||||
// slices.SortFunc(dir.entries, func(a, b fs.DirEntry) int {
|
||||
// if a.Name() == b.Name() {
|
||||
// return 0
|
||||
// }
|
||||
|
||||
// if a.Name() < b.Name() {
|
||||
// return 1
|
||||
// }
|
||||
|
||||
// return -1
|
||||
// })
|
||||
|
||||
// // prepare for readdir with positive n
|
||||
// dir.entriesRemaining = dir.entries
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
// ensure that it implements all necessary interfaces.
|
||||
var _ fs.ReadDirFile = &SnapshotsDir{}
|
||||
|
||||
// Close closes the snapshots dir.
|
||||
func (dir *SnapshotsDir) Close() error {
|
||||
debug.Log("Close()")
|
||||
|
||||
// reset readdir list
|
||||
// dir.entriesRemaining = dir.entries
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read is not implemented for a dir.
|
||||
func (dir *SnapshotsDir) Read([]byte) (int, error) {
|
||||
return 0, &fs.PathError{
|
||||
Op: "read",
|
||||
Err: fs.ErrInvalid,
|
||||
}
|
||||
}
|
||||
|
||||
// Stat returns information about the dir.
|
||||
func (dir *SnapshotsDir) Stat() (fs.FileInfo, error) {
|
||||
debug.Log("Stat(root)")
|
||||
|
||||
fi := FileInfo{
|
||||
name: "root", // use special name, this is the root node
|
||||
size: 0,
|
||||
modtime: dir.lastUpdate,
|
||||
mode: 0755,
|
||||
}
|
||||
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// ReadDir returns a list of entries.
|
||||
func (dir *SnapshotsDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
if n < 0 {
|
||||
debug.Log("Readdir(root, %v), return %v entries", n, len(dir.entries))
|
||||
return dir.entries, nil
|
||||
}
|
||||
|
||||
// complicated pointer handling
|
||||
if n > len(dir.entriesRemaining) {
|
||||
n = len(dir.entriesRemaining)
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
list := dir.entriesRemaining[:n]
|
||||
dir.entriesRemaining = dir.entriesRemaining[n:]
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// DirEntry returns meta data about the dir snapshots dir itself.
|
||||
func (dir *SnapshotsDir) DirEntry() fs.DirEntry {
|
||||
return dirEntry{
|
||||
fileInfo: FileInfo{
|
||||
name: "snapshots",
|
||||
mode: fs.ModeDir | 0755,
|
||||
modtime: dir.lastUpdate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens the dir for reading.
|
||||
func (dir *SnapshotsDir) Open() (fs.File, error) {
|
||||
d := &openDir{
|
||||
path: "snapshots",
|
||||
fileInfo: FileInfo{
|
||||
name: "snapshots",
|
||||
mode: fs.ModeDir | 0555,
|
||||
modtime: dir.lastUpdate,
|
||||
},
|
||||
entries: dirMap2DirEntry(dir.entries),
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
152
internal/server/rofs/webdav_fs.go
Normal file
152
internal/server/rofs/webdav_fs.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package rofs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
// WebDAVFS returns a file system suitable for use with the WebDAV server.
|
||||
func WebDAVFS(fs *ROFS) webdav.FileSystem {
|
||||
return &webDAVFS{FS: fs}
|
||||
}
|
||||
|
||||
// webDAVFS wraps an fs.FS and returns a (read-only) filesystem suitable for use with WebDAV.
|
||||
type webDAVFS struct {
|
||||
fs.FS
|
||||
}
|
||||
|
||||
// ensure that WebDAVFS can be used for webdav.
|
||||
var _ webdav.FileSystem = &webDAVFS{}
|
||||
|
||||
func (*webDAVFS) Mkdir(_ context.Context, name string, _ fs.FileMode) error {
|
||||
return &fs.PathError{
|
||||
Op: "Mkdir",
|
||||
Path: name,
|
||||
Err: fs.ErrPermission,
|
||||
}
|
||||
}
|
||||
|
||||
func (*webDAVFS) RemoveAll(_ context.Context, name string) error {
|
||||
return &fs.PathError{
|
||||
Op: "RemoveAll",
|
||||
Path: name,
|
||||
Err: fs.ErrPermission,
|
||||
}
|
||||
}
|
||||
|
||||
func (*webDAVFS) Rename(_ context.Context, from string, to string) error {
|
||||
return &fs.PathError{
|
||||
Op: "Rename",
|
||||
Path: from,
|
||||
Err: fs.ErrPermission,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *webDAVFS) Open(name string) (fs.File, error) {
|
||||
// use relative paths for FS
|
||||
name = path.Join(".", name)
|
||||
|
||||
return w.FS.Open(name)
|
||||
}
|
||||
|
||||
func (w *webDAVFS) OpenFile(ctx context.Context, name string, flag int, perm fs.FileMode) (webdav.File, error) {
|
||||
// use relative paths for FS
|
||||
name = path.Join(".", name)
|
||||
|
||||
if flag != os.O_RDONLY {
|
||||
return nil, &fs.PathError{
|
||||
Op: "OpenFile",
|
||||
Path: name,
|
||||
Err: fs.ErrPermission,
|
||||
}
|
||||
}
|
||||
|
||||
f, err := w.FS.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
readdirFile, ok := f.(fs.ReadDirFile)
|
||||
if !ok {
|
||||
readdirFile = nil
|
||||
}
|
||||
|
||||
seeker, ok := f.(io.Seeker)
|
||||
if !ok {
|
||||
seeker = nil
|
||||
}
|
||||
|
||||
return &readOnlyFile{File: f, readDirFile: readdirFile, Seeker: seeker}, nil
|
||||
}
|
||||
|
||||
func (w *webDAVFS) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
|
||||
// use relative paths for FS
|
||||
name = path.Join(".", name)
|
||||
|
||||
f, err := w.FS.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
type readOnlyFile struct {
|
||||
fs.File
|
||||
readDirFile fs.ReadDirFile
|
||||
io.Seeker
|
||||
}
|
||||
|
||||
func (f readOnlyFile) Write([]byte) (int, error) {
|
||||
return 0, fs.ErrPermission
|
||||
}
|
||||
|
||||
func (f readOnlyFile) Seek(offset int64, whence int) (int64, error) {
|
||||
if f.Seeker == nil {
|
||||
return 0, fs.ErrInvalid
|
||||
}
|
||||
|
||||
return f.Seeker.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (f readOnlyFile) Readdir(n int) ([]fs.FileInfo, error) {
|
||||
if f.readDirFile == nil {
|
||||
return nil, fs.ErrInvalid
|
||||
}
|
||||
|
||||
entries, err := f.readDirFile.ReadDir(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]fs.FileInfo, 0, len(entries))
|
||||
|
||||
for _, entry := range entries {
|
||||
fi, err := entry.Info()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get fileinfo: %w", err)
|
||||
}
|
||||
|
||||
result = append(result, fi)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
103
internal/webdav/dir.go
Normal file
103
internal/webdav/dir.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/anacrolix/fuse"
|
||||
"github.com/anacrolix/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
106
internal/webdav/file.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/anacrolix/fuse"
|
||||
"github.com/anacrolix/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
|
||||
}
|
39
internal/webdav/fileinfo.go
Normal file
39
internal/webdav/fileinfo.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package webdav
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/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
102
internal/webdav/fs.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/anacrolix/fuse"
|
||||
fusefs "github.com/anacrolix/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
52
internal/webdav/link.go
Normal 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
39
internal/webdav/webdav.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue