serve webdav: add serve rc interface - fixes #4505

This commit is contained in:
Nick Craig-Wood 2025-03-28 18:09:03 +00:00
parent 780f4040ea
commit e37775bb41
2 changed files with 70 additions and 27 deletions

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"mime" "mime"
"net"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -21,8 +22,10 @@ import (
"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/configstruct"
"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"
"github.com/rclone/rclone/fs/rc"
libhttp "github.com/rclone/rclone/lib/http" 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/lib/systemd" "github.com/rclone/rclone/lib/systemd"
@ -70,6 +73,28 @@ func init() {
vfsflags.AddFlags(flagSet) vfsflags.AddFlags(flagSet)
proxyflags.AddFlags(flagSet) proxyflags.AddFlags(flagSet)
cmdserve.Command.AddCommand(Command) cmdserve.Command.AddCommand(Command)
cmdserve.AddRc("webdav", func(ctx context.Context, f fs.Fs, in rc.Params) (cmdserve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
if err != nil {
return nil, err
}
// Read Proxy Opts
var proxyOpt = proxy.Opt // set default opts
err = configstruct.SetAny(in, &proxyOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
if err != nil {
return nil, err
}
// Create server
return newWebDAV(ctx, f, &opt, &vfsOpt, &proxyOpt)
})
} }
// Command definition for cobra // Command definition for cobra
@ -145,17 +170,12 @@ done by the permissions on the socket.
cmd.CheckArgs(0, 0, command, args) cmd.CheckArgs(0, 0, command, args)
} }
cmd.Run(false, false, command, func() error { cmd.Run(false, false, command, func() error {
s, err := newWebDAV(context.Background(), f, &Opt) s, err := newWebDAV(context.Background(), f, &Opt, &vfscommon.Opt, &proxy.Opt)
if err != nil {
return err
}
err = s.serve()
if err != nil { if err != nil {
return err return err
} }
defer systemd.Notify()() defer systemd.Notify()()
s.Wait() return s.Serve()
return nil
}) })
return nil return nil
}, },
@ -174,7 +194,7 @@ done by the permissions on the socket.
// 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 {
*libhttp.Server server *libhttp.Server
opt Options 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
@ -188,7 +208,7 @@ 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 *Options) (w *WebDAV, err error) { func newWebDAV(ctx context.Context, f fs.Fs, opt *Options, vfsOpt *vfscommon.Options, proxyOpt *proxy.Options) (w *WebDAV, err error) {
w = &WebDAV{ w = &WebDAV{
f: f, f: f,
ctx: ctx, ctx: ctx,
@ -206,15 +226,15 @@ func newWebDAV(ctx context.Context, f fs.Fs, opt *Options) (w *WebDAV, err error
if w.etagHashType != hash.None { if w.etagHashType != hash.None {
fs.Debugf(f, "Using hash %v for ETag", w.etagHashType) fs.Debugf(f, "Using hash %v for ETag", w.etagHashType)
} }
if proxy.Opt.AuthProxy != "" { if proxyOpt.AuthProxy != "" {
w.proxy = proxy.New(ctx, &proxy.Opt, &vfscommon.Opt) w.proxy = proxy.New(ctx, proxyOpt, vfsOpt)
// override auth // override auth
w.opt.Auth.CustomAuthFn = w.auth w.opt.Auth.CustomAuthFn = w.auth
} else { } else {
w._vfs = vfs.New(f, &vfscommon.Opt) w._vfs = vfs.New(f, vfsOpt)
} }
w.Server, err = libhttp.NewServer(ctx, w.server, err = libhttp.NewServer(ctx,
libhttp.WithConfig(w.opt.HTTP), libhttp.WithConfig(w.opt.HTTP),
libhttp.WithAuth(w.opt.Auth), libhttp.WithAuth(w.opt.Auth),
libhttp.WithTemplate(w.opt.Template), libhttp.WithTemplate(w.opt.Template),
@ -234,7 +254,7 @@ func newWebDAV(ctx context.Context, f fs.Fs, opt *Options) (w *WebDAV, err error
} }
w.webdavhandler = webdavHandler w.webdavhandler = webdavHandler
router := w.Server.Router() router := w.server.Router()
router.Use( router.Use(
middleware.SetHeader("Accept-Ranges", "bytes"), middleware.SetHeader("Accept-Ranges", "bytes"),
middleware.SetHeader("Server", "rclone/"+fs.Version), middleware.SetHeader("Server", "rclone/"+fs.Version),
@ -382,7 +402,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.Server.HTMLTemplate()) directory := serve.NewDirectory(dirRemote, w.server.HTMLTemplate())
for _, node := range dirEntries { for _, node := range dirEntries {
if vfscommon.Opt.NoModTime { if vfscommon.Opt.NoModTime {
directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{}) directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{})
@ -398,15 +418,26 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str
directory.Serve(rw, r) directory.Serve(rw, r)
} }
// serve runs the http server in the background. // Serve HTTP until the server is shutdown
// //
// 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 {
w.Serve() w.server.Serve()
fs.Logf(w.f, "WebDav Server started on %s", w.URLs()) fs.Logf(w.f, "WebDav Server started on %s", w.server.URLs())
w.server.Wait()
return nil return nil
} }
// Addr returns the first address of the server
func (w *WebDAV) Addr() net.Addr {
return w.server.Addr()
}
// Shutdown the server
func (w *WebDAV) Shutdown() error {
return w.server.Shutdown()
}
// logRequest is called by the webdav module on every request // logRequest is called by the webdav module on every request
func (w *WebDAV) logRequest(r *http.Request, err error) { func (w *WebDAV) logRequest(r *http.Request, err error) {
fs.Infof(r.URL.Path, "%s from %s", r.Method, r.RemoteAddr) fs.Infof(r.URL.Path, "%s from %s", r.Method, r.RemoteAddr)

View File

@ -18,11 +18,14 @@ import (
"time" "time"
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/cmd/serve/proxy"
"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"
"github.com/rclone/rclone/fs/config/obscure" "github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/filter" "github.com/rclone/rclone/fs/filter"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/net/webdav" "golang.org/x/net/webdav"
@ -56,22 +59,23 @@ func TestWebDav(t *testing.T) {
opt.EtagHash = "MD5" opt.EtagHash = "MD5"
// Start the server // Start the server
w, err := newWebDAV(context.Background(), f, &opt) w, err := newWebDAV(context.Background(), f, &opt, &vfscommon.Opt, &proxy.Opt)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, w.serve()) go func() {
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": "rclone", "vendor": "rclone",
"url": w.Server.URLs()[0], "url": w.server.URLs()[0],
"user": testUser, "user": testUser,
"pass": obscure.MustObscure(testPass), "pass": obscure.MustObscure(testPass),
} }
return config, func() { return config, func() {
assert.NoError(t, w.Shutdown()) assert.NoError(t, w.Shutdown())
w.Wait()
} }
} }
@ -102,14 +106,15 @@ func TestHTTPFunction(t *testing.T) {
opt.Template.Path = testTemplate opt.Template.Path = testTemplate
// Start the server // Start the server
w, err := newWebDAV(context.Background(), f, &opt) w, err := newWebDAV(context.Background(), f, &opt, &vfscommon.Opt, &proxy.Opt)
assert.NoError(t, err) assert.NoError(t, err)
require.NoError(t, w.serve()) go func() {
require.NoError(t, w.Serve())
}()
defer func() { defer func() {
assert.NoError(t, w.Shutdown()) assert.NoError(t, w.Shutdown())
w.Wait()
}() }()
testURL := w.Server.URLs()[0] testURL := w.server.URLs()[0]
pause := time.Millisecond pause := time.Millisecond
i := 0 i := 0
for ; i < 10; i++ { for ; i < 10; i++ {
@ -259,3 +264,10 @@ func HelpTestGET(t *testing.T, testURL string) {
checkGolden(t, test.Golden, body) checkGolden(t, test.Golden, body)
} }
} }
func TestRc(t *testing.T) {
servetest.TestRc(t, rc.Params{
"type": "webdav",
"vfs_cache_mode": "off",
})
}