filebrowser/file.go

488 lines
10 KiB
Go
Raw Normal View History

package filebrowser
2017-06-24 11:12:15 +00:00
import (
2017-07-02 16:53:47 +00:00
"bytes"
2017-06-27 14:44:20 +00:00
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash"
"io"
2017-06-24 11:12:15 +00:00
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
2017-06-27 18:00:58 +00:00
"sort"
2017-06-24 11:12:15 +00:00
"strings"
"time"
2017-06-29 09:17:35 +00:00
2017-08-11 16:45:30 +00:00
"github.com/gohugoio/hugo/parser"
2018-07-29 11:22:11 +00:00
"github.com/maruel/natural"
2017-06-24 11:12:15 +00:00
)
2017-08-18 08:00:32 +00:00
// File contains the information about a particular file or directory.
type File struct {
2017-08-19 11:35:44 +00:00
// Indicates the Kind of view on the front-end (Listing, editor or preview).
2017-07-02 16:40:52 +00:00
Kind string `json:"kind"`
2017-06-27 18:00:58 +00:00
// The name of the file.
2017-06-28 15:05:30 +00:00
Name string `json:"name"`
2017-06-27 18:00:58 +00:00
// The Size of the file.
2017-06-28 15:05:30 +00:00
Size int64 `json:"size"`
2017-06-27 18:00:58 +00:00
// The absolute URL.
2017-06-28 15:05:30 +00:00
URL string `json:"url"`
2017-06-27 18:00:58 +00:00
// The extension of the file.
2017-06-28 15:05:30 +00:00
Extension string `json:"extension"`
2017-06-27 18:00:58 +00:00
// The last modified time.
2017-06-28 15:05:30 +00:00
ModTime time.Time `json:"modified"`
2017-06-27 18:00:58 +00:00
// The File Mode.
2017-06-28 15:05:30 +00:00
Mode os.FileMode `json:"mode"`
2017-06-27 18:00:58 +00:00
// Indicates if this file is a directory.
2017-06-28 15:05:30 +00:00
IsDir bool `json:"isDir"`
2017-06-25 13:24:26 +00:00
// Absolute path.
2017-06-28 15:05:30 +00:00
Path string `json:"path"`
2017-06-25 13:24:26 +00:00
// Relative path to user's virtual File System.
2017-06-28 15:05:30 +00:00
VirtualPath string `json:"virtualPath"`
2017-06-25 13:24:26 +00:00
// Indicates the file content type: video, text, image, music or blob.
2017-06-28 15:05:30 +00:00
Type string `json:"type"`
2017-06-28 10:45:41 +00:00
// Stores the content of a text file.
2017-06-29 09:17:35 +00:00
Content string `json:"content,omitempty"`
2017-08-19 11:35:44 +00:00
*Listing `json:",omitempty"`
2017-07-01 07:50:42 +00:00
Metadata string `json:"metadata,omitempty"`
Language string `json:"language,omitempty"`
2017-06-24 11:12:15 +00:00
}
2017-08-19 11:35:44 +00:00
// A Listing is the context used to fill out a template.
type Listing struct {
2017-06-27 18:00:58 +00:00
// The items (files and folders) in the path.
2017-08-18 08:00:32 +00:00
Items []*File `json:"items"`
2017-08-19 11:35:44 +00:00
// The number of directories in the Listing.
2017-06-28 15:05:30 +00:00
NumDirs int `json:"numDirs"`
2017-08-19 11:35:44 +00:00
// The number of files (items that aren't directories) in the Listing.
2017-06-28 15:05:30 +00:00
NumFiles int `json:"numFiles"`
2017-06-27 18:00:58 +00:00
// Which sorting order is used.
2017-06-28 15:05:30 +00:00
Sort string `json:"sort"`
2017-06-27 18:00:58 +00:00
// And which order.
2017-06-28 15:05:30 +00:00
Order string `json:"order"`
2017-06-29 09:17:35 +00:00
}
2017-08-18 08:00:32 +00:00
// GetInfo gets the file information and, in case of error, returns the
2017-06-24 11:12:15 +00:00
// respective HTTP error code
func GetInfo(url *url.URL, c *FileBrowser, u *User) (*File, error) {
2017-06-24 11:12:15 +00:00
var err error
2017-08-18 08:00:32 +00:00
i := &File{
2017-07-26 14:55:39 +00:00
URL: "/files" + url.String(),
2017-07-02 16:40:52 +00:00
VirtualPath: url.Path,
2017-08-20 08:21:36 +00:00
Path: filepath.Join(u.Scope, url.Path),
2017-07-02 16:40:52 +00:00
}
2017-06-24 11:12:15 +00:00
info, err := u.FileSystem.Stat(url.Path)
2017-06-24 11:12:15 +00:00
if err != nil {
return i, err
}
i.Name = info.Name()
i.ModTime = info.ModTime()
i.Mode = info.Mode()
i.IsDir = info.IsDir()
i.Size = info.Size()
i.Extension = filepath.Ext(i.Name)
2017-07-03 14:19:17 +00:00
if i.IsDir && !strings.HasSuffix(i.URL, "/") {
i.URL += "/"
}
2017-06-24 11:12:15 +00:00
return i, nil
}
2017-08-18 08:00:32 +00:00
// GetListing gets the information about a specific directory and its files.
func (i *File) GetListing(u *User, r *http.Request) error {
2017-06-27 18:00:58 +00:00
// Gets the directory information using the Virtual File System of
// the user configuration.
2017-08-18 08:00:32 +00:00
f, err := u.FileSystem.OpenFile(i.VirtualPath, os.O_RDONLY, 0)
2017-06-27 18:00:58 +00:00
if err != nil {
2017-06-29 09:17:35 +00:00
return err
2017-06-27 18:00:58 +00:00
}
2017-06-29 09:17:35 +00:00
defer f.Close()
2017-06-27 18:00:58 +00:00
// Reads the directory and gets the information about the files.
2017-06-29 09:17:35 +00:00
files, err := f.Readdir(-1)
2017-06-27 18:00:58 +00:00
if err != nil {
2017-06-29 09:17:35 +00:00
return err
2017-06-27 18:00:58 +00:00
}
var (
2017-08-18 08:00:32 +00:00
fileinfos []*File
2017-06-27 18:00:58 +00:00
dirCount, fileCount int
)
2017-07-26 14:55:39 +00:00
baseurl, err := url.PathUnescape(i.URL)
if err != nil {
return err
}
2017-06-27 18:00:58 +00:00
for _, f := range files {
name := f.Name()
2017-08-18 08:00:32 +00:00
allowed := u.Allowed("/" + name)
2017-06-27 18:00:58 +00:00
if !allowed {
continue
}
if strings.HasPrefix(f.Mode().String(), "L") {
2018-02-18 09:49:40 +00:00
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead if the target's.
info, err := os.Stat(f.Name())
2018-02-18 09:49:40 +00:00
if err == nil {
f = info
}
}
2017-06-27 18:00:58 +00:00
if f.IsDir() {
name += "/"
dirCount++
} else {
fileCount++
}
// Absolute URL
2017-07-26 14:55:39 +00:00
url := url.URL{Path: baseurl + name}
2017-06-27 18:00:58 +00:00
2017-08-18 08:00:32 +00:00
i := &File{
2017-07-08 11:46:19 +00:00
Name: f.Name(),
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
URL: url.String(),
Extension: filepath.Ext(name),
VirtualPath: filepath.Join(i.VirtualPath, name),
Path: filepath.Join(i.Path, name),
2017-06-27 18:00:58 +00:00
}
2017-07-26 08:44:09 +00:00
i.GetFileType(false)
2017-06-27 18:00:58 +00:00
fileinfos = append(fileinfos, i)
}
2017-08-19 11:35:44 +00:00
i.Listing = &Listing{
2017-06-27 18:00:58 +00:00
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
2017-06-29 09:17:35 +00:00
}
return nil
}
2017-08-18 08:00:32 +00:00
// GetEditor gets the editor based on a Info struct
func (i *File) GetEditor() error {
2017-07-01 07:50:42 +00:00
i.Language = editorLanguage(i.Extension)
// If the editor will hold only content, leave now.
if editorMode(i.Language) == "content" {
return nil
2017-06-29 09:17:35 +00:00
}
2017-07-01 07:50:42 +00:00
// If the file doesn't have any kind of metadata, leave now.
2017-07-02 16:53:47 +00:00
if !hasRune(i.Content) {
2017-07-01 07:50:42 +00:00
return nil
2017-06-29 09:17:35 +00:00
}
2017-07-02 16:53:47 +00:00
buffer := bytes.NewBuffer([]byte(i.Content))
page, err := parser.ReadFrom(buffer)
2017-06-29 09:17:35 +00:00
2017-07-02 16:53:47 +00:00
// If there is an error, just ignore it and return nil.
// This way, the file can be served for editing.
if err != nil {
return nil
}
2017-06-29 09:17:35 +00:00
2017-07-02 16:53:47 +00:00
i.Content = strings.TrimSpace(string(page.Content()))
i.Metadata = strings.TrimSpace(string(page.FrontMatter()))
2017-06-29 09:17:35 +00:00
return nil
2017-06-24 11:12:15 +00:00
}
2017-07-26 08:44:09 +00:00
// GetFileType obtains the mimetype and converts it to a simple
2017-06-25 13:24:26 +00:00
// type nomenclature.
2017-08-18 08:00:32 +00:00
func (i *File) GetFileType(checkContent bool) error {
2017-06-29 09:17:35 +00:00
var content []byte
var err error
2017-06-25 13:24:26 +00:00
// Tries to get the file mimetype using its extension.
mimetype := mime.TypeByExtension(i.Extension)
2017-06-24 11:12:15 +00:00
2017-07-08 11:46:19 +00:00
if mimetype == "" && checkContent {
2017-07-10 07:43:46 +00:00
file, err := os.Open(i.Path)
2017-06-24 11:12:15 +00:00
if err != nil {
return err
}
2017-07-10 07:43:46 +00:00
defer file.Close()
// Only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil && err != io.EOF {
return err
}
2017-06-24 11:12:15 +00:00
2017-06-25 13:24:26 +00:00
// Tries to get the file mimetype using its first
// 512 bytes.
2017-07-10 07:43:46 +00:00
mimetype = http.DetectContentType(buffer)
2017-06-24 11:12:15 +00:00
}
2017-06-25 13:24:26 +00:00
if strings.HasPrefix(mimetype, "video") {
2017-06-24 11:12:15 +00:00
i.Type = "video"
return nil
}
2017-06-25 13:24:26 +00:00
if strings.HasPrefix(mimetype, "audio") {
2017-06-24 11:12:15 +00:00
i.Type = "audio"
return nil
}
2017-06-25 13:24:26 +00:00
if strings.HasPrefix(mimetype, "image") {
2017-06-24 11:12:15 +00:00
i.Type = "image"
return nil
}
2017-06-25 13:24:26 +00:00
if strings.HasPrefix(mimetype, "text") {
2017-06-24 11:12:15 +00:00
i.Type = "text"
2017-06-29 09:17:35 +00:00
goto End
2017-06-24 11:12:15 +00:00
}
2017-06-25 13:24:26 +00:00
if strings.HasPrefix(mimetype, "application/javascript") {
2017-06-24 11:12:15 +00:00
i.Type = "text"
2017-06-29 09:17:35 +00:00
goto End
2017-06-24 11:12:15 +00:00
}
// If the type isn't text (and is blob for example), it will check some
// common types that are mistaken not to be text.
if isInTextExtensions(i.Name) {
i.Type = "text"
} else {
i.Type = "blob"
2017-06-24 11:12:15 +00:00
}
2017-06-29 09:17:35 +00:00
End:
// If the file type is text, save its content.
if i.Type == "text" {
if len(content) == 0 {
content, err = ioutil.ReadFile(i.Path)
if err != nil {
return err
}
}
2017-06-24 11:12:15 +00:00
2017-06-29 09:17:35 +00:00
i.Content = string(content)
2017-06-24 11:12:15 +00:00
}
2017-06-29 09:17:35 +00:00
2017-06-24 11:12:15 +00:00
return nil
}
2017-08-18 08:00:32 +00:00
// Checksum retrieves the checksum of a file.
func (i File) Checksum(algo string) (string, error) {
2017-06-27 14:44:20 +00:00
file, err := os.Open(i.Path)
if err != nil {
return "", err
}
defer file.Close()
var h hash.Hash
2017-08-18 08:00:32 +00:00
switch algo {
2017-06-27 14:44:20 +00:00
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
2017-08-19 11:35:44 +00:00
return "", ErrInvalidOption
2017-06-27 14:44:20 +00:00
}
_, err = io.Copy(h, file)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
2017-06-24 11:12:15 +00:00
// CanBeEdited checks if the extension of a file is supported by the editor
2017-08-18 08:00:32 +00:00
func (i File) CanBeEdited() bool {
2017-06-24 11:12:15 +00:00
return i.Type == "text"
}
2017-06-27 18:00:58 +00:00
// ApplySort applies the sort order using .Order and .Sort
2017-08-19 11:35:44 +00:00
func (l Listing) ApplySort() {
2017-06-27 18:00:58 +00:00
// 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 "modified":
sort.Sort(sort.Reverse(byModified(l)))
2017-06-27 18:00:58 +00:00
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 "modified":
sort.Sort(byModified(l))
2017-06-27 18:00:58 +00:00
default:
sort.Sort(byName(l))
return
}
}
}
2017-08-19 11:35:44 +00:00
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
2017-06-27 18:00:58 +00:00
// 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
}
2018-07-29 11:22:11 +00:00
return natural.Less(l.Items[i].Name, l.Items[j].Name)
2017-06-27 18:00:58 +00:00
}
// 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 Modified
func (l byModified) Len() int {
return len(l.Items)
}
func (l byModified) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
func (l byModified) Less(i, j int) bool {
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
return iModified.Sub(jModified) < 0
}
// textExtensions is the sorted list of text extensions which
// can be edited.
var textExtensions = []string{
".ad", ".ada", ".adoc", ".asciidoc",
".bas", ".bash", ".bat",
".c", ".cc", ".cmd", ".conf", ".cpp", ".cr", ".cs", ".css", ".csv",
".d",
".f", ".f90",
".h", ".hh", ".hpp", ".htaccess", ".html",
".ini",
".java", ".js", ".json",
".markdown", ".md", ".mdown", ".mmark",
".nim",
".php", ".pl", ".ps1", ".py",
".rss", ".rst", ".rtf",
".sass", ".scss", ".sh", ".sty",
".tex", ".tml", ".toml", ".txt",
".vala", ".vapi",
".xml",
".yaml", ".yml",
2017-06-27 18:00:58 +00:00
"Caddyfile",
}
// isInTextExtensions checks if a file can be edited by its extensions.
func isInTextExtensions(name string) bool {
search := filepath.Ext(name)
if search == "" {
search = name
}
i := sort.SearchStrings(textExtensions, search)
return i < len(textExtensions) && textExtensions[i] == search
2017-06-27 18:00:58 +00:00
}
2017-06-29 09:17:35 +00:00
2017-07-02 16:53:47 +00:00
// hasRune checks if the file has the frontmatter rune
func hasRune(file string) bool {
return strings.HasPrefix(file, "---") ||
strings.HasPrefix(file, "+++") ||
strings.HasPrefix(file, "{")
}
2017-06-29 09:17:35 +00:00
func editorMode(language string) string {
switch language {
case "markdown", "asciidoc", "rst":
2017-07-01 07:50:42 +00:00
return "content+metadata"
2017-06-29 09:17:35 +00:00
}
2017-07-01 07:50:42 +00:00
return "content"
2017-06-29 09:17:35 +00:00
}
func editorLanguage(mode string) string {
2017-06-30 17:03:08 +00:00
mode = strings.TrimPrefix(mode, ".")
2017-06-29 09:17:35 +00:00
switch mode {
case "md", "markdown", "mdown", "mmark":
mode = "markdown"
2017-06-30 17:03:08 +00:00
case "yml":
mode = "yaml"
2017-06-29 09:17:35 +00:00
case "asciidoc", "adoc", "ad":
mode = "asciidoc"
case "rst":
mode = "rst"
case "html", "htm", "xml":
mode = "htmlmixed"
2017-06-29 09:17:35 +00:00
case "js":
mode = "javascript"
case "go":
mode = "golang"
case "":
mode = "text"
2017-06-29 09:17:35 +00:00
}
return mode
}