mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 02:27:19 -04:00 
			
		
		
		
	
						commit
						47096e112a
					
				| @ -202,9 +202,33 @@ th { | |||||||
| 		<main> | 		<main> | ||||||
| 			<table> | 			<table> | ||||||
| 				<tr> | 				<tr> | ||||||
| 					<th>Name</th> | 					<th> | ||||||
| 					<th>Size</th> | 						{{if and (eq .Sort "name") (ne .Order "desc")}} | ||||||
| 					<th class="hideable">Modified</th> | 						<a href="?sort=name&order=desc">Name↓</a> | ||||||
|  | 						{{else if and (eq .Sort "name") (ne .Order "asc")}} | ||||||
|  | 						<a href="?sort=name&order=asc">Name↑</a> | ||||||
|  | 						{{else}} | ||||||
|  | 						<a href="?sort=name&order=asc">Name</a> | ||||||
|  | 						{{end}} | ||||||
|  | 					</th> | ||||||
|  | 					<th> | ||||||
|  | 						{{if and (eq .Sort "size") (ne .Order "desc")}} | ||||||
|  | 						<a href="?sort=size&order=desc">Size↓</a> | ||||||
|  | 						{{else if and (eq .Sort "size") (ne .Order "asc")}} | ||||||
|  | 						<a href="?sort=size&order=asc">Size↑</a> | ||||||
|  | 						{{else}} | ||||||
|  | 						<a href="?sort=size&order=asc">Size</a> | ||||||
|  | 						{{end}} | ||||||
|  | 					</th> | ||||||
|  | 					<th class="hideable"> | ||||||
|  | 						{{if and (eq .Sort "time") (ne .Order "desc")}} | ||||||
|  | 						<a href="?sort=time&order=desc">Modified↓</a> | ||||||
|  | 						{{else if and (eq .Sort "time") (ne .Order "asc")}} | ||||||
|  | 						<a href="?sort=time&order=asc">Modified↑</a> | ||||||
|  | 						{{else}} | ||||||
|  | 						<a href="?sort=time&order=asc">Modified</a> | ||||||
|  | 						{{end}} | ||||||
|  | 					</th> | ||||||
| 				</tr> | 				</tr> | ||||||
| 				{{range .Items}} | 				{{range .Items}} | ||||||
| 				<tr> | 				<tr> | ||||||
|  | |||||||
| @ -4,11 +4,13 @@ package browse | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"errors" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
|  | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| @ -43,6 +45,12 @@ type Listing struct { | |||||||
| 
 | 
 | ||||||
| 	// The items (files and folders) in the path | 	// The items (files and folders) in the path | ||||||
| 	Items []FileInfo | 	Items []FileInfo | ||||||
|  | 
 | ||||||
|  | 	// Which sorting order is used | ||||||
|  | 	Sort string | ||||||
|  | 
 | ||||||
|  | 	// And which order | ||||||
|  | 	Order string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FileInfo is the info about a particular file or directory | // FileInfo is the info about a particular file or directory | ||||||
| @ -55,6 +63,61 @@ type FileInfo struct { | |||||||
| 	Mode    os.FileMode | 	Mode    os.FileMode | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Implement sorting for Listing | ||||||
|  | type byName Listing | ||||||
|  | type bySize Listing | ||||||
|  | type byTime Listing | ||||||
|  | 
 | ||||||
|  | // By Name | ||||||
|  | func (l byName) Len() int      { return len(l.Items) } | ||||||
|  | func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } | ||||||
|  | 
 | ||||||
|  | // Treat upper and lower case equally | ||||||
|  | func (l byName) Less(i, j int) bool { | ||||||
|  | 	return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // By Size | ||||||
|  | func (l bySize) Len() int           { return len(l.Items) } | ||||||
|  | func (l bySize) Swap(i, j int)      { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } | ||||||
|  | func (l bySize) Less(i, j int) bool { return l.Items[i].Size < l.Items[j].Size } | ||||||
|  | 
 | ||||||
|  | // By Time | ||||||
|  | func (l byTime) Len() int           { return len(l.Items) } | ||||||
|  | func (l byTime) Swap(i, j int)      { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] } | ||||||
|  | func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Unix() < l.Items[j].ModTime.Unix() } | ||||||
|  | 
 | ||||||
|  | // Add sorting method to "Listing" | ||||||
|  | // it will apply what's in ".Sort" and ".Order" | ||||||
|  | func (l Listing) applySort() { | ||||||
|  | 	// Check '.Order' to know how to sort | ||||||
|  | 	if l.Order == "desc" { | ||||||
|  | 		switch l.Sort { | ||||||
|  | 		case "name": | ||||||
|  | 			sort.Sort(sort.Reverse(byName(l))) | ||||||
|  | 		case "size": | ||||||
|  | 			sort.Sort(sort.Reverse(bySize(l))) | ||||||
|  | 		case "time": | ||||||
|  | 			sort.Sort(sort.Reverse(byTime(l))) | ||||||
|  | 		default: | ||||||
|  | 			// If not one of the above, do nothing | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} else { // If we had more Orderings we could add them here | ||||||
|  | 		switch l.Sort { | ||||||
|  | 		case "name": | ||||||
|  | 			sort.Sort(byName(l)) | ||||||
|  | 		case "size": | ||||||
|  | 			sort.Sort(bySize(l)) | ||||||
|  | 		case "time": | ||||||
|  | 			sort.Sort(byTime(l)) | ||||||
|  | 		default: | ||||||
|  | 			// If not one of the above, do nothing | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // HumanSize returns the size of the file as a human-readable string. | // HumanSize returns the size of the file as a human-readable string. | ||||||
| func (fi FileInfo) HumanSize() string { | func (fi FileInfo) HumanSize() string { | ||||||
| 	return humanize.Bytes(uint64(fi.Size)) | 	return humanize.Bytes(uint64(fi.Size)) | ||||||
| @ -72,6 +135,42 @@ var IndexPages = []string{ | |||||||
| 	"default.htm", | 	"default.htm", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listing, error) { | ||||||
|  | 	var fileinfos []FileInfo | ||||||
|  | 	for _, f := range files { | ||||||
|  | 		name := f.Name() | ||||||
|  | 
 | ||||||
|  | 		// Directory is not browsable if it contains index file | ||||||
|  | 		for _, indexName := range IndexPages { | ||||||
|  | 			if name == indexName { | ||||||
|  | 				return Listing{}, errors.New("Directory contains index file, not browsable!") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if f.IsDir() { | ||||||
|  | 			name += "/" | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		url := url.URL{Path: name} | ||||||
|  | 
 | ||||||
|  | 		fileinfos = append(fileinfos, FileInfo{ | ||||||
|  | 			IsDir:   f.IsDir(), | ||||||
|  | 			Name:    f.Name(), | ||||||
|  | 			Size:    f.Size(), | ||||||
|  | 			URL:     url.String(), | ||||||
|  | 			ModTime: f.ModTime(), | ||||||
|  | 			Mode:    f.Mode(), | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return Listing{ | ||||||
|  | 		Name:    path.Base(urlPath), | ||||||
|  | 		Path:    urlPath, | ||||||
|  | 		CanGoUp: canGoUp, | ||||||
|  | 		Items:   fileinfos, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ServeHTTP implements the middleware.Handler interface. | // ServeHTTP implements the middleware.Handler interface. | ||||||
| func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | ||||||
| 	filename := b.Root + r.URL.Path | 	filename := b.Root + r.URL.Path | ||||||
| @ -113,42 +212,6 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | |||||||
| 			return http.StatusForbidden, err | 			return http.StatusForbidden, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Assemble listing of directory contents |  | ||||||
| 		var fileinfos []FileInfo |  | ||||||
| 		var abort bool // we bail early if we find an index file |  | ||||||
| 		for _, f := range files { |  | ||||||
| 			name := f.Name() |  | ||||||
| 
 |  | ||||||
| 			// Directory is not browseable if it contains index file |  | ||||||
| 			for _, indexName := range IndexPages { |  | ||||||
| 				if name == indexName { |  | ||||||
| 					abort = true |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			if abort { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if f.IsDir() { |  | ||||||
| 				name += "/" |  | ||||||
| 			} |  | ||||||
| 			url := url.URL{Path: name} |  | ||||||
| 
 |  | ||||||
| 			fileinfos = append(fileinfos, FileInfo{ |  | ||||||
| 				IsDir:   f.IsDir(), |  | ||||||
| 				Name:    f.Name(), |  | ||||||
| 				Size:    f.Size(), |  | ||||||
| 				URL:     url.String(), |  | ||||||
| 				ModTime: f.ModTime(), |  | ||||||
| 				Mode:    f.Mode(), |  | ||||||
| 			}) |  | ||||||
| 		} |  | ||||||
| 		if abort { |  | ||||||
| 			// this dir has an index file, so not browsable |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Determine if user can browse up another folder | 		// Determine if user can browse up another folder | ||||||
| 		var canGoUp bool | 		var canGoUp bool | ||||||
| 		curPath := strings.TrimSuffix(r.URL.Path, "/") | 		curPath := strings.TrimSuffix(r.URL.Path, "/") | ||||||
| @ -158,14 +221,24 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { | |||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 		// Assemble listing of directory contents | ||||||
| 		listing := Listing{ | 		listing, err := directoryListing(files, r.URL.Path, canGoUp) | ||||||
| 			Name:    path.Base(r.URL.Path), | 		if err != nil { // directory isn't browsable | ||||||
| 			Path:    r.URL.Path, | 			continue | ||||||
| 			CanGoUp: canGoUp, |  | ||||||
| 			Items:   fileinfos, |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		// Get the query vales and store them in the Listing struct | ||||||
|  | 		listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order") | ||||||
|  | 
 | ||||||
|  | 		// If the query 'sort' is empty, default to "name" and "asc" | ||||||
|  | 		if listing.Sort == "" { | ||||||
|  | 			listing.Sort = "name" | ||||||
|  | 			listing.Order = "asc" | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Apply the sorting | ||||||
|  | 		listing.applySort() | ||||||
|  | 
 | ||||||
| 		var buf bytes.Buffer | 		var buf bytes.Buffer | ||||||
| 		err = bc.Template.Execute(&buf, listing) | 		err = bc.Template.Execute(&buf, listing) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | |||||||
							
								
								
									
										96
									
								
								middleware/browse/browse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								middleware/browse/browse_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | |||||||
|  | package browse | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"sort" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // "sort" package has "IsSorted" function, but no "IsReversed"; | ||||||
|  | func isReversed(data sort.Interface) bool { | ||||||
|  | 	n := data.Len() | ||||||
|  | 	for i := n - 1; i > 0; i-- { | ||||||
|  | 		if !data.Less(i, i-1) { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSort(t *testing.T) { | ||||||
|  | 	// making up []fileInfo with bogus values; | ||||||
|  | 	// to be used to make up our "listing" | ||||||
|  | 	fileInfos := []FileInfo{ | ||||||
|  | 		{ | ||||||
|  | 			Name:    "fizz", | ||||||
|  | 			Size:    4, | ||||||
|  | 			ModTime: time.Now().AddDate(-1, 1, 0), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:    "buzz", | ||||||
|  | 			Size:    2, | ||||||
|  | 			ModTime: time.Now().AddDate(0, -3, 3), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:    "bazz", | ||||||
|  | 			Size:    1, | ||||||
|  | 			ModTime: time.Now().AddDate(0, -2, -23), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:    "jazz", | ||||||
|  | 			Size:    3, | ||||||
|  | 			ModTime: time.Now(), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	listing := Listing{ | ||||||
|  | 		Name:    "foobar", | ||||||
|  | 		Path:    "/fizz/buzz", | ||||||
|  | 		CanGoUp: false, | ||||||
|  | 		Items:   fileInfos, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// sort by name | ||||||
|  | 	listing.Sort = "name" | ||||||
|  | 	listing.applySort() | ||||||
|  | 	if !sort.IsSorted(byName(listing)) { | ||||||
|  | 		t.Errorf("The listing isn't name sorted: %v", listing.Items) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// sort by size | ||||||
|  | 	listing.Sort = "size" | ||||||
|  | 	listing.applySort() | ||||||
|  | 	if !sort.IsSorted(bySize(listing)) { | ||||||
|  | 		t.Errorf("The listing isn't size sorted: %v", listing.Items) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// sort by Time | ||||||
|  | 	listing.Sort = "time" | ||||||
|  | 	listing.applySort() | ||||||
|  | 	if !sort.IsSorted(byTime(listing)) { | ||||||
|  | 		t.Errorf("The listing isn't time sorted: %v", listing.Items) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// reverse by name | ||||||
|  | 	listing.Sort = "name" | ||||||
|  | 	listing.Order = "desc" | ||||||
|  | 	listing.applySort() | ||||||
|  | 	if !isReversed(byName(listing)) { | ||||||
|  | 		t.Errorf("The listing isn't reversed by name: %v", listing.Items) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// reverse by size | ||||||
|  | 	listing.Sort = "size" | ||||||
|  | 	listing.Order = "desc" | ||||||
|  | 	listing.applySort() | ||||||
|  | 	if !isReversed(bySize(listing)) { | ||||||
|  | 		t.Errorf("The listing isn't reversed by size: %v", listing.Items) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// reverse by time | ||||||
|  | 	listing.Sort = "time" | ||||||
|  | 	listing.Order = "desc" | ||||||
|  | 	listing.applySort() | ||||||
|  | 	if !isReversed(byTime(listing)) { | ||||||
|  | 		t.Errorf("The listing isn't reversed by time: %v", listing.Items) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user