forked from TrueCloudLab/rclone
serve webdav: refactor to use lib/http
This commit is contained in:
parent
08a1ca434b
commit
4444d2d102
2 changed files with 123 additions and 69 deletions
|
@ -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 = ©Opt
|
|
||||||
} 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++ {
|
||||||
|
|
Loading…
Add table
Reference in a new issue