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 private_class_method :new # Mount objects are returned by the Sys::Filesystem.mounts method. 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 # Stat objects are returned by the Sys::Filesystem.stat method. 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. 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 # The total amount of free space on the partition. attr_reader :bytes_free # The amount of free space available to unprivileged processes. attr_reader :bytes_available 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. #-- # rubocop:disable Metrics/BlockLength # 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 do |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 end mounts # Nil if the block form was used. end # rubocop:enable Metrics/BlockLength # 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.to_s.wincode) if PathStripToRootW(wfile) wfile.read_string(wfile.size).split("\000\000").first.tr(0.chr, '') 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: # # # Regular directory # Sys::Filesystem.stat("C:\\") # Sys::Filesystem.stat("C:\\Documents and Settings\\some_user") # # # Pathname # pathname = Pathname.new("C:\\") # Sys::Filesystem.stat(pathname) # # # Dir # dir = Dir.open("C:\\") # Sys::Filesystem.stat(dir) # # Note that on Windows you cannot stat a regular file because # Windows won't like it. It must be a directory in some form. # def self.stat(path) path = path.path if path.respond_to?(:path) # Dir path = path.to_s if path.respond_to?(:to_s) # Pathname 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).to_s mpoint << '/' unless mpoint.end_with?('/') wpath = path.to_s.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_ptr = FFI::MemoryPointer.new(:ulong_long) bytes_ptr = FFI::MemoryPointer.new(:ulong_long) free_ptr = FFI::MemoryPointer.new(:ulong_long) total_ptr = 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_ptr, bytes_ptr, free_ptr, total_ptr) raise SystemCallError.new('GetDiskFreeSpace', FFI.errno) end sectors_per_cluster = sectors_ptr.read_ulong_long bytes_per_sector = bytes_ptr.read_ulong_long free_ptr.free total_ptr.free block_size = sectors_per_cluster * bytes_per_sector blocks_avail = bytes_avail / block_size blocks_free = bytes_free / block_size total_blocks = total_bytes / 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('GetVolumeInformation', 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, '') # Lets explicitly free our pointers vol_name_ptr.free vol_serial_ptr.free name_max_ptr.free flags_ptr.free base_type_ptr.free sectors_ptr.free bytes_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, total_blocks) 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.instance_variable_set(:@bytes_available, bytes_avail) stat_obj.freeze # Read-only object end # Associate a volume with a drive letter or a directory on another volume. # def self.mount(target, source) targetw = target.to_s.wincode sourcew = source.to_s.wincode volume_namew = (0.chr * 256).wincode unless GetVolumeNameForVolumeMountPointW(sourcew, volume_namew, volume_namew.size) raise SystemCallError.new('GetVolumeNameForVolumeMountPoint', FFI.errno) end unless SetVolumeMountPointW(targetw, volume_namew) raise SystemCallError.new('SetVolumeMountPoint', FFI.errno) end self end # Deletes a drive letter or mounted folder. # def self.umount(mount_point) unless DeleteVolumeMountPoint(mount_point) raise SystemCallError.new('DeleteVolumeMountPoint', FFI.errno) end self end # 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' ole = wmi.ExecQuery(query).ItemIndex(0) time_array = Time.parse(ole.LastBootupTime.split('.').first) Time.mktime(*time_array) end end private_class_method :get_boot_time # 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[1..-1] # Ignore the first comma end private_class_method :get_options end end