diff --git a/OLD/listing.go b/OLD/listing.go deleted file mode 100644 index 44ef5e33..00000000 --- a/OLD/listing.go +++ /dev/null @@ -1,172 +0,0 @@ -package filemanager - -import ( - "fmt" - "net/http" - "net/url" - "os" - "path" - "strings" - - "github.com/mholt/caddy/caddyhttp/httpserver" - "github.com/mholt/caddy/caddyhttp/staticfiles" -) - -// A Listing is the context used to fill out a template. -type Listing struct { - // The name of the directory (the last element of the path) - Name string - - // The full path of the request - Path string - - // Whether the parent directory is browsable - CanGoUp bool - - // The items (files and folders) in the path - Items []FileInfo - - // The number of directories in the listing - NumDirs int - - // The number of files (items that aren't directories) in the listing - NumFiles int - - // Which sorting order is used - Sort string - - // And which order - Order string - - // If ≠0 then Items have been limited to that many elements - ItemsLimitedTo int - - // Optional custom variables for use in browse templates - User interface{} - - httpserver.Context -} - -func (f FileManager) loadDirectoryContents(requestedFilepath http.File, urlPath string) (*Listing, bool, error) { - files, err := requestedFilepath.Readdir(-1) - if err != nil { - return nil, false, err - } - - // Determine if user can browse up another folder - // TODO: review this - var canGoUp bool - curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/")) - for _, other := range f.Configs { - if strings.HasPrefix(curPathDir, other.PathScope) { - canGoUp = true - break - } - } - - // Assemble listing of directory contents - listing, _ := directoryListing(files, canGoUp, urlPath) - return &listing, false, nil -} - -// ServeListing returns a formatted view of 'requestedFilepath' contents'. -func (f FileManager) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) { - listing, containsIndex, err := f.loadDirectoryContents(requestedFilepath, r.URL.Path) - if err != nil { - fmt.Println(err) - switch { - case os.IsPermission(err): - return http.StatusForbidden, err - case os.IsExist(err): - return http.StatusGone, err - default: - return http.StatusInternalServerError, err - } - } - - if containsIndex && !f.IgnoreIndexes { // directory isn't browsable - return f.Next.ServeHTTP(w, r) - } - listing.Context = httpserver.Context{ - Root: bc.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 = f.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 - } - - acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ",")) - - page := &Page{ - Info: &PageInfo{ - Name: listing.Name, - Path: listing.Path, - Data: listing, - }, - } - - if strings.Contains(acceptHeader, "application/json") { - return page.PrintAsJSON(w) - } - - return page.PrintAsHTML(w, "listing") -} - -func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listing, bool) { - var ( - fileinfos []FileInfo - dirCount, fileCount int - hasIndexFile bool - ) - - for _, f := range files { - name := f.Name() - - for _, indexName := range staticfiles.IndexPages { - if name == indexName { - hasIndexFile = true - break - } - } - - if f.IsDir() { - name += "/" - dirCount++ - } else { - fileCount++ - } - - url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name - - fileinfos = append(fileinfos, FileInfo{ - IsDir: f.IsDir(), - Name: f.Name(), - Size: f.Size(), - URL: url.String(), - ModTime: f.ModTime().UTC(), - Mode: f.Mode(), - }) - } - - return Listing{ - Name: path.Base(urlPath), - Path: urlPath, - CanGoUp: canGoUp, - Items: fileinfos, - NumDirs: dirCount, - NumFiles: fileCount, - }, hasIndexFile -} diff --git a/OLD/sort.go b/OLD/sort.go deleted file mode 100644 index 02199159..00000000 --- a/OLD/sort.go +++ /dev/null @@ -1,112 +0,0 @@ -package filemanager - -import ( - "net/http" - "sort" - "strconv" - "strings" -) - -// 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 (f FileManager) 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 = "name" - if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { - sort = sortCookie.Value - } - case "name", "size", "type": - 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 -} - -// 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] } - -const directoryOffset = -1 << 31 // = math.MinInt32 -func (l bySize) Less(i, j int) bool { - iSize, jSize := l.Items[i].Size, l.Items[j].Size - if l.Items[i].IsDir { - iSize = directoryOffset + iSize - } - if l.Items[j].IsDir { - jSize = directoryOffset + jSize - } - return iSize < jSize -} - -// 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.Before(l.Items[j].ModTime) } - -// 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 - } - } -} diff --git a/assets/templates/listing.tmpl b/assets/templates/listing.tmpl index 5c52187d..14ef36bd 100644 --- a/assets/templates/listing.tmpl +++ b/assets/templates/listing.tmpl @@ -44,17 +44,7 @@ - {{- if .CanGoUp}} - - - - Go up - - - — - — - - {{- end}} + {{- range .Items}} diff --git a/fileinfo.go b/fileinfo.go index c166a9e2..b23f7893 100644 --- a/fileinfo.go +++ b/fileinfo.go @@ -1,16 +1,19 @@ package filemanager import ( + "fmt" "io/ioutil" "mime" "net/http" "net/url" "os" + "path" "path/filepath" "strings" "time" "github.com/dustin/go-humanize" + "github.com/mholt/caddy/caddyhttp/httpserver" ) // FileInfo is the information about a particular file or directory @@ -131,6 +134,14 @@ func (fi FileInfo) Rename(w http.ResponseWriter, r *http.Request) (int, error) { // ServeAsHTML is used to serve single file pages func (fi FileInfo) ServeAsHTML(w http.ResponseWriter, r *http.Request, c *Config) (int, error) { + if fi.IsDir { + return fi.serveListing(w, r, c) + } + + return fi.serveSingleFile(w, r) +} + +func (fi FileInfo) serveSingleFile(w http.ResponseWriter, r *http.Request) (int, error) { err := fi.GetExtendedFileInfo() if err != nil { return ErrorToHTTPCode(err), err @@ -147,6 +158,106 @@ func (fi FileInfo) ServeAsHTML(w http.ResponseWriter, r *http.Request, c *Config return page.PrintAsHTML(w, "single") } +func (fi FileInfo) serveListing(w http.ResponseWriter, r *http.Request, c *Config) (int, error) { + var err error + + file, err := c.Root.Open("/" + fi.Path) + if err != nil { + return ErrorToHTTPCode(err), err + } + defer file.Close() + + listing, err := fi.loadDirectoryContents(file, c) + if err != nil { + fmt.Println(err) + switch { + case os.IsPermission(err): + return http.StatusForbidden, err + case os.IsExist(err): + return http.StatusGone, err + default: + return http.StatusInternalServerError, err + } + } + + listing.Context = httpserver.Context{ + Root: c.Root, + Req: r, + URL: r.URL, + } + + // Copy the query values into the Listing struct + var limit int + listing.Sort, listing.Order, limit, err = handleSortOrder(w, r, c.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 + } + + page := &Page{ + Info: &PageInfo{ + Name: listing.Name, + Path: listing.Path, + Data: listing, + }, + } + + return page.PrintAsHTML(w, "listing") +} + +func (fi FileInfo) loadDirectoryContents(file http.File, c *Config) (*Listing, error) { + files, err := file.Readdir(-1) + if err != nil { + return nil, err + } + + listing := directoryListing(files, fi.Path) + return &listing, nil +} + +func directoryListing(files []os.FileInfo, urlPath string) Listing { + var ( + fileinfos []FileInfo + dirCount, fileCount int + ) + + for _, f := range files { + name := f.Name() + + if f.IsDir() { + name += "/" + dirCount++ + } else { + fileCount++ + } + + url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name + + fileinfos = append(fileinfos, FileInfo{ + IsDir: f.IsDir(), + Name: f.Name(), + Size: f.Size(), + URL: url.String(), + ModTime: f.ModTime().UTC(), + Mode: f.Mode(), + }) + } + + return Listing{ + Name: path.Base(urlPath), + Path: urlPath, + Items: fileinfos, + NumDirs: dirCount, + NumFiles: fileCount, + } +} + // SimplifyMimeType returns the base type of a file func SimplifyMimeType(name string) string { if strings.HasPrefix(name, "video") { diff --git a/filemanager.go b/filemanager.go index cb747ff7..c955fc79 100644 --- a/filemanager.go +++ b/filemanager.go @@ -64,19 +64,16 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err return ServeAssets(w, r, c) } - if fi.IsDir { - //return f.ServeListing(w, r, file.File, c) - return http.StatusNotImplemented, nil - } + if !fi.IsDir { + query := r.URL.Query() + if val, ok := query["raw"]; ok && val[0] == "true" { + return f.Next.ServeHTTP(w, r) + } - query := r.URL.Query() - if val, ok := query["raw"]; ok && val[0] == "true" { - return f.Next.ServeHTTP(w, r) - } - - if val, ok := query["download"]; ok && val[0] == "true" { - w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name) - return f.Next.ServeHTTP(w, r) + if val, ok := query["download"]; ok && val[0] == "true" { + w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name) + return f.Next.ServeHTTP(w, r) + } } return fi.ServeAsHTML(w, r, c) diff --git a/listing.go b/listing.go index 2e5b8ff3..b2794579 100644 --- a/listing.go +++ b/listing.go @@ -1 +1,133 @@ package filemanager + +import ( + "net/http" + "sort" + "strconv" + "strings" + + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +// A Listing is the context used to fill out a template. +type Listing struct { + // The name of the directory (the last element of the path) + Name string + // The full path of the request + Path string + // The items (files and folders) in the path + Items []FileInfo + // The number of directories in the listing + NumDirs int + // The number of files (items that aren't directories) in the listing + NumFiles int + // Which sorting order is used + Sort string + // And which order + Order string + // If ≠0 then Items have been limited to that many elements + ItemsLimitedTo int + httpserver.Context +} + +// handleSortOrder gets and stores for a Listing the 'sort' and 'order', +// and reads 'limit' if given. The latter is 0 if not given. Sets cookies. +func 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 = "name" + if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { + sort = sortCookie.Value + } + case "name", "size", "type": + 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 +} + +// 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] } + +const directoryOffset = -1 << 31 // = math.MinInt32 +func (l bySize) Less(i, j int) bool { + iSize, jSize := l.Items[i].Size, l.Items[j].Size + if l.Items[i].IsDir { + iSize = directoryOffset + iSize + } + if l.Items[j].IsDir { + jSize = directoryOffset + jSize + } + return iSize < jSize +} + +// 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.Before(l.Items[j].ModTime) } + +// 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 + } + } +}