serve webdav: refactor to use lib/http

This commit is contained in:
Nick Craig-Wood 2022-12-11 18:53:55 +00:00
parent 08a1ca434b
commit 4444d2d102
2 changed files with 123 additions and 69 deletions

View file

@ -10,14 +10,15 @@ import (
"strings" "strings"
"time" "time"
chi "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rclone/rclone/cmd" "github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/cmd/serve/httplib"
"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
"github.com/rclone/rclone/cmd/serve/proxy" "github.com/rclone/rclone/cmd/serve/proxy"
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags" "github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/hash" "github.com/rclone/rclone/fs/hash"
libhttp "github.com/rclone/rclone/lib/http"
"github.com/rclone/rclone/lib/http/serve" "github.com/rclone/rclone/lib/http/serve"
"github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfsflags" "github.com/rclone/rclone/vfs/vfsflags"
@ -25,19 +26,37 @@ import (
"golang.org/x/net/webdav" "golang.org/x/net/webdav"
) )
var ( // Options required for http server
hashName string type Options struct {
hashType = hash.None Auth libhttp.AuthConfig
disableGETDir = false HTTP libhttp.Config
) Template libhttp.TemplateConfig
HashName string
HashType hash.Type
DisableGETDir bool
}
// DefaultOpt is the default values used for Options
var DefaultOpt = Options{
Auth: libhttp.DefaultAuthCfg(),
HTTP: libhttp.DefaultCfg(),
Template: libhttp.DefaultTemplateCfg(),
HashType: hash.None,
DisableGETDir: false,
}
// Opt is options set by command line flags
var Opt = DefaultOpt
func init() { func init() {
flagSet := Command.Flags() flagSet := Command.Flags()
httpflags.AddFlags(flagSet) libhttp.AddAuthFlagsPrefix(flagSet, "", &Opt.Auth)
libhttp.AddHTTPFlagsPrefix(flagSet, "", &Opt.HTTP)
libhttp.AddTemplateFlagsPrefix(flagSet, "", &Opt.Template)
vfsflags.AddFlags(flagSet) vfsflags.AddFlags(flagSet)
proxyflags.AddFlags(flagSet) proxyflags.AddFlags(flagSet)
flags.StringVarP(flagSet, &hashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off") flags.StringVarP(flagSet, &Opt.HashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off")
flags.BoolVarP(flagSet, &disableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory") flags.BoolVarP(flagSet, &Opt.DisableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory")
} }
// Command definition for cobra // Command definition for cobra
@ -60,7 +79,7 @@ supported hash on the backend or you can use a named hash such as
"MD5" or "SHA-1". Use the [hashsum](/commands/rclone_hashsum/) command "MD5" or "SHA-1". Use the [hashsum](/commands/rclone_hashsum/) command
to see the full list. to see the full list.
` + httplib.Help + vfs.Help + proxy.Help, ` + libhttp.Help + libhttp.TemplateHelp + libhttp.AuthHelp + vfs.Help + proxy.Help,
Annotations: map[string]string{ Annotations: map[string]string{
"versionIntroduced": "v1.39", "versionIntroduced": "v1.39",
}, },
@ -72,21 +91,24 @@ to see the full list.
} else { } else {
cmd.CheckArgs(0, 0, command, args) cmd.CheckArgs(0, 0, command, args)
} }
hashType = hash.None Opt.HashType = hash.None
if hashName == "auto" { if Opt.HashName == "auto" {
hashType = f.Hashes().GetOne() Opt.HashType = f.Hashes().GetOne()
} else if hashName != "" { } else if Opt.HashName != "" {
err := hashType.Set(hashName) err := Opt.HashType.Set(Opt.HashName)
if err != nil { if err != nil {
return err return err
} }
} }
if hashType != hash.None { if Opt.HashType != hash.None {
fs.Debugf(f, "Using hash %v for ETag", hashType) fs.Debugf(f, "Using hash %v for ETag", Opt.HashType)
} }
cmd.Run(false, false, command, func() error { cmd.Run(false, false, command, func() error {
s := newWebDAV(context.Background(), f, &httpflags.Opt) s, err := newWebDAV(context.Background(), f, &Opt)
err := s.serve() if err != nil {
return err
}
err = s.serve()
if err != nil { if err != nil {
return err return err
} }
@ -110,7 +132,8 @@ to see the full list.
// might apply". In particular, whether or not renaming a file or directory // might apply". In particular, whether or not renaming a file or directory
// overwriting another existing file or directory is an error is OS-dependent. // overwriting another existing file or directory is an error is OS-dependent.
type WebDAV struct { type WebDAV struct {
*httplib.Server *libhttp.Server
opt Options
f fs.Fs f fs.Fs
_vfs *vfs.VFS // don't use directly, use getVFS _vfs *vfs.VFS // don't use directly, use getVFS
webdavhandler *webdav.Handler webdavhandler *webdav.Handler
@ -122,29 +145,63 @@ type WebDAV struct {
var _ webdav.FileSystem = (*WebDAV)(nil) var _ webdav.FileSystem = (*WebDAV)(nil)
// Make a new WebDAV to serve the remote // Make a new WebDAV to serve the remote
func newWebDAV(ctx context.Context, f fs.Fs, opt *httplib.Options) *WebDAV { func newWebDAV(ctx context.Context, f fs.Fs, opt *Options) (w *WebDAV, err error) {
w := &WebDAV{ w = &WebDAV{
f: f, f: f,
ctx: ctx, ctx: ctx,
opt: *opt,
} }
if proxyflags.Opt.AuthProxy != "" { if proxyflags.Opt.AuthProxy != "" {
w.proxy = proxy.New(ctx, &proxyflags.Opt) w.proxy = proxy.New(ctx, &proxyflags.Opt)
// override auth // override auth
copyOpt := *opt w.opt.Auth.CustomAuthFn = w.auth
copyOpt.Auth = w.auth
opt = &copyOpt
} else { } else {
w._vfs = vfs.New(f, &vfsflags.Opt) w._vfs = vfs.New(f, &vfsflags.Opt)
} }
w.Server = httplib.NewServer(http.HandlerFunc(w.handler), opt)
w.Server, err = libhttp.NewServer(ctx,
libhttp.WithConfig(w.opt.HTTP),
libhttp.WithAuth(w.opt.Auth),
libhttp.WithTemplate(w.opt.Template),
)
if err != nil {
return nil, fmt.Errorf("failed to init server: %w", err)
}
webdavHandler := &webdav.Handler{ webdavHandler := &webdav.Handler{
Prefix: w.Server.Opt.BaseURL, Prefix: w.opt.HTTP.BaseURL,
FileSystem: w, FileSystem: w,
LockSystem: webdav.NewMemLS(), LockSystem: webdav.NewMemLS(),
Logger: w.logRequest, // FIXME Logger: w.logRequest, // FIXME
} }
w.webdavhandler = webdavHandler w.webdavhandler = webdavHandler
return w
router := w.Server.Router()
router.Use(
middleware.SetHeader("Accept-Ranges", "bytes"),
middleware.SetHeader("Server", "rclone/"+fs.Version),
)
router.Handle("/*", w)
// Webdav only methods not defined in chi
methods := []string{
"COPY", // Copies the resource.
"LOCK", // Locks the resource.
"MKCOL", // Creates the collection specified.
"MOVE", // Moves the resource.
"PROPFIND", // Performs a property find on the server.
"PROPPATCH", // Sets or removes properties on the server.
"UNLOCK", // Unlocks the resource.
}
for _, method := range methods {
chi.RegisterMethod(method)
router.Method(method, "/*", w)
}
w.Server.Serve()
return w, nil
} }
// Gets the VFS in use for this request // Gets the VFS in use for this request
@ -152,7 +209,7 @@ func (w *WebDAV) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) {
if w._vfs != nil { if w._vfs != nil {
return w._vfs, nil return w._vfs, nil
} }
value := ctx.Value(httplib.ContextAuthKey) value := libhttp.CtxGetAuth(ctx)
if value == nil { if value == nil {
return nil, errors.New("no VFS found in context") return nil, errors.New("no VFS found in context")
} }
@ -172,14 +229,11 @@ func (w *WebDAV) auth(user, pass string) (value interface{}, err error) {
return VFS, err return VFS, err
} }
func (w *WebDAV) handler(rw http.ResponseWriter, r *http.Request) { func (w *WebDAV) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
urlPath, ok := w.Path(rw, r) urlPath := r.URL.Path
if !ok {
return
}
isDir := strings.HasSuffix(urlPath, "/") isDir := strings.HasSuffix(urlPath, "/")
remote := strings.Trim(urlPath, "/") remote := strings.Trim(urlPath, "/")
if !disableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir { if !w.opt.DisableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir {
w.serveDir(rw, r, remote) w.serveDir(rw, r, remote)
return return
} }
@ -217,7 +271,7 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
} }
// Make the entries for display // Make the entries for display
directory := serve.NewDirectory(dirRemote, w.HTMLTemplate) directory := serve.NewDirectory(dirRemote, w.Server.HTMLTemplate())
for _, node := range dirEntries { for _, node := range dirEntries {
if vfsflags.Opt.NoModTime { if vfsflags.Opt.NoModTime {
directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{}) directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{})
@ -237,11 +291,8 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
// //
// Use s.Close() and s.Wait() to shutdown server // Use s.Close() and s.Wait() to shutdown server
func (w *WebDAV) serve() error { func (w *WebDAV) serve() error {
err := w.Serve() w.Serve()
if err != nil { fs.Logf(w.f, "WebDav Server started on %s", w.URLs())
return err
}
fs.Logf(w.f, "WebDav Server started on %s", w.URL())
return nil return nil
} }
@ -276,7 +327,7 @@ func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.F
if err != nil { if err != nil {
return nil, err return nil, err
} }
return Handle{f}, nil return Handle{Handle: f, w: w}, nil
} }
// RemoveAll removes a file or a directory and its contents // RemoveAll removes a file or a directory and its contents
@ -318,12 +369,13 @@ func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err err
if err != nil { if err != nil {
return nil, err return nil, err
} }
return FileInfo{fi}, nil return FileInfo{FileInfo: fi, w: w}, nil
} }
// Handle represents an open file // Handle represents an open file
type Handle struct { type Handle struct {
vfs.Handle vfs.Handle
w *WebDAV
} }
// Readdir reads directory entries from the handle // Readdir reads directory entries from the handle
@ -334,7 +386,7 @@ func (h Handle) Readdir(count int) (fis []os.FileInfo, err error) {
} }
// Wrap each FileInfo // Wrap each FileInfo
for i := range fis { for i := range fis {
fis[i] = FileInfo{fis[i]} fis[i] = FileInfo{FileInfo: fis[i], w: h.w}
} }
return fis, nil return fis, nil
} }
@ -345,19 +397,20 @@ func (h Handle) Stat() (fi os.FileInfo, err error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return FileInfo{fi}, nil return FileInfo{FileInfo: fi, w: h.w}, nil
} }
// FileInfo represents info about a file satisfying os.FileInfo and // FileInfo represents info about a file satisfying os.FileInfo and
// also some additional interfaces for webdav for ETag and ContentType // also some additional interfaces for webdav for ETag and ContentType
type FileInfo struct { type FileInfo struct {
os.FileInfo os.FileInfo
w *WebDAV
} }
// ETag returns an ETag for the FileInfo // ETag returns an ETag for the FileInfo
func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) { func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
// defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err) // defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err)
if hashType == hash.None { if fi.w.opt.HashType == hash.None {
return "", webdav.ErrNotImplemented return "", webdav.ErrNotImplemented
} }
node, ok := (fi.FileInfo).(vfs.Node) node, ok := (fi.FileInfo).(vfs.Node)
@ -370,7 +423,7 @@ func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
if !ok { if !ok {
return "", webdav.ErrNotImplemented return "", webdav.ErrNotImplemented
} }
hash, err := o.Hash(ctx, hashType) hash, err := o.Hash(ctx, fi.w.opt.HashType)
if err != nil || hash == "" { if err != nil || hash == "" {
return "", webdav.ErrNotImplemented return "", webdav.ErrNotImplemented
} }

View file

@ -19,7 +19,6 @@ import (
"time" "time"
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/cmd/serve/httplib"
"github.com/rclone/rclone/cmd/serve/servetest" "github.com/rclone/rclone/cmd/serve/servetest"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/config/configmap"
@ -40,9 +39,9 @@ const (
// check interfaces // check interfaces
var ( var (
_ os.FileInfo = FileInfo{nil} _ os.FileInfo = FileInfo{nil, nil}
_ webdav.ETager = FileInfo{nil} _ webdav.ETager = FileInfo{nil, nil}
_ webdav.ContentTyper = FileInfo{nil} _ webdav.ContentTyper = FileInfo{nil, nil}
) )
// TestWebDav runs the webdav server then runs the unit tests for the // TestWebDav runs the webdav server then runs the unit tests for the
@ -50,28 +49,29 @@ var (
func TestWebDav(t *testing.T) { func TestWebDav(t *testing.T) {
// Configure and start the server // Configure and start the server
start := func(f fs.Fs) (configmap.Simple, func()) { start := func(f fs.Fs) (configmap.Simple, func()) {
opt := httplib.DefaultOpt opt := DefaultOpt
opt.ListenAddr = testBindAddress opt.HTTP.ListenAddr = []string{testBindAddress}
opt.BasicUser = testUser opt.Auth.BasicUser = testUser
opt.BasicPass = testPass opt.Auth.BasicPass = testPass
opt.Template = testTemplate opt.Template.Path = testTemplate
hashType = hash.MD5 opt.HashType = hash.MD5
// Start the server // Start the server
w := newWebDAV(context.Background(), f, &opt) w, err := newWebDAV(context.Background(), f, &opt)
assert.NoError(t, w.serve()) require.NoError(t, err)
require.NoError(t, w.serve())
// Config for the backend we'll use to connect to the server // Config for the backend we'll use to connect to the server
config := configmap.Simple{ config := configmap.Simple{
"type": "webdav", "type": "webdav",
"vendor": "other", "vendor": "other",
"url": w.Server.URL(), "url": w.Server.URLs()[0],
"user": testUser, "user": testUser,
"pass": obscure.MustObscure(testPass), "pass": obscure.MustObscure(testPass),
} }
return config, func() { return config, func() {
w.Close() assert.NoError(t, w.Shutdown())
w.Wait() w.Wait()
} }
} }
@ -98,18 +98,19 @@ func TestHTTPFunction(t *testing.T) {
f, err := fs.NewFs(context.Background(), "../http/testdata/files") f, err := fs.NewFs(context.Background(), "../http/testdata/files")
assert.NoError(t, err) assert.NoError(t, err)
opt := httplib.DefaultOpt opt := DefaultOpt
opt.ListenAddr = testBindAddress opt.HTTP.ListenAddr = []string{testBindAddress}
opt.Template = testTemplate opt.Template.Path = testTemplate
// Start the server // Start the server
w := newWebDAV(context.Background(), f, &opt) w, err := newWebDAV(context.Background(), f, &opt)
assert.NoError(t, w.serve()) assert.NoError(t, err)
require.NoError(t, w.serve())
defer func() { defer func() {
w.Close() assert.NoError(t, w.Shutdown())
w.Wait() w.Wait()
}() }()
testURL := w.Server.URL() testURL := w.Server.URLs()[0]
pause := time.Millisecond pause := time.Millisecond
i := 0 i := 0
for ; i < 10; i++ { for ; i < 10; i++ {