Need to upload a file (file.txt) to a server (ftp.server.com) from Excel VBA.
(does not have to be necessarily FTP, just need to be able to put the file there and get it back, and I've got a server on GoDaddy shared hosting)
What I tried was to run this script:
ftp -s:script.txt
script.txt:
open ftp.server.com
USER
PASS
lcd c:\
put file.txt
disconnect
bye
The error I get is:
425 Could not open data connection to port 53637: Connection timed out
Google tells me I need to go to passive mode, but the command-line ftp.exe client doesn't allow that.
Do I have an easier alternative to FTP, or is there a better way to upload a file via VBA (without the command-line workaround)?
I'm thinking about using DROPBOX (but I really don't want to have to install this program on all the workstations that will need the program).
If you cannot use the Windows ftp.exe (particularly because it does not support the passive mode and TLS/SSL), you can use another command-line FTP client.
For example to upload a file using WinSCP scripting, use:
Call Shell( _
"C:\path\WinSCP.com /log=C:\path\excel.log /command " & _
"""open ftp://user:password#example.com/"" " & _
"""put C:\path\file.txt /path/"" " & _
"""exit""")
To ease reading, the above runs these WinSCP commands:
open ftp://user:password#example.com/
put C:\path\file.txt /path/
exit
You can put the commands to a script file and run the script with /script= command-line parameter, similarly to the ftp -s:, instead of the /command.
See the guide to Converting Windows FTP script to WinSCP script.
You can even have WinSCP GUI generate the FTP upload script for you.
WinSCP defaults to the passive mode.
You can also use FTPS (TLS/SSL):
open ftpes://user:password#example.com/
Alternatively you can use WinSCP .NET assembly via COM from the VBA code.
(I'm the author of WinSCP)
Diego, I've used the code below successfully for years. The code gets files from the host, but I'm sure it can be modified to put files there instead.
'Start Code
Set FSO = CreateObject("scripting.filesystemobject")
'************************************************************************************** '*** Create FTP Action File & Initiate FTP File Transfer
'************************************************************************************** VREDET = filename1 'Variable holding name of file to get
F = "C:\Volume\Temp\FTPScript.txt" 'creates the file that holds the FTP commands
Open F For Output As #1
Print #1, "open ftp.server" 'replace ftp.server with the server address
Print #1, ID 'login id here
Print #1, PW 'login password here
Print #1, "cd " & " Folder1" 'Directory of file location
Print #1, "cd " & " Folder2" 'Sub-Directory of file location
Print #1, "ascii"
Print #1, "prompt"
'Get the file from the host and save it to the specified directory and filename
Print #1, "get " & VREDET; " C:\some\directory\" & another-filename & ".CSV"
Print #1, "disconnect" 'disconnect the session
Print #1, "bye"
Print #1, "exit"
Close #1
'identify folder where ftp resides and execute the FTPScript.txt file
'vbHide - hides the FTP session
If FSO.FolderExists("C:\Windows\System32") = False Then
Shell "C:\WINNT\system32\ftp.exe -s:C:\Volume\Temp\FTPScript.txt", vbHide
Else
Shell "C:\WINDOWS\system32\ftp.exe -s:C:\Volume\Temp\FTPScript.txt", vbHide
End If
'end code
http://winscp.net is free, scriptable, supports passive mode and is definitely EXCELLENT.
After lot of research I found a method to upload file to FTP location without any .ocx file internet control file. This worked for me....
Declare PtrSafe Function InternetConnect Lib "wininet.dll" Alias "InternetConnectA" ( _
ByVal hInternetSession As Long, ByVal sServerName As String, _
ByVal nServerPort As Integer, ByVal sUserName As String, _
ByVal sPassword As String, ByVal lService As Long, _
ByVal lFlags As Long, ByVal lContext As Long) As Long
Declare PtrSafe Function InternetOpen Lib "wininet.dll" Alias "InternetOpenA" ( _
ByVal sAgent As String, ByVal lAccessType As Long, _
ByVal sProxyName As String, _
ByVal sProxyBypass As String, ByVal lFlags As Long) As Long
Declare PtrSafe Function FtpSetCurrentDirectory Lib "wininet.dll" Alias _
"FtpSetCurrentDirectoryA" (ByVal hFtpSession As Long, _
ByVal lpszDirectory As String) As Boolean
Declare PtrSafe Function FtpPutFile Lib "wininet.dll" Alias "FtpPutFileA" ( _
ByVal hConnect As Long, _
ByVal lpszLocalFile As String, _
ByVal lpszNewRemoteFile As String, _
ByVal dwFlags As Long, _
ByRef dwContext As Long) As Boolean
Sub simpleFtpFileUpload()
Dim ftp, FTP_PORT, user, password, loc_file, remote_file, ftp_folder As Variant
ftp_folder = "/EXPORT"
loc_file = ThisWorkbook.Path & "\readme.txt"
remote_file = ftp_folder & "/readme.txt"
FTP_PORT = "2221"
user = "ajay"
password = "ajay"
ftp = "192.168.1.110"
Internet_OK = InternetOpen("", 1, "", "", 0)
If Internet_OK Then
FTP_OK = InternetConnect(Internet_OK, ftp, FTP_PORT, user, password, 1, 0, 0) ' INTERNET_DEFAULT_FTP_PORT or port no
If FtpSetCurrentDirectory(FTP_OK, "/") Then
Success = FtpPutFile(FTP_OK, loc_file, remote_file, FTP_TRANSFER_TYPE_BINARY, 0)
End If
End If
If Success Then
Debug.Print "ftp success ;)"
MsgBox "ftp success ;)"
Else
Debug.Print "ftp failure :("
MsgBox "ftp failure :("
End If
End Sub
Please change values as per your needs
ftp_folder = "/EXPORT"
loc_file = ThisWorkbook.Path & "\readme.txt"
remote_file = ftp_folder & "/readme.txt"
FTP_PORT = "2221"
user = "ajay"
password = "ajay"
ftp = "192.168.1.110"
I could not make the passive mode work with the command prompt either, but I found out explorer works faster and more efficiently.
I have specified in an other subroutine what could change:
strFileName = "file.txt"
strMyFile = "C:\path\file.txt"
strFTP = "ftp.server.com"
strUser = "ID"
strPW = "PWD"
strSubfolder = "/subfolder"
And the code I found and modified to my use:
Sub cmdFTPviaExplorer()
Set oShell = CreateObject("Shell.Application")
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const copyType = 16
strFTP = "ftp://" & strUser & ":" & strPW & "#" & strFTP & strSubfolder
Set objFTP = oShell.Namespace(strFTP)
'Upload single file
If objFSO.FileExists(strMyFile) Then
Set objFile = objFSO.getFile(strMyFile)
strParent = objFile.ParentFolder
Set objFolder = oShell.Namespace(strParent)
Set objItem = objFolder.ParseName(objFile.Name)
objFTP.CopyHere objItem, copyType
End If
'Pop-up message box
MsgBox strFileName & " file created and uploaded"
End Sub
The above script is great I used the following commands to upload files as well as log the output to a file which is useful when debugging also it is a common misconception that windows ftp cannot do passive mode the command to go passive is "quote pasv" (I have added this to the script
Sub FtpFileto()
Set FSO = CreateObject("scripting.filesystemobject")
F = "C:\FTPScript.txt"
' Create the ftpscript to be run
Open F For Output As #1
Print #1, "open ftp.server.com" 'replace ftp.server with the server address
Print #1, "ID" 'login id here
Print #1, "PWD" 'login password here
Print #1, "quote pasv" ' passive mode ftp if needed
Print #1, "cd " & " /dir" 'Directory of file location
Print #1, "cd " & " subdir" 'Sub-Directory of file location
Print #1, "ascii"
Print #1, "prompt"
'Put the file from the host and save it to the specified directory and filename
Print #1, "put " & VREDET; """C:\file1.csv"""; ""
Print #1, "put " & VREDET; """C:\file2.csv"""; ""
Print #1, "put " & VREDET; """C:\file3.csv"""; ""
Print #1, "disconnect" 'disconnect the session
Print #1, "bye"
Print #1, "exit"
Close #1
'Now for the command to upload to the ftpsite and log it to a text file
' the trick is to use the standard command shell which allows logging
Shell "cmd /c C:\WINDOWS\system32\ftp.exe -i -s:C:\FTPScript.txt > c:\ftpuploadlog.txt", vbHide
End Sub
Related
I am trying to link Sharepoint 365 into an Excel Workbook. Previously, I was using wshell to mount the Sharepoint drive to my computer and access everything locally. Sharepoint 365 doesn't allow you to do that, so I am using the API. Here are my steps:
Login to Sharepoint and get an access token (OAuth2 handshake)
Search for a file or navigate Sharepoint List/Folder/File tree to find a file (this is done through various Sharepoint API calls returning the relevant objects I am looking for)
Download the file from Sharepoint onto the local drive (read only operations at the moment)
There are a bunch of automated procedures I have been using to interact with data downloaded from various files, this will not change.
With the Sharepoint 365 API, I am stuck at step 3.
I'm using a class to instantiate my Sharepoint session and keep track of my file object. My unit test looks like this:
Sub testDownload()
Dim spFile As New sp365
Dim reqObj As Object
Dim jsonObj As Object
Dim dlStatus As Long
'first log in
spFile.login
'now get a request object that contains filenames and their relative URLs
Set reqObj = spFile.testQuery
'extract the info to a JSON object
Set jsonObj = jsonify(reqObj.responseText, "")
'hardcoding these parameters for now because I just want to download this one file
Debug.Print "Filename: " & jsonObj("d.results(0).Name")
Debug.Print "Relative Url: " & jsonObj("d.results(0).ServerRelativeUrl")
dlStatus = spFile.downloadTemporaryFile(jsonObj("d.results(0).ServerRelativeUrl"), jsonObj("d.results(0).Name"))
If dlStatus = 0 Then
Debug.Print "File Created"
Else
Debug.Print "File not created. Status = " & dlStatus
End If
out:
Exit Sub
End Sub
The relevant code here lies in downloadTemporaryFile. Obviously, I am using the windows urlmon code, which seems to be the de facto way to download files in Excel:
Private Declare Function URLDownloadToFile Lib "urlmon" Alias _
"URLDownloadToFileA" ( _
ByVal pCaller As Long, _
ByVal szURL As String, _
ByVal szFileName As String, _
ByVal dwReserved As Long, _
ByVal lpfnCB As Long) As Long
And downloading the (temporary) file is here:
Function downloadTemporaryFile(relativeUrl As String, fileName As String) As Boolean
On Error GoTo errHandler
'Download a file to a temporary path
'Keep the file inside the class object until it is closed
Dim userPath As String
Dim filePath As String
Dim url As String
Dim wshShell As Object
'Get the windows shell version for special folders
Set wshShell = CreateObject("WScript.Shell")
'get the documents folder for this computer
userPath = wshShell.SpecialFolders("MyDocuments")
'all logs are stored in <user>/Documents/logs
filePath = userPath & SHAREPOINT_TEMP_FOLDER
'Check if the 'SharepointTemp' folder exists
'if not, create the directory
If Dir(filePath, vbDirectory) = "" Then MkDir (filePath)
'Extract the site (this can differ based on the connected app)
'FYI: TENANT_DOMAIN is obtained during the Sharepoint Login procedure
url = "https://" & TENANT_DOMAIN & relativeUrl
'download it now
Debug.Print "Downloading to: " & filePath & fileName
Debug.Print "Downloading from: " & url
downloadTemporaryFile = URLDownloadToFile(0, url, filePath & fileName, 0, 0)
out:
Exit Function
errHandler:
logDump "Error", "sp365.downloadTemporaryFile", Err.Number & ";" & Err.source & ";" & Err.description
End Function
So this seems like it would work, and URLDownloadToFile returns 0 (S_OK). But only a tiny part of the file is in my download folder. The file I am trying to download in the example is 2MB, the file in my folder is only 4kb and it won't open. I haven't even gotten to cksum yet, but of course it would fail. I've tried other Sharepoint download links (like .linkingUrl and .linkingUri), but I get the same result. When I paste the url the way I have constructed it above into my browser, the file downloads just fine.
Edit: The file is actually an HTML file. It looks like this:
<html><head><title>Working...</title>
</head><body><form method="POST" name="hiddenform" action="https://keysighttech.sharepoint.com/_forms/default.aspx">
<input type="hidden" name="code" value="..." />
<input type="hidden" name="id_token" value= "..."/>
<input type="hidden" name="session_state" value= "..." />
<input type="hidden" name="correlation_id" value="..."/>
<noscript><p>Script is disabled. Click Submit to continue.</p>
<input type="submit" value="Submit" /></noscript></form>
<script language="javascript">document.forms[0].submit();</script></body></html>
How can I proceed with the download? Any suggestions?
Thank you in advance!
I figured it out. Basically, the UrlDownloadToFile routine does not pass any authentication along with it. So when I send a request for a file, either I get a 401 Unauthorized, an error which basically just spits my request back to me, or the "hint" i posted above, which basically is a redirect with all of the tenant and authentication methods. So instead, I went ahead get authorized and included the headers that I usually use with standard Sharepoint API requests and it returned the file to me in a bit stream. The final class function looks something like this:
Dim url As String
Dim filePtr As Long
Dim oResp() As Byte 'byte array to store the response object
Dim reqObj As Object
'make sure we can navigate to the right folder on people's computers
Dim userPath As String
Dim filePath As String
Dim wshShell As Object
Dim reqKey() As String
Dim reqVal() As String
'Get the windows shell version for special folders
Set wshShell = CreateObject("WScript.Shell")
'get the documents folder for this computer
userPath = wshShell.SpecialFolders("MyDocuments")
filePath = userPath & SHAREPOINT_TEMP_FOLDER
'Check if the 'SharepointTemp' folder exists
'if not, create the directory
If Dir(filePath, vbDirectory) = "" Then MkDir (filePath)
reqKey = sharepointHeadersKeys
reqVal = sharepointHeadersVals
'Extract the site (this can differ based on the connected app)
url = relativeUrl & SHAREPOINT_BINARY_REQUEST
Set reqObj = getRequest(url, bearer:=AuthToken.item("access_token"), key:=reqKey, value:=reqVal, blnAsync:=True)
'now the file should be in reqObj
oResp = reqObj.responseBody
'Create a local file and save the results
filePtr = FreeFile
Debug.Print "Downloading to: " & filePath & fileName
If Dir(filePath & fileName) <> "" Then Kill filePath & fileName
Open filePath & fileName For Binary As #filePtr
Put #filePtr, , oResp
Close #filePtr
Now I can use the file from my temp folder as I was before. I am using the metadata.uri returned from the API call and associated with the file object that I queried. This seems to me to be the easiest and cleanest way to do it - especially because I can search the file binary if I am looking for specific text or keywords and save the overhead of opening the file at all. But, of course, I am open to other methods and suggestions.
I am trying to run an ssh command, and capture the output to a variable in VBA (using OpenSSH). I can get this to work fine in the command line:
ssh user#ip python C:\Temp\Remote.py
The results are a list of values returned to the command line window. I would like to read this into a VBA variable.
I found this, this and this. The first two do not seem to send or recieve the command correctly, as the code gets hung up on oShell.Exec(...). The shell looks like it is executing, but it just gets hung. If I close the hanging command window, the results in VBA are blank.
Dim sCmd As String
sCmd = "ssh user#ip python C:\Temp\Remote.py"
Dim oShell As Object
Set oShell = CreateObject("WScript.Shell")
Dim oExec As Object
Dim oOutput As Object
Set oExec = oShell.Exec(sCmd)
Set oOutput = oExec.StdOut
Dim s As String
Dim sLine As String
While Not oOutput.AtEndOfStream
sLine = oOutput.ReadLine
If sLine <> "" Then s = s & sLine & vbCrLf
Wend
The third seems to partially work (Using 'retVal = Shell("ssh user#ip python C:\Temp\Remote.py", vbNormalFocus)' ), but I cannot get the value out. I can see the command window open, the values come back, but I get some integer back in retVal.
Any help?
The WScript.Shell object's .Exec() method is the method that is designed to (a) run a console command / application, and (b) capture its output via properties of the WshScriptExec instance it returns.
At this point it is a mystery as to why your ssh-based command doesn't work - your code generally does work with console commands.
That said, there is another reason not to use .Exec(), when run from a GUI application, such as a VBA-hosting application, the console window is invariably made visible during execution, which can be visually disruptive.
The following alternatives (optionally) allow running a command hidden (without a visible window), but note that neither directly supports capturing output from the invoked command:
VBA's own Shell() function, which, however, invariably executes asynchronously, so it's cumbersome to determine when the command finishes.
The WScriptShell object's .Run() method, which optionally allows waiting for the command to finish.
To get both invisible execution and output capturing, you can:
combine the WScriptShell object's .Run() method
with sending the command's output to a temporary file,
and, upon command completion, read the temporary file into memory.
Dim cmd as String
Dim exitCode As Integer
Dim tempFile As String
Dim capturedOutput As String
' Construct the name of a temporary file to capture the command's stdout output in.
tempFile = Environ$("TEMP") & "\" & CreateObject("Scripting.FileSystemObject").GetTempName()
' Define the command to invoke.
cmd = "ssh user#ip python C:\Temp\Remote.py"
' Use .Run() to invoke the command.
' - In order to use output redirection, the command must be prefixed with 'cmd /c '.
' - Setting the last argument to `True` makes the invocation synchronous.
' - Replace vbNormalFocus with vbHidden to run the command *invisibly*.
exitCode = CreateObject("WScript.Shell").Run("cmd /c " & cmd & " >" & tempFile, vbNormalFocus, True)
If exitCode = 0 Then ' Command succeeded.
' Read the output file, then delete the temporary file.
capturedOutput = CreateObject("Scripting.FileSystemObject").OpenTextFile(tempFile).ReadAll()
CreateObject("Scripting.FileSystemObject").DeleteFile(tempFile)
' Display the output.
MsgBox "Captured Output:" & vbNewLine & capturedOutput
Else ' Command indicated failure.
MsgBox "An unexpected error occurred.", vbExclamation
End If
Here's an alternative based on Shell() - inspired by this implementation.
As you can see, making Shell() synchronous requires a lot more effort and requires use of the Windows API:
Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccessas As Long, ByVal bInheritHandle As Long, ByVal dwProcId As Long) As Long
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32.dll" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32.dll" (ByVal hProcess As Long, ByRef lpExitCodeOut As Long) As Integer
Sub Main()
Dim cmd As String
Dim taskId As Integer
Dim exitCode As Integer
Dim tempFile As String
Dim capturedOutput As String
' Construct the name of a temporary file to capture the command's stdout output in.
tempFile = Environ$("TEMP") & "\" & CreateObject("Scripting.FileSystemObject").GetTempName()
' Define the command to run.
cmd = "ssh user#ip python C:\Temp\Remote.py"
' Use the SyncShell() helper function defined below to invoke the command
' synchronously and to obtain its exit code.
' - In order to use output redirection, the command must be prefixed with 'cmd /c '.
' - Add a 3rd argument with a timeout value in seconds if you don't want to wait
' indefinitely for the process to complete.
' - Replace vbNormalFocus with vbHidden to run the command *invisibly*.
exitCode = SyncShell("cmd /c " & cmd & " >" & tempFile, vbNormalFocus)
If exitCode = 0 Then ' Command succeeded.
' Read the output file and delete the temporary file.
capturedOutput = CreateObject("Scripting.FileSystemObject").OpenTextFile(tempFile).ReadAll()
CreateObject("Scripting.FileSystemObject").DeleteFile (tempFile)
' Display the output.
MsgBox "Captured Output:" & vbNewLine & capturedOutput
Else ' Command indicated failure.
MsgBox "An unexpected error occurred.", vbExclamation
End If
End Sub
' Helper function
Private Function SyncShell(ByVal cmd As String, Optional ByVal windowStyle As VbAppWinStyle = vbMinimizedFocus, Optional ByVal timeoutInSecs As Double = -1) As Long
Dim pid As Long ' PID (Process ID) as returned by Shell().
Dim h As Long ' Process handle
Dim sts As Long ' WinAPI return value
Dim timeoutMs As Long ' WINAPI timeout value
Dim exitCode As Long
' Invoke the command (invariably asynchronously) and store the PID returned.
' Note that the invocation may fail.
pid = Shell(cmd, windowStyle)
' Translate the PIP into a process *handle* with the SYNCHRONIZE and PROCESS_QUERY_LIMITED_INFORMATION access rights,
' so we can wait for the process to terminate and query its exit code.
h = OpenProcess(&H100000 Or &H1000, 0, pid) ' &H100000 == SYNCHRONIZE, &H1000 == PROCESS_QUERY_LIMITED_INFORMATION
If h = 0 Then Err.Raise vbObjectError + 1024, , "Failed to obtain process handle for process with ID " & pid & "."
' Now wait for the process to terminate.
If timeoutInSecs = -1 Then
timeoutMs = &HFFFF ' INFINITE
Else
timeoutMs = timeoutInSecs * 1000
End If
sts = WaitForSingleObject(h, timeoutMs)
If sts <> 0 Then Err.Raise vbObjectError + 1025, , "Waiting for process with ID " & pid & " to terminate timed out, or an unexpected error occurred."
' Obtain the process's exit code.
sts = GetExitCodeProcess(h, exitCode) ' Return value is a BOOL: 1 for true, 0 for false
If sts <> 1 Then Err.Raise vbObjectError + 1026, , "Failed to obtain exit code for process ID " & pid & "."
CloseHandle h
' Return the exit code.
SyncShell = exitCode
End Function
I think you should try using the "command spec" at the start of the command. On your machine this can be obtained in VBA with VBA.Environ$("comspec") which on my my machine returns C:\WINDOWS\system32\cmd.exe
So try
sCmd = VBA.Environ$("comspec") & " /C " & " ssh user#ip python C:\Temp\Remote.py"
The command spec needs switches in some instances cmd switches
I do not have Python installed so cannot test.
I have a folder with 30 or so .gz zipped files and 1 .zip files. I can ue code to utilise Windows Explorer to unzip the 1 .zip file, but unfortunately, Windows explorer does not unzip .gz files. I have created code which utilises Winzip to open all these files, but unfortunately this opens up the path folder, every time it unzips, I end up with 30+ open folders, which I then close, one by one with further code - unnecessary. A process that takes near 10 minutes.
Scouring the net, I've found and adapted a Ron De Bruin code that utilises '7-zip' software , open source and freely available online, to unzip without opening up a new folder each time. It unzips all files effortlessly in about a minute, far better. The code is below (mainly comments so not as long as it first looks!). My only problem is that sometimes this unzips files, and sometimes this runs without unzipping any files. When it runs perfectly, it toggles the 'GetExitCodePorcess hProcess, ExitCode' line longer, there I'm assuming it is processes to get an ExitCode which allows it to unzip the file. When it isn't working, it only toggles once or twice and moves onto the next stage, therefore, I assume that it generated the wrong exit code.
Is the problem the PtrSafe Function? Or is it in my ShellStr, or anywhere else? Please help, as I want to avoid using the Winzip method. If anyone has any other alternatives, please suggest!
#If VBA7 Then
Private Declare PtrSafe Function OpenProcess Lib "kernel32" _
(ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Private Declare PtrSafe Function GetExitCodeProcess Lib "kernel32" _
(ByVal hProcess As Long, _
lpExitCode As Long) As Long
#Else
Private Declare Function OpenProcess Lib "kernel32" _
(ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" _
(ByVal hProcess As Long, _
lpExitCode As Long) As Long
#End If
Public Const PROCESS_QUERY_INFORMATION = &H400
Public Const STILL_ACTIVE = &H103
Public Sub ShellAndWait(ByVal PathName As String, Optional WindowState)
Dim hProg As Long
Dim hProcess As Long, ExitCode As Long
'fill in the missing parameter and execute the program
If IsMissing(WindowState) Then WindowState = 1
hProg = Shell(PathName, WindowState)
'hProg is a process ID under Win32. To get the process handle:
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, hProg)
Do
'populate Exitcode variable
GetExitCodeProcess hProcess, ExitCode
DoEvents
Loop While ExitCode = STILL_ACTIVE
End Sub
'With this example you unzip a fixed zip file: FileNameZip = "C:\Users\Ron\Test.zip"
'Note this file must exist, this is the only thing that you must change before you test it
'The zip file will be unzipped in a new folder in: Application.DefaultFilePath
'Normal if you have not change it this will be your Documents folder
'The name of the folder that the code create in this folder is the Date/Time
'You can change this folder to this if you want to use a fixed folder:
'NameUnZipFolder = "C:\Users\Ron\TestFolder\"
'Read the comments in the code about the commands/Switches in the ShellStr
Public Sub B_UnZip_Zip_File_Fixed()
Dim PathZipProgram As String, FolderPath As String
Dim UnzipFile As Variant, ShellStr As String
FolderPath = _
ThisWorkbook.Path
If Right(FolderPath, 1) <> "\" Then
FolderPath = FolderPath & "\"
End If
'Path of the Zip program
PathZipProgram = "C:\program files\7-Zip\"
If Right(PathZipProgram, 1) <> "\" Then
PathZipProgram = PathZipProgram & "\"
End If
'Check if this is the path where 7z is installed.
If Dir(PathZipProgram & "7z.exe") = "" Then
MsgBox "Please find your copy of 7z.exe and try again"
Exit Sub
End If
UnzipFile = _
Dir(FolderPath & "*.gz")
While UnzipFile <> _
""
If InStr(1, UnzipFile, ".gz") > _
0 Then
ShellStr = PathZipProgram & "7z.exe e -aoa -r" _
& " " & Chr(34) & UnzipFile & Chr(34) _
& " -o" & Chr(34) & FolderPath & Chr(34) & " " & "*.*"
ShellAndWait ShellStr, vbHide
End If
UnzipFile = _
Dir
Wend
'Create path and name of the normal folder to unzip the files in
'In this example we use: Application.DefaultFilePath
'Normal if you have not change it this will be your Documents folder
'The name of the folder that the code create in this folder is the Date/Time
'NameUnZipFolder = Application.DefaultFilePath & "\" & Format(Now, "yyyy-mm-dd h-mm-ss")
'You can also use a fixed path like
'NameUnZipFolder = "C:\Users\Ron\TestFolder\"
'Name of the zip file that you want to unzip (.zip or .7z files)
'FileNameZip = "C:\Users\Ron\Test.zip"
'There are a few commands/Switches that you can change in the ShellStr
'We use x command now to keep the folder stucture, replace it with e if you want only the files
'-aoa Overwrite All existing files without prompt.
'-aos Skip extracting of existing files.
'-aou aUto rename extracting file (for example, name.txt will be renamed to name_1.txt).
'-aot auto rename existing file (for example, name.txt will be renamed to name_1.txt).
'Use -r if you also want to unzip the subfolders from the zip file
'You can add -ppassword if you want to unzip a zip file with password (only .7z files)
'Change "*.*" to for example "*.txt" if you only want to unzip the txt files
'Use "*.xl*" for all Excel files: xls, xlsx, xlsm, xlsb
'MsgBox "Look in " & NameUnZipFolder & " for extracted files"
End Sub
No, the exit code tells you the result of the external process that you spawned. For Windows 0 indicates success, non-zero indicates failure (or something else that meant the process wasn't successful)
So basically for some of .gz files 7zip can't complete successfully. You as the coder need to deal with this likely eventuality.
So your best bet is to print/log the 7zip command that it ran ShellStr and run that yourself manually in a command prompt/dos window to see the reason why.
I need to import a bunch of tables stored in .txt files into an Access database. When they are done importing, I use an ADO connection to communicate between the database and an Excel workbook. I have the Access database set to compact and repair on close.
The problem is, when I close the database after importing the files, I am unable to connect using ADO without waiting for an arbitrary amount of time. The Access window appears to be closed when I try to connect and fail. I have found that the amount of time I have to wait is related to the size of the database after import. After importing the largest sets of files, even a 60 second wait is not enough.
Is there some way I could force the connection to open? Of failing that, how could I check if it was ready to connect?
Here is some of the code I'm using:
MDB_Address = "C:\example.mdb"
Shell "cmd /c " & Chr(34) & MDB_Address & Chr(34), vbHide
'Some code that tests if it has opened happens here
...
Set ObjAccess = GetObject("C:\example.mdb")
' Import tables here
ObjAccess.Quit
Call CloseAccess
Call Wait
mdbPath = "C:\example.mdb"
Set mdbConnection = CreateObject("ADODB.Connection")
' The line below gives a run time error. The description is "Automation error Unspecified Error"
mdbConnection.Open "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=" & mdbPath
Sub CloseAccess
' I have set up the access database to write a flag to a .txt file when a
userform closes and use this to help check if it has closed.
End Sub
Sub Wait
' Wait 5 seconds. The access window appears to be closed.
Dim nHour As Date, nMinute As Date, nSecond As Date, waitTime As Date
nHour = Hour(Now())
nMinute = Minute(Now())
nSecond = Second(Now()) + 5
waitTime = TimeSerial(nHour, nMinute, nSecond)
Application.Wait waitTime
End Sub
Here is what I have ended up doing to test if the database is closed. I use Windows API functions to get the process handle for the Access database and then get its exit status.
This seems to work pretty well. There are surely other ways of accomplishing this - I think there is an ldb file that is created in the directory and it would probably work to check for its existence.
Private Declare Function OpenProcess Lib "kernel32" _
(ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" _
(ByVal hProcess As Long, lpExitCode As Long) As Long
'Open the data base
TaskID = Shell("cmd /c " & Chr(34) & MDB_Address & Chr(34), vbHide)
ACCESS_TYPE = &H400
hProc = OpenProcess(ACCESS_TYPE, False, TaskID)
'Some code that tests if it has opened happens here
...
Set ObjAccess = GetObject("C:\example.mdb")
' Import tables here
ObjAccess.Quit
Call CloseAccess
Sub CloseAccess()
Dim test As Long
'Test if the database has closed
Do Until lExitCode <> 259 And test <> 0
test = GetExitCodeProcess(hProc, lExitCode)
DoEvents
Loop
End Sub
I wrote VBA code which creates a .txt file with Job-Code for an IBM host based on Excel data (Websphere MQ Define Job).
It would be cool to have the possibility to tranfer this file to the host automatically via FTP. At this point I do this manually via:
(comment: LPAR = Host-Name)
ftp <LPAR>
'user'
'password'
put 'dateiname'
It works quite fine. But I don't know how to tranfer this to the VBA code. I found a similiar question here and there this solution was posted:
Public Sub FtpSend()
Dim vPath As String
Dim vFile As String
Dim vFTPServ As String
Dim fNum As Long
vPath = ThisWorkbook.path
vFile = "path"
vFTPServ = "<LPAR>"
'Mounting file command for ftp.exe
fNum = FreeFile()
Open vPath & "\FtpComm.txt" For Output As #fNum
Print #1, "user *******" ' your login and password"
'Print #1, "cd TargetDir" 'change to dir on server
Print #1, "bin" ' bin or ascii file type to send
Print #1, "put " & vPath & "\" & vFile & " " & vFile ' upload local filename to server file
Print #1, "close" ' close connection
Print #1, "quit" ' Quit ftp program Close
Shell "ftp -n -i -g -s:" & vPath & "\FtpComm.txt " & vFTPServ, vbNormalNoFocus
SetAttr vPath & "\FtpComm.txt", vbNormal
Kill vPath & "\FtpComm.txt"
End Sub
I'm not sure if I understand the code completely. I think I create a dummy file, FtpComm.txt, with the user data and the content and use this file to open the connection and send the data.
It works, somehow, until the point
*SetAttr vPath & "\FtpComm.txt", vbNormal*
There I get the error
Runtime-Error: 55 - File already opened.
The connection to the LPAR is set at this point. But what does "SetAttr..." do? Is this the point where the Input is done? What should I do?
Two things.
First, you are opening a file under file number #fNum obtained from fNum = FreeFile(), and then just assuming that this will return 1, as in Print #1 etc.:
Open vPath & "\FtpComm.txt" For Output As #fNum
Print #1, "user *******" ' your login and password"
Change all your Print #1 to Print #fNum.
Second, you open your file for I/O, but never close it. So of course, when you try to modify its attributes or delete it, the system will complain that it's already open and in use. The solution is to close the file -- which you should always do as soon as you're done writing to/reading from a file.
Close #fNum ' add this line
' Can now manipulate the file without risking conflict
Shell "ftp -n -i -g -s:" & vPath & "\FtpComm.txt " & vFTPServ, vbNormalNoFocus
SetAttr vPath & "\FtpComm.txt", vbNormal
Kill vPath & "\FtpComm.txt"
As for what your code is doing on a high level: if you type ftp -help at the command line, you will see what those -n -i -g -s flags mean. So yes, you're writing FTP commands to file FtpComm.txt, and then supplying that file to ftp via the -s flag.
-s:filename Specifies a text file containing FTP commands; the
commands will automatically run after FTP starts.
You're on the right path and following Jean-François's answer should get you there.
If you want something you can cut and paste, you can try the code below. It's a variation of something I wrote several years ago.
The biggest caveat is that once you shell the FTP command, you have no idea if the file actually transferred until you manually check.
But, if you're limited to strict VBA, then this is the way to go:
Option Explicit
Const FTP_ADDRESS = "ftp.yourdestination.com"
Const FTP_USERID = "anon"
Const FTP_PASSWORD = "anon"
Sub Macro1()
If Not SendFtpFile_F() Then
MsgBox "Could not ftp file"
Else
MsgBox "Sent"
End If
End Sub
Function SendFtpFile_F() As Boolean
Dim rc As Integer
Dim iFreeFile As Integer
Dim sFTPUserID As String
Dim sFTPPassWord As String
Dim sWorkingDirectory As String
Dim sFileToSend As String
Const FTP_BATCH_FILE_NAME = "myFtpFile.ftp"
Const INCREASED_BUFFER_SIZE = 20480
SendFtpFile_F = False
sWorkingDirectory = "C:\YourWorkingDirectory\"
sFileToSend = "NameOfFile.txt"
On Error GoTo FtpNECAFile_EH
'Kill FTP process file if it exists
If Dir(sWorkingDirectory & FTP_BATCH_FILE_NAME) <> "" Then
Kill sWorkingDirectory & FTP_BATCH_FILE_NAME
End If
'Create FTP process file
iFreeFile = FreeFile
Open sWorkingDirectory & FTP_BATCH_FILE_NAME For Output As #iFreeFile
Print #iFreeFile, "open " & FTP_ADDRESS
Print #iFreeFile, FTP_USERID
Print #iFreeFile, FTP_PASSWORD
Print #iFreeFile, "mput " & sWorkingDirectory & sFileToSend
Print #iFreeFile, "quit"
Close #iFreeFile
'Shell command the FTP file to the server
Shell "ftp -i -w:20480 -s:" & sWorkingDirectory & FTP_BATCH_FILE_NAME
SendFtpFile_F = True
GoTo FtpNECAFile_EX
FtpNECAFile_EH:
MsgBox "Err", Err.Name
FtpNECAFile_EX:
Exit Function
End Function
Note: While stepping through code, you could type the following command in the immediate window and then copy/paste the results into a Command Window:
? Shell "ftp -i -w:20480 -s:" & sWorkingDirectory & FTP_BATCH_FILE_NAME
SetAttr is used to set the attributes of a file (or a folder). In your case it sets it back to Normal (so not hidden, readonly, etc.). However, since your file is still open from the 'Open For Output' call it will fail with the 'File already opened' error.
I don't see any reason why the SetAttr function is called though, as you are not changing the attributes anywhere else. So feel free to remove it and see if it still works.
In the first example, after the sentence
Print #1, "quit" ' Quit ftp program Close
you must close the file used for connection with the sentence:
Close #fNum
Because this file is currently used.