diff --git a/cmd/serve/http/http.go b/cmd/serve/http/http.go
index 7e66f3509..7c71eb312 100644
--- a/cmd/serve/http/http.go
+++ b/cmd/serve/http/http.go
@@ -1,8 +1,6 @@
package http
import (
- "fmt"
- "html/template"
"net/http"
"os"
"path"
@@ -12,9 +10,9 @@ import (
"github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/cmd/serve/httplib"
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
+ "github.com/ncw/rclone/cmd/serve/httplib/serve"
"github.com/ncw/rclone/fs"
"github.com/ncw/rclone/fs/accounting"
- "github.com/ncw/rclone/lib/rest"
"github.com/ncw/rclone/vfs"
"github.com/ncw/rclone/vfs/vfsflags"
"github.com/spf13/cobra"
@@ -105,62 +103,6 @@ func (s *server) handler(w http.ResponseWriter, r *http.Request) {
}
}
-// entry is a directory entry
-type entry struct {
- remote string
- URL string
- Leaf string
-}
-
-// entries represents a directory
-type entries []entry
-
-// addEntry adds an entry to that directory
-func (es *entries) addEntry(node interface {
- Path() string
- Name() string
- IsDir() bool
-}) {
- remote := node.Path()
- leaf := node.Name()
- urlRemote := leaf
- if node.IsDir() {
- leaf += "/"
- urlRemote += "/"
- }
- *es = append(*es, entry{remote: remote, URL: rest.URLPathEscape(urlRemote), Leaf: leaf})
-}
-
-// indexPage is a directory listing template
-var indexPage = `
-
-
-
-{{ .Title }}
-
-
-{{ .Title }}
-{{ range $i := .Entries }}{{ $i.Leaf }}
-{{ end }}
-
-`
-
-// indexTemplate is the instantiated indexPage
-var indexTemplate = template.Must(template.New("index").Parse(indexPage))
-
-// indexData is used to fill in the indexTemplate
-type indexData struct {
- Title string
- Entries entries
-}
-
-// error returns an http.StatusInternalServerError and logs the error
-func internalError(what interface{}, w http.ResponseWriter, text string, err error) {
- fs.CountError(err)
- fs.Errorf(what, "%s: %v", text, err)
- http.Error(w, text+".", http.StatusInternalServerError)
-}
-
// serveDir serves a directory index at dirRemote
func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote string) {
// List the directory
@@ -169,7 +111,7 @@ func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote stri
http.Error(w, "Directory not found", http.StatusNotFound)
return
} else if err != nil {
- internalError(dirRemote, w, "Failed to list directory", err)
+ serve.Error(dirRemote, w, "Failed to list directory", err)
return
}
if !node.IsDir() {
@@ -179,28 +121,17 @@ func (s *server) serveDir(w http.ResponseWriter, r *http.Request, dirRemote stri
dir := node.(*vfs.Dir)
dirEntries, err := dir.ReadDirAll()
if err != nil {
- internalError(dirRemote, w, "Failed to list directory", err)
+ serve.Error(dirRemote, w, "Failed to list directory", err)
return
}
- var out entries
+ // Make the entries for display
+ directory := serve.NewDirectory(dirRemote)
for _, node := range dirEntries {
- out.addEntry(node)
+ directory.AddEntry(node.Path(), node.IsDir())
}
- // Account the transfer
- accounting.Stats.Transferring(dirRemote)
- defer accounting.Stats.DoneTransferring(dirRemote, true)
-
- fs.Infof(dirRemote, "%s: Serving directory", r.RemoteAddr)
- err = indexTemplate.Execute(w, indexData{
- Entries: out,
- Title: fmt.Sprintf("Directory listing of /%s", dirRemote),
- })
- if err != nil {
- internalError(dirRemote, w, "Failed to render template", err)
- return
- }
+ directory.Serve(w, r)
}
// serveFile serves a file object at remote
@@ -211,7 +142,7 @@ func (s *server) serveFile(w http.ResponseWriter, r *http.Request, remote string
http.Error(w, "File not found", http.StatusNotFound)
return
} else if err != nil {
- internalError(remote, w, "Failed to find file", err)
+ serve.Error(remote, w, "Failed to find file", err)
return
}
if !node.IsFile() {
@@ -245,7 +176,7 @@ func (s *server) serveFile(w http.ResponseWriter, r *http.Request, remote string
// open the object
in, err := file.Open(os.O_RDONLY)
if err != nil {
- internalError(remote, w, "Failed to open file", err)
+ serve.Error(remote, w, "Failed to open file", err)
return
}
defer func() {
diff --git a/cmd/serve/http/http_test.go b/cmd/serve/http/http_test.go
index bc98c7111..fafd4cfe1 100644
--- a/cmd/serve/http/http_test.go
+++ b/cmd/serve/http/http_test.go
@@ -7,7 +7,6 @@ import (
"io/ioutil"
"net"
"net/http"
- "path"
"strings"
"testing"
"time"
@@ -202,36 +201,6 @@ func TestGET(t *testing.T) {
}
}
-type mockNode struct {
- path string
- isdir bool
-}
-
-func (n mockNode) Path() string { return n.path }
-func (n mockNode) Name() string {
- if n.path == "" {
- return ""
- }
- return path.Base(n.path)
-}
-func (n mockNode) IsDir() bool { return n.isdir }
-
-func TestAddEntry(t *testing.T) {
- var es entries
- es.addEntry(mockNode{path: "", isdir: true})
- es.addEntry(mockNode{path: "dir", isdir: true})
- es.addEntry(mockNode{path: "a/b/c/d.txt", isdir: false})
- es.addEntry(mockNode{path: "a/b/c/colon:colon.txt", isdir: false})
- es.addEntry(mockNode{path: "\"quotes\".txt", isdir: false})
- assert.Equal(t, entries{
- {remote: "", URL: "/", Leaf: "/"},
- {remote: "dir", URL: "dir/", Leaf: "dir/"},
- {remote: "a/b/c/d.txt", URL: "d.txt", Leaf: "d.txt"},
- {remote: "a/b/c/colon:colon.txt", URL: "./colon:colon.txt", Leaf: "colon:colon.txt"},
- {remote: "\"quotes\".txt", URL: "%22quotes%22.txt", Leaf: "\"quotes\".txt"},
- }, es)
-}
-
func TestFinalise(t *testing.T) {
httpServer.Close()
httpServer.Wait()
diff --git a/cmd/serve/httplib/serve/dir.go b/cmd/serve/httplib/serve/dir.go
new file mode 100644
index 000000000..e27a7ca66
--- /dev/null
+++ b/cmd/serve/httplib/serve/dir.go
@@ -0,0 +1,102 @@
+package serve
+
+import (
+ "fmt"
+ "html/template"
+ "net/http"
+ "net/url"
+ "path"
+
+ "github.com/ncw/rclone/fs"
+ "github.com/ncw/rclone/fs/accounting"
+ "github.com/ncw/rclone/lib/rest"
+)
+
+// DirEntry is a directory entry
+type DirEntry struct {
+ remote string
+ URL string
+ Leaf string
+}
+
+// Directory represents a directory
+type Directory struct {
+ DirRemote string
+ Title string
+ Entries []DirEntry
+ Query string
+}
+
+// NewDirectory makes an empty Directory
+func NewDirectory(dirRemote string) *Directory {
+ d := &Directory{
+ DirRemote: dirRemote,
+ Title: fmt.Sprintf("Directory listing of /%s", dirRemote),
+ }
+ return d
+}
+
+// SetQuery sets the query parameters for each URL
+func (d *Directory) SetQuery(queryParams url.Values) *Directory {
+ d.Query = ""
+ if len(queryParams) > 0 {
+ d.Query = "?" + queryParams.Encode()
+ }
+ return d
+}
+
+// AddEntry adds an entry to that directory
+func (d *Directory) AddEntry(remote string, isDir bool) {
+ leaf := path.Base(remote)
+ if leaf == "." {
+ leaf = ""
+ }
+ urlRemote := leaf
+ if isDir {
+ leaf += "/"
+ urlRemote += "/"
+ }
+ d.Entries = append(d.Entries, DirEntry{
+ remote: remote,
+ URL: rest.URLPathEscape(urlRemote) + d.Query,
+ Leaf: leaf,
+ })
+}
+
+// Error returns an http.StatusInternalServerError and logs the error
+func Error(what interface{}, w http.ResponseWriter, text string, err error) {
+ fs.CountError(err)
+ fs.Errorf(what, "%s: %v", text, err)
+ http.Error(w, text+".", http.StatusInternalServerError)
+}
+
+// Serve serves a directory
+func (d *Directory) Serve(w http.ResponseWriter, r *http.Request) {
+ // Account the transfer
+ accounting.Stats.Transferring(d.DirRemote)
+ defer accounting.Stats.DoneTransferring(d.DirRemote, true)
+
+ fs.Infof(d.DirRemote, "%s: Serving directory", r.RemoteAddr)
+ err := indexTemplate.Execute(w, d)
+ if err != nil {
+ Error(d.DirRemote, w, "Failed to render template", err)
+ return
+ }
+}
+
+// indexPage is a directory listing template
+var indexPage = `
+
+
+
+{{ .Title }}
+
+
+{{ .Title }}
+{{ range $i := .Entries }}{{ $i.Leaf }}
+{{ end }}
+
+`
+
+// indexTemplate is the instantiated indexPage
+var indexTemplate = template.Must(template.New("index").Parse(indexPage))
diff --git a/cmd/serve/httplib/serve/dir_test.go b/cmd/serve/httplib/serve/dir_test.go
new file mode 100644
index 000000000..e4d0ffec4
--- /dev/null
+++ b/cmd/serve/httplib/serve/dir_test.go
@@ -0,0 +1,88 @@
+package serve
+
+import (
+ "errors"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewDirectory(t *testing.T) {
+ d := NewDirectory("z")
+ assert.Equal(t, "z", d.DirRemote)
+ assert.Equal(t, "Directory listing of /z", d.Title)
+}
+
+func TestSetQuery(t *testing.T) {
+ d := NewDirectory("z")
+ assert.Equal(t, "", d.Query)
+ d.SetQuery(url.Values{"potato": []string{"42"}})
+ assert.Equal(t, "?potato=42", d.Query)
+ d.SetQuery(url.Values{})
+ assert.Equal(t, "", d.Query)
+}
+
+func TestAddEntry(t *testing.T) {
+ var d = NewDirectory("z")
+ d.AddEntry("", true)
+ d.AddEntry("dir", true)
+ d.AddEntry("a/b/c/d.txt", false)
+ d.AddEntry("a/b/c/colon:colon.txt", false)
+ d.AddEntry("\"quotes\".txt", false)
+ assert.Equal(t, []DirEntry{
+ {remote: "", URL: "/", Leaf: "/"},
+ {remote: "dir", URL: "dir/", Leaf: "dir/"},
+ {remote: "a/b/c/d.txt", URL: "d.txt", Leaf: "d.txt"},
+ {remote: "a/b/c/colon:colon.txt", URL: "./colon:colon.txt", Leaf: "colon:colon.txt"},
+ {remote: "\"quotes\".txt", URL: "%22quotes%22.txt", Leaf: "\"quotes\".txt"},
+ }, d.Entries)
+
+ // Now test with a query parameter
+ d = NewDirectory("z").SetQuery(url.Values{"potato": []string{"42"}})
+ d.AddEntry("file", false)
+ d.AddEntry("dir", true)
+ assert.Equal(t, []DirEntry{
+ {remote: "file", URL: "file?potato=42", Leaf: "file"},
+ {remote: "dir", URL: "dir/?potato=42", Leaf: "dir/"},
+ }, d.Entries)
+}
+
+func TestError(t *testing.T) {
+ w := httptest.NewRecorder()
+ err := errors.New("help")
+ Error("potato", w, "sausage", err)
+ resp := w.Result()
+ assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
+ body, _ := ioutil.ReadAll(resp.Body)
+ assert.Equal(t, "sausage.\n", string(body))
+}
+
+func TestServe(t *testing.T) {
+ d := NewDirectory("aDirectory")
+ d.AddEntry("file", false)
+ d.AddEntry("dir", true)
+
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://example.com/aDirectory/", nil)
+ d.Serve(w, r)
+ resp := w.Result()
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ body, _ := ioutil.ReadAll(resp.Body)
+ assert.Equal(t, `
+
+
+
+Directory listing of /aDirectory
+
+
+Directory listing of /aDirectory
+file
+dir/
+
+
+`, string(body))
+}
diff --git a/cmd/serve/httplib/serve/serve.go b/cmd/serve/httplib/serve/serve.go
index dfd98210a..cc16affc8 100644
--- a/cmd/serve/httplib/serve/serve.go
+++ b/cmd/serve/httplib/serve/serve.go
@@ -15,8 +15,12 @@ import (
func Object(w http.ResponseWriter, r *http.Request, o fs.Object) {
if r.Method != "HEAD" && r.Method != "GET" {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+ return
}
+ // Show that we accept ranges
+ w.Header().Set("Accept-Ranges", "bytes")
+
// Set content length since we know how long the object is
if o.Size() >= 0 {
w.Header().Set("Content-Length", strconv.FormatInt(o.Size(), 10))
diff --git a/cmd/serve/httplib/serve/serve_test.go b/cmd/serve/httplib/serve/serve_test.go
new file mode 100644
index 000000000..b190ed66c
--- /dev/null
+++ b/cmd/serve/httplib/serve/serve_test.go
@@ -0,0 +1,76 @@
+package serve
+
+import (
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/ncw/rclone/fstest/mockobject"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestObjectBadMethod(t *testing.T) {
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("BADMETHOD", "http://example.com/aFile", nil)
+ o := mockobject.New("aFile")
+ Object(w, r, o)
+ resp := w.Result()
+ assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
+ body, _ := ioutil.ReadAll(resp.Body)
+ assert.Equal(t, "Method Not Allowed\n", string(body))
+}
+
+func TestObjectHEAD(t *testing.T) {
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("HEAD", "http://example.com/aFile", nil)
+ o := mockobject.New("aFile").WithContent([]byte("hello"), mockobject.SeekModeNone)
+ Object(w, r, o)
+ resp := w.Result()
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ assert.Equal(t, "5", resp.Header.Get("Content-Length"))
+ assert.Equal(t, "bytes", resp.Header.Get("Accept-Ranges"))
+ body, _ := ioutil.ReadAll(resp.Body)
+ assert.Equal(t, "", string(body))
+}
+
+func TestObjectGET(t *testing.T) {
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://example.com/aFile", nil)
+ o := mockobject.New("aFile").WithContent([]byte("hello"), mockobject.SeekModeNone)
+ Object(w, r, o)
+ resp := w.Result()
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ assert.Equal(t, "5", resp.Header.Get("Content-Length"))
+ assert.Equal(t, "bytes", resp.Header.Get("Accept-Ranges"))
+ body, _ := ioutil.ReadAll(resp.Body)
+ assert.Equal(t, "hello", string(body))
+}
+
+func TestObjectRange(t *testing.T) {
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://example.com/aFile", nil)
+ r.Header.Add("Range", "bytes=3-5")
+ o := mockobject.New("aFile").WithContent([]byte("0123456789"), mockobject.SeekModeNone)
+ Object(w, r, o)
+ resp := w.Result()
+ assert.Equal(t, http.StatusPartialContent, resp.StatusCode)
+ assert.Equal(t, "3", resp.Header.Get("Content-Length"))
+ assert.Equal(t, "bytes", resp.Header.Get("Accept-Ranges"))
+ assert.Equal(t, "bytes 3-5/10", resp.Header.Get("Content-Range"))
+ body, _ := ioutil.ReadAll(resp.Body)
+ assert.Equal(t, "345", string(body))
+}
+
+func TestObjectBadRange(t *testing.T) {
+ w := httptest.NewRecorder()
+ r := httptest.NewRequest("GET", "http://example.com/aFile", nil)
+ r.Header.Add("Range", "xxxbytes=3-5")
+ o := mockobject.New("aFile").WithContent([]byte("0123456789"), mockobject.SeekModeNone)
+ Object(w, r, o)
+ resp := w.Result()
+ assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
+ assert.Equal(t, "10", resp.Header.Get("Content-Length"))
+ body, _ := ioutil.ReadAll(resp.Body)
+ assert.Equal(t, "Bad Request\n", string(body))
+}