filebrowser/file.go

376 lines
7.9 KiB
Go
Raw Normal View History

2017-06-24 11:12:15 +00:00
package filemanager
import (
2017-06-27 18:00:58 +00:00
"context"
2017-06-27 14:44:20 +00:00
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"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-27 14:44:20 +00:00
var (
errInvalidOption = errors.New("Invalid option")
)
2017-06-25 12:03:59 +00:00
// fileInfo contains the information about a particular file or directory.
type fileInfo struct {
2017-06-25 13:24:26 +00:00
// Used to store the file's content temporarily.
2017-06-24 11:12:15 +00:00
content []byte
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-28 15:05:30 +00:00
Content string `json:"content"`
2017-06-24 11:12:15 +00:00
}
2017-06-27 18:00:58 +00:00
// A listing is the context used to fill out a template.
type listing struct {
2017-06-28 21:20:28 +00:00
*fileInfo
2017-06-27 18:00:58 +00:00
// The items (files and folders) in the path.
2017-06-28 15:05:30 +00:00
Items []fileInfo `json:"items"`
2017-06-27 18:00:58 +00:00
// The number of directories in the listing.
2017-06-28 15:05:30 +00:00
NumDirs int `json:"numDirs"`
2017-06-27 18:00:58 +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-27 18:00:58 +00:00
// If ≠0 then Items have been limited to that many elements.
2017-06-28 15:05:30 +00:00
ItemsLimitedTo int `json:"ItemsLimitedTo"`
Display string `json:"display"`
2017-06-27 18:00:58 +00:00
}
2017-06-25 12:03:59 +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
2017-06-25 14:24:16 +00:00
func getInfo(url *url.URL, c *FileManager, u *User) (*fileInfo, error) {
2017-06-24 11:12:15 +00:00
var err error
i := &fileInfo{URL: c.RootURL() + url.Path}
i.VirtualPath = url.Path
2017-06-24 11:12:15 +00:00
i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
i.VirtualPath = "/" + i.VirtualPath
i.Path = u.scope + i.VirtualPath
2017-06-24 11:12:15 +00:00
i.Path = filepath.Clean(i.Path)
info, err := os.Stat(i.Path)
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)
return i, nil
}
2017-06-27 18:00:58 +00:00
// getListing gets the information about a specific directory and its files.
2017-06-28 21:20:28 +00:00
func getListing(u *User, filePath string, baseURL string, i *fileInfo) (*listing, error) {
2017-06-27 18:00:58 +00:00
// Gets the directory information using the Virtual File System of
// the user configuration.
file, err := u.fileSystem.OpenFile(context.TODO(), filePath, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer file.Close()
// Reads the directory and gets the information about the files.
files, err := file.Readdir(-1)
if err != nil {
return nil, err
}
var (
fileinfos []fileInfo
dirCount, fileCount int
)
for _, f := range files {
name := f.Name()
allowed := u.Allowed("/" + name)
if !allowed {
continue
}
if f.IsDir() {
name += "/"
dirCount++
} else {
fileCount++
}
// Absolute URL
url := url.URL{Path: baseURL + name}
i := fileInfo{
Name: f.Name(),
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
URL: url.String(),
}
i.RetrieveFileType()
fileinfos = append(fileinfos, i)
}
return &listing{
2017-06-28 21:20:28 +00:00
fileInfo: i,
2017-06-27 18:00:58 +00:00
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
}, nil
2017-06-24 11:12:15 +00:00
}
2017-06-25 13:24:26 +00:00
// RetrieveFileType obtains the mimetype and converts it to a simple
// type nomenclature.
2017-06-25 12:03:59 +00:00
func (i *fileInfo) RetrieveFileType() 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-06-25 13:24:26 +00:00
if mimetype == "" {
2017-06-24 11:12:15 +00:00
err := i.Read()
if err != nil {
return err
}
2017-06-25 13:24:26 +00:00
// Tries to get the file mimetype using its first
// 512 bytes.
mimetype = http.DetectContentType(i.content)
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"
return nil
}
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"
return nil
}
// If the type isn't text (and is blob for example), it will check some
// common types that are mistaken not to be text.
for _, extension := range textExtensions {
if strings.HasSuffix(i.Name, extension) {
i.Type = "text"
return nil
}
}
i.Type = "blob"
return nil
}
// Reads the file.
2017-06-25 12:03:59 +00:00
func (i *fileInfo) Read() error {
2017-06-24 11:12:15 +00:00
if len(i.content) != 0 {
return nil
}
var err error
i.content, err = ioutil.ReadFile(i.Path)
if err != nil {
return err
}
return nil
}
2017-06-27 14:44:20 +00:00
func (i fileInfo) Checksum(kind string) (string, error) {
file, err := os.Open(i.Path)
if err != nil {
return "", err
}
defer file.Close()
var h hash.Hash
switch kind {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return "", errInvalidOption
}
_, err = io.Copy(h, file)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
2017-06-25 13:24:26 +00:00
// StringifyContent returns a string with the file content.
2017-06-25 12:03:59 +00:00
func (i fileInfo) StringifyContent() string {
2017-06-24 11:12:15 +00:00
return string(i.content)
}
// CanBeEdited checks if the extension of a file is supported by the editor
2017-06-25 12:03:59 +00:00
func (i fileInfo) 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
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)
}
var textExtensions = [...]string{
".md", ".markdown", ".mdown", ".mmark",
".asciidoc", ".adoc", ".ad",
".rst",
".json", ".toml", ".yaml", ".csv", ".xml", ".rss", ".conf", ".ini",
".tex", ".sty",
".css", ".sass", ".scss",
".js",
".html",
".txt", ".rtf",
".sh", ".bash", ".ps1", ".bat", ".cmd",
".php", ".pl", ".py",
"Caddyfile",
".c", ".cc", ".h", ".hh", ".cpp", ".hpp", ".f90",
".f", ".bas", ".d", ".ada", ".nim", ".cr", ".java", ".cs", ".vala", ".vapi",
}