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.
352 lines
8.7 KiB
Go
352 lines
8.7 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, skip
|
|
// final byte which is '\n'
|
|
ret = append(ret, line[:len(line)-1]...)
|
|
}
|
|
|
|
// 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)
|
|
}
|