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) }