require 'rufus/lru' # Ext3 stuff. require 'fs/ext3/superblock' require 'fs/ext3/directory_entry' require 'fs/ext3/directory' require 'fs/ext3/file_data' # Ext3 file system interface to MiqFS. module Ext3 # Default directory cache size. DEF_CACHE_SIZE = 50 # Members (these become members of an MiqFS instance). attr_accessor :superblock, :rootDir, :entry_cache, :dir_cache, :cache_hits # Top level file object. class FileObject include Ext3 attr_reader :path, :de, :inode, :data, :miqfs # NOTE: miqfs will always be an MiqFS instance. # fs_fileOpen passes 'self' into .. er, 'this'. def initialize(path, miqfs) @path = path @miqfs = miqfs end def open(mode = "r") # Ext3 module methods use miqfs instance accessors to touch @boot_sector. @mode = mode.downcase @de = ifs_getFile(@path, @miqfs) unless @de.nil? raise "File is directory: '#{@path}'" if @de.isDir? end if mode.include?("r") raise "File not found: '#{@path}'" if @de.nil? @inode = @miqfs.superblock.getInode(@de.inode) @data = FileData.new(@inode, @miqfs.superblock) end end end # File system interface. def fs_init # puts "Ext3::fs_init(#{@dobj.dInfo.fileName})" self.fsType = "Ext3" # Initialize bs & read root dir. @dobj.seek(0, IO::SEEK_SET) self.superblock = Superblock.new(@dobj) @fsId = superblock.fsId.to_s @volName = superblock.volName self.rootDir = Directory.new(superblock) self.entry_cache = LruHash.new(DEF_CACHE_SIZE) self.dir_cache = LruHash.new(DEF_CACHE_SIZE) self.cache_hits = 0 end # Returns free space on file system in bytes. def fs_freeBytes @superblock.freeBytes end # # Directory instance methods. # # Returns String array of all names, sans path. def fs_dirEntries(p) # Get path directory. dir = ifs_getDir(p) return nil if dir.nil? dir.globNames end # Make a directory. Parent must exist. def fs_dirMkdir(_p) raise "Write functionality is not yet supported on Ext3." end # Remove a directory. def fs_dirRmdir(_p) raise "Write functionality is not yet supported on Ext3." end # # File instance methods. # # Returns true if name exists, false if not. def fs_fileExists?(p) de = ifs_getFile(p) return false if de.nil? true end # Returns true if name is a regular file. def fs_fileFile?(p) de = ifs_getFile(p) return false if de.nil? de.fileType == DirectoryEntry::FT_FILE end # Returns true if name is a directory. def fs_fileDirectory?(p) de = ifs_getFile(p) return false if de.nil? de.fileType == DirectoryEntry::FT_DIRECTORY end # Returns size in bytes. def fs_fileSize(p) de = ifs_getFile(p) return nil if de.nil? @superblock.getInode(de.inode).length end # Delete file. def fs_fileDelete(_p) raise "Write functionality is not yet supported on Ext3." end # Returns Ruby Time object. def fs_fileAtime(p) de = ifs_getFile(p) return nil if de.nil? @superblock.getInode(de.inode).aTime end # Returns Ruby Time object. def fs_fileCtime(p) de = ifs_getFile(p) return nil if de.nil? @superblock.getInode(de.inode).cTime end # Returns Ruby Time object. def fs_fileMtime(p) de = ifs_getFile(p) return nil if de.nil? @superblock.getInode(de.inode).mTime end # Return true if p is a path to a symbolic link. def fs_isSymLink?(p) de = ifs_getFile(p) return false if de.nil? de.isSymLink? end # In these, fobj is a FileObject. def fs_fileSize_obj(fobj) fobj.inode.length end # Returns a Ruby Time object. def fs_fileAtime_obj(fobj) fobj.inode.aTime end # Returns a Ruby Time object. def fs_fileCtime_obj(fobj) fobj.inode.cTime end # Returns a Ruby Time obect. def fs_fileMtime_obj(fobj) fobj.inode.mTime end # New FileObject instance. # NOTE: FileObject must have access to Ext3 members. # This is kind of like a 'skip this' thing. Ext3 methods # use stuff owned by MiqFS, so this is necessary. def fs_fileOpen(p, mode = "r") fobj = FileObject.new(p, self) fobj.open(mode) fobj end # Returns current file position. def fs_fileSeek(fobj, offset, whence) fobj.data.seek(offset, whence) end # Returns a Ruby String object. def fs_fileRead(fobj, len) fobj.data.read(len) end def fs_fileWrite(_fobj, _buf, _len) raise "Write functionality is not yet supported on Ext3." # fobj.data.write(buf, len) end # Write changes & destroy. def fs_fileClose(_fobj) # TODO: unrem when write is supported. # fobj.data.close fobj = nil end # IFS members: internal file system. # Return a DirectoryEntry for a given file or nil if not exist. def ifs_getFile(p, miqfs = nil) # If this is being called from a FileObject instance, then MiqFS owns contained instance members. # If this is being called from an Ext3 module method, then self owns contained instance members. miqfs = self if miqfs.nil? # Preprocess path. p = unnormalizePath(p) dir, fname = File.split(p) # Fix for FB#835: if fil == root then fil needs to be "." fname = "." if fname == "/" || fname == "\\" # Check for this file in the cache. cache_name = "#{dir == '/' ? '' : dir}/#{fname}" if miqfs.entry_cache.key?(cache_name) miqfs.cache_hits += 1 return miqfs.entry_cache[cache_name] end # Look for file in dir, but don't error if it doesn't exist. # NOTE: if p is a directory that's ok, find it. begin dirObj = ifs_getDir(dir, miqfs) dirEnt = dirObj.nil? ? nil : dirObj.findEntry(fname) rescue RuntimeError dirEnt = nil end miqfs.entry_cache[cache_name] = dirEnt end # Create a directory entry. def ifs_putFile(p, miqfs = nil) raise "Write functionality is not yet supported on Ext3." # Commented out for now to avoid unreachable code # # # If this is being called from a FileObject instance, then MiqFS owns contained instance members. # # If this is being called from an Ext3 module method, then self owns contained instance members. # miqfs = self if miqfs.nil? # # Preprocess path. # p = unnormalizePath(p) # dir, fil = File.split(p) # # Parent directory must exist. # dirObj = ifs_getDir(dir, miqfs) # return nil if dir.nil? # dirObj.createFile(fil) end # Return a Directory object for a path. # Raise error if path doesn't exist. def ifs_getDir(p, miqfs = nil) # If this is being called from a FileObject instance, then MiqFS owns contained instance members. # If this is being called from an Ext3 module method, then self owns contained instance members. miqfs = self if miqfs.nil? # Wack leading drive. p = unnormalizePath(p) # Get an array of directory names, kill off the first (it's always empty). names = p.split(/[\\\/]/) names.shift dir = ifs_getDirR(names, miqfs) $log.info("Directory '#{p}' not found") if dir.nil? dir end # Return Directory recursively for given directory or nil if not exist. def ifs_getDirR(names, miqfs) return miqfs.rootDir if names.empty? # Check for this path in the cache. fname = names.join('/') if miqfs.dir_cache.key?(fname) miqfs.cache_hits += 1 return miqfs.dir_cache[fname] end name = names.pop pdir = ifs_getDirR(names, miqfs) return nil if pdir.nil? de = pdir.findEntry(name, DirectoryEntry::FT_DIRECTORY) return nil if de.nil? miqfs.entry_cache[fname] = de dir = Directory.new(miqfs.superblock, de.inode) return nil if dir.nil? miqfs.dir_cache[fname] = dir end # Wack leading drive leter & colon. def unnormalizePath(p) p[1] == 58 ? p[2, p.size] : p end end