diff --git a/assets/embed/templates/single.tmpl b/assets/embed/templates/single.tmpl index bd8f077d..d70d1646 100644 --- a/assets/embed/templates/single.tmpl +++ b/assets/embed/templates/single.tmpl @@ -8,7 +8,7 @@ {{ else if eq .Type "video" }} {{ else}} -
{{ .Content }}
+ {{ .StringifyContent }}
{{ end }}
{{ end }}
diff --git a/config/config.go b/config/config.go
index 952cb134..cf008598 100644
--- a/config/config.go
+++ b/config/config.go
@@ -3,7 +3,6 @@ package config
import (
"fmt"
"io/ioutil"
- "net/http"
"regexp"
"strconv"
"strings"
@@ -17,16 +16,14 @@ import (
// Config is a configuration for browsing in a particualr path.
type Config struct {
*User
- BaseURL string
- AbsoluteURL string
- AddrPath string
- Token string // Anti CSRF token
- HugoEnabled bool // Enables the Hugo plugin for File Manager
- Users map[string]*User
- WebDav bool
- WebDavURL string
- WebDavHandler *webdav.Handler
- CurrentUser *User
+ BaseURL string
+ AbsoluteURL string
+ AddrPath string
+ Token string // Anti CSRF token
+ HugoEnabled bool // Enables the Hugo plugin for File Manager
+ Users map[string]*User
+ WebDavURL string
+ CurrentUser *User
}
// Rule is a dissalow/allow rule
@@ -48,8 +45,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
appendConfig := func(cfg Config) error {
for _, c := range configs {
- if c.PathScope == cfg.PathScope {
- return fmt.Errorf("duplicate file managing config for %s", c.PathScope)
+ if c.Scope == cfg.Scope {
+ return fmt.Errorf("duplicate file managing config for %s", c.Scope)
}
}
configs = append(configs, cfg)
@@ -59,8 +56,8 @@ func Parse(c *caddy.Controller) ([]Config, error) {
for c.Next() {
// Initialize the configuration with the default settings
cfg := Config{User: &User{}}
- cfg.PathScope = "."
- cfg.Root = http.Dir(cfg.PathScope)
+ cfg.Scope = "."
+ cfg.FileSystem = webdav.Dir(cfg.Scope)
cfg.BaseURL = ""
cfg.FrontMatter = "yaml"
cfg.HugoEnabled = false
@@ -69,7 +66,6 @@ func Parse(c *caddy.Controller) ([]Config, error) {
cfg.AllowEdit = true
cfg.AllowNew = true
cfg.Commands = []string{"git", "svn", "hg"}
- cfg.WebDav = true
cfg.Rules = []*Rule{&Rule{
Regex: true,
Allow: false,
@@ -121,9 +117,9 @@ func Parse(c *caddy.Controller) ([]Config, error) {
return configs, c.ArgErr()
}
- user.PathScope = c.Val()
- user.PathScope = strings.TrimSuffix(user.PathScope, "/")
- user.Root = http.Dir(user.PathScope)
+ user.Scope = c.Val()
+ user.Scope = strings.TrimSuffix(user.Scope, "/")
+ user.FileSystem = webdav.Dir(user.Scope)
case "styles":
if !c.NextArg() {
return configs, c.ArgErr()
@@ -227,16 +223,16 @@ func Parse(c *caddy.Controller) ([]Config, error) {
user.AllowNew = cfg.AllowEdit
user.Commands = cfg.Commands
user.FrontMatter = cfg.FrontMatter
- user.PathScope = cfg.PathScope
- user.Root = cfg.Root
+ user.Scope = cfg.Scope
+ user.FileSystem = cfg.FileSystem
user.Rules = cfg.Rules
user.StyleSheet = cfg.StyleSheet
}
}
- cfg.WebDavHandler = &webdav.Handler{
+ cfg.Handler = &webdav.Handler{
Prefix: cfg.WebDavURL,
- FileSystem: webdav.Dir(cfg.PathScope),
+ FileSystem: cfg.FileSystem,
LockSystem: webdav.NewMemLS(),
}
diff --git a/config/user.go b/config/user.go
index abc07789..4f72d2ce 100644
--- a/config/user.go
+++ b/config/user.go
@@ -1,21 +1,23 @@
package config
import (
- "net/http"
"strings"
+
+ "golang.org/x/net/webdav"
)
// User contains the configuration for each user
type User struct {
- PathScope string `json:"-"` // Path the user have access
- Root http.FileSystem `json:"-"` // The virtual file system the user have access
- StyleSheet string `json:"-"` // Costum stylesheet
- FrontMatter string `json:"-"` // Default frontmatter to save files in
- AllowNew bool // Can create files and folders
- AllowEdit bool // Can edit/rename files
- AllowCommands bool // Can execute commands
- Commands []string // Available Commands
- Rules []*Rule `json:"-"` // Access rules
+ Scope string `json:"-"` // Path the user have access
+ FileSystem webdav.FileSystem `json:"-"` // The virtual file system the user have access
+ Handler *webdav.Handler `json:"-"` // The WebDav HTTP Handler
+ StyleSheet string `json:"-"` // Costum stylesheet
+ FrontMatter string `json:"-"` // Default frontmatter to save files in
+ AllowNew bool // Can create files and folders
+ AllowEdit bool // Can edit/rename files
+ AllowCommands bool // Can execute commands
+ Commands []string // Available Commands
+ Rules []*Rule `json:"-"` // Access rules
}
// Allowed checks if the user has permission to access a directory/file
diff --git a/directory/file.go b/directory/file.go
deleted file mode 100644
index 300691da..00000000
--- a/directory/file.go
+++ /dev/null
@@ -1,299 +0,0 @@
-package directory
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/dustin/go-humanize"
- "github.com/hacdias/caddy-filemanager/config"
- p "github.com/hacdias/caddy-filemanager/page"
- "github.com/hacdias/caddy-filemanager/utils/errors"
- "github.com/mholt/caddy/caddyhttp/httpserver"
-)
-
-// Info is the information about a particular file or directory
-type Info struct {
- IsDir bool
- Name string
- Size int64
- URL string
- Path string // The relative Path of the file/directory relative to Caddyfile.
- RootPath string // The Path of the file/directory on http.FileSystem.
- ModTime time.Time
- Mode os.FileMode
- Mimetype string
- Content string
- Raw []byte
- Type string
- UserAllowed bool // Indicates if the user has permissions to open this directory
-}
-
-// GetInfo gets the file information and, in case of error, returns the
-// respective HTTP error code
-func GetInfo(url *url.URL, c *config.Config, u *config.User) (*Info, int, error) {
- var err error
-
- rootPath := strings.Replace(url.Path, c.BaseURL, "", 1)
- rootPath = strings.TrimPrefix(rootPath, "/")
- rootPath = "/" + rootPath
-
- relpath := u.PathScope + rootPath
- relpath = strings.Replace(relpath, "\\", "/", -1)
- relpath = filepath.Clean(relpath)
-
- file := &Info{
- URL: url.Path,
- RootPath: rootPath,
- Path: relpath,
- }
- f, err := u.Root.Open(rootPath)
- if err != nil {
- return file, errors.ToHTTPCode(err), err
- }
- defer f.Close()
-
- info, err := f.Stat()
- if err != nil {
- return file, errors.ToHTTPCode(err), err
- }
-
- file.IsDir = info.IsDir()
- file.ModTime = info.ModTime()
- file.Name = info.Name()
- file.Size = info.Size()
- return file, 0, nil
-}
-
-// GetExtendedInfo is used to get extra parameters for FileInfo struct
-func (i *Info) GetExtendedInfo() error {
- err := i.Read()
- if err != nil {
- return err
- }
-
- i.Type = SimplifyMimeType(i.Mimetype)
- return nil
-}
-
-// Read is used to read a file and store its content
-func (i *Info) Read() error {
- raw, err := ioutil.ReadFile(i.Path)
- if err != nil {
- return err
- }
- i.Mimetype = http.DetectContentType(raw)
- i.Content = string(raw)
- i.Raw = raw
- return nil
-}
-
-// HumanSize returns the size of the file as a human-readable string
-// in IEC format (i.e. power of 2 or base 1024).
-func (i Info) HumanSize() string {
- return humanize.IBytes(uint64(i.Size))
-}
-
-// HumanModTime returns the modified time of the file as a human-readable string.
-func (i Info) HumanModTime(format string) string {
- return i.ModTime.Format(format)
-}
-
-// ServeAsHTML is used to serve single file pages
-func (i *Info) ServeAsHTML(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.GetExtendedInfo()
- if err != nil {
- return errors.ToHTTPCode(err), err
- }
-
- if i.Type == "blob" {
- http.Redirect(w, r, c.AddrPath+r.URL.Path+"?download=true", http.StatusTemporaryRedirect)
- return 0, nil
- }
-
- page := &p.Page{
- Info: &p.Info{
- Name: i.Name,
- Path: i.RootPath,
- IsDir: false,
- Data: i,
- User: u,
- Config: c,
- },
- }
-
- if CanBeEdited(i.Name) && u.AllowEdit {
- editor, err := i.GetEditor()
-
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- page.Info.Data = editor
- return page.PrintAsHTML(w, "frontmatter", "editor")
- }
-
- return page.PrintAsHTML(w, "single")
-}
-
-func (i *Info) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
- var err error
-
- file, err := u.Root.Open(i.RootPath)
- if err != nil {
- return errors.ToHTTPCode(err), err
- }
- defer file.Close()
-
- listing, err := i.loadDirectoryContents(file, r.URL.Path, u)
- 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
- }
-
- 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 := &p.Page{
- Info: &p.Info{
- Name: listing.Name,
- Path: i.RootPath,
- 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, path string, u *config.User) (*Listing, error) {
- files, err := file.Readdir(-1)
- if err != nil {
- return nil, err
- }
-
- listing := directoryListing(files, i.RootPath, path, u)
- return &listing, nil
-}
-
-func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *config.User) Listing {
- var (
- fileinfos []Info
- dirCount, fileCount int
- )
-
- for _, f := range files {
- name := f.Name()
-
- if f.IsDir() {
- name += "/"
- dirCount++
- } else {
- fileCount++
- }
-
- // Absolute URL
- url := url.URL{Path: basePath + name}
- fileinfos = append(fileinfos, Info{
- IsDir: f.IsDir(),
- Name: f.Name(),
- Size: f.Size(),
- URL: url.String(),
- ModTime: f.ModTime().UTC(),
- Mode: f.Mode(),
- UserAllowed: u.Allowed(url.String()),
- })
- }
-
- 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") {
- return "video"
- }
-
- if strings.HasPrefix(name, "audio") {
- return "audio"
- }
-
- if strings.HasPrefix(name, "image") {
- return "image"
- }
-
- if strings.HasPrefix(name, "text") {
- return "text"
- }
-
- if strings.HasPrefix(name, "application/javascript") {
- return "text"
- }
-
- return "blob"
-}
diff --git a/directory/editor.go b/editor.go
similarity index 72%
rename from directory/editor.go
rename to editor.go
index b6b43371..d26efe0d 100644
--- a/directory/editor.go
+++ b/editor.go
@@ -1,4 +1,4 @@
-package directory
+package filemanager
import (
"bytes"
@@ -18,10 +18,10 @@ type Editor struct {
}
// GetEditor gets the editor based on a FileInfo struct
-func (i *Info) GetEditor() (*Editor, error) {
+func (i *FileInfo) GetEditor() (*Editor, error) {
// Create a new editor variable and set the mode
editor := new(Editor)
- editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name), ".")
+ editor.Mode = strings.TrimPrefix(filepath.Ext(i.Name()), ".")
switch editor.Mode {
case "md", "markdown", "mdown", "mmark":
@@ -42,20 +42,20 @@ func (i *Info) GetEditor() (*Editor, error) {
// Handle the content depending on the file extension
switch editor.Mode {
case "markdown", "asciidoc", "rst":
- if !HasFrontMatterRune(i.Raw) {
+ if !hasFrontMatterRune(i.Content) {
editor.Class = "content-only"
- editor.Content = i.Content
+ editor.Content = i.StringifyContent()
break
}
// Starts a new buffer and parses the file using Hugo's functions
- buffer := bytes.NewBuffer(i.Raw)
+ buffer := bytes.NewBuffer(i.Content)
page, err = parser.ReadFrom(buffer)
editor.Class = "complete"
if err != nil {
editor.Class = "content-only"
- editor.Content = i.Content
+ editor.Content = i.StringifyContent()
break
}
@@ -67,35 +67,35 @@ func (i *Info) GetEditor() (*Editor, error) {
editor.Class = "frontmatter-only"
// Checks if the file already has the frontmatter rune and parses it
- if HasFrontMatterRune(i.Raw) {
- editor.FrontMatter, _, err = frontmatter.Pretty(i.Raw)
+ if hasFrontMatterRune(i.Content) {
+ editor.FrontMatter, _, err = frontmatter.Pretty(i.Content)
} else {
- editor.FrontMatter, _, err = frontmatter.Pretty(AppendFrontMatterRune(i.Raw, editor.Mode))
+ editor.FrontMatter, _, err = frontmatter.Pretty(appendFrontMatterRune(i.Content, editor.Mode))
}
// Check if there were any errors
if err != nil {
editor.Class = "content-only"
- editor.Content = i.Content
+ editor.Content = i.StringifyContent()
break
}
default:
editor.Class = "content-only"
- editor.Content = i.Content
+ editor.Content = i.StringifyContent()
}
return editor, nil
}
-// HasFrontMatterRune checks if the file has the frontmatter rune
-func HasFrontMatterRune(file []byte) bool {
+// hasFrontMatterRune checks if the file has the frontmatter rune
+func hasFrontMatterRune(file []byte) bool {
return strings.HasPrefix(string(file), "---") ||
strings.HasPrefix(string(file), "+++") ||
strings.HasPrefix(string(file), "{")
}
-// AppendFrontMatterRune appends the frontmatter rune to a file
-func AppendFrontMatterRune(frontmatter []byte, language string) []byte {
+// appendFrontMatterRune appends the frontmatter rune to a file
+func appendFrontMatterRune(frontmatter []byte, language string) []byte {
switch language {
case "yaml":
return []byte("---\n" + string(frontmatter) + "\n---")
@@ -108,8 +108,8 @@ func AppendFrontMatterRune(frontmatter []byte, language string) []byte {
return frontmatter
}
-// CanBeEdited checks if the extension of a file is supported by the editor
-func CanBeEdited(filename string) bool {
+// canBeEdited checks if the extension of a file is supported by the editor
+func canBeEdited(filename string) bool {
extensions := [...]string{
"md", "markdown", "mdown", "mmark",
"asciidoc", "adoc", "ad",
diff --git a/filemanager.go b/filemanager.go
index 3efc3cd6..bc8f1e18 100644
--- a/filemanager.go
+++ b/filemanager.go
@@ -16,9 +16,7 @@ import (
"github.com/hacdias/caddy-filemanager/assets"
"github.com/hacdias/caddy-filemanager/config"
- "github.com/hacdias/caddy-filemanager/directory"
"github.com/hacdias/caddy-filemanager/errors"
- "github.com/hacdias/caddy-filemanager/page"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@@ -33,7 +31,7 @@ type FileManager struct {
func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var (
c *config.Config
- fi *directory.Info
+ fi *FileInfo
code int
err error
user *config.User
@@ -78,7 +76,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
}
}
- c.WebDavHandler.ServeHTTP(w, r)
+ c.Handler.ServeHTTP(w, r)
return 0, nil
}
@@ -96,7 +94,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
if r.Method == http.MethodGet {
// Gets the information of the directory/file
- fi, code, err = directory.GetInfo(r.URL, c, user)
+ fi, code, err = GetInfo(r.URL, c, user)
if err != nil {
if r.Method == http.MethodGet {
return errors.PrintHTML(w, code, err)
@@ -106,7 +104,7 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
// If it's a dir and the path doesn't end with a trailing slash,
// redirect the user.
- if fi.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
+ if fi.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, c.AddrPath+r.URL.Path+"/", http.StatusTemporaryRedirect)
return 0, nil
}
@@ -114,23 +112,23 @@ func (f FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
// Generate anti security token.
c.GenerateToken()
- if !fi.IsDir {
+ if !fi.IsDir() {
query := r.URL.Query()
if val, ok := query["raw"]; ok && val[0] == "true" {
r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1)
- c.WebDavHandler.ServeHTTP(w, r)
+ c.Handler.ServeHTTP(w, r)
return 0, nil
}
if val, ok := query["download"]; ok && val[0] == "true" {
- w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name)
+ w.Header().Set("Content-Disposition", "attachment; filename="+fi.Name())
r.URL.Path = strings.Replace(r.URL.Path, c.BaseURL, c.WebDavURL, 1)
- c.WebDavHandler.ServeHTTP(w, r)
+ c.Handler.ServeHTTP(w, r)
return 0, nil
}
}
- code, err := fi.ServeAsHTML(w, r, c, user)
+ code, err := fi.ServeHTTP(w, r, c, user)
if err != nil {
return errors.PrintHTML(w, code, err)
}
@@ -189,7 +187,7 @@ func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config
return http.StatusNotImplemented, nil
}
- path := strings.Replace(r.URL.Path, c.BaseURL, c.PathScope, 1)
+ path := strings.Replace(r.URL.Path, c.BaseURL, c.Scope, 1)
path = filepath.Clean(path)
cmd := exec.Command(command[0], command[1:len(command)]...)
@@ -200,6 +198,6 @@ func command(w http.ResponseWriter, r *http.Request, c *config.Config, u *config
return http.StatusInternalServerError, err
}
- page := &page.Page{Info: &page.Info{Data: string(output)}}
- return page.PrintAsJSON(w)
+ p := &page{pageInfo: &pageInfo{Data: string(output)}}
+ return p.PrintAsJSON(w)
}
diff --git a/info.go b/info.go
new file mode 100644
index 00000000..ed6da747
--- /dev/null
+++ b/info.go
@@ -0,0 +1,165 @@
+package filemanager
+
+import (
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+
+ humanize "github.com/dustin/go-humanize"
+ "github.com/hacdias/caddy-filemanager/config"
+)
+
+// FileInfo contains the information about a particular file or directory
+type FileInfo struct {
+ os.FileInfo
+ URL string
+ Path string // Relative path to Caddyfile
+ VirtualPath string // Relative path to u.FileSystem
+ Mimetype string
+ Content []byte
+ Type string
+ UserAllowed bool // Indicates if the user has enough permissions
+}
+
+// GetInfo gets the file information and, in case of error, returns the
+// respective HTTP error code
+func GetInfo(url *url.URL, c *config.Config, u *config.User) (*FileInfo, int, error) {
+ var err error
+
+ i := &FileInfo{URL: url.Path}
+ i.VirtualPath = strings.Replace(url.Path, c.BaseURL, "", 1)
+ i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
+ i.VirtualPath = "/" + i.VirtualPath
+
+ i.Path = u.Scope + i.VirtualPath
+ i.Path = strings.Replace(i.Path, "\\", "/", -1)
+ i.Path = filepath.Clean(i.Path)
+
+ i.FileInfo, err = os.Stat(i.Path)
+ 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 i, code, err
+ }
+
+ return i, 0, nil
+}
+
+func (i *FileInfo) Read() error {
+ var err error
+ i.Content, err = ioutil.ReadFile(i.Path)
+ if err != nil {
+ return err
+ }
+ i.Mimetype = http.DetectContentType(i.Content)
+ i.Type = SimplifyMimeType(i.Mimetype)
+ return nil
+}
+
+func (i FileInfo) StringifyContent() string {
+ return string(i.Content)
+}
+
+// HumanSize returns the size of the file as a human-readable string
+// in IEC format (i.e. power of 2 or base 1024).
+func (i FileInfo) HumanSize() string {
+ return humanize.IBytes(uint64(i.Size()))
+}
+
+// HumanModTime returns the modified time of the file as a human-readable string.
+func (i FileInfo) HumanModTime(format string) string {
+ return i.ModTime().Format(format)
+}
+
+func (i *FileInfo) 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 *FileInfo) 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{
+ pageInfo: &pageInfo{
+ Name: i.Name(),
+ Path: i.VirtualPath,
+ IsDir: false,
+ Data: i,
+ User: u,
+ Config: c,
+ },
+ }
+
+ if (canBeEdited(i.Name()) || i.Type == "text") && 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 SimplifyMimeType(name string) string {
+ if strings.HasPrefix(name, "video") {
+ return "video"
+ }
+
+ if strings.HasPrefix(name, "audio") {
+ return "audio"
+ }
+
+ if strings.HasPrefix(name, "image") {
+ return "image"
+ }
+
+ if strings.HasPrefix(name, "text") {
+ return "text"
+ }
+
+ if strings.HasPrefix(name, "application/javascript") {
+ return "text"
+ }
+
+ return "blob"
+}
diff --git a/directory/listing.go b/listing.go
similarity index 52%
rename from directory/listing.go
rename to listing.go
index 0aa87fa8..78dc6b47 100644
--- a/directory/listing.go
+++ b/listing.go
@@ -1,11 +1,18 @@
-package directory
+package filemanager
import (
+ "encoding/json"
+ "fmt"
"net/http"
+ "net/url"
+ "os"
+ "path"
"sort"
"strconv"
"strings"
+ "github.com/hacdias/caddy-filemanager/config"
+ "github.com/hacdias/caddy-filemanager/utils/errors"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
@@ -16,7 +23,7 @@ type Listing struct {
// The full path of the request
Path string
// The items (files and folders) in the path
- Items []Info
+ Items []FileInfo
// The number of directories in the listing
NumDirs int
// The number of files (items that aren't directories) in the listing
@@ -77,15 +84,15 @@ 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 {
+ if l.Items[i].IsDir() && !l.Items[j].IsDir() {
return true
}
- if !l.Items[i].IsDir && l.Items[j].IsDir {
+ if !l.Items[i].IsDir() && l.Items[j].IsDir() {
return false
}
- return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
+ return strings.ToLower(l.Items[i].Name()) < strings.ToLower(l.Items[j].Name())
}
// By Size
@@ -94,11 +101,11 @@ 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, jSize := l.Items[i].Size(), l.Items[j].Size()
+ if l.Items[i].IsDir() {
iSize = directoryOffset + iSize
}
- if l.Items[j].IsDir {
+ if l.Items[j].IsDir() {
jSize = directoryOffset + jSize
}
return iSize < jSize
@@ -107,7 +114,7 @@ func (l bySize) Less(i, j int) bool {
// 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) }
+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"
@@ -139,3 +146,121 @@ func (l Listing) applySort() {
}
}
}
+
+func (i *FileInfo) serveListing(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
+ var err error
+
+ file, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
+ if err != nil {
+ return errors.ToHTTPCode(err), err
+ }
+ defer file.Close()
+
+ listing, err := i.loadDirectoryContents(file, r.URL.Path, u)
+ 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: 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{
+ pageInfo: &pageInfo{
+ 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 FileInfo) loadDirectoryContents(file http.File, path string, u *config.User) (*Listing, error) {
+ files, err := file.Readdir(-1)
+ if err != nil {
+ return nil, err
+ }
+
+ listing := directoryListing(files, i.VirtualPath, path, u)
+ return &listing, nil
+}
+
+func directoryListing(files []os.FileInfo, urlPath string, basePath string, u *config.User) Listing {
+ var (
+ fileinfos []FileInfo
+ dirCount, fileCount int
+ )
+
+ for _, f := range files {
+ name := f.Name()
+
+ if f.IsDir() {
+ name += "/"
+ dirCount++
+ } else {
+ fileCount++
+ }
+
+ // Absolute URL
+ url := url.URL{Path: basePath + name}
+ fileinfos = append(fileinfos, FileInfo{
+ FileInfo: f,
+ URL: url.String(),
+ UserAllowed: u.Allowed(url.String()),
+ })
+ }
+
+ return Listing{
+ Name: path.Base(urlPath),
+ Path: urlPath,
+ Items: fileinfos,
+ NumDirs: dirCount,
+ NumFiles: fileCount,
+ }
+}
diff --git a/page/page.go b/page.go
similarity index 85%
rename from page/page.go
rename to page.go
index a8266dc6..f511b7a8 100644
--- a/page/page.go
+++ b/page.go
@@ -1,4 +1,4 @@
-package page
+package filemanager
import (
"bytes"
@@ -13,14 +13,14 @@ import (
"github.com/hacdias/caddy-filemanager/utils/variables"
)
-// Page contains the informations and functions needed to show the page
-type Page struct {
- *Info
+// page contains the informations and functions needed to show the page
+type page struct {
+ *pageInfo
Minimal bool
}
-// Info contains the information of a page
-type Info struct {
+// pageInfo contains the information of a page
+type pageInfo struct {
Name string
Path string
IsDir bool
@@ -31,7 +31,7 @@ type Info struct {
// BreadcrumbMap returns p.Path where every element is a map
// of URLs and path segment names.
-func (i Info) BreadcrumbMap() map[string]string {
+func (i pageInfo) BreadcrumbMap() map[string]string {
result := map[string]string{}
if len(i.Path) == 0 {
@@ -62,7 +62,7 @@ func (i Info) BreadcrumbMap() map[string]string {
}
// PreviousLink returns the path of the previous folder
-func (i Info) PreviousLink() string {
+func (i pageInfo) PreviousLink() string {
path := strings.TrimSuffix(i.Path, "/")
path = strings.TrimPrefix(path, "/")
path = i.Config.AbsoluteURL + "/" + path
@@ -76,7 +76,7 @@ func (i Info) PreviousLink() string {
}
// PrintAsHTML formats the page in HTML and executes the template
-func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) {
+func (p page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, error) {
// Create the functions map, then the template, check for erros and
// execute the template if there aren't errors
functions := template.FuncMap{
@@ -124,7 +124,7 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
}
buf := &bytes.Buffer{}
- err := tpl.Execute(buf, p.Info)
+ err := tpl.Execute(buf, p.pageInfo)
if err != nil {
return http.StatusInternalServerError, err
@@ -136,8 +136,8 @@ func (p Page) PrintAsHTML(w http.ResponseWriter, templates ...string) (int, erro
}
// PrintAsJSON prints the current page infromation in JSON
-func (p Page) PrintAsJSON(w http.ResponseWriter) (int, error) {
- marsh, err := json.Marshal(p.Info.Data)
+func (p page) PrintAsJSON(w http.ResponseWriter) (int, error) {
+ marsh, err := json.Marshal(p.pageInfo.Data)
if err != nil {
return http.StatusInternalServerError, err
}
diff --git a/directory/update.go b/preput.go
similarity index 77%
rename from directory/update.go
rename to preput.go
index 10483769..f2285910 100644
--- a/directory/update.go
+++ b/preput.go
@@ -1,4 +1,4 @@
-package directory
+package filemanager
import (
"bytes"
@@ -15,7 +15,7 @@ import (
)
// Update is used to update a file that was edited
-func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
+func (i *FileInfo) Update(w http.ResponseWriter, r *http.Request, c *config.Config, u *config.User) (int, error) {
var (
data map[string]interface{}
file []byte
@@ -38,7 +38,7 @@ func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config,
switch kind {
case "frontmatter-only":
- if file, code, err = ParseFrontMatterOnlyFile(data, i.Name); err != nil {
+ if file, code, err = parseFrontMatterOnlyFile(data, i.Name()); err != nil {
return http.StatusInternalServerError, err
}
case "content-only":
@@ -46,7 +46,7 @@ func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config,
mainContent = strings.TrimSpace(mainContent)
file = []byte(mainContent)
case "complete":
- if file, code, err = ParseCompleteFile(data, i.Name, u.FrontMatter); err != nil {
+ if file, code, err = parseCompleteFile(data, i.Name(), u.FrontMatter); err != nil {
return http.StatusInternalServerError, err
}
default:
@@ -58,10 +58,10 @@ func (i *Info) Update(w http.ResponseWriter, r *http.Request, c *config.Config,
return code, nil
}
-// ParseFrontMatterOnlyFile parses a frontmatter only file
-func ParseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, error) {
+// parseFrontMatterOnlyFile parses a frontmatter only file
+func parseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, error) {
frontmatter := strings.TrimPrefix(filepath.Ext(filename), ".")
- f, code, err := ParseFrontMatter(data, frontmatter)
+ f, code, err := parseFrontMatter(data, frontmatter)
fString := string(f)
// If it's toml or yaml, strip frontmatter identifier
@@ -79,8 +79,8 @@ func ParseFrontMatterOnlyFile(data interface{}, filename string) ([]byte, int, e
return f, code, err
}
-// ParseFrontMatter is the frontmatter parser
-func ParseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) {
+// parseFrontMatter is the frontmatter parser
+func parseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error) {
var mark rune
switch frontmatter {
@@ -103,8 +103,8 @@ func ParseFrontMatter(data interface{}, frontmatter string) ([]byte, int, error)
return f, http.StatusOK, nil
}
-// ParseCompleteFile parses a complete file
-func ParseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, int, error) {
+// parseCompleteFile parses a complete file
+func parseCompleteFile(data map[string]interface{}, filename string, frontmatter string) ([]byte, int, error) {
mainContent := ""
if _, ok := data["content"]; ok {
@@ -120,7 +120,7 @@ func ParseCompleteFile(data map[string]interface{}, filename string, frontmatter
data["date"] = data["date"].(string) + ":00"
}
- front, code, err := ParseFrontMatter(data, frontmatter)
+ front, code, err := parseFrontMatter(data, frontmatter)
if err != nil {
fmt.Println(frontmatter)