diff --git a/swarm/api/api.go b/swarm/api/api.go index a5941fb5cd..79de29a1cf 100644 --- a/swarm/api/api.go +++ b/swarm/api/api.go @@ -144,9 +144,13 @@ func (self *Api) Get(key storage.Key, path string) (reader storage.LazySectionRe if entry != nil { key = common.Hex2Bytes(entry.Hash) status = entry.Status - mimeType = entry.ContentType - log.Trace(fmt.Sprintf("content lookup key: '%v' (%v)", key, mimeType)) - reader = self.dpa.Retrieve(key) + if status == http.StatusMultipleChoices { + return + } else { + mimeType = entry.ContentType + log.Trace(fmt.Sprintf("content lookup key: '%v' (%v)", key, mimeType)) + reader = self.dpa.Retrieve(key) + } } else { status = http.StatusNotFound err = fmt.Errorf("manifest entry for '%s' not found", path) diff --git a/swarm/api/http/error.go b/swarm/api/http/error.go index ebb5e3ebe2..b4d46b3c48 100644 --- a/swarm/api/http/error.go +++ b/swarm/api/http/error.go @@ -25,9 +25,11 @@ import ( "fmt" "html/template" "net/http" + "strings" "time" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/swarm/api" ) //templateMap holds a mapping of an HTTP error code to a template @@ -51,12 +53,14 @@ func initErrHandling() { //pages are saved as strings - get these strings genErrPage := GetGenericErrorPage() notFoundPage := GetNotFoundErrorPage() + multipleChoicesPage := GetMultipleChoicesErrorPage() //map the codes to the available pages tnames := map[int]string{ - 0: genErrPage, //default - 400: genErrPage, - 404: notFoundPage, - 500: genErrPage, + 0: genErrPage, //default + http.StatusBadRequest: genErrPage, + http.StatusNotFound: notFoundPage, + http.StatusMultipleChoices: multipleChoicesPage, + http.StatusInternalServerError: genErrPage, } templateMap = make(map[int]*template.Template) for code, tname := range tnames { @@ -65,6 +69,40 @@ func initErrHandling() { } } +//ShowMultipeChoices is used when a user requests a resource in a manifest which results +//in ambiguous results. It returns a HTML page with clickable links of each of the entry +//in the manifest which fits the request URI ambiguity. +//For example, if the user requests bzz://read and that manifest containes entries +//"readme.md" and "readinglist.txt", a HTML page is returned with this two links. +//This only applies if the manifest has no default entry +func ShowMultipleChoices(w http.ResponseWriter, r *http.Request, list api.ManifestList) { + msg := "" + if list.Entries == nil { + ShowError(w, r, "Internal Server Error", http.StatusInternalServerError) + return + } + //make links relative + //requestURI comes with the prefix of the ambiguous path, e.g. "read" for "readme.md" and "readinglist.txt" + //to get clickable links, need to remove the ambiguous path, i.e. "read" + idx := strings.LastIndex(r.RequestURI, "/") + if idx == -1 { + ShowError(w, r, "Internal Server Error", http.StatusInternalServerError) + return + } + //remove ambiguous part + base := r.RequestURI[:idx+1] + for _, e := range list.Entries { + //create clickable link for each entry + msg += "" + e.Path + "
" + } + respond(w, r, &ErrorParams{ + Code: http.StatusMultipleChoices, + Details: template.HTML(msg), + Timestamp: time.Now().Format(time.RFC1123), + template: getTemplate(http.StatusMultipleChoices), + }) +} + //ShowError is used to show an HTML error page to a client. //If there is an `Accept` header of `application/json`, JSON will be returned instead //The function just takes a string message which will be displayed in the error page. diff --git a/swarm/api/http/error_templates.go b/swarm/api/http/error_templates.go index 29bd3bfbb1..2c20ba8f99 100644 --- a/swarm/api/http/error_templates.go +++ b/swarm/api/http/error_templates.go @@ -165,7 +165,7 @@ func GetGenericErrorPage() string { - {{.Msg}} + {{.Msg}} @@ -339,7 +339,184 @@ func GetNotFoundErrorPage() string { - {{.Msg}} + {{.Msg}} + + + + + + Error code: + + + + + {{.Code}} + + + + + + + + + + + + + + + +` + return page +} + +//This returns the HTML for a page listing disambiguation options +//i.e. if user requested bzz://read and the manifest contains "readme.md" and "readinglist.txt", +//this page is returned with a clickable list the existing disambiguation links in the manifest +func GetMultipleChoicesErrorPage() string { + page := ` + + + + + + + + + + + + Swarm::HTTP Disambiguation Page + + + + +
+ +
+
+ +
+
+

Swarm: disambiguation

+
+
+
{{.Timestamp}}
+
+
+ + +
+ + + + + + + + + + diff --git a/swarm/api/http/server.go b/swarm/api/http/server.go index a637b87351..65f6afab72 100644 --- a/swarm/api/http/server.go +++ b/swarm/api/http/server.go @@ -441,14 +441,37 @@ func (s *Server) HandleGetList(w http.ResponseWriter, r *Request) { return } - walker, err := s.api.NewManifestWalker(key, nil) + list, err := s.getManifestList(key, r.uri.Path) + if err != nil { s.Error(w, r, err) return } - var list api.ManifestList - prefix := r.uri.Path + // if the client wants HTML (e.g. a browser) then render the list as a + // HTML index with relative URLs + if strings.Contains(r.Header.Get("Accept"), "text/html") { + w.Header().Set("Content-Type", "text/html") + err := htmlListTemplate.Execute(w, &htmlListData{ + URI: r.uri, + List: &list, + }) + if err != nil { + s.logError("error rendering list HTML: %s", err) + } + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(&list) +} + +func (s *Server) getManifestList(key storage.Key, prefix string) (list api.ManifestList, err error) { + walker, err := s.api.NewManifestWalker(key, nil) + if err != nil { + return + } + err = walker.Walk(func(entry *api.ManifestEntry) error { // handle non-manifest files if entry.ContentType != api.ManifestType { @@ -495,27 +518,8 @@ func (s *Server) HandleGetList(w http.ResponseWriter, r *Request) { // so just skip it return api.SkipManifest }) - if err != nil { - s.Error(w, r, err) - return - } - // if the client wants HTML (e.g. a browser) then render the list as a - // HTML index with relative URLs - if strings.Contains(r.Header.Get("Accept"), "text/html") { - w.Header().Set("Content-Type", "text/html") - err := htmlListTemplate.Execute(w, &htmlListData{ - URI: r.uri, - List: &list, - }) - if err != nil { - s.logError("error rendering list HTML: %s", err) - } - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&list) + return list, nil } // HandleGetFile handles a GET request to bzz:/// and responds @@ -544,6 +548,22 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *Request) { return } + //the request results in ambiguous files + //e.g. /read with readme.md and readinglist.txt available in manifest + if status == http.StatusMultipleChoices { + list, err := s.getManifestList(key, r.uri.Path) + + if err != nil { + s.Error(w, r, err) + return + } + + s.logDebug(fmt.Sprintf("Multiple choices! --> %v", list)) + //show a nice page links to available entries + ShowMultipleChoices(w, &r.Request, list) + return + } + // check the root chunk exists by retrieving the file's size if _, err := reader.Size(nil); err != nil { s.NotFound(w, r, fmt.Errorf("File not found %s: %s", r.uri, err)) diff --git a/swarm/api/manifest.go b/swarm/api/manifest.go index 90f287677b..d3eced1980 100644 --- a/swarm/api/manifest.go +++ b/swarm/api/manifest.go @@ -22,6 +22,8 @@ import ( "errors" "fmt" "io" + "net/http" + "strings" "sync" "time" @@ -422,25 +424,47 @@ func (self *manifestTrie) findPrefixOf(path string, quitC chan bool) (entry *man return self.entries[256], 0 } + //see if first char is in manifest entries b := byte(path[0]) entry = self.entries[b] if entry == nil { return self.entries[256], 0 } + epl := len(entry.Path) log.Trace(fmt.Sprintf("path = %v entry.Path = %v epl = %v", path, entry.Path, epl)) - if (len(path) >= epl) && (path[:epl] == entry.Path) { + if len(path) <= epl { + if entry.Path[:len(path)] == path { + if entry.ContentType == ManifestType { + entry.Status = http.StatusMultipleChoices + } + pos = len(path) + return + } + return nil, 0 + } + if path[:epl] == entry.Path { log.Trace(fmt.Sprintf("entry.ContentType = %v", entry.ContentType)) - if entry.ContentType == ManifestType { + //the subentry is a manifest, load subtrie + if entry.ContentType == ManifestType && (strings.Contains(entry.Path, path) || strings.Contains(path, entry.Path)) { err := self.loadSubTrie(entry, quitC) if err != nil { return nil, 0 } - entry, pos = entry.subtrie.findPrefixOf(path[epl:], quitC) - if entry != nil { + sub, pos := entry.subtrie.findPrefixOf(path[epl:], quitC) + if sub != nil { + entry = sub pos += epl + return sub, pos + } else if path == entry.Path { + entry.Status = http.StatusMultipleChoices } + } else { + //entry is not a manifest, return it + if path != entry.Path { + return nil, 0 + } pos = epl } } diff --git a/swarm/api/manifest_test.go b/swarm/api/manifest_test.go index 0208848a36..f048627c51 100644 --- a/swarm/api/manifest_test.go +++ b/swarm/api/manifest_test.go @@ -17,7 +17,6 @@ package api import ( - // "encoding/json" "bytes" "encoding/json" "fmt" @@ -72,11 +71,21 @@ func TestGetEntry(t *testing.T) { testGetEntry(t, "/a", "", "") testGetEntry(t, "/a/b", "a/b", "a/b") // longest/deepest math - testGetEntry(t, "a/b", "-", "a", "a/ba", "a/b/c") + testGetEntry(t, "read", "read", "readme.md", "readit.md") + testGetEntry(t, "rf", "-", "readme.md", "readit.md") + testGetEntry(t, "readme", "readme", "readme.md") + testGetEntry(t, "readme", "-", "readit.md") + testGetEntry(t, "readme.md", "readme.md", "readme.md") + testGetEntry(t, "readme.md", "-", "readit.md") + testGetEntry(t, "readmeAmd", "-", "readit.md") + testGetEntry(t, "readme.mdffff", "-", "readme.md") + testGetEntry(t, "ab", "ab", "ab/cefg", "ab/cedh", "ab/kkkkkk") + testGetEntry(t, "ab/ce", "ab/ce", "ab/cefg", "ab/cedh", "ab/ceuuuuuuuuuu") + testGetEntry(t, "abc", "abc", "abcd", "abczzzzef", "abc/def", "abc/e/g") + testGetEntry(t, "a/b", "a/b", "a", "a/bc", "a/ba", "a/b/c") testGetEntry(t, "a/b", "a/b", "a", "a/b", "a/bb", "a/b/c") testGetEntry(t, "//a//b//", "a/b", "a", "a/b", "a/bb", "a/b/c") } - func TestDeleteEntry(t *testing.T) { }
+ Your request yields ambiguous results! +
+ Your request may refer to: +
+ {{ .Details}}