gophi/fs.go
kim (grufwub) 0f2a2099f2 improve code commenting
Signed-off-by: kim (grufwub) <grufwub@gmail.com>
2020-04-24 12:09:54 +01:00

309 lines
8.8 KiB
Go

package main
import (
"os"
"sync"
"path"
"bytes"
"time"
"io"
"sort"
"bufio"
)
/* FileSystemRequest:
* Makes a request to the filesystem either through
* the FileCache or directly to a function like listDir().
* It carries the requested filesystem path and any extra
* needed information, for the moment just a set of details
* about the virtual host.. Opens things up a lot more for
* the future :)
*/
type FileSystemRequest struct {
Path string
Host *ConnHost
}
/* File:
* Wraps around the cached contents of a file and
* helps with management of this content by the
* global FileCache objects.
*/
type File struct {
contents FileContents
mutex sync.RWMutex
isFresh bool
lastRefresh int64
}
func NewFile(contents FileContents) *File {
f := new(File)
f.contents = contents
f.mutex = sync.RWMutex{}
f.isFresh = true
f.lastRefresh = 0
return f
}
func (f *File) Contents(request *FileSystemRequest) []byte {
return f.contents.Render(request)
}
func (f *File) LoadContents() *GophorError {
/* Clear current file contents */
f.contents.Clear()
/* Reload the file */
gophorErr := f.contents.Load()
if gophorErr != nil {
return gophorErr
}
/* Update lastRefresh, set fresh, unset deletion (not likely set) */
f.lastRefresh = time.Now().UnixNano()
f.isFresh = true
return nil
}
func (f *File) IsFresh() bool {
return f.isFresh
}
func (f *File) SetUnfresh() {
f.isFresh = false
}
func (f *File) LastRefresh() int64 {
return f.lastRefresh
}
func (f *File) Lock() {
f.mutex.Lock()
}
func (f *File) Unlock() {
f.mutex.Unlock()
}
func (f *File) RLock() {
f.mutex.RLock()
}
func (f *File) RUnlock() {
f.mutex.RUnlock()
}
/* FileContents:
* Interface that provides an adaptable implementation
* for holding onto some level of information about
* the contents of a file, also methods for processing
* and returning the results when the file contents
* are requested.
*/
type FileContents interface {
Render(*FileSystemRequest) []byte
Load() *GophorError
Clear()
}
/* Perform simple buffered read on a file at path */
func bufferedRead(path string) ([]byte, *GophorError) {
/* Open file */
fd, err := os.Open(path)
if err != nil {
return nil, &GophorError{ FileOpenErr, err }
}
defer fd.Close()
/* Setup buffers */
var count int
contents := make([]byte, 0)
buf := make([]byte, FileReadBufSize)
/* Setup reader */
reader := bufio.NewReader(fd)
/* Read through buffer until error or null bytes! */
for {
count, err = reader.Read(buf)
if err != nil {
if err == io.EOF {
break
}
return nil, &GophorError{ FileReadErr, err }
}
contents = append(contents, buf[:count]...)
if count < FileReadBufSize {
break
}
}
return contents, nil
}
/* Perform buffered read on file at path, then scan through with supplied iterator func */
func bufferedScan(path string, scanIterator func(*bufio.Scanner) bool) *GophorError {
/* First, read raw file contents */
contents, gophorErr := bufferedRead(path)
if gophorErr != nil {
return gophorErr
}
/* Create reader and scanner from this */
reader := bytes.NewReader(contents)
scanner := bufio.NewScanner(reader)
/* If contains DOS line-endings, split by DOS! Else, split by Unix */
if bytes.Contains(contents, []byte(DOSLineEnd)) {
scanner.Split(dosLineEndSplitter)
} else {
scanner.Split(unixLineEndSplitter)
}
/* Scan through file contents using supplied iterator */
for scanner.Scan() && scanIterator(scanner) {}
/* Check scanner finished cleanly */
if scanner.Err() != nil {
return &GophorError{ FileReadErr, scanner.Err() }
}
return nil
}
func dosLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
/* At EOF, no more data */
return 0, nil, nil
}
if i := bytes.Index(data, []byte("\r\n")); i >= 0 {
/* We have a full new-line terminate line */
return i+2, data[:i], nil
}
/* Request more data */
return 0, nil, nil
}
func unixLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
/* At EOF, no more data */
return 0, nil, nil
}
if i := bytes.Index(data, []byte("\n")); i >= 0 {
/* We have a full new-line terminate line */
return i+1, data[:i], nil
}
/* Request more data */
return 0, nil, nil
}
/* listDir():
* Here we use an empty function pointer, and set the correct
* function to be used during the restricted files regex parsing.
* This negates need to check if RestrictedFilesRegex is nil every
* single call.
*/
var listDir func(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError)
func _listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
return _listDirBase(request, func(dirContents *[]byte, file os.FileInfo) {
/* If requested hidden */
if _, ok := hidden[file.Name()]; ok {
return
}
/* Handle file, directory or ignore others */
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
itemPath := path.Join(request.Path, file.Name())
*dirContents = append(*dirContents, buildLine(TypeDirectory, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := path.Join(request.Path, file.Name())
itemType := getItemType(itemPath)
*dirContents = append(*dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
default:
/* Ignore */
}
})
}
func _listDirRegexMatch(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
return _listDirBase(request, func(dirContents *[]byte, file os.FileInfo) {
/* If regex match in restricted files || requested hidden */
if isRestrictedFile(file.Name()) {
return
} else if _, ok := hidden[file.Name()]; ok {
return
}
/* Handle file, directory or ignore others */
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
itemPath := path.Join(request.Path, file.Name())
*dirContents = append(*dirContents, buildLine(TypeDirectory, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := path.Join(request.Path, file.Name())
itemType := getItemType(itemPath)
*dirContents = append(*dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
default:
/* Ignore */
}
})
}
func _listDirBase(request *FileSystemRequest, iterFunc func(dirContents *[]byte, file os.FileInfo)) ([]byte, *GophorError) {
/* Open directory file descriptor */
fd, err := os.Open(request.Path)
if err != nil {
Config.LogSystemError("failed to open %s: %s\n", request.Path, err.Error())
return nil, &GophorError{ FileOpenErr, err }
}
/* Read files in directory */
files, err := fd.Readdir(-1)
if err != nil {
Config.LogSystemError("failed to enumerate dir %s: %s\n", request.Path, err.Error())
return nil, &GophorError{ DirListErr, err }
}
/* Sort the files by name */
sort.Sort(byName(files))
/* Create directory content slice, ready */
dirContents := make([]byte, 0)
/* First add a title + a space */
dirContents = append(dirContents, buildLine(TypeInfo, "[ "+request.Host.Name+request.Path+" ]", "TITLE", NullHost, NullPort)...)
dirContents = append(dirContents, buildInfoLine("")...)
/* Add a 'back' entry. GoLang Readdir() seems to miss this */
dirContents = append(dirContents, buildLine(TypeDirectory, "..", path.Join(fd.Name(), ".."), request.Host.Name, request.Host.Port)...)
/* Walk through files :D */
for _, file := range files { iterFunc(&dirContents, file) }
return dirContents, nil
}
/* Took a leaf out of go-gopher's book here. */
type byName []os.FileInfo
func (s byName) Len() int { return len(s) }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }