FtpFindFirstFile always returns zero

vincehardwick

New Member
Joined
Feb 4, 2021
Messages
20
Office Version
  1. 365
Platform
  1. Windows
I've hit a brick wall trying to get FTP working in Excel VBA (64-bit Office on 64-bit Windows 10). As an early proof of concept, I'm just trying to list the name of the single text file that I've uploaded to the FTP server.

The sub I'm running is ListFilesOnFTP. hOpen and hConnection both get set to handle values successfully by InternetOpen and InternetConnect respecitvely.

blReturn is set to True by FtpSetCurrentDirectory, indicating that this is not failing.

The problem I have is in EnumFiles - no matter what combination of wildcards I use for lpszSearchfile, FtpFindFirstFile always returns zero, and therefore EnumFiles exits immediately.

Obviously I have provided placeholder values below for strFTPServerIP, strUsername, strPassword and strRemoteDirectory, but I am 100% certain that the IP address and credentials are correct, and that the directory with the provided name does exist under the root of the FTP server.

Any ideas where I'm going wrong here?

Relevant constant and type declarations:

VBA Code:
Private Const MAX_PATH As Integer = 260
Private Const INTERNET_FLAG_RELOAD = &H80000000
Private Const INTERNET_FLAG_NO_CACHE_WRITE = &H4000000
Private Const INTERNET_OPEN_TYPE_PRECONFIG = 0
Private Const INTERNET_DEFAULT_FTP_PORT = 21
Private Const INTERNET_SERVICE_FTP = 1
Private Const INTERNET_FLAG_PASSIVE = &H8000000
Private Const INTERNET_NO_CALLBACK = 0

Private Type FILETIME

    dwLowDateTime As Long
    dwHighDateTime As Long
    
End Type

Private Type WIN32_FIND_DATA

    dwFileAttributes As Long
    ftCreationTime As FILETIME
    ftLastAccessTime As FILETIME
    ftLastWriteTime As FILETIME
    nFileSizeHigh As Long
    nFileSizeLow As Long
    dwReserved0 As Long
    dwReserved1 As Long
    cFileName As String * MAX_PATH
    cAlternate As String * 14
    
End Type

Relevant wininet.dll function declarations (please note - I do have these wrapped in the usual #If VBA7 Then... #Else... #End If conditional compilation structures, with 32-bit compatible declarations in the else clause, but for brevity I have only provided the PtrSafe functions here):

VBA Code:
    Private Declare PtrSafe Function InternetCloseHandle Lib "wininet.dll" ( _
        ByVal hInet As LongPtr) As LongPtr

    Private Declare PtrSafe Function InternetOpen Lib "wininet.dll" Alias "InternetOpenA" ( _
        ByVal sAgent As String, _
        ByVal lAccessType As LongPtr, _
        ByVal sProxyName As String, _
        ByVal sProxyBypass As String, _
        ByVal lFlags As LongPtr) As LongPtr

    Private Declare PtrSafe Function InternetConnect Lib "wininet.dll" Alias "InternetConnectA" ( _
        ByVal hInternetSession As LongPtr, _
        ByVal sServerName As String, _
        ByVal nServerPort As LongPtr, _
        ByVal sUsername As String, _
        ByVal sPassword As String, _
        ByVal lService As LongPtr, _
        ByVal lFlags As LongPtr, _
        ByVal lContext As LongPtr) As LongPtr

    Private Declare PtrSafe Function FtpSetCurrentDirectory Lib "wininet.dll" Alias "FtpSetCurrentDirectoryA" ( _
        ByVal hFtpSession As LongPtr, _
        ByVal lpszDirectory As String) As Boolean

    Private Declare PtrSafe Function FtpGetCurrentDirectory Lib "wininet.dll" Alias "FtpGetCurrentDirectoryA" ( _
        ByVal hFtpSession As LongPtr, _
        ByVal lpszCurrentDirectory As String, _
        ByVal lpdwCurrentDirectory As LongPtr) As LongPtr

    Private Declare PtrSafe Function FtpFindFirstFile Lib "wininet.dll" Alias "FtpFindFirstFileA" ( _
        ByVal hFtpSession As LongPtr, _
        ByVal lpszSearchFile As String, _
        ByRef lpFindFileData As WIN32_FIND_DATA, _
        ByVal dwFlags As LongPtr, _
        ByVal dwContent As LongPtr) As LongPtr

Procedures using the above:

VBA Code:
Public Sub EnumFiles(ByVal hConnection As LongPtr)

    Dim pData As WIN32_FIND_DATA
    
    #If VBA7 Then
        Dim hFind As LongPtr, lRet As LongPtr
    #Else
        Dim hFind As Long, lRet As Long
    #End If

    ' Create a buffer
    pData.cFileName = String(MAX_PATH, vbNullChar)

    ' Find the first file
    hFind = FtpFindFirstFile(hConnection, "*.*", pData, INTERNET_FLAG_RELOAD Or INTERNET_FLAG_NO_CACHE_WRITE, 0)
    
    ' If there's no file, then exit sub
    If hFind = 0 Then Exit Sub

    ' Show the filename
    MsgBox Left$(pData.cFileName, InStr(1, pData.cFileName, String(1, 0), vbBinaryCompare) - 1)

    Do
    
        ' Create a buffer
        pData.cFileName = String(MAX_PATH, vbNullChar)
        
        ' Find the next file
        lRet = InternetFindNextFile(hFind, pData)
        
        ' If there's no next file, exit loop
        If lRet = 0 Then Exit Do
        
        ' Show the filename
        MsgBox Left$(pData.cFileName, InStr(1, pData.cFileName, String(1, 0), vbBinaryCompare) - 1)
    
    Loop

    ' Close the search handle
    InternetCloseHandle hFind

End Sub

Public Sub ListFilesOnFTP()

    #If VBA7 Then
        Dim hOpen As LongPtr, hConnection As LongPtr
    #Else
        Dim hOpen As Long, hConnection As Long
    #End If
    
    Dim blReturn As Boolean
    Dim strFTPServerIP As String, strUsername As String, strPassword As String, _
        strRemoteDirectory As String
    
    strFTPServerIP = "12.345.678.901"
    strUsername = "username"
    strPassword = "password"
    strRemoteDirectory = "directory_name/"
    
    ' Open an internet connection
    hOpen = InternetOpen("FTP", _
        INTERNET_OPEN_TYPE_PRECONFIG, _
        vbNullString, _
        vbNullString, _
        0)
    
    hConnection = InternetConnect( _
        hOpen, _
        strFTPServerIP, _
        INTERNET_DEFAULT_FTP_PORT, _
        strUsername, _
        strPassword, _
        INTERNET_SERVICE_FTP, _
        INTERNET_FLAG_PASSIVE, _
        INTERNET_NO_CALLBACK)
    
    blReturn = FtpSetCurrentDirectory(hConnection, strRemoteDirectory)
    
    Call EnumFiles(hConnection)
    
    InternetCloseHandle hConnection
    InternetCloseHandle hOpen

End Sub
 

Excel Facts

Back into an answer in Excel
Use Data, What-If Analysis, Goal Seek to find the correct input cell value to reach a desired result
Hi vincehardwick,
I've got a couple of processes that require getting data from an FTP server, I use a powershell script to handle the FTP process and include a directory listing piped back to a local file where I then get file names, sizes and creation dates. I came across a technique on a forum for making an FTP script file within the Powershell script then running FTP calling that script file. Reason for this was I wanted to do FTP with parameters that were date dependent and FTP scripts are a bit limited.
I see you've put in a lot of work on doing this via VBA and I wish you well in fixing the bugs. If you don't sort it out, the Powershell script option does work well.
 
Upvote 0
Cross-posting (posting the same question in more than one forum) is not against our rules, but the method of doing so is covered by #13 of the Forum Rules.

Be sure to follow & read the link at the end of the rule too!

Cross posted at: FtpFindFirstFile always returns zero
If you have posted the question at more places, please provide links to those as well.

If you do cross-post in the future and also provide links, then there shouldn’t be a problem.
 
Upvote 0
Cross-posting (posting the same question in more than one forum) is not against our rules, but the method of doing so is covered by #13 of the Forum Rules.

Be sure to follow & read the link at the end of the rule too!

Cross posted at: FtpFindFirstFile always returns zero
If you have posted the question at more places, please provide links to those as well.

If you do cross-post in the future and also provide links, then there shouldn’t be a problem.
Apologies Fluff, also cross-posted here: FtpFindFirstFile always returns zero
 
Upvote 0
Hi vincehardwick,
I've got a couple of processes that require getting data from an FTP server, I use a powershell script to handle the FTP process and include a directory listing piped back to a local file where I then get file names, sizes and creation dates. I came across a technique on a forum for making an FTP script file within the Powershell script then running FTP calling that script file. Reason for this was I wanted to do FTP with parameters that were date dependent and FTP scripts are a bit limited.
I see you've put in a lot of work on doing this via VBA and I wish you well in fixing the bugs. If you don't sort it out, the Powershell script option does work well.
Thanks Rondeondo, I'm avoiding using shell/cmd/PowerShell as many of our clients will have a security config that will flag VBA calls to Shell objects, or attempts to run scripts, as potential malware and just quarantine/block the .xlsm.
 
Upvote 0
Thanks Rondeondo, I'm avoiding using shell/cmd/PowerShell as many of our clients will have a security config that will flag VBA calls to Shell objects, or attempts to run scripts, as potential malware and just quarantine/block the .xlsm.
understand. joys of being hobbled with productivity tools. I had to abandon sending confirmation e-mail from excel as it was easiest to send through outlook but MS don't want automated use of outlook... There's probably another way
 
Upvote 0
Hi Vince. I just stumbled across this thread, and was curious if you had managed to solve it or not. I don't use FTP much so I doubt I can be of much help, but I was interested to see how you went about making the API calls (I also use 64bit, and can sometimes cause me headaches when it comes to networking APIs... or APIs in general..).

One thing I noticed was the use of willdcards in the second argument of the FtpFindFirstFile API (link). Checking the documentation:
Pointer to a null-terminated string that specifies a valid directory path or file name for the FTP server's file system. The string can contain wildcards, but no blank spaces are allowed. If the value of lpszSearchFile is NULL or if it is an empty string, the function finds the first file in the current directory on the server.
Did you end up converting that into a null-terminated string? I noticed that you had used them elsewhere in the code, and just thought I'd see if you had done so here. Separately, failing that, did you try replacing it with Null or an empty string to see what, if anything, it returned?

Just some thoughts.
 
Upvote 0
Hi Vince. I just stumbled across this thread, and was curious if you had managed to solve it or not. I don't use FTP much so I doubt I can be of much help, but I was interested to see how you went about making the API calls (I also use 64bit, and can sometimes cause me headaches when it comes to networking APIs... or APIs in general..).

One thing I noticed was the use of willdcards in the second argument of the FtpFindFirstFile API (link). Checking the documentation:

Did you end up converting that into a null-terminated string? I noticed that you had used them elsewhere in the code, and just thought I'd see if you had done so here. Separately, failing that, did you try replacing it with Null or an empty string to see what, if anything, it returned?

Just some thoughts.
Hi Dan,

Thanks for getting in touch with your suggestions.

I did try appending vbNull onto the lpszSearchFile argument I was passing to FtpFindFirstFile after reading that same documentation, but unfortunately that didn't do the trick.

I also found this article - Optimize string handling in Visual Basic 6.0 - Part II - which seems to confirm that, in memory, VBA strings are null-terminated anyway, so conversion/appending should not be necessary.

After trying an empty string and vbNull on its own, and still getting nowhere, I abandoned trying to get wininet.dll and FTP to play nice in VBA altogether. From what I could gather, the dll dates back to when Internet Explorer wasn't out in the cold, so it was probably optimistic thinking I could get this working. For the purpose I had in mind for this, I'm moving on to C#/.NET and putting together an OData feed that will serve up data from SQL on Azure. Hopefully more modern technologies will be less frustrating!
 
Upvote 0
Well thanks for the update, and for the article. I didn't have time to read it properly, but I will check it over the coming weekend. Hopefully you find a hassle-free solution!
One thing that just jumped out at me now, though, is your API Declarations - I could be wrong but it seems as though you've changed everything that was a Long into LongPtr. Is that right? There are a few arguments in there that I wouldn't have thought are pointers, are so would have thought that they should stay as Longs.
 
Upvote 0
Well thanks for the update, and for the article. I didn't have time to read it properly, but I will check it over the coming weekend. Hopefully you find a hassle-free solution!
One thing that just jumped out at me now, though, is your API Declarations - I could be wrong but it seems as though you've changed everything that was a Long into LongPtr. Is that right? There are a few arguments in there that I wouldn't have thought are pointers, are so would have thought that they should stay as Longs.
I believe so yeah - although now that I check (LongPtr data type), it's only supposed to be used as the return type for functions which get a pointer or a handle, so I've probably misused it when declaring the datatype of arguments to those functions as LongPtrs.

It didn't affect the InternetConnectA or InternetOpenA functions so I think it's safe enough, they returned values just fine. As the link above mentions, "LongPtr is not a true data type because it transforms to a Long in 32-bit environments, or a LongLong in 64-bit environments". I wrapped the function declarations in #If VBA7... #Else... #End If anyway, and only used LongPtr in the case where VBA7 was true, so I think it would be neither here nor there.

Even then, I probably should have used #If Win64... anyway, strictly speaking.
 
Upvote 0

Forum statistics

Threads
1,214,621
Messages
6,120,568
Members
448,972
Latest member
Shantanu2024

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top