lib/windows/sys/filesystem.rb in sys-filesystem-1.0.0 vs lib/windows/sys/filesystem.rb in sys-filesystem-1.1.0

- old
+ new

@@ -1,408 +1,394 @@ -require 'windows/error' -require 'windows/path' -require 'windows/filesystem' -require 'windows/volume' -require 'windows/handle' -require 'windows/limits' +require File.join(File.dirname(__FILE__), 'filesystem', 'constants') +require File.join(File.dirname(__FILE__), 'filesystem', 'functions') +require File.join(File.dirname(__FILE__), '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 Windows::Error - include Windows::Handle - include Windows::Limits + # The Filesystem class encapsulates information about your filesystem. + class Filesystem + include Sys::Filesystem::Constants + extend Sys::Filesystem::Functions - extend Windows::Error - extend Windows::FileSystem - extend Windows::Volume - extend Windows::Path + # Error typically raised if any of the Sys::Filesystem methods fail. + class Error < StandardError; end - # 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.0' - CASE_SENSITIVE_SEARCH = 0x00000001 - CASE_PRESERVED_NAMES = 0x00000002 - UNICODE_ON_DISK = 0x00000004 - PERSISTENT_ACLS = 0x00000008 - FILE_COMPRESSION = 0x00000010 - VOLUME_QUOTAS = 0x00000020 - SUPPORTS_SPARSE_FILES = 0x00000040 - SUPPORTS_REPARSE_POINTS = 0x00000080 - SUPPORTS_REMOTE_STORAGE = 0x00000100 - VOLUME_IS_COMPRESSED = 0x00008000 - SUPPORTS_OBJECT_IDS = 0x00010000 - SUPPORTS_ENCRYPTION = 0x00020000 - NAMED_STREAMS = 0x00040000 - READ_ONLY_VOLUME = 0x00080000 - - # The version of the sys-filesystem library. - VERSION = '1.0.0' + class Mount + # The name of the volume. This is the device mapping. + attr_reader :name - 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 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 type of mount, e.g. NTFS, UDF, etc. - attr_reader :mount_type + # The volume mount point, e.g. 'C:\' + attr_reader :mount_point - # The volume mount point, e.g. 'C:\' - attr_reader :mount_point + # Various comma separated options that reflect the volume's features + attr_reader :options - # 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 :pass_number + # Always nil on MS Windows. Provided for interface compatibility only. + attr_reader :frequency - # 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 + 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 + 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 + # 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 + # 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 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. + attr_reader :blocks_free - # The total number of unused blocks available to unprivileged - # processes. Identical to +blocks+ at the moment. - attr_reader :blocks_available + # 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 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 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 + # 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 + # The file system volume id. + attr_reader :filesystem_id - # A bit mask of file system flags. - attr_reader :flags + # 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 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 + # The file system type, e.g. NTFS, FAT, etc. + attr_reader :base_type - alias inodes files - alias inodes_free files_free - alias inodes_available files_available + alias inodes files + alias inodes_free files_free + alias inodes_available files_available + 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 - # 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. - # - def self.mounts - buffer = 0.chr * MAXPATH - length = GetLogicalDriveStrings(buffer.size, buffer) + mounts = block_given? ? nil : [] - if length == 0 - raise Error, get_last_error - end + # Try again with new buffer size + if GetLogicalDriveStringsA(buffer.size, buffer) == 0 + raise SystemCallError.new('GetLogicalDriveStrings', FFI.errno) + end - mounts = block_given? ? nil : [] + drives = buffer.split(0.chr) - # Try again if it fails because the buffer is too small - if length > buffer.size - buffer = 0.chr * length - if GetLogicalDriveStrings(buffer.size, buffer) == 0 - raise Error, get_last_error - end - end + boot_time = get_boot_time - boot_time = get_boot_time + drives.each{ |drive| + mount = Mount.new + volume = FFI::MemoryPointer.new(:char, MAXPATH) + fsname = FFI::MemoryPointer.new(:char, MAXPATH) - drives = buffer.strip.split("\0") + mount.instance_variable_set(:@mount_point, drive) + mount.instance_variable_set(:@mount_time, boot_time) - drives.each{ |drive| - mount = Mount.new - volume = 0.chr * MAXPATH - fsname = 0.chr * MAXPATH + volume_serial_number = FFI::MemoryPointer.new(:ulong) + max_component_length = FFI::MemoryPointer.new(:ulong) + filesystem_flags = FFI::MemoryPointer.new(:ulong) - mount.instance_variable_set(:@mount_point, drive) - mount.instance_variable_set(:@mount_time, boot_time) + bool = GetVolumeInformationA( + drive, + volume, + volume.size, + volume_serial_number, + max_component_length, + filesystem_flags, + fsname, + fsname.size + ) - volume_serial_number = [0].pack('L') - max_component_length = [0].pack('L') - filesystem_flags = [0].pack('L') + # 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 - bool = GetVolumeInformation( - drive, - volume, - volume.size, - volume_serial_number, - max_component_length, - filesystem_flags, - fsname, - fsname.size - ) + filesystem_flags = filesystem_flags.read_ulong + fsname = fsname.read_string - # Skip unmounted floppies or cd-roms - unless bool - errnum = GetLastError() - if errnum == ERROR_NOT_READY - next - else - raise Error, get_last_error(errnum) - end - end + name = 0.chr * MAXPATH - filesystem_flags = filesystem_flags.unpack('L')[0] + if QueryDosDeviceA(drive[0,2], name, name.size) == 0 + raise SystemCallError.new('QueryDosDevice', FFI.errno) + end - name = 0.chr * MAXPATH + mount.instance_variable_set(:@name, name.strip) + mount.instance_variable_set(:@mount_type, fsname) + mount.instance_variable_set(:@options, get_options(filesystem_flags)) - if QueryDosDevice(drive[0,2], name, name.size) == 0 - raise Error, get_last_error - end + if block_given? + yield mount + else + mounts << mount + end + } - mount.instance_variable_set(:@name, name.strip) - mount.instance_variable_set(:@mount_type, fsname.strip) - mount.instance_variable_set(:@options, get_options(filesystem_flags)) + mounts # Nil if the block form was used. + end - if block_given? - yield mount - else - mounts << mount - 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) - mounts # Nil if the block form was used. + if PathStripToRootW(wfile) + wfile.read_string(wfile.size).split("\000\000").first.tr(0.chr, '') + else + nil end + 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) - file = file.tr(File::SEPARATOR, File::ALT_SEPARATOR) - PathStripToRoot(file) - file[/^[^\0]*/] - end + # Returns a Filesystem::Stat object that contains information about the + # +path+ file system. + # + # Examples: + # + # File.stat("C:\\") + # File.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) - # Returns a Filesystem::Stat object that contains information about the - # +path+ file system. - # - # Examples: - # - # File.stat("C:\\") - # File.stat("C:\\Documents and Settings\\some_user") - # - def self.stat(path) - bytes_avail = [0].pack('Q') - bytes_free = [0].pack('Q') - total_bytes = [0].pack('Q') + wpath = path.wincode - unless GetDiskFreeSpaceEx(path, bytes_avail, total_bytes, bytes_free) - raise Error, get_last_error - end + unless GetDiskFreeSpaceExW(wpath, bytes_avail, total_bytes, bytes_free) + raise SystemCallError.new('GetDiskFreeSpaceEx', FFI.errno) + end - bytes_avail = bytes_avail.unpack('Q').first - bytes_free = bytes_free.unpack('Q').first - total_bytes = total_bytes.unpack('Q').first + bytes_avail = bytes_avail.read_ulong_long + bytes_free = bytes_free.read_ulong_long + total_bytes = total_bytes.read_ulong_long - sectors = [0].pack('Q') - bytes = [0].pack('Q') - free = [0].pack('Q') - total = [0].pack('Q') + sectors = FFI::MemoryPointer.new(:ulong_long) + bytes = FFI::MemoryPointer.new(:ulong_long) + free = FFI::MemoryPointer.new(:ulong_long) + total = FFI::MemoryPointer.new(:ulong_long) - unless GetDiskFreeSpace(path, sectors, bytes, free, total) - raise Error, get_last_error - end + unless GetDiskFreeSpaceW(wpath, sectors, bytes, free, total) + raise SystemCallError.new('GetDiskFreeSpace', FFI.errno) + end - sectors = sectors.unpack('Q').first - bytes = bytes.unpack('Q').first - free = free.unpack('Q').first - total = total.unpack('Q').first + 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 + block_size = sectors * bytes + blocks_avail = total_bytes / block_size + blocks_free = bytes_free / block_size - vol_name = 0.chr * 260 - base_type = 0.chr * 260 - vol_serial = [0].pack('L') - name_max = [0].pack('L') - flags = [0].pack('L') + vol_name = FFI::MemoryPointer.new(:char, MAXPATH) + base_type = FFI::MemoryPointer.new(:char, MAXPATH) + vol_serial = FFI::MemoryPointer.new(:ulong) + name_max = FFI::MemoryPointer.new(:ulong) + flags = FFI::MemoryPointer.new(:ulong) - bool = GetVolumeInformation( - path, - vol_name, - vol_name.size, - vol_serial, - name_max, - flags, - base_type, - base_type.size - ) + bool = GetVolumeInformationW( + wpath, + vol_name, + vol_name.size, + vol_serial, + name_max, + flags, + base_type, + base_type.size + ) - unless bool - raise Error, get_last_error - end - - vol_serial = vol_serial.unpack('L').first - name_max = name_max.unpack('L').first - flags = flags.unpack('L').first - base_type = base_type[/^[^\0]*/] - - 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.freeze # Read-only object + unless bool + raise SystemCallError.new('GetVolumInformation', FFI.errno) end - private + vol_serial = vol_serial.read_ulong + name_max = name_max.read_ulong + flags = flags.read_ulong + base_type = base_type.read_string(base_type.size).tr(0.chr, '') - # 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 + 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) - # 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 + stat_obj.freeze # Read-only object + end - str.tr!(' ', ',') - str = str[1..-1] # Ignore the first comma - str + 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 - 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 Fixnum - # call-seq: - # <tt>fix</tt>.to_kb - # - # Returns +fix+ in terms of kilobytes. - def to_kb - self / 1024 - end - - # call-seq: - # <tt>fix</tt>.to_mb - # - # Returns +fix+ in terms of megabytes. - def to_mb - self / 1048576 - end - - # call-seq: - # <tt>fix</tt>.to_gb - # - # Returns +fix+ in terms of gigabytes. - def to_gb - self / 1073741824 - end + # call-seq: + # <tt>fix</tt>.to_kb + # + # Returns +fix+ in terms of kilobytes. + def to_kb + self / 1024 + end + + # call-seq: + # <tt>fix</tt>.to_mb + # + # Returns +fix+ in terms of megabytes. + def to_mb + self / 1048576 + end + + # call-seq: + # <tt>fix</tt>.to_gb + # + # Returns +fix+ in terms of gigabytes. + def to_gb + self / 1073741824 + end end