I've been meaning to look at RAR files and ColdFusion for some time, mainly as a way to work with CBR files (these are digital comic books stored in RAR format). Unfortunately, ColdFusion's built in Zip functionality only works with Zip and JAR files. After some Googling for a Java based solution, I was only available to find a good RAR list program. I was not able to find anything that would actually list as well as extract files from a RAR file. I decided to tackle the solution via another route - cfexecute. Here is what I came up with.
If you've never used cfexecute before, you can think of it as a way for ColdFusion to work with other programs on your server. cfexecute will run any command line program that the service has access to. There are a few important things to keep in mind though:
First - cfexecute does not execute programs on the user's machine. That is impossible.
Secondly - while you can run any program via cfexecute, only command line programs make sense. So for example, I can start Firefox at the command line, but it will pop open a window on my machine. ColdFusion can do the same, but if you want to actually run a program and do something with the result, the program must return something to the command line itself.
I decided to make use of 7-zip. 7-zip is free, open source software that works with a variety of compression formats, including RAR. It includes both visual as well as command line interfaces. Making use of 7-zip via cfexecute simply comes down to figuring out the proper way to execute the program and dealing with the responses. I began by working on a list interface. To list files in an archive, you can use this set of arguments at the command line:
C:\Program Files\7-Zip\7z.exe l somefile.rar
The "l" argument means list. So now let's look at the cfexecute version of this:
<cfset args = []>
<cfset args[1] = "l">
<cfset args[2] = theFile>
<cfexecute name="#sevenZipExe#" arguments="#args#" variable="result" errorvariable="errorresult" timeout="99" />
<cfset sevenZipexe = "C:\Program Files\7-Zip\7z.exe">
<cfset theFile = "H:\comics\Guardians of the Galaxy\Guardians of the Galaxy v2 013.cbr">
In the example above I've created a variable for the 7-Zip command line program and the archive file I want to work with. I've then passed the arguments into an array. Finally I run cfexecute. Make note of a few things. First, I can ask for ColdFusion to gather any result, or error, into two variables: result and errorresult. Secondly, the default mode of operation for cfexecute is "fire and forget" - i.e., don't wait for a response. By adding in a specific timeout value I can ensure ColdFusion will wait (for a while anyway) and get the response.
Now for the hard part. Some command line programs (like SVN and Git) will actually allow you to get a formatted response. 7-zip does not. It returns a nicely formatted table that looks like this:
If we are going to work with this we will need to parse that string into something more readable. (I found out later that there is an argument you can pass to make for a slightly more parsed format. I'm going to skip mentioning that for now, but do know that a slightly better option exists and I may switch to it in the future.) I whipped up the following code to strip out the header and footer.
<!--- remove header --->
<cfset result = trim(rereplace(result, ".*?------------------- ----- ------------ ------------ ------------------------", ""))>
<!--- remove footer --->
<cfset result = trim(rereplace(result, "------------------- ----- ------------ ------------ ------------------------.*", ""))>
This left me with N lines of text delimited by a space. The final part of the line, the file, could have spaces in it, but everything before that should be safe to treat as space delimited. Here is what I used:
<cfloop index="line" list="#result#" delimiters="#chr(13)##chr(10)#">
<cfset queryAddRow(files)>
<cfset line = trim(line)> <cfset date = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "date", date)> <cfset time = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "time", time)> <cfset attr = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "attr", attr)> <cfset size = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "size", size)> <cfset compressed = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "compressed", compressed)> <cfset name = trim(line)>
<cfset querySetCell(files, "name", name)> </cfloop>
<cfset files = queryNew("compressed,name,size,date,time,attr","double,varchar,double,date,time,varchar")>
Not rocket science but it works ok. So - what about extraction? I was concerned with extracting one file at a time, so I first figured out that syntax:
C:\Program Files\7-Zip\7z.exe e -aoa -oc:\loc somefile.rar somefile.txt
In this example, e stands for extra. -aoa stands for overwrite. -o is the output directory. The next argument is the archive. And the final argument is the specific file you want to extract. With this syntax in place, it was then easy to call it via cfexecute:
<cfset args[4] = theFile>
<cfset args[5] = "1602 001 001.jpg"> <cfexecute name="#sevenZip#" arguments="#args#" variable="result" errorvariable="errorresult" timeout="99" />
<cfset args = []>
<cfset args[1] = "e">
<cfset args[2] = "-aoa">
<cfset args[3] = "-oc:\Users\Raymond\Desktop\">
The next logical step was to wrap this up into a nice CFC. Here is my first version of a 7-zip wrapper. It isn't the most stable wrapper, but it fits my needs. I'll post my use case for this in the next blog entry.
<cffunction name="init" access="public" output="false">
<cfargument name="sevenZipExe" type="string" required="true"> <cfif not fileExists(arguments.sevenZipExe)>
<cfthrow message="Invalid 7Zip executable path: #arguments.sevenZipExe#">
</cfif> <cfset variables.sevenzipexe = arguments.sevenzipexe>
<cfreturn this>
</cffunction> <cffunction name="extract" access="public" returnType="boolean" output="false">
<cfargument name="archivefile" type="string" required="true">
<cfargument name="file" type="string" required="true">
<cfargument name="destination" type="string" required="true">
<cfset var result = "">
<cfset var errorresult = ""> <cfif not fileExists(arguments.archivefile)>
<cfthrow message="Unable to work with #arguments.arvhiefile#, it does not exist.">
</cfif> <cfset var args = []>
<cfset args[1] = "e">
<cfset args[2] = "-aoa">
<cfset args[3] = "-o#arguments.destination#">
<cfset args[4] = arguments.archivefile>
<cfset args[5] = arguments.file> <cfexecute name="#variables.sevenZipexe#" arguments="#args#" variable="result" errorvariable="errorresult" timeout="99" /> <cfif findNoCase("Everything is ok", result)>
<cfreturn true>
<cfelse>
<cfreturn false>
</cfif> </cffunction> <cffunction name="list" access="public" returnType="query" output="false">
<cfargument name="file" type="string" required="true">
<cfset var result = "">
<cfset var errorresult = "">
<cfset files = queryNew("compressed,name,size,date,time,attr","double,varchar,double,date,time,varchar")>
<cfset var line = ""> <cfif not fileExists(arguments.file)>
<cfthrow message="Unable to work with #arguments.file#, it does not exist.">
</cfif> <cflog file="application" text="Working with #arguments.file#">
<cfset var args = []>
<cfset args[1] = "l">
<cfset args[2] = arguments.file>
<cfexecute name="#variables.sevenzipexe#" arguments="#args#" variable="result" errorvariable="errorresult" timeout="99" /> <cfif len(errorresult)>
<cfthrow message="Error from SevenZip: #errorresult#">
</cfif> <cfif find("is not supported archive", result)>
<cfthrow message="#arguments.file# was not a supported archive.">
</cfif> <!--- remove header --->
<cfset result = trim(rereplace(result, ".?------------------- ----- ------------ ------------ ------------------------", ""))>
<!--- remove footer --->
<cfset result = trim(rereplace(result, "------------------- ----- ------------ ------------ ------------------------.", ""))> <cfloop index="line" list="#result#" delimiters="#chr(13)##chr(10)#">
<cfset queryAddRow(files)>
<cfset line = trim(line)>
<cfset date = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "date", date)> <cfset time = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "time", time)> <cfset attr = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "attr", attr)> <cfset size = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "size", size)> <cfset compressed = listFirst(line, " ")>
<cfset line = listRest(line, " ")>
<cfset querySetCell(files, "compressed", compressed)> <cfset name = trim(line)>
<cfset querySetCell(files, "name", name)> </cfloop> <cfreturn files>
</cffunction> </cfcomponent>
<cfcomponent output="false">
Finally, here is a quick example of using the zip:
<cfset sevenzipcfc.extract(thefile,files.name[1],"c:\Users\Raymond\Desktop")>
<cfset sevenZipexe = "C:\Program Files\7-Zip\7z.exe">
<cfset sevenzipcfc = new sevenzip(sevenzipexe)>
<cfset files = sevenzipcfc.list(theFile)>
<cfdump var="#files#">