diff --git a/assets/embed/templates/single.tmpl b/assets/embed/templates/single.tmpl index d70d1646..f360663e 100644 --- a/assets/embed/templates/single.tmpl +++ b/assets/embed/templates/single.tmpl @@ -6,7 +6,9 @@ {{ else if eq .Type "audio" }} {{ else if eq .Type "video" }} - + + {{ else if eq .Type "blob" }} + Download {{ else}}
{{ .StringifyContent }}
{{ end }}
diff --git a/file/download.go b/file/download.go
index ce751e3f..b691ba57 100644
--- a/file/download.go
+++ b/file/download.go
@@ -1,56 +1 @@
package file
-
-import (
- "io"
- "io/ioutil"
- "net/http"
- "os"
- "path/filepath"
-
- "github.com/mholt/archiver"
-)
-
-// DownloadAs creates an archieve in one of the supported formats (zip, tar,
-// tar.gz or tar.bz2) and sends it to be downloaded.
-func (i *Info) DownloadAs(w http.ResponseWriter, query string) (int, error) {
- var (
- extension string
- temp string
- err error
- tempfile string
- )
-
- temp, err = ioutil.TempDir("", "")
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- defer os.RemoveAll(temp)
- tempfile = filepath.Join(temp, "temp")
-
- switch query {
- case "zip":
- extension, err = ".zip", archiver.Zip.Make(tempfile, []string{i.Path})
- case "tar":
- extension, err = ".tar", archiver.Tar.Make(tempfile, []string{i.Path})
- case "targz":
- extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, []string{i.Path})
- case "tarbz2":
- extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, []string{i.Path})
- default:
- return http.StatusNotImplemented, nil
- }
-
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- file, err := os.Open(temp + "/temp")
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- w.Header().Set("Content-Disposition", "attachment; filename="+i.Name()+extension)
- io.Copy(w, file)
- return http.StatusOK, nil
-}
diff --git a/file/info.go b/file/info.go
index 533bae86..9ffaa66d 100644
--- a/file/info.go
+++ b/file/info.go
@@ -10,7 +10,6 @@ import (
humanize "github.com/dustin/go-humanize"
"github.com/hacdias/caddy-filemanager/config"
- "github.com/hacdias/caddy-filemanager/page"
"github.com/hacdias/caddy-filemanager/utils"
)
@@ -75,63 +74,6 @@ func (i Info) HumanModTime(format string) string {
return i.ModTime().Format(format)
}
-func (i *Info) ServeHTTP(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- if i.IsDir() {
- return i.serveListing(w, r, c, u)
- }
-
- return i.serveSingleFile(w, r, c, u)
-}
-
-func (i *Info) serveSingleFile(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- err := i.Read()
- if err != nil {
- code := http.StatusInternalServerError
-
- switch {
- case os.IsPermission(err):
- code = http.StatusForbidden
- case os.IsNotExist(err):
- code = http.StatusGone
- case os.IsExist(err):
- code = http.StatusGone
- }
-
- return code, err
- }
-
- if i.Type == "blob" {
- http.Redirect(
- w, r,
- c.AddrPath+r.URL.Path+"?download=true",
- http.StatusTemporaryRedirect,
- )
- return 0, nil
- }
-
- p := &page.Page{
- Info: &page.Info{
- Name: i.Name(),
- Path: i.VirtualPath,
- IsDir: false,
- Data: i,
- User: u,
- Config: c,
- },
- }
-
- if i.CanBeEdited() && u.AllowEdit {
- p.Data, err = i.GetEditor()
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- return p.PrintAsHTML(w, "frontmatter", "editor")
- }
-
- return p.PrintAsHTML(w, "single")
-}
-
func simplifyMediaType(name string) string {
if strings.HasPrefix(name, "video") {
return "video"
diff --git a/file/listing.go b/file/listing.go
index 757a9080..91ff2b1a 100644
--- a/file/listing.go
+++ b/file/listing.go
@@ -1,16 +1,13 @@
package file
import (
- "encoding/json"
- "net/http"
"net/url"
"os"
"path"
+ "sort"
"strings"
"github.com/hacdias/caddy-filemanager/config"
- "github.com/hacdias/caddy-filemanager/page"
- "github.com/hacdias/caddy-filemanager/utils"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@@ -19,7 +16,7 @@ import (
type Listing struct {
// The name of the directory (the last element of the path)
Name string
- // The full path of the request
+ // The full path of the request relatively to a File System
Path string
// The items (files and folders) in the path
Items []Info
@@ -36,76 +33,17 @@ type Listing struct {
httpserver.Context `json:"-"`
}
-func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- var err error
-
+// GetListing gets the information about a specific directory and its files.
+func GetListing(u *config.User, filePath string, baseURL string) (*Listing, error) {
// Gets the directory information using the Virtual File System of
- // the user configuration
- file, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
+ // the user configuration.
+ file, err := u.FileSystem.OpenFile(filePath, os.O_RDONLY, 0)
if err != nil {
- return utils.ErrorToHTTPCode(err, true), err
+ return nil, err
}
defer file.Close()
- // Loads the content of the directory
- listing, err := i.loadDirectoryContents(file, r.URL.Path, u)
- if err != nil {
- return utils.ErrorToHTTPCode(err, true), err
- }
-
- listing.Context = httpserver.Context{
- Root: http.Dir(u.Scope),
- 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.Scope)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- listing.applySort()
-
- if limit > 0 && limit <= len(listing.Items) {
- listing.Items = listing.Items[:limit]
- listing.ItemsLimitedTo = limit
- }
-
- if strings.Contains(r.Header.Get("Accept"), "application/json") {
- marsh, err := json.Marshal(listing.Items)
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
- if _, err := w.Write(marsh); err != nil {
- return http.StatusInternalServerError, err
- }
-
- return http.StatusOK, nil
- }
-
- page := &page.Page{
- Info: &page.Info{
- Name: listing.Name,
- Path: i.VirtualPath,
- IsDir: true,
- User: u,
- Config: c,
- Data: listing,
- },
- }
-
- if r.Header.Get("Minimal") == "true" {
- page.Minimal = true
- }
-
- return page.PrintAsHTML(w, "listing")
-}
-
-func (i Info) loadDirectoryContents(file http.File, basePath string, u *config.User) (*Listing, error) {
+ // Reads the directory and gets the information about the files.
files, err := file.Readdir(-1)
if err != nil {
return nil, err
@@ -127,19 +65,108 @@ func (i Info) loadDirectoryContents(file http.File, basePath string, u *config.U
}
// Absolute URL
- url := url.URL{Path: basePath + name}
+ url := url.URL{Path: baseURL + name}
fileinfos = append(fileinfos, Info{
FileInfo: f,
URL: url.String(),
- UserAllowed: u.Allowed(i.VirtualPath),
+ UserAllowed: u.Allowed(filePath),
})
}
return &Listing{
- Name: path.Base(i.VirtualPath),
- Path: i.VirtualPath,
+ Name: path.Base(filePath),
+ Path: filePath,
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
}, nil
}
+
+// ApplySort applies the sort order using .Order and .Sort
+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:
+ sort.Sort(byName(l))
+ 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 {
+ if l.Items[i].IsDir() && !l.Items[j].IsDir() {
+ return true
+ }
+
+ if !l.Items[i].IsDir() && l.Items[j].IsDir() {
+ return false
+ }
+
+ 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())
+}
diff --git a/file/listing_sort.go b/file/listing_sort.go
deleted file mode 100644
index 54076fda..00000000
--- a/file/listing_sort.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package file
-
-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. Sets cookies.
-func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
- sort = r.URL.Query().Get("sort")
- order = r.URL.Query().Get("order")
- limitQuery := 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 the 'limit' query can't be interpreted as a number, return err.
- if err != nil {
- return
- }
- }
-
- return
-}
-
-// 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:
- sort.Sort(byName(l))
- 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 {
- if l.Items[i].IsDir() && !l.Items[j].IsDir() {
- return true
- }
-
- if !l.Items[i].IsDir() && l.Items[j].IsDir() {
- return false
- }
-
- 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())
-}
diff --git a/filemanager.go b/filemanager.go
index a915ba5b..23d31bbd 100644
--- a/filemanager.go
+++ b/filemanager.go
@@ -10,13 +10,12 @@ package filemanager
import (
e "errors"
"net/http"
- "os/exec"
- "path/filepath"
"strings"
"github.com/hacdias/caddy-filemanager/assets"
"github.com/hacdias/caddy-filemanager/config"
"github.com/hacdias/caddy-filemanager/file"
+ "github.com/hacdias/caddy-filemanager/handlers"
"github.com/hacdias/caddy-filemanager/page"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@@ -126,36 +125,22 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
// Generate anti security token.
c.GenerateToken()
- if fi.IsDir() {
- if val, ok := r.URL.Query()["download"]; ok && val[0] != "" {
- return fi.DownloadAs(w, val[0])
- }
+ switch {
+ case r.URL.Query().Get("download") != "":
+ code, err = handlers.Download(w, r, c, fi)
+ case r.URL.Query().Get("raw") == "true" && !fi.IsDir():
+ http.ServeFile(w, r, fi.Path)
+ code, err = 0, nil
+ case fi.IsDir():
+ code, err = handlers.ServeListing(w, r, c, user, fi)
+ default:
+ code, err = handlers.ServeSingle(w, r, c, user, fi)
}
- if !fi.IsDir() {
- query := r.URL.Query()
- webdav := false
-
- if val, ok := query["raw"]; ok && val[0] == "true" {
- webdav = true
- }
-
- if val, ok := query["download"]; ok && val[0] == "true" {
- w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name())
- webdav = true
- }
-
- if webdav {
- r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1)
- c.Handler.ServeHTTP(w, r)
- return 0, nil
- }
- }
-
- code, err := fi.ServeHTTP(w, r, c, user)
if err != nil {
- return page.PrintErrorHTML(w, code, err)
+ code, err = page.PrintErrorHTML(w, code, err)
}
+
return code, err
}
@@ -172,7 +157,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
return http.StatusUnauthorized, nil
}
- return command(w, r, c, user)
+ return handlers.Command(w, r, c, user)
}
}
@@ -182,40 +167,3 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
return f.Next.ServeHTTP(w, r)
}
-
-// command handles the requests for VCS related commands: git, svn and mercurial
-func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- command := strings.Split(r.Header.Get("command"), " ")
-
- // Check if the command is allowed
- mayContinue := false
-
- for _, cmd := range u.Commands {
- if cmd == command[0] {
- mayContinue = true
- }
- }
-
- if !mayContinue {
- return http.StatusForbidden, nil
- }
-
- // Check if the program is talled is installed on the computer
- if _, err := exec.LookPath(command[0]); err != nil {
- return http.StatusNotImplemented, nil
- }
-
- path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1)
- path = filepath.Clean(path)
-
- cmd := exec.Command(command[0], command[1:len(command)]...)
- cmd.Dir = path
- output, err := cmd.CombinedOutput()
-
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- p := &page.Page{Info: &page.Info{Data: string(output)}}
- return p.PrintAsJSON(w)
-}
diff --git a/handlers/command.go b/handlers/command.go
new file mode 100644
index 00000000..e2690c42
--- /dev/null
+++ b/handlers/command.go
@@ -0,0 +1,48 @@
+package handlers
+
+import (
+ "net/http"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/page"
+)
+
+// Command handles the requests for VCS related commands: git, svn and mercurial
+func Command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
+ command := strings.Split(r.Header.Get("command"), " ")
+
+ // Check if the command is allowed
+ mayContinue := false
+
+ for _, cmd := range u.Commands {
+ if cmd == command[0] {
+ mayContinue = true
+ }
+ }
+
+ if !mayContinue {
+ return http.StatusForbidden, nil
+ }
+
+ // Check if the program is talled is installed on the computer
+ if _, err := exec.LookPath(command[0]); err != nil {
+ return http.StatusNotImplemented, nil
+ }
+
+ path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1)
+ path = filepath.Clean(path)
+
+ cmd := exec.Command(command[0], command[1:len(command)]...)
+ cmd.Dir = path
+ output, err := cmd.CombinedOutput()
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ p := &page.Page{Info: &page.Info{Data: string(output)}}
+ return p.PrintAsJSON(w)
+}
diff --git a/handlers/download.go b/handlers/download.go
new file mode 100644
index 00000000..f4fc95a2
--- /dev/null
+++ b/handlers/download.go
@@ -0,0 +1,70 @@
+package handlers
+
+import (
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/mholt/archiver"
+)
+
+// Download creates an archieve in one of the supported formats (zip, tar,
+// tar.gz or tar.bz2) and sends it to be downloaded.
+func Download(w http.ResponseWriter, r *http.Request, c *config.Config, i *file.Info) (int, error) {
+ query := r.URL.Query().Get("download")
+
+ if !i.IsDir() {
+ w.Header().Set("Content-Disposition", "attachment; filename="+i.Name())
+ http.ServeFile(w, r, i.Path)
+ return 0, nil
+ }
+
+ if query == "true" {
+ query = "zip"
+ }
+
+ var (
+ extension string
+ temp string
+ err error
+ tempfile string
+ )
+
+ temp, err = ioutil.TempDir("", "")
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ defer os.RemoveAll(temp)
+ tempfile = filepath.Join(temp, "temp")
+
+ switch query {
+ case "zip":
+ extension, err = ".zip", archiver.Zip.Make(tempfile, []string{i.Path})
+ case "tar":
+ extension, err = ".tar", archiver.Tar.Make(tempfile, []string{i.Path})
+ case "targz":
+ extension, err = ".tar.gz", archiver.TarGz.Make(tempfile, []string{i.Path})
+ case "tarbz2":
+ extension, err = ".tar.bz2", archiver.TarBz2.Make(tempfile, []string{i.Path})
+ default:
+ return http.StatusNotImplemented, nil
+ }
+
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ file, err := os.Open(temp + "/temp")
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ w.Header().Set("Content-Disposition", "attachment; filename="+i.Name()+extension)
+ io.Copy(w, file)
+ return http.StatusOK, nil
+}
diff --git a/handlers/listing.go b/handlers/listing.go
new file mode 100644
index 00000000..13b36d33
--- /dev/null
+++ b/handlers/listing.go
@@ -0,0 +1,123 @@
+package handlers
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/hacdias/caddy-filemanager/page"
+ "github.com/hacdias/caddy-filemanager/utils"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+// ServeListing presents the user with a listage of a directory folder.
+func ServeListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) {
+ var err error
+
+ // Loads the content of the directory
+ listing, err := file.GetListing(u, i.VirtualPath, r.URL.Path)
+ if err != nil {
+ return utils.ErrorToHTTPCode(err, true), err
+ }
+
+ listing.Context = httpserver.Context{
+ Root: http.Dir(u.Scope),
+ 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.Scope)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ listing.ApplySort()
+
+ if limit > 0 && limit <= len(listing.Items) {
+ listing.Items = listing.Items[:limit]
+ listing.ItemsLimitedTo = limit
+ }
+
+ if strings.Contains(r.Header.Get("Accept"), "application/json") {
+ marsh, err := json.Marshal(listing.Items)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ if _, err := w.Write(marsh); err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return http.StatusOK, nil
+ }
+
+ page := &page.Page{
+ Minimal: r.Header.Get("Minimal") == "true",
+ Info: &page.Info{
+ Name: listing.Name,
+ Path: i.VirtualPath,
+ IsDir: true,
+ User: u,
+ Config: c,
+ Data: listing,
+ },
+ }
+
+ return page.PrintAsHTML(w, "listing")
+}
+
+// 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 = r.URL.Query().Get("sort")
+ order = r.URL.Query().Get("order")
+ limitQuery := 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 the 'limit' query can't be interpreted as a number, return err.
+ if err != nil {
+ return
+ }
+ }
+
+ return
+}
diff --git a/handlers/single.go b/handlers/single.go
new file mode 100644
index 00000000..93dd3b85
--- /dev/null
+++ b/handlers/single.go
@@ -0,0 +1,41 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/file"
+ "github.com/hacdias/caddy-filemanager/page"
+ "github.com/hacdias/caddy-filemanager/utils"
+)
+
+// ServeSingle serves a single file in an editor (if it is editable), shows the
+// plain file, or downloads it if it can't be shown.
+func ServeSingle(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) {
+ err := i.Read()
+ if err != nil {
+ return utils.ErrorToHTTPCode(err, true), err
+ }
+
+ p := &page.Page{
+ Info: &page.Info{
+ Name: i.Name(),
+ Path: i.VirtualPath,
+ IsDir: false,
+ Data: i,
+ User: u,
+ Config: c,
+ },
+ }
+
+ if i.CanBeEdited() && u.AllowEdit {
+ p.Data, err = i.GetEditor()
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ return p.PrintAsHTML(w, "frontmatter", "editor")
+ }
+
+ return p.PrintAsHTML(w, "single")
+}
diff --git a/preprocess.go b/preprocess.go
index 0d039252..bec8b2f7 100644
--- a/preprocess.go
+++ b/preprocess.go
@@ -16,7 +16,13 @@ import (
)
// processPUT is used to update a file that was edited
-func processPUT(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User, i *file.Info) (int, error) {
+func processPUT(
+ w http.ResponseWriter,
+ r *http.Request,
+ c *config.Config,
+ u *config.User,
+ i *file.Info,
+) (int, error) {
var (
data map[string]interface{}
file []byte