Friday, March 03, 2006

.Net in Ruby

While creating a simple backup script (see previous post), I needed to copy more than the file's data. The backup copy needed to include the meta-data that Windows keeps for the file. Ruby (and Java or Ant) can easily copy the contents of a file, but they don't know how to copy the meta-data. The solution is to use .Net, but I wanted to write my script in Ruby.

I found the Ruby/.Net Bridge worked like magic. For example to copy a file, just use...
require 'dotnet' # Ruby / .Net bridge
System.IO.File.Copy( srcpath, destpath )


The install of Ruby/.Net Bridge was very simple. Just download, unzip, and run deploy.cmd. The ease of installation made me very happy.

After I created my backup script. I wondered if this Bridge worked with .Net v2.0. So I took a spare machine and removed .Net v1.1 to make sure the Bridge could only use v2.0. Everything worked perfect!

The one area that I had trouble was handling .Net's Enum. In .Net, the FileAttributes class is an Enum which is also a bit field (see FlagsAttribute). Using the Ruby/.Net Bridge, I could not perform bit-wise math operations on the Enum. I asked the authors, who gave me a good solution using the Parse method. For example:
require 'dotnet' # Ruby / .Net bridge

def attributeRemove( fileattribute, attribute )
Enum.parse( System.IO.FileAttributes, (fileattribute.ToString().split(', ') - [attribute]).join( ', ' ) )
end

def setReadWrite( dospath )
attrs = attributeRemove( System.IO.File.GetAttributes( dospath ), 'ReadOnly' )
System.IO.File.SetAttributes( dospath, attrs )
end

The last feature that I wanted, was to decrypt files and folders that were stored in encrypted file systems. Since Windows encryption used a key tied to the machine and user, the backup should not stay encrypted. If the machine failed it may be impossible to get the necessary key to decrypt. Unfortunately there wasn't a .Net method to decrypt the file or folder. Instead I had to call the win32api. This was pretty easy to do (however, I found the available documentation very lacking.) The following is a combination of some Utility methods, and the DecryptFile method I created for my script.
require 'Win32API'
require 'dotnet' # Ruby / .Net bridge http://www.saltypickle.com/rubydotnet/ just download it, uncompress and run deploy.cmd (per instructions on site)

class Utils
def Utils.attribute?( dospath, attribute )
if attribute.is_a?(String)
return System.IO.File.GetAttributes( dospath ).ToString().split(', ').include?( attribute )
else
raise "attribute parameter must be a String. attribute.class=#{attribute.class}"
end
end

def Utils.attributeAdd( fileattribute, attribute )
Enum.parse( System.IO.FileAttributes, fileattribute.ToString() + ', ' + attribute )
end

def Utils.attributeRemove( fileattribute, attribute )
Enum.parse( System.IO.FileAttributes, (fileattribute.ToString().split(', ') - [attribute]).join( ', ' ) )
end

def Utils.setReadWrite( dospath )
attrs = Utils.attributeRemove( System.IO.File.GetAttributes( dospath ), 'ReadOnly' )
System.IO.File.SetAttributes( dospath, attrs )
end

def Utils.setReadOnly( dospath )
attrs = Utils.attributeAdd( System.IO.File.GetAttributes( dospath ), 'ReadOnly' )
System.IO.File.SetAttributes( dospath, attrs )
end
end

class Win32API
def Win32API.DecryptFile( dospath )
Utils.setReadWrite( dospath ) if isReadOnly = Utils.attribute?( dospath, 'ReadOnly' )
result = Win32API.new('Advapi32', 'DecryptFile', %w(p i), 'i').call(dospath,0)
#todo better error checking
if result == 0
Utils.setReadOnly( dospath ) if isReadOnly
raise "DecryptFile call failed on path: \"#{dospath}\". see also: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/decryptfile.asp"
end
Utils.setReadOnly( dospath ) if isReadOnly
return result
end
end


An example usage...
if Utils.attribute?( path, 'Encrypted' )
Win32API.DecryptFile( path )
end

No comments: