diff options
| author | Matthew Holt <mholt@users.noreply.github.com> | 2019-05-20 15:46:34 -0600 | 
|---|---|---|
| committer | Matthew Holt <mholt@users.noreply.github.com> | 2019-05-20 15:46:52 -0600 | 
| commit | 22995e5655b3d5117743d98bf5c7ba8ed335a2c5 (patch) | |
| tree | f13543b62f6b6d082a845355ccc30dcbec3bf582 /modules/caddyhttp/staticfiles/browse.go | |
| parent | 043eb1d9e5db456b9b78c0423cb44716fc81a932 (diff) | |
Implement most of browse; fix a couple obvious bugs; some cleanup
Diffstat (limited to 'modules/caddyhttp/staticfiles/browse.go')
| -rw-r--r-- | modules/caddyhttp/staticfiles/browse.go | 308 | 
1 files changed, 130 insertions, 178 deletions
diff --git a/modules/caddyhttp/staticfiles/browse.go b/modules/caddyhttp/staticfiles/browse.go index 15ff105..2bb130f 100644 --- a/modules/caddyhttp/staticfiles/browse.go +++ b/modules/caddyhttp/staticfiles/browse.go @@ -1,58 +1,65 @@  package staticfiles  import ( +	"bytes" +	"encoding/json" +	"html/template"  	"net/http" +	"os" +	"path" +	"strings" + +	"bitbucket.org/lightcodelabs/caddy2" +	"bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"  )  // Browse configures directory browsing.  type Browse struct { -} +	TemplateFile string `json:"template_file"` -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -// If so, control is handed over to ServeListing. -func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) error { -	// TODO: convert this handler -	return nil +	template *template.Template +} -	// // Browse works on existing directories; delegate everything else -	// requestedFilepath, err := bc.Fs.Root.Open(r.URL.Path) -	// if err != nil { -	// 	switch { -	// 	case os.IsPermission(err): -	// 		return http.StatusForbidden, err -	// 	case os.IsExist(err): -	// 		return http.StatusNotFound, err -	// 	default: -	// 		return b.Next.ServeHTTP(w, r) -	// 	} -	// } -	// defer requestedFilepath.Close() +func (sf *StaticFiles) serveBrowse(dirPath string, w http.ResponseWriter, r *http.Request) error { +	dir, err := sf.openFile(dirPath, w) +	if err != nil { +		return err +	} +	defer dir.Close() + +	repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer) + +	listing, err := sf.loadDirectoryContents(dir, r.URL.Path, repl) +	switch { +	case os.IsPermission(err): +		return caddyhttp.Error(http.StatusForbidden, err) +	case os.IsNotExist(err): +		return caddyhttp.Error(http.StatusNotFound, err) +	case err != nil: +		return caddyhttp.Error(http.StatusInternalServerError, err) +	} + +	sf.browseApplyQueryParams(w, r, &listing) + +	// write response as either JSON or HTML +	var buf *bytes.Buffer +	acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) +	if strings.Contains(acceptHeader, "application/json") { +		if buf, err = sf.browseWriteJSON(listing); err != nil { +			return caddyhttp.Error(http.StatusInternalServerError, err) +		} +		w.Header().Set("Content-Type", "application/json; charset=utf-8") +	} else { +		if buf, err = sf.browseWriteHTML(listing); err != nil { +			return caddyhttp.Error(http.StatusInternalServerError, err) +		} +		w.Header().Set("Content-Type", "text/html; charset=utf-8") +	} +	buf.WriteTo(w) -	// info, err := requestedFilepath.Stat() -	// if err != nil { -	// 	switch { -	// 	case os.IsPermission(err): -	// 		return http.StatusForbidden, err -	// 	case os.IsExist(err): -	// 		return http.StatusGone, err -	// 	default: -	// 		return b.Next.ServeHTTP(w, r) -	// 	} -	// } -	// if !info.IsDir() { -	// 	return b.Next.ServeHTTP(w, r) -	// } - -	// // Do not reply to anything else because it might be nonsensical -	// switch r.Method { -	// case http.MethodGet, http.MethodHead: -	// 	// proceed, noop -	// case "PROPFIND", http.MethodOptions: -	// 	return http.StatusNotImplemented, nil -	// default: -	// 	return b.Next.ServeHTTP(w, r) -	// } +	return nil +	// TODO: Sigh... do we have to put this here?  	// // Browsing navigation gets messed up if browsing a directory  	// // that doesn't end in "/" (which it should, anyway)  	// u := *r.URL @@ -68,138 +75,83 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) error {  	// return b.ServeListing(w, r, requestedFilepath, bc)  } -// func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string, config *Config) (*Listing, bool, error) { -// 	files, err := requestedFilepath.Readdir(-1) -// 	if err != nil { -// 		return nil, false, err -// 	} - -// 	// Determine if user can browse up another folder -// 	var canGoUp bool -// 	curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) -// 	for _, other := range b.Configs { -// 		if strings.HasPrefix(curPathDir, other.PathScope) { -// 			canGoUp = true -// 			break -// 		} -// 	} - -// 	// Assemble listing of directory contents -// 	listing, hasIndex := directoryListing(files, canGoUp, urlPath, config) - -// 	return &listing, hasIndex, nil -// } - -// // handleSortOrder gets and stores for a Listing the 'sort' and 'order', -// // and reads 'limit' if given. The latter is 0 if not given. -// // -// // This sets Cookies. -// func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) { -// 	sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit") - -// 	// If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies -// 	switch sort { -// 	case "": -// 		sort = sortByNameDirFirst -// 		if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { -// 			sort = sortCookie.Value -// 		} -// 	case sortByName, sortByNameDirFirst, sortBySize, sortByTime: -// 		http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil}) -// 	} - -// 	switch order { -// 	case "": -// 		order = "asc" -// 		if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { -// 			order = orderCookie.Value -// 		} -// 	case "asc", "desc": -// 		http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil}) -// 	} - -// 	if limitQuery != "" { -// 		limit, err = strconv.Atoi(limitQuery) -// 		if err != nil { // if the 'limit' query can't be interpreted as a number, return err -// 			return -// 		} -// 	} - -// 	return -// } - -// // ServeListing returns a formatted view of 'requestedFilepath' contents'. -// func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) { -// 	listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path, bc) -// 	if err != nil { -// 		switch { -// 		case os.IsPermission(err): -// 			return http.StatusForbidden, err -// 		case os.IsExist(err): -// 			return http.StatusGone, err -// 		default: -// 			return http.StatusInternalServerError, err -// 		} -// 	} -// 	if containsIndex && !b.IgnoreIndexes { // directory isn't browsable -// 		return b.Next.ServeHTTP(w, r) -// 	} -// 	listing.Context = httpserver.Context{ -// 		Root: bc.Fs.Root, -// 		Req:  r, -// 		URL:  r.URL, -// 	} -// 	listing.User = bc.Variables - -// 	// Copy the query values into the Listing struct -// 	var limit int -// 	listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope) -// 	if err != nil { -// 		return http.StatusBadRequest, err -// 	} - -// 	listing.applySort() - -// 	if limit > 0 && limit <= len(listing.Items) { -// 		listing.Items = listing.Items[:limit] -// 		listing.ItemsLimitedTo = limit -// 	} - -// 	var buf *bytes.Buffer -// 	acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) -// 	switch { -// 	case strings.Contains(acceptHeader, "application/json"): -// 		if buf, err = b.formatAsJSON(listing, bc); err != nil { -// 			return http.StatusInternalServerError, err -// 		} -// 		w.Header().Set("Content-Type", "application/json; charset=utf-8") - -// 	default: // There's no 'application/json' in the 'Accept' header; browse normally -// 		if buf, err = b.formatAsHTML(listing, bc); err != nil { -// 			return http.StatusInternalServerError, err -// 		} -// 		w.Header().Set("Content-Type", "text/html; charset=utf-8") - -// 	} - -// 	_, _ = buf.WriteTo(w) - -// 	return http.StatusOK, nil -// } - -// func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) { -// 	marsh, err := json.Marshal(listing.Items) -// 	if err != nil { -// 		return nil, err -// 	} - -// 	buf := new(bytes.Buffer) -// 	_, err = buf.Write(marsh) -// 	return buf, err -// } - -// func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) { -// 	buf := new(bytes.Buffer) -// 	err := bc.Template.Execute(buf, listing) -// 	return buf, err -// } +func (sf *StaticFiles) loadDirectoryContents(dir *os.File, urlPath string, repl caddy2.Replacer) (browseListing, error) { +	files, err := dir.Readdir(-1) +	if err != nil { +		return browseListing{}, err +	} + +	// determine if user can browse up another folder +	curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) +	canGoUp := strings.HasPrefix(curPathDir, sf.Root) + +	return sf.directoryListing(files, canGoUp, urlPath, repl), nil +} + +// browseApplyQueryParams applies query parameters to the listing. +// It mutates the listing and may set cookies. +func (sf *StaticFiles) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseListing) { +	sortParam := r.URL.Query().Get("sort") +	orderParam := r.URL.Query().Get("order") +	limitParam := r.URL.Query().Get("limit") + +	// first figure out what to sort by +	switch sortParam { +	case "": +		sortParam = sortByNameDirFirst +		if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { +			sortParam = sortCookie.Value +		} +	case sortByName, sortByNameDirFirst, sortBySize, sortByTime: +		http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil}) +	} + +	// then figure out the order +	switch orderParam { +	case "": +		orderParam = "asc" +		if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { +			orderParam = orderCookie.Value +		} +	case "asc", "desc": +		http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) +	} + +	// finally, apply the sorting and limiting +	listing.applySortAndLimit(sortParam, orderParam, limitParam) +} + +func (sf *StaticFiles) browseWriteJSON(listing browseListing) (*bytes.Buffer, error) { +	buf := new(bytes.Buffer) +	err := json.NewEncoder(buf).Encode(listing.Items) +	return buf, err +} + +func (sf *StaticFiles) browseWriteHTML(listing browseListing) (*bytes.Buffer, error) { +	buf := new(bytes.Buffer) +	err := sf.Browse.template.Execute(buf, listing) +	return buf, err +} + +// isSymlink return true if f is a symbolic link +func isSymlink(f os.FileInfo) bool { +	return f.Mode()&os.ModeSymlink != 0 +} + +// isSymlinkTargetDir return true if f's symbolic link target +// is a directory. Return false if not a symbolic link. +// TODO: Re-implement +func isSymlinkTargetDir(f os.FileInfo, urlPath string) bool { +	// if !isSymlink(f) { +	// 	return false +	// } + +	// // TODO: Ensure path is sanitized +	// target:= path.Join(root, urlPath, f.Name())) +	// targetInfo, err := os.Stat(target) +	// if err != nil { +	// 	return false +	// } +	// return targetInfo.IsDir() +	return false +}  | 
