You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gophi/core/filesystem.go

351 lines
8.6 KiB
Go

package core
import (
"io"
"os"
"sort"
"time"
)
var (
// FileReadBufSize is the file read buffer size
fileReadBufSize int
// MonitorSleepTime is the duration the goroutine should periodically sleep before running file cache freshness checks
monitorSleepTime time.Duration
// FileSizeMax is the maximum file size that is alloewd to be cached
fileSizeMax int64
// FileSystem is the global FileSystem object
FileSystem *FileSystemObject
// userDir is the set subdir name to be looked for under user's home folders
userDir string
)
// FileSystemObject holds onto an LRUCacheMap and manages access to it, handless freshness checking and multi-threading
type FileSystemObject struct {
cache *lruCacheMap
UpgradeableMutex
}
// NewFileSystemObject returns a new FileSystemObject
func newFileSystemObject(size int) *FileSystemObject {
return &FileSystemObject{
newLRUCacheMap(size),
UpgradeableMutex{},
}
}
// StartMonitor starts the FileSystemObject freshness check monitor in its own goroutine
func (fs *FileSystemObject) StartMonitor() {
for {
// Sleep to not take up all the precious CPU time :)
time.Sleep(monitorSleepTime)
// Check file cache freshness
fs.checkCacheFreshness()
}
}
// checkCacheFreshness iterates through FileSystemObject's cache and check for freshness
func (fs *FileSystemObject) checkCacheFreshness() {
// Before anything get cache lock
fs.Lock()
fs.cache.Iterate(func(path string, f *file) {
// If this is a generated file we skip
if isGeneratedType(f) {
return
}
// Check file still exists on disk
stat, err := os.Stat(path)
if err != nil {
SystemLog.Error(cacheFileStatErrStr, path)
fs.cache.Remove(path)
return
}
// Get last mod time and check freshness
lastMod := stat.ModTime().UnixNano()
if f.IsFresh() && f.LastRefresh() < lastMod {
f.SetUnfresh()
}
})
// Done! Unlock (:
fs.Unlock()
}
// OpenFile opens a file for reading (read-only, world-readable)
func (fs *FileSystemObject) OpenFile(p *Path) (*os.File, Error) {
fd, err := os.OpenFile(p.Absolute(), os.O_RDONLY, 0444)
if err != nil {
return nil, WrapError(FileOpenErr, err)
}
return fd, nil
}
// StatFile performs a file stat on a file at path
func (fs *FileSystemObject) StatFile(p *Path) (os.FileInfo, Error) {
stat, err := os.Stat(p.Absolute())
if err != nil {
return nil, WrapError(FileStatErr, err)
}
return stat, nil
}
// ReadFile reads a supplied file descriptor into a return byte slice, or error
func (fs *FileSystemObject) ReadFile(fd *os.File) ([]byte, Error) {
// Return slice
ret := make([]byte, 0)
// Get read buffers, defer putting back
br := fileBufferedReaderPool.Get(fd)
defer fileBufferedReaderPool.Put(br)
// Read through file until null bytes / error
for {
// Read line
line, err := br.ReadBytes('\n')
if err != nil {
if err == io.EOF {
// EOF, add current to return slice and
// break-out. WIll not have hit delim
ret = append(ret, line...)
break
} else {
// Bad error, return
return nil, WrapError(FileReadErr, err)
}
}
// Add current line to return slice
ret = append(ret, line...)
}
// Return!
return ret, nil
}
// ScanFile scans a supplied file at file descriptor, using iterator function
func (fs *FileSystemObject) ScanFile(fd *os.File, iterator func(string) bool) Error {
// Get read buffer, defer putting back
br := fileBufferedReaderPool.Get(fd)
defer fileBufferedReaderPool.Put(br)
// Iterate through file!
for {
// Read a line
line, err := br.ReadString('\n')
if err != nil {
if err == io.EOF {
// Reached end of file, perform final iteration
// and break-out. Will not have hit delim
iterator(line)
break
} else {
// Bad error, return
return WrapError(FileReadErr, err)
}
}
// Run scan iterator on this line, breaking out if requested,
// skipping final byte which is '\n'
if !iterator(line[:len(line)-1]) {
break
}
}
// Return no errors :)
return nil
}
// ScanDirectory reads the contents of a directory and performs the iterator function on each os.FileInfo entry returned
func (fs *FileSystemObject) ScanDirectory(fd *os.File, p *Path, iterator func(os.FileInfo, *Path)) Error {
dirList, err := fd.Readdir(-1)
if err != nil {
return WrapError(DirectoryReadErr, err)
}
// Sort by name
sort.Sort(byName(dirList))
// Walk through the directory list using supplied iterator function
for _, info := range dirList {
// Make new Path object
fp := p.JoinPath(info.Name())
// Skip restricted files
if IsRestrictedPath(fp) || IsHiddenPath(fp) || WithinCGIDir(fp) {
continue
}
// Perform iterator
iterator(info, p.JoinPath(info.Name()))
}
return nil
}
// AddGeneratedFile adds a generated file content byte slice to the file cache, with supplied path as the key
func (fs *FileSystemObject) AddGeneratedFile(p *Path, b []byte) {
// Get write lock, defer unlock
fs.Lock()
defer fs.Unlock()
// Create new generatedFileContents
contents := &generatedFileContents{b}
// Wrap contents in File
file := newFile(contents)
// Add to cache!
fs.cache.Put(p.Absolute(), file)
}
// HandleClient handles a Client, attempting to serve their request from the filesystem whether a regular file, gophermap, dir listing or CGI script
func (fs *FileSystemObject) HandleClient(client *Client, request *Request, newFileContents func(*Path) FileContents, handleDirectory func(*FileSystemObject, *Client, *os.File, *Path) Error) Error {
// If restricted, return error
if IsRestrictedPath(request.Path()) {
return NewError(RestrictedPathErr)
}
// Try remap request, log if so
ok := RemapRequest(request)
if ok {
client.LogInfo(requestRemappedStr, request.Path().Selector(), request.Params())
}
// First check for file on disk
fd, err := fs.OpenFile(request.Path())
if err != nil {
// Get read-lock, defer unlock
fs.RLock()
defer fs.RUnlock()
// Don't throw in the towel yet! Check for generated file in cache
file, ok := fs.cache.Get(request.Path().Absolute())
if !ok {
return err
}
// We got a generated file! Close and send as-is
return file.WriteToClient(client, request.Path())
}
defer fd.Close()
// Get stat
stat, goErr := fd.Stat()
if goErr != nil {
// Unlock, return error
fs.RUnlock()
return WrapError(FileStatErr, goErr)
}
switch {
// Directory
case stat.Mode()&os.ModeDir != 0:
// Don't support CGI script dir enumeration
if WithinCGIDir(request.Path()) {
return NewError(RestrictedPathErr)
}
// Else enumerate dir
return handleDirectory(fs, client, fd, request.Path())
// Regular file
case stat.Mode()&os.ModeType == 0:
// Execute script if within CGI dir
if WithinCGIDir(request.Path()) {
return ExecuteCGIScript(client, request)
}
// Else just fetch
return fs.FetchFile(client, fd, stat, request.Path(), newFileContents)
// Unsupported type
default:
return NewError(FileTypeErr)
}
}
// FetchFile attempts to fetch a file from the cache, using the supplied file stat, Path and serving client. Returns Error status
func (fs *FileSystemObject) FetchFile(client *Client, fd *os.File, stat os.FileInfo, p *Path, newFileContents func(*Path) FileContents) Error {
// If file too big, write direct to client
if stat.Size() > fileSizeMax {
return client.Conn().ReadFrom(fd)
}
// Get cache read lock, defer unlock
fs.RLock()
defer fs.RUnlock()
// Now check for file in cache
f, ok := fs.cache.Get(p.Absolute())
if !ok {
// Create new file contents with supplied function
contents := newFileContents(p)
// Wrap contents in file
f = newFile(contents)
// Cache the file contents
err := f.CacheContents(fd, p)
if err != nil {
// Unlock, return error
return err
}
// Try upgrade our lock, else error out (have to remember to unlock!!)
if !fs.UpgradeLock() {
fs.Unlock()
return NewError(MutexUpgradeErr)
}
// Put file in cache
fs.cache.Put(p.Absolute(), f)
// Try downgrade our lock, else error out (have to remember to runlock!!)
if !fs.DowngradeLock() {
fs.RUnlock()
return NewError(MutexDowngradeErr)
}
// Get file read lock
f.RLock()
} else {
// Get file read lock
f.RLock()
// Check for file freshness
if !f.IsFresh() {
// Try upgrade file lock, else error out (have to remember to unlock!!)
if !f.UpgradeLock() {
f.Unlock()
return NewError(MutexUpgradeErr)
}
// Refresh file contents
err := f.CacheContents(fd, p)
if err != nil {
// Unlock file, return error
f.Unlock()
return err
}
// Try downgrade file lock, else error out (have to remember to runlock!!)
if !f.DowngradeLock() {
f.RUnlock()
return NewError(MutexDowngradeErr)
}
}
}
// Defer file read unlock, write to client
defer f.RUnlock()
return f.WriteToClient(client, p)
}