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)