Tuesday, March 29, 2005

FTP in PeopleCode

I was recently working on an interface that needed to pull files from the vendor via FTP. More specifically, I needed to retrieve an unknown number of ASCII files from a vendor's server, with no specific naming convention. I have done this before using a 'call system' command to kick off a bat script that would handle staging the files. This worked pretty good, and I eventually got the bat script to perform fairly decent error handling. But this interface was being written in App Engine... well, more accurately, a PeopleCode program kicked off by an App Engine shell (I still hate writing App Engine programs the "PeopleSoft recommended" way).

PeopleCode does provide the GetAttachment function, but this seemed quite limiting in that it required me knowing the4name of the file I wanted to retrieve. I needed something more flexible. Searching the internet, I found a Slerp article by David Jen that explains how to use PeopleCode's external library declaration to utilize the FTP functionality in the wininet.dll library. This was great, but it still required me to know the name of the file I wanted to 'get'. But, at least, I now had another road to pursue. After understanding more of what was available to me in the wininet.dll and playing around with different configurations, I came up with the following:

/************************* DECLARE EXTERNAL FUNCTIONS *************************/
Declare Function GetLastError Library "kernel32"
() Returns long As number;
Declare Function FormatMessageA Library "kernel32"
(long Value As number, long Value As number, long Value As number, long Value As number, string Ref As string, long Value As number, long Value As number) Returns long As number;
Declare Function InternetOpenA Library "wininet.dll"
(string Value As string, long Value As number, string Value As string, string Value As string, long Value As number) Returns long As number;
Declare Function InternetConnectA Library "wininet.dll"
(long Value As number, string Value As string, integer Value As number, string Value As string, string Value As string, long Value As number, long Value As number, long Value As number) Returns long As number;
Declare Function FtpSetCurrentDirectoryA Library "wininet.dll"
(long Value As number, string Value As string) Returns long As number;
Declare Function FtpGetFileA Library "wininet.dll"
(long Value As number, string Value As string, string Value As string, long Value As number, long Value As number, long Value As number, long Value As number) Returns boolean;
Declare Function FtpDeleteFileA Library "wininet.dll"
(long Value As number, string Value As string) Returns boolean;
Declare Function FtpCommandA Library "wininet.dll"
(long Value As number, long Value As number, long Value As number, string Value As string, long Value As number, long Ref As number) Returns boolean;
Declare Function InternetReadFile Library "wininet.dll"
(long Value As number, string Ref As string, integer Value As number, integer Ref As number) Returns boolean;
Declare Function InternetCloseHandle Library "wininet.dll"
(long Value As number) Returns integer As number;
/*********************** END DECLARE EXTERNAL FUNCTIONS ***********************/

/* Program Variable Declarations */
Local array of string &gFileList;
Local number &gHostOpen, &gHostConnect;
Local string &gHostFileName;
Local number &gReturnCode;
Local boolean &gReturnStatus;

Function GetNTMessage(&inReturnCode As number) Returns string;
Local number &l_MsgLength;
Local string &l_ReturnMsg;

&l_MsgLength = FormatMessageA(4096, 0, &inReturnCode, 0, &l_ReturnMsg, 256, 0);
If &l_MsgLength > 0 Then
Return &l_ReturnMsg;
Else
Return "";
End-If;
End-Function;

Function FTPOpenHostConnection(&inLogFile As File) Returns number;
Local number &l_OPEN_PRECONFIG = 0;
Local number &l_Handle;

/* Open connection to host */
&l_Handle = InternetOpenA("Peoplecode FTP", &l_OPEN_PRECONFIG, "", "", 0);
If &l_Handle = 0 Then
&inLogFile.WriteLine("FTP ERROR: Unable to open Internet connection.");
End-If;
Return &l_Handle;
End-Function;

Function FTPConnectToHost(&inLogFile As File, &inHandleOpen As number, &inHostSystem As string, &inHostDirectory As string, &inHostFTPAccount As string, &inHostPassword As string) Returns number;
Local number &l_FTP_PORT = 21;
Local number &l_INET_FTP = 1;
Local number &l_INET_PASSIVE = 134217728;

Local number &l_HostDirSet, &l_Handle;
Local number &l_ReturnCode;

/* Login to Host */
&l_Handle = InternetConnectA(&inHandleOpen, &inHostSystem, &l_FTP_PORT, &inHostFTPAccount, &inHostPassword, &l_INET_FTP, &l_INET_PASSIVE, 0);

If &l_Handle = 0 Then
&inLogFile.WriteLine("FTP ERROR: Unable to open connection to " | &inHostSystem | " !!!");
Return 0;
End-If;

/* Change remote directory */
If All(&inHostDirectory) Then /* If host directory passed, set it. */
&l_HostDirSet = FtpSetCurrentDirectoryA(&l_Handle, &inHostDirectory);
Else
&l_HostDirSet = 1;
End-If;

If &l_HostDirSet = 0 Then
&inLogFile.WriteLine("FTP ERROR: Unable to set directory to " | &inHostDirectory | " !!!");
&l_ReturnCode = InternetCloseHandle(&l_Handle);
Return 0;
End-If;

Return &l_Handle;
End-Function;

Function FTPGetFileList(&inLogFile As File, &inHandleConnect As number) Returns array of string;
Local string &l_FTP_CMD = "NLST";
Local number &l_ASCII = 1;

Local array of string &l_FileList = CreateArrayRept("", 0);

Local number &l_FTPHandle;
Local string &l_Out, &l_Text_Buffer;
Local number &l_Bytes = 100;
Local number &l_Bytes_Read;
Local boolean &l_ReturnStatus;
Local number &l_ReturnCode;

&l_ReturnStatus = FtpCommandA(&inHandleConnect, 1, &l_ASCII, &l_FTP_CMD, 0, &l_FTPHandle);
If &l_FTPHandle = 0 Then
&inLogFile.WriteLine("FTP ERROR: Unable to get directory !!!");
Return &l_FileList;
Else
&inLogFile.WriteLine("Getting List of files from host.");
&l_ReturnStatus = InternetReadFile(&l_FTPHandle, &l_Text_Buffer, &l_Bytes, &l_Bytes_Read);
While &l_Bytes_Read > 0
&l_Out = &l_Out | &l_Text_Buffer;
&l_ReturnStatus = InternetReadFile(&l_FTPHandle, &l_Text_Buffer, &l_Bytes, &l_Bytes_Read);
End-While;
&l_ReturnCode = InternetCloseHandle(&l_FTPHandle);
End-If;
&l_FileList = Split(&l_Out, Char(13) | Char(10));
&inLogFile.WriteLine(String(&l_FileList.Len) | " file(s) found.");
Return &l_FileList;
End-Function;

Function FTPGetFile(&inLogFile As File, &inHandleConnect As number, &inHostFileName As string, &inLocalDirectory) Returns boolean;
Local number &l_FTP_ASCII = 1;
Local string &l_LocalFileName;
Local number &l_ReturnCode;
Local string &l_ReturnMsg;
Local boolean &l_GetFile;

/* Get (Session, local file, remote file, failexist, flags&attibutes, flags, context */
&inLogFile.WriteLine("Getting " | &inHostFileName | "...");
&l_LocalFileName = &inLocalDirectory | &inHostFileName;
&l_GetFile = FtpGetFileA(&inHandleConnect, &inHostFileName, &l_LocalFileName, 0, 0, &l_FTP_ASCII, 0);

If Not &l_GetFile Then
&l_ReturnCode = GetLastError();
&l_ReturnMsg = GetNTMessage(&l_ReturnCode);
&inLogFile.WriteLine("FTP ERROR: " | String(&l_ReturnCode) | " : " | &l_ReturnMsg);
&inLogFile.WriteLine("FTP ERROR: Unable to retrieve file !!!");
Return False;
End-If;

/* Sucessful Get */
&inLogFile.WriteLine(" Get Sucessful.");
Return True;
End-Function;

Function FTPDeleteFile(&inLogFile As File, &inHandleConnect As number, &inHostFileName As string) Returns boolean;
Local boolean &l_DeleteFile;
Local number &l_ReturnCode;
Local string &l_ReturnMsg;

&l_DeleteFile = FtpDeleteFileA(&inHandleConnect, &inHostFileName);

If Not &l_DeleteFile Then
&l_ReturnCode = GetLastError();
&l_ReturnMsg = GetNTMessage(&l_ReturnCode);
&inLogFile.WriteLine(" FTP ERROR: " | String(&l_ReturnCode) | " : " | &l_ReturnMsg);
&inLogFile.WriteLine(" FTP ERROR: Unable to delete file.");
Return False;
End-If;

/* SUCCESS */
&inLogFile.WriteLine(" File Deleted from HRSmart server.");
Return True;
End-Function;

/*****************************************************************************/
/* MAIN PROGRAM
/*****************************************************************************/

/* Log File */
Local File &gLogFile;

/* Host information */
Local string &HOSTSERVER = "www.example.com";
Local string &HOSTACCOUNT = "usename";
Local string &HOSTPASSWORD = "password";
Local string &HOSTDIRECTORY = "";
Local string &FILE_MASK = "%";

/* A File Object &gLogFile needs to have already been open prior to calling this routine. */
/* Log information is written to this file */

&gHostOpen = FTPOpenHostConnection(&gLogFile);
If &gHostOpen > 0 Then
&gHostConnect = FTPConnectToHost(&gLogFile, &gHostOpen, &HOSTSERVER, &HOSTDIRECTORY, &HOSTACCOUNT, &HOSTPASSWORD);
If &gHostConnect > 0 Then
&gFileList = FTPGetFileList(&gLogFile, &gHostConnect);
While &gFileList.Len > 0
&gHostFileName = &gFileList.Shift();
If DBPatternMatch(&gHostFileName, &FILE_MASK, False) Then
If FTPGetFile(&gLogFile, &gHostConnect, &gHostFileName, &LOCALDIRECTORY) Then
&gReturnStatus = FTPDeleteFile(&gLogFile, &gHostConnect, &gHostFileName);
End-If;
End-If;
End-While;
&gReturnCode = InternetCloseHandle(&gHostConnect);
&gReturnCode = InternetCloseHandle(&gHostOpen);
Else
&gReturnCode = InternetCloseHandle(&gHostOpen);
End-If;
End-If;

One thing to note... it seems that PeopleCode can only use DLL functions when they return datatypes that are understood by PeopleCode (strings, numbers, boolean). In trying to find a way to search the FTP directory, I came across a couple functions named FindFirstFile and FindNextFile. Unfortunately, these returned a pointer to a pointer to a WIN32_FIND_DATA structure. I could not figure out a way to use these. As a result, I had to take a slightly longer approach using the FTPCommand function.

Any ideas of another way to implement this?

5 comments:

Anonymous said...

You are using &LOCALDIRECTORY in one of your function calls, but don't declare it anywhere.

Anonymous said...

I can't get this to work. Help.

David said...

ji David
its been years ago, good to see you were able to utilize the function for some good. peopleCode datatypes are mapped to what can accommodate C data types used by the dll, as long as data size matches. i need to double check as it's been so long ago, but oi think "Reference" can be used as pointer to a data structure. anyway things are all different now. chances are low these code will be run today. regards, david

Raja said...

Excellent David , i was really working hard on this for two days as getattachment ditched me !!
My Requirement was to fetch a single file where the file name could vary each time but had a naming convention.
Thanks a lot Sir.

Anonymous said...

Generally I do not learn post on blogs, however I would like to say that this write-up very forced me to try and
do it! Your writing taste has been surprised me.
Thanks, quite nice post.