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"
"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/serve/httplib"
"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
"github.com/rclone/rclone/cmd/serve/proxy"
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/hash"
libhttp "github.com/rclone/rclone/lib/http"
"github.com/rclone/rclone/lib/http/serve"
"github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfsflags"
@ -25,19 +26,37 @@ import (
"golang.org/x/net/webdav"
)
var (
hashName string
hashType = hash.None
disableGETDir = false
)
// Options required for http server
type Options struct {
Auth libhttp.AuthConfig
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() {
flagSet := Command.Flags()
httpflags.AddFlags(flagSet)
libhttp.AddAuthFlagsPrefix(flagSet, "", &Opt.Auth)
libhttp.AddHTTPFlagsPrefix(flagSet, "", &Opt.HTTP)
libhttp.AddTemplateFlagsPrefix(flagSet, "", &Opt.Template)
vfsflags.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.BoolVarP(flagSet, &disableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory")
flags.StringVarP(flagSet, &Opt.HashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off")
flags.BoolVarP(flagSet, &Opt.DisableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory")
}
// 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
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{
"versionIntroduced": "v1.39",
},
@ -72,21 +91,24 @@ to see the full list.
} else {
cmd.CheckArgs(0, 0, command, args)
}
hashType = hash.None
if hashName == "auto" {
hashType = f.Hashes().GetOne()
} else if hashName != "" {
err := hashType.Set(hashName)
Opt.HashType = hash.None
if Opt.HashName == "auto" {
Opt.HashType = f.Hashes().GetOne()
} else if Opt.HashName != "" {
err := Opt.HashType.Set(Opt.HashName)
if err != nil {
return err
}
}
if hashType != hash.None {
fs.Debugf(f, "Using hash %v for ETag", hashType)
if Opt.HashType != hash.None {
fs.Debugf(f, "Using hash %v for ETag", Opt.HashType)
}
cmd.Run(false, false, command, func() error {
s := newWebDAV(context.Background(), f, &httpflags.Opt)
err := s.serve()
s, err := newWebDAV(context.Background(), f, &Opt)
if err != nil {
return err
}
err = s.serve()
if err != nil {
return err
}
@ -110,7 +132,8 @@ to see the full list.
// might apply". In particular, whether or not renaming a file or directory
// overwriting another existing file or directory is an error is OS-dependent.
type WebDAV struct {
*httplib.Server
*libhttp.Server
opt Options
f fs.Fs
_vfs *vfs.VFS // don't use directly, use getVFS
webdavhandler *webdav.Handler
@ -122,29 +145,63 @@ type WebDAV struct {
var _ webdav.FileSystem = (*WebDAV)(nil)
// Make a new WebDAV to serve the remote
func newWebDAV(ctx context.Context, f fs.Fs, opt *httplib.Options) *WebDAV {
w := &WebDAV{
func newWebDAV(ctx context.Context, f fs.Fs, opt *Options) (w *WebDAV, err error) {
w = &WebDAV{
f: f,
ctx: ctx,
opt: *opt,
}
if proxyflags.Opt.AuthProxy != "" {
w.proxy = proxy.New(ctx, &proxyflags.Opt)
// override auth
copyOpt := *opt
copyOpt.Auth = w.auth
opt = &copyOpt
w.opt.Auth.CustomAuthFn = w.auth
} else {
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{
Prefix: w.Server.Opt.BaseURL,
Prefix: w.opt.HTTP.BaseURL,
FileSystem: w,
LockSystem: webdav.NewMemLS(),
Logger: w.logRequest, // FIXME
}
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
@ -152,7 +209,7 @@ func (w *WebDAV) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) {
if w._vfs != nil {
return w._vfs, nil
}
value := ctx.Value(httplib.ContextAuthKey)
value := libhttp.CtxGetAuth(ctx)
if value == nil {
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
}
func (w *WebDAV) handler(rw http.ResponseWriter, r *http.Request) {
urlPath, ok := w.Path(rw, r)
if !ok {
return
}
func (w *WebDAV) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
isDir := strings.HasSuffix(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)
return
}
@ -217,7 +271,7 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
}
// Make the entries for display
directory := serve.NewDirectory(dirRemote, w.HTMLTemplate)
directory := serve.NewDirectory(dirRemote, w.Server.HTMLTemplate())
for _, node := range dirEntries {
if vfsflags.Opt.NoModTime {
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
func (w *WebDAV) serve() error {
err := w.Serve()
if err != nil {
return err
}
fs.Logf(w.f, "WebDav Server started on %s", w.URL())
w.Serve()
fs.Logf(w.f, "WebDav Server started on %s", w.URLs())
return nil
}
@ -276,7 +327,7 @@ func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.F
if err != nil {
return nil, err
}
return Handle{f}, nil
return Handle{Handle: f, w: w}, nil
}
// 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 {
return nil, err
}
return FileInfo{fi}, nil
return FileInfo{FileInfo: fi, w: w}, nil
}
// Handle represents an open file
type Handle struct {
vfs.Handle
w *WebDAV
}
// 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
for i := range fis {
fis[i] = FileInfo{fis[i]}
fis[i] = FileInfo{FileInfo: fis[i], w: h.w}
}
return fis, nil
}
@ -345,19 +397,20 @@ func (h Handle) Stat() (fi os.FileInfo, err error) {
if err != nil {
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
// also some additional interfaces for webdav for ETag and ContentType
type FileInfo struct {
os.FileInfo
w *WebDAV
}
// ETag returns an ETag for the FileInfo
func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
// defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err)
if hashType == hash.None {
if fi.w.opt.HashType == hash.None {
return "", webdav.ErrNotImplemented
}
node, ok := (fi.FileInfo).(vfs.Node)
@ -370,7 +423,7 @@ func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
if !ok {
return "", webdav.ErrNotImplemented
}
hash, err := o.Hash(ctx, hashType)
hash, err := o.Hash(ctx, fi.w.opt.HashType)
if err != nil || hash == "" {
return "", webdav.ErrNotImplemented
}

View File

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