require_relative 'filesystem/constants' require_relative 'filesystem/functions' require_relative 'filesystem/helper' require 'socket' require 'win32ole' require 'date' require 'time' # The Sys module serves as a namespace only. module Sys # The Filesystem class encapsulates information about your filesystem. class Filesystem include Sys::Filesystem::Constants extend Sys::Filesystem::Functions # Error typically raised if any of the Sys::Filesystem methods fail. class Error < StandardError; end # The version of the sys-filesystem library. VERSION = '1.1.4' class Mount # The name of the volume. This is the device mapping. attr_reader :name # The last time the volume was mounted. For MS Windows this equates # to your system's boot time. attr_reader :mount_time # The type of mount, e.g. NTFS, UDF, etc. attr_reader :mount_type # The volume mount point, e.g. 'C:\' attr_reader :mount_point # Various comma separated options that reflect the volume's features attr_reader :options # Always nil on MS Windows. Provided for interface compatibility only. attr_reader :pass_number # Always nil on MS Windows. Provided for interface compatibility only. attr_reader :frequency alias fsname name alias dir mount_point alias opts options alias passno pass_number alias freq frequency end class Stat # The path of the file system. attr_reader :path # The file system block size. MS Windows typically defaults to 4096. attr_reader :block_size # Fragment size. Meaningless at the moment. attr_reader :fragment_size # The total number of blocks available (used or unused) on the file # system. attr_reader :blocks # The total number of unused blocks. attr_reader :blocks_free # The total number of unused blocks available to unprivileged # processes. Identical to +blocks+ at the moment. attr_reader :blocks_available # Total number of files/inodes that can be created on the file system. # This attribute is always nil on MS Windows. attr_reader :files # Total number of free files/inodes that can be created on the file # system. This attribute is always nil on MS Windows. attr_reader :files_free # Total number of available files/inodes for unprivileged processes # that can be created on the file system. This attribute is always # nil on MS Windows. attr_reader :files_available # The file system volume id. attr_reader :filesystem_id # A bit mask of file system flags. attr_reader :flags # The maximum length of a file name permitted on the file system. attr_reader :name_max # The file system type, e.g. NTFS, FAT, etc. attr_reader :base_type # Returns the total amount of free space on the partition. attr_reader :bytes_free alias inodes files alias inodes_free files_free alias inodes_available files_available # Returns the total space on the partition. def bytes_total blocks * block_size end # Returns the total amount of used space on the partition. def bytes_used bytes_total - bytes_free end # Returns the percentage of the partition that has been used. def percent_used 100 - (100.0 * bytes_free.to_f / bytes_total.to_f) end end # Yields a Filesystem::Mount object for each volume on your system in # block form. Returns an array of Filesystem::Mount objects in non-block # form. # # Example: # # Sys::Filesystem.mounts{ |mount| # p mt.name # => \\Device\\HarddiskVolume1 # p mt.mount_point # => C:\ # p mt.mount_time # => Thu Dec 18 20:12:08 -0700 2008 # p mt.mount_type # => NTFS # p mt.options # => casepres,casesens,ro,unicode # p mt.pass_number # => nil # p mt.dump_freq # => nil # } # # This method is a bit of a fudge for MS Windows in the name of interface # compatibility because this method deals with volumes, not actual mount # points. But, I believe it provides the sort of information many users # want at a glance. # # The possible values for the +options+ and their meanings are as follows: # # casepres => The filesystem preserves the case of file names when it places a name on disk. # casesens => The filesystem supports case-sensitive file names. # compression => The filesystem supports file-based compression. # namedstreams => The filesystem supports named streams. # pacls => The filesystem preserves and enforces access control lists. # ro => The filesystem is read-only. # encryption => The filesystem supports the Encrypted File System (EFS). # objids => The filesystem supports object identifiers. # rpoints => The filesystem supports reparse points. # sparse => The filesystem supports sparse files. # unicode => The filesystem supports Unicode in file names as they appear on disk. # compressed => The filesystem is compressed. # #-- # I couldn't really find a good reason to use the wide functions for this # method. If you have one, patches welcome. # def self.mounts # First call, get needed buffer size buffer = 0.chr length = GetLogicalDriveStringsA(buffer.size, buffer) if length == 0 raise SystemCallError.new('GetLogicalDriveStrings', FFI.errno) else buffer = 0.chr * length end mounts = block_given? ? nil : [] # Try again with new buffer size if GetLogicalDriveStringsA(buffer.size, buffer) == 0 raise SystemCallError.new('GetLogicalDriveStrings', FFI.errno) end drives = buffer.split(0.chr) boot_time = get_boot_time drives.each{ |drive| mount = Mount.new volume = FFI::MemoryPointer.new(:char, MAXPATH) fsname = FFI::MemoryPointer.new(:char, MAXPATH) mount.instance_variable_set(:@mount_point, drive) mount.instance_variable_set(:@mount_time, boot_time) volume_serial_number = FFI::MemoryPointer.new(:ulong) max_component_length = FFI::MemoryPointer.new(:ulong) filesystem_flags = FFI::MemoryPointer.new(:ulong) bool = GetVolumeInformationA( drive, volume, volume.size, volume_serial_number, max_component_length, filesystem_flags, fsname, fsname.size ) # Skip unmounted floppies or cd-roms, or inaccessible drives unless bool if [5,21].include?(FFI.errno) # ERROR_NOT_READY or ERROR_ACCESS_DENIED next else raise SystemCallError.new('GetVolumeInformation', FFI.errno) end end filesystem_flags = filesystem_flags.read_ulong fsname = fsname.read_string name = 0.chr * MAXPATH if QueryDosDeviceA(drive[0,2], name, name.size) == 0 raise SystemCallError.new('QueryDosDevice', FFI.errno) end mount.instance_variable_set(:@name, name.strip) mount.instance_variable_set(:@mount_type, fsname) mount.instance_variable_set(:@options, get_options(filesystem_flags)) if block_given? yield mount else mounts << mount end } mounts # Nil if the block form was used. end # Returns the mount point for the given +file+. For MS Windows this # means the root of the path. # # Example: # # File.mount_point("C:\\Documents and Settings") # => "C:\\' # def self.mount_point(file) wfile = FFI::MemoryPointer.from_string(file.wincode) if PathStripToRootW(wfile) wfile.read_string(wfile.size).split("\000\000").first.tr(0.chr, '') else nil end end # Returns a Filesystem::Stat object that contains information about the # +path+ file system. On Windows this will default to using the root # path for volume information. # # Examples: # # Sys::Filesystem.stat("C:\\") # Sys::Filesystem.stat("C:\\Documents and Settings\\some_user") # def self.stat(path) bytes_avail = FFI::MemoryPointer.new(:ulong_long) bytes_free = FFI::MemoryPointer.new(:ulong_long) total_bytes = FFI::MemoryPointer.new(:ulong_long) mpoint = mount_point(path) wpath = path.wincode # We need this call for the 64 bit support unless GetDiskFreeSpaceExW(wpath, bytes_avail, total_bytes, bytes_free) raise SystemCallError.new('GetDiskFreeSpaceEx', FFI.errno) end bytes_avail = bytes_avail.read_ulong_long bytes_free = bytes_free.read_ulong_long total_bytes = total_bytes.read_ulong_long sectors = FFI::MemoryPointer.new(:ulong_long) bytes = FFI::MemoryPointer.new(:ulong_long) free = FFI::MemoryPointer.new(:ulong_long) total = FFI::MemoryPointer.new(:ulong_long) # We need this call for the total/cluster info, which is not in the Ex call. unless GetDiskFreeSpaceW(wpath, sectors, bytes, free, total) raise SystemCallError.new('GetDiskFreeSpace', FFI.errno) end sectors = sectors.read_ulong_long bytes = bytes.read_ulong_long free = free.read_ulong_long total = total.read_ulong_long block_size = sectors * bytes blocks_avail = total_bytes / block_size blocks_free = bytes_free / block_size vol_name_ptr = FFI::MemoryPointer.new(:char, MAXPATH) base_type_ptr = FFI::MemoryPointer.new(:char, MAXPATH) vol_serial_ptr = FFI::MemoryPointer.new(:ulong) name_max_ptr = FFI::MemoryPointer.new(:ulong) flags_ptr = FFI::MemoryPointer.new(:ulong) bool = GetVolumeInformationW( mpoint.wincode, vol_name_ptr, vol_name_ptr.size, vol_serial_ptr, name_max_ptr, flags_ptr, base_type_ptr, base_type_ptr.size ) unless bool raise SystemCallError.new('GetVolumInformation', FFI.errno) end vol_serial = vol_serial_ptr.read_ulong name_max = name_max_ptr.read_ulong flags = flags_ptr.read_ulong base_type = base_type_ptr.read_string(base_type_ptr.size).tr(0.chr, '') vol_name_ptr.free vol_serial_ptr.free name_max_ptr.free flags_ptr.free base_type_ptr.free stat_obj = Stat.new stat_obj.instance_variable_set(:@path, path) stat_obj.instance_variable_set(:@block_size, block_size) stat_obj.instance_variable_set(:@blocks, blocks_avail) stat_obj.instance_variable_set(:@blocks_available, blocks_avail) stat_obj.instance_variable_set(:@blocks_free, blocks_free) stat_obj.instance_variable_set(:@name_max, name_max) stat_obj.instance_variable_set(:@base_type, base_type) stat_obj.instance_variable_set(:@flags, flags) stat_obj.instance_variable_set(:@filesystem_id, vol_serial) stat_obj.instance_variable_set(:@bytes_free, bytes_free) stat_obj.freeze # Read-only object end private # This method is used to get the boot time of the system, which is used # for the mount_time attribute within the File.mounts method. # def self.get_boot_time host = Socket.gethostname cs = "winmgmts://#{host}/root/cimv2" begin wmi = WIN32OLE.connect(cs) rescue WIN32OLERuntimeError => e raise Error, e else query = 'select LastBootupTime from Win32_OperatingSystem' results = wmi.ExecQuery(query) results.each{ |ole| time_array = Time.parse(ole.LastBootupTime.split('.').first) return Time.mktime(*time_array) } end end # Private method that converts filesystem flags into a comma separated # list of strings. The presentation is meant as a rough analogue to the # way options are presented for Unix filesystems. # def self.get_options(flags) str = "" str << " casepres" if CASE_PRESERVED_NAMES & flags > 0 str << " casesens" if CASE_SENSITIVE_SEARCH & flags > 0 str << " compression" if FILE_COMPRESSION & flags > 0 str << " namedstreams" if NAMED_STREAMS & flags > 0 str << " pacls" if PERSISTENT_ACLS & flags > 0 str << " ro" if READ_ONLY_VOLUME & flags > 0 str << " encryption" if SUPPORTS_ENCRYPTION & flags > 0 str << " objids" if SUPPORTS_OBJECT_IDS & flags > 0 str << " rpoints" if SUPPORTS_REPARSE_POINTS & flags > 0 str << " sparse" if SUPPORTS_SPARSE_FILES & flags > 0 str << " unicode" if UNICODE_ON_DISK & flags > 0 str << " compressed" if VOLUME_IS_COMPRESSED & flags > 0 str.tr!(' ', ',') str = str[1..-1] # Ignore the first comma str end end end # Some convenient methods for converting bytes to kb, mb, and gb. # class Numeric # call-seq: # num.to_kb # # Returns +num+ in terms of kilobytes. def to_kb self / 1024 end # call-seq: # num.to_mb # # Returns +num+ in terms of megabytes. def to_mb self / 1048576 end # call-seq: # num.to_gb # # Returns +num+ in terms of gigabytes. def to_gb self / 1073741824 end # call-seq: # num.to_gb # # Returns +num+ in terms of terabytes. def to_tb self / 1099511627776 end end