This recipe will show you how to download a file using the XMLHttp
object, which we have seen in action in the Checking whether page links are broken recipe. Here we will expand on a theme and see how to synchronize our script using the onreadystatechange
event handler to report on the progress of our download. The code includes the required modifications, and is the same as the one I used in a project a few years ago.
From the File menu, navigate to New | Function Library or use the Alt + Shift + N shortcut. Name the new function library as Web_Download.vbs
. To use the AutoItX
COM object, go to https://www.autoitscript.com/site/autoit/downloads/ to download and install AutoIt. This is absolutely necessary in order to ensure that the code given here will work properly with regard to the notifications display.
This recipe will demonstrate how to download the last build JAR file from a remote build machine and deploy it to the local machine. This is very useful to automate daily build updates and trigger automated tests to check the new build for sanity. Please take note that this solution comprises several components and is quite complex to grasp:
Http
class, which handles the download operationStateChangeHandler
class, which listens to the onreadystatechange
event and handles notifications about the progressAutoIt
class, which is a utility wrapper for the AutoItX
COM objectApp_GetLastBuild
class, which controls the whole processProceed with the following steps:
Environment
variables):const S_OK = 0 const APP_PATH = "C:Program FilesMyApp" const DOWNLOAD_PATH = "C:Documents and SettingsadminMy DocumentsDownloads" const BUILD_PATH = "http://repository.app:8081/builds/last/" const TMP_JAR = "App-1.0.0-build1.jar" const APP_JAR = "App.jar" const RES_ZIP = "App-1.0.0-build1-resources.zip"
Http
class to handle the download process. The process is explained as follows:class Http Public m_objXMLHttp'The XMLHttp objectPrivate m_objHandler 'Stores a reference to the handler of the event onreadystatechange Private m_strLocalfilename 'Name of local filename Private m_strUrl 'Address of download location
private sub class_initialize Handler = new StateChangeHandler Handler.Http = Me XML = createobject("MSXML2.XMLHTTP") end sub private sub class_terminate Handler = Nothing XML = Nothing end sub
XMLHttp
property, which is used to assign the XMLHttp
object to the m_objXMLHttp
field and also to set the StateChangeHandler
object to the object's onreadystatechange
event.public property get XML set XML = m_objXML end property private property let XML(byref objXML) set m_objXML = objXML if typename(objXML) <> "Nothing" then _ m_objXML.onreadystatechange = Handler end property
public property get LocalFilename LocalFilename = m_strLocalfilename end property private property let LocalFilename(byval strFilename) m_strLocalfilename = strFilename end property public property get Filename Filename = createobject("Scripting.FileSystemObject").GetFileName(Localfilename) end property public property get URL URL = m_strUrl end property private property let URL(byval strUrl) m_strUrl = strUrl end property private property get Handler set Handler = m_objHandler end property private property let Handler(byref objHandler) set m_objHandler = objHandler end property
DownBinFile
method handles the process as shown in the following code:public function DownBinFile(byval strURL, byval strDownloadPath) const adTypeBinary = 1 const adModeReadWrite = 3 const adSaveCreateOverwrite = 2 dim arrTmp, oStream, intStatus, FSO, strInfo arrTmp = Split(strURL, "/") URL = strURL LocalFilename = strDownloadPath & Unescape(arrTmp(UBound(arrTmp))) if XML.open("GET", strURL, false) = S_OK then XML.send set oStream = createobject("ADODB.Stream") with oStream .type = adTypeBinary .mode = adModeReadWrite .open do Until XML.readyState = 4 Wscript.Sleep 500 Loop .write XML.responseBody .SaveToFile LocalFilename, adSaveCreateOverwrite strInfo = "Download of file '" & strURL & "' finished with " set FSO = createobject("Scripting.FileSystemObject") if FSO.FileExists(LocalFilename) then strInfo = strInfo & "success." DownBinFile = 0 else strInfo = strInfo & "failure." DownBinFile = 1 end if with oAutoIt.Object .ToolTip strInfo, 1100, 1000 .Sleep 7000 .ToolTip("") end with end with 'ADODB.Stream set oStream = Nothing else XML.abort strInfo = "Send download command to server failed" & vbNewLine & XML.statusText with oAutoIt.Object call .ToolTip(strInfo, 1100, 1000) .Sleep 7000 call .ToolTip("") end with exit function end if end function end class
Http
class here refers to StateChangeHandler
, which in turn uses the AutoItX
COM object to display a notification to inform about the progress of the download process.The Exec
method is defined as Public Default
so that it is automatically triggered when the object is referenced. As an instance of this object is assigned to the onreadystatechange
event of the Http
request object, every time readystate
changes, this function is performed to display the updated data on the download process in the notification area on the taskbar.
class StateChangeHandler public m_objHttp public default function Exec() dim strInfo, intDelay intDelay = 0 strInfo = "State changed: " & Http.XML.readyState & vbNewLine & "Downloading file: " & Http.Filename Select Case Http.XML.readyState Case "3" strInfo = strInfo & vbNewLine & "Please wait..." Case "4" strInfo = strInfo & vbNewLine & "Finished. Total " & len(Http.XML.responseBody)512 & "KB downloaded." intDelay = 1500 Case else End Select 'with AutoIt with oAutoIt.Object .ToolTip strInfo, 1100, 1000 .Sleep 500+intDelay .ToolTip("") end with end function public property get Http set Http = m_objHttp end property public property let Http(byval objHttp) set m_objHttp = objHttp end property end class
AutoItX
COM object is wrapped by the AutoIt
class for easier use:class AutoIt private m_oAutoIt public default property Get Object set Object = m_oAutoIt end property private property let Object(byval AutoItX) set m_oAutoIt = AutoItX end property private sub class_initialize Object = createobject("AutoItX3.Control") end sub private sub class_terminate Object = Nothing end sub end class
App_GetLastBuild
class controls the whole process. The whole process is explained as follows:class App_GetLastBuild private oHttp private Status private FSO private FoldersToDelete 'Local folders to delete' private ResourcesZIP private OrigJar private DestJar private BuildPath private ExtractPath
public function SetArgs() FoldersToDelete = Array(APP_PATH & "images", APP_PATH & "properties", APP_PATH & "wizards") ResourcesZIP = RES_ZIP OrigJar = TMP_JAR DestJar = APP_JAR ExtractPath = APP_PATH BuildPath = BUILD_PATH end function
Exec
method as default; a method that will control the whole process:public default function Exec() call SetArgs() '1) Delete local folders DeleteFolders(FoldersToDelete) '2) Download resources zip Status = oHttp.DownBinFile(BUILD_PATH & ResourcesZIP, DOWNLOAD_PATH) if Status <> 0 then exit function 'Extract the resources call ExtractZipFile(DOWNLOAD_PATH & ResourcesZIP, APP_PATH) 'Delete resources zip FSO.DeleteFile(DOWNLOAD_PATH & ResourcesZIP) '3) Delete main GUI jar FSO.DeleteFile(APP_PATH & "lib" & DestJar) '4) Download the updated GUI jar Status = oHttp.DownBinFile(BUILD_PATH & OrigJar, DOWNLOAD_PATH) if Status <> 0 then exit function 'Copy the updated GUI jar call FSO.CopyFile(DOWNLOAD_PATH & OrigJar, APP_PATH & "lib", true) 'Rename GUI jar call FSO.MoveFile(APP_PATH & "lib" & OrigJar, APP_PATH & "lib" & DestJar) 'Delete the downloaded GUI jar FSO.DeleteFile(DOWNLOAD_PATH & OrigJar) end function
public function DeleteFolders(byval arrFolders) dim ix for ix = 0 To UBound(arrFolders) if FSO.FolderExists(arrFolders(ix)) then FSO.DeleteFile(arrFolders(ix) & "*.*") FSO.DeleteFolder(arrFolders(ix)) end if next end function
public function ExtractZipFile(byval strZIPFile, byval strExtractToPath) dim objShellApp dim WsShell dim objZippedFiles set objShellApp = createobject("Shell.Application") set WsShell = createobject("Wscript.Shell") set objZippedFiles = objShellApp.NameSpace(strZIPFile).items objShellApp.NameSpace(strExtractToPath).CopyHere(objZippedFiles) 'Free Objects set objZippedFiles = Nothing set objShellApp = Nothing end function
private sub class_initialize set FSO = createobject("Scripting.FileSystemObject") set oHttp = new Http end sub private sub class_terminate set FSO = Nothing set oHttp = Nothing end sub end class
Action1
, the following code will launch the download process:dim oAutoIt dim oDownload set oAutoIt = new AutoIt set oDownload = new App_GetLastBuild oDownload.Exec set oDownload = Nothing Set oAutoIt = nothing
As mentioned in the previous section, the solution involves a complex architecture using VBScript classes and several COM objects (AutoItX
, FileSystemObject
, ADODB.Stream
, and so on). A detailed explanation of this architecture is provided here.
Let us start with the main process in Action1
and then delve into the intricacies of our more complex architecture. The first step involves the instantiation of two of our custom classes, namely, AutoIt
and App_GetLastBuild
. After our objects are already loaded and initialized, we call objDownload.Exec
, which triggers and controls the whole download scenario. In this method we do the following:
We then start the process for the main JAR file as follows:
Now, let us examine what happens behind the scenes after the two classes are instantiated, as mentioned in this section.
The AutoIt
class automatically instantiates the AutoItX.Control
object through its Private Sub Class_Initialize
subroutine. The App_GetLastBuild
class, through its own Sub Class_Initialize
, automatically creates these objects, namely, a Scripting.FileSystemObject
object and an instance of our Http
custom class.
Let us take a close look at the Sub Class_Initialize
of the Http
class:
private sub class_initialize Handler = new StateChangeHandler Handler.Http = Me XML = createobject("MSXML2.XMLHTTP") end sub
Here we can see a strange thing. Our Http
object creates an instance of the StateChangeHandler
class and immediately assigns the Http
property of the Handler
object a reference to itself )Handler.Http = me
). That is, the parent object (Http
) has a reference to the child object (StateChangeHandler
) and the latter has a reference to the parent object stored in its Http
property. The purpose of this seemingly strange design is to enable interoperability between both objects.
Finally, an instance of XMLHttp
is created. As seen in our recipe, on checking for broken links, this object provides the services we need to manage communication with a web server through the HTTP protocol. Here, however, there is something extra because we want to notify the end user about the progress of the download. Let us take a closer look at the way this instantiation is handled:
private property let XML(byref objXML) set m_objXML = objXML if typename(objXML) <> "Nothing" then _ m_objXML.onreadystatechange = Handler end property
We pass the XMLHttp
object created in our Sub Class_Initialize
subroutine and check if it is a real object (which it always should be because of the way we have designed our code). Then, we implicitly assign our handler's default method to the XMLHttp
event onreadystatechange
. Recall that a public default function in a VBScript class is executed whenever an object instance of the class is referenced without explicitly calling a method or property using the dot operator. This way, whenever the readystate
property of the XMLHttp
object changes, the default Exec
method of the StateChangeHandler
object is automatically executed, and a notification about the status of the process is displayed using the AutoIt
COM object. Just one thing is missing from our code, a check of the Http
status after XMLHttp.send
and later on.