diff --git a/cmd/serve/http/http.go b/cmd/serve/http/http.go index 0acc5149f..e45c0ab85 100644 --- a/cmd/serve/http/http.go +++ b/cmd/serve/http/http.go @@ -41,9 +41,10 @@ var OptionsInfo = fs.Options{}. // Options required for http server type Options struct { - Auth libhttp.AuthConfig - HTTP libhttp.Config - Template libhttp.TemplateConfig + Auth libhttp.AuthConfig + HTTP libhttp.Config + Template libhttp.TemplateConfig + DisableZip bool } // DefaultOpt is the default values used for Options @@ -69,6 +70,7 @@ func init() { flags.AddFlagsFromOptions(flagSet, "", OptionsInfo) vfsflags.AddFlags(flagSet) proxyflags.AddFlags(flagSet) + flagSet.BoolVar(&Opt.DisableZip, "disable-zip", false, "Disable zip download of directories") cmdserve.Command.AddCommand(Command) cmdserve.AddRc("http", func(ctx context.Context, f fs.Fs, in rc.Params) (cmdserve.Handle, error) { // Read VFS Opts @@ -257,6 +259,24 @@ func (s *HTTP) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string return } dir := node.(*vfs.Dir) + + if r.URL.Query().Get("download") == "zip" && !s.opt.DisableZip { + fs.Infof(dirRemote, "%s: Zipping directory", r.RemoteAddr) + zipName := path.Base(dirRemote) + if dirRemote == "" { + zipName = "root" + } + w.Header().Set("Content-Disposition", "attachment; filename=\""+zipName+".zip\"") + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) + err := vfs.CreateZip(ctx, dir, w) + if err != nil { + serve.Error(ctx, dirRemote, w, "Failed to create zip", err) + return + } + return + } + dirEntries, err := dir.ReadDirAll() if err != nil { serve.Error(ctx, dirRemote, w, "Failed to list directory", err) @@ -280,6 +300,8 @@ func (s *HTTP) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string // Set the Last-Modified header to the timestamp w.Header().Set("Last-Modified", dir.ModTime().UTC().Format(http.TimeFormat)) + directory.DisableZip = s.opt.DisableZip + directory.Serve(w, r) } diff --git a/cmd/serve/http/http_test.go b/cmd/serve/http/http_test.go index 3044c141a..5d8ccdc06 100644 --- a/cmd/serve/http/http_test.go +++ b/cmd/serve/http/http_test.go @@ -4,6 +4,7 @@ import ( "context" "flag" "io" + stdfs "io/fs" "net/http" "os" "path/filepath" @@ -75,6 +76,16 @@ func start(ctx context.Context, t *testing.T, f fs.Fs) (s *HTTP, testURL string) return s, testURL } +// setAllModTimes walks root and sets atime/mtime to t for every file & directory. +func setAllModTimes(root string, t time.Time) error { + return filepath.WalkDir(root, func(path string, d stdfs.DirEntry, err error) error { + if err != nil { + return err + } + return os.Chtimes(path, t, t) + }) +} + var ( datedObject = "two.txt" expectedTime = time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC) @@ -123,6 +134,8 @@ func testGET(t *testing.T, useProxy bool) { f = nil } else { + // set all the mod times to expectedTime + require.NoError(t, setAllModTimes("testdata/files", expectedTime)) // Create a test Fs var err error f, err = fs.NewFs(context.Background(), "testdata/files") @@ -233,6 +246,16 @@ func testGET(t *testing.T, useProxy bool) { Range: "bytes=3-", Golden: "testdata/golden/two3-.txt", }, + { + URL: "/?download=zip", + Status: http.StatusOK, + Golden: "testdata/golden/root.zip", + }, + { + URL: "/three/?download=zip", + Status: http.StatusOK, + Golden: "testdata/golden/three.zip", + }, } { method := test.Method if method == "" { diff --git a/cmd/serve/http/testdata/golden/root.zip b/cmd/serve/http/testdata/golden/root.zip new file mode 100644 index 000000000..a3a710df4 Binary files /dev/null and b/cmd/serve/http/testdata/golden/root.zip differ diff --git a/cmd/serve/http/testdata/golden/three.zip b/cmd/serve/http/testdata/golden/three.zip new file mode 100644 index 000000000..bcffe9819 Binary files /dev/null and b/cmd/serve/http/testdata/golden/three.zip differ diff --git a/lib/http/serve/dir.go b/lib/http/serve/dir.go index ae1ead6a0..fd53256b3 100644 --- a/lib/http/serve/dir.go +++ b/lib/http/serve/dir.go @@ -21,6 +21,7 @@ import ( type DirEntry struct { remote string URL string + ZipURL string Leaf string IsDir bool Size int64 @@ -32,6 +33,8 @@ type Directory struct { DirRemote string Title string Name string + ZipURL string + DisableZip bool Entries []DirEntry Query string HTMLTemplate *template.Template @@ -70,6 +73,7 @@ func NewDirectory(dirRemote string, htmlTemplate *template.Template) *Directory DirRemote: dirRemote, Title: fmt.Sprintf("Directory listing of /%s", dirRemote), Name: fmt.Sprintf("/%s", dirRemote), + ZipURL: "?download=zip", HTMLTemplate: htmlTemplate, Breadcrumb: breadcrumb, } @@ -99,11 +103,15 @@ func (d *Directory) AddHTMLEntry(remote string, isDir bool, size int64, modTime d.Entries = append(d.Entries, DirEntry{ remote: remote, URL: rest.URLPathEscape(urlRemote) + d.Query, + ZipURL: "", Leaf: leaf, IsDir: isDir, Size: size, ModTime: modTime, }) + if isDir { + d.Entries[len(d.Entries)-1].ZipURL = rest.URLPathEscape(urlRemote) + "?download=zip" + } } // AddEntry adds an entry to that directory diff --git a/lib/http/serve/dir_test.go b/lib/http/serve/dir_test.go index 19ab22f57..3fd146724 100644 --- a/lib/http/serve/dir_test.go +++ b/lib/http/serve/dir_test.go @@ -46,11 +46,11 @@ func TestAddHTMLEntry(t *testing.T) { d.AddHTMLEntry("a/b/c/colon:colon.txt", false, 64, modtime) d.AddHTMLEntry("\"quotes\".txt", false, 64, modtime) assert.Equal(t, []DirEntry{ - {remote: "", URL: "/", Leaf: "/", IsDir: true, Size: 0, ModTime: modtime}, - {remote: "dir", URL: "dir/", Leaf: "dir/", IsDir: true, Size: 0, ModTime: modtime}, - {remote: "a/b/c/d.txt", URL: "d.txt", Leaf: "d.txt", IsDir: false, Size: 64, ModTime: modtime}, - {remote: "a/b/c/colon:colon.txt", URL: "./colon:colon.txt", Leaf: "colon:colon.txt", IsDir: false, Size: 64, ModTime: modtime}, - {remote: "\"quotes\".txt", URL: "%22quotes%22.txt", Leaf: "\"quotes\".txt", Size: 64, IsDir: false, ModTime: modtime}, + {remote: "", URL: "/", ZipURL: "/?download=zip", Leaf: "/", IsDir: true, Size: 0, ModTime: modtime}, + {remote: "dir", URL: "dir/", ZipURL: "dir/?download=zip", Leaf: "dir/", IsDir: true, Size: 0, ModTime: modtime}, + {remote: "a/b/c/d.txt", URL: "d.txt", ZipURL: "", Leaf: "d.txt", IsDir: false, Size: 64, ModTime: modtime}, + {remote: "a/b/c/colon:colon.txt", URL: "./colon:colon.txt", ZipURL: "", Leaf: "colon:colon.txt", IsDir: false, Size: 64, ModTime: modtime}, + {remote: "\"quotes\".txt", URL: "%22quotes%22.txt", ZipURL: "", Leaf: "\"quotes\".txt", Size: 64, IsDir: false, ModTime: modtime}, }, d.Entries) // Now test with a query parameter @@ -58,8 +58,8 @@ func TestAddHTMLEntry(t *testing.T) { d.AddHTMLEntry("file", false, 64, modtime) d.AddHTMLEntry("dir", true, 0, modtime) assert.Equal(t, []DirEntry{ - {remote: "file", URL: "file?potato=42", Leaf: "file", IsDir: false, Size: 64, ModTime: modtime}, - {remote: "dir", URL: "dir/?potato=42", Leaf: "dir/", IsDir: true, Size: 0, ModTime: modtime}, + {remote: "file", URL: "file?potato=42", ZipURL: "", Leaf: "file", IsDir: false, Size: 64, ModTime: modtime}, + {remote: "dir", URL: "dir/?potato=42", ZipURL: "dir/?download=zip", Leaf: "dir/", IsDir: true, Size: 0, ModTime: modtime}, }, d.Entries) } diff --git a/lib/http/server.go b/lib/http/server.go index 25e11f269..83d0162dd 100644 --- a/lib/http/server.go +++ b/lib/http/server.go @@ -59,6 +59,8 @@ inserts leading and trailing "/" on ` + "`--{{ .Prefix }}baseurl`" + `, so ` + " ` + "`--{{ .Prefix }}baseurl \"/rclone\"` and `--{{ .Prefix }}baseurl \"/rclone/\"`" + ` are all treated identically. +` + "`--{{ .Prefix }}disable-zip`" + ` may be set to disable the zipping download option. + #### TLS (SSL) By default this will serve over http. If you want you can serve over diff --git a/lib/http/templates/index.html b/lib/http/templates/index.html index 348050c02..cec58f1d7 100644 --- a/lib/http/templates/index.html +++ b/lib/http/templates/index.html @@ -21,7 +21,7 @@ Modifications: Adapted to rclone markup --> -
@@ -206,6 +219,9 @@ footer {