package filemanager import ( "bytes" "context" "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/hex" "errors" "hash" "io" "io/ioutil" "mime" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "time" "github.com/hacdias/filemanager/frontmatter" "github.com/spf13/hugo/parser" ) var ( errInvalidOption = errors.New("Invalid option") ) // file contains the information about a particular file or directory. type file struct { // The name of the file. Name string `json:"name"` // The Size of the file. Size int64 `json:"size"` // The absolute URL. URL string `json:"url"` // The extension of the file. Extension string `json:"extension"` // The last modified time. ModTime time.Time `json:"modified"` // The File Mode. Mode os.FileMode `json:"mode"` // Indicates if this file is a directory. IsDir bool `json:"isDir"` // Absolute path. Path string `json:"path"` // Relative path to user's virtual File System. VirtualPath string `json:"virtualPath"` // Indicates the file content type: video, text, image, music or blob. Type string `json:"type"` // Stores the content of a text file. Content string `json:"content,omitempty"` Editor *editor `json:"editor,omitempty"` *listing `json:",omitempty"` } // A listing is the context used to fill out a template. type listing struct { // The items (files and folders) in the path. Items []file `json:"items"` // The number of directories in the listing. NumDirs int `json:"numDirs"` // The number of files (items that aren't directories) in the listing. NumFiles int `json:"numFiles"` // Which sorting order is used. Sort string `json:"sort"` // And which order. Order string `json:"order"` // Displays in mosaic or list. Display string `json:"display"` } // editor contains the information to fill the editor template. type editor struct { // Indicates if the content has only frontmatter, only content, or both. Mode string `json:"type"` // File content language. Language string `json:"language"` // This indicates if the editor should be visual or not. Visual bool `json:"visual"` FrontMatter struct { Content *frontmatter.Content `json:"content"` Rune rune `json:"rune"` } `json:"frontmatter"` } // getInfo gets the file information and, in case of error, returns the // respective HTTP error code func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { var err error i := &file{URL: c.RootURL() + url.Path} i.VirtualPath = url.Path i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/") i.VirtualPath = "/" + i.VirtualPath i.Path = u.scope + i.VirtualPath 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 } // getListing gets the information about a specific directory and its files. func (i *file) getListing(c *requestContext, r *http.Request) error { baseURL := c.fm.RootURL() + r.URL.Path // Gets the directory information using the Virtual File System of // the user configuration. f, err := c.us.fileSystem.OpenFile(context.TODO(), c.fi.VirtualPath, os.O_RDONLY, 0) if err != nil { return err } defer f.Close() // Reads the directory and gets the information about the files. files, err := f.Readdir(-1) if err != nil { return err } var ( fileinfos []file dirCount, fileCount int ) for _, f := range files { name := f.Name() allowed := c.us.Allowed("/" + name) if !allowed { continue } if f.IsDir() { name += "/" dirCount++ } else { fileCount++ } // Absolute URL url := url.URL{Path: baseURL + name} i := file{ Name: f.Name(), Size: f.Size(), ModTime: f.ModTime(), Mode: f.Mode(), IsDir: f.IsDir(), URL: url.String(), } i.RetrieveFileType() fileinfos = append(fileinfos, i) } i.listing = &listing{ Items: fileinfos, NumDirs: dirCount, NumFiles: fileCount, } return nil } // getEditor gets the editor based on a Info struct func (i *file) getEditor(r *http.Request) error { var err error // Create a new editor variable and set the mode e := &editor{ Language: editorLanguage(i.Extension), } e.Mode = editorMode(e.Language) if e.Mode == "frontmatter-only" || e.Mode == "complete" { e.Visual = true } if r.URL.Query().Get("visual") == "false" { e.Mode = "content-only" } hasRune := frontmatter.HasRune(i.Content) if e.Mode == "frontmatter-only" && !hasRune { e.FrontMatter.Rune, err = frontmatter.StringFormatToRune(e.Mode) if err != nil { goto Error } i.Content = frontmatter.AppendRune(i.Content, e.FrontMatter.Rune) hasRune = true } if e.Mode == "frontmatter-only" && hasRune { e.FrontMatter.Content, _, err = frontmatter.Pretty([]byte(i.Content)) if err != nil { goto Error } } if e.Mode == "complete" && hasRune { var page parser.Page content := []byte(i.Content) // Starts a new buffer and parses the file using Hugo's functions buffer := bytes.NewBuffer(content) page, err = parser.ReadFrom(buffer) if err != nil { goto Error } // Parses the page content and the frontmatter i.Content = strings.TrimSpace(string(page.Content())) e.FrontMatter.Rune = rune(content[0]) e.FrontMatter.Content, _, err = frontmatter.Pretty(page.FrontMatter()) } if e.Mode == "complete" && !hasRune { err = errors.New("Complete but without rune") } Error: if e.Mode == "content-only" || err != nil { e.Mode = "content-only" } i.Editor = e return nil } // RetrieveFileType obtains the mimetype and converts it to a simple // type nomenclature. func (i *file) RetrieveFileType() error { var content []byte var err error // Tries to get the file mimetype using its extension. mimetype := mime.TypeByExtension(i.Extension) if mimetype == "" { content, err = ioutil.ReadFile(i.Path) if err != nil { return err } // Tries to get the file mimetype using its first // 512 bytes. mimetype = http.DetectContentType(content) } if strings.HasPrefix(mimetype, "video") { i.Type = "video" return nil } if strings.HasPrefix(mimetype, "audio") { i.Type = "audio" return nil } if strings.HasPrefix(mimetype, "image") { i.Type = "image" return nil } if strings.HasPrefix(mimetype, "text") { i.Type = "text" goto End } if strings.HasPrefix(mimetype, "application/javascript") { i.Type = "text" goto End } // 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" 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 } } i.Content = string(content) } return nil } func (i file) 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 } // CanBeEdited checks if the extension of a file is supported by the editor func (i file) CanBeEdited() bool { return i.Type == "text" } // 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", } func editorMode(language string) string { switch language { case "json", "toml", "yaml": return "frontmatter-only" case "markdown", "asciidoc", "rst": return "complete" } return "content-only" } func editorLanguage(mode string) string { mode = strings.TrimPrefix(".", mode) switch mode { case "md", "markdown", "mdown", "mmark": mode = "markdown" case "asciidoc", "adoc", "ad": mode = "asciidoc" case "rst": mode = "rst" case "html", "htm": mode = "html" case "js": mode = "javascript" case "go": mode = "golang" } return mode }