Skip to content

Latest commit

 

History

History
610 lines (501 loc) · 17.2 KB

sample_344.md

File metadata and controls

610 lines (501 loc) · 17.2 KB

Home

Custom FTP Class for Visual FoxPro application

Note that this document contains some links to the old news2news website which does not work at the moment. This material will be available sometime in the future.

Before you begin:

Programming File Transfer Protocol in Visual FoxPro

The class implements basic FTP operations: connection, directories, downloading and uploading files. All functionality comes from the Microsoft Wininet API library.

Simple FTP manager form built on this class:

Here is a sample code that uses this class.

The interface:

FUNCTION  FtpConnect(lcHost, lcUsr, lcPswd)  
PROCEDURE FtpDisconnect  
PROPERTY  IsConnected  

FUNCTION  GetCurrentDir  
FUNCTION  SetCurrentDir(lcDir)  

FUNCTION  RemoveDir(lcDir)  
FUNCTION  CreateDir(lcDir)  
FUNCTION  DirExists(lcDir)  

FUNCTION  Dir2Cursor(lcCursor)  

FUNCTION  PutToFtp(lcLocalFile, lcRemoteFile, lnChunkSize)  
FUNCTION  GetFromFtp(lcRemoteFile, lcLocalFile, lnChunkSize)  

FUNCTION  BeforePutFile(lcLocalFile, lcRemoteFile)  
PROCEDURE AfterPutFile(lcLocalFile, lcRemoteFile, lResult)  
PROCEDURE OnPutChunk(lcLocalFile, lcRemoteFile, lnBytesWritten)  

FUNCTION  BeforeGetFile(lcRemoteFile, lcLocalFile)  
PROCEDURE AfterGetFile(lcRemoteFile, lcLocalFile, lResult)  
PROCEDURE OnGetChunk(lcRemoteFile, lcLocalFile, lnBytesWritten)

See also:

More advanced VFP-based FTP solution:
Download FTP Class Library


Code:

DEFINE CLASS clsFtp As Custom

#DEFINE INTERNET_OPEN_TYPE_DIRECT 1
#DEFINE INTERNET_INVALID_PORT_NUMBER 0
#DEFINE INTERNET_SERVICE_FTP 1
#DEFINE INTERNET_FLAG_NEED_FILE 16
#DEFINE FTP_TRANSFER_TYPE_ASCII 1
#DEFINE FTP_TRANSFER_TYPE_BINARY 2
#DEFINE FILE_ATTRIBUTE_READONLY 1
#DEFINE FILE_ATTRIBUTE_HIDDEN 2
#DEFINE FILE_ATTRIBUTE_SYSTEM 4
#DEFINE FILE_ATTRIBUTE_DIRECTORY 16
#DEFINE FILE_ATTRIBUTE_ARCHIVE 32
#DEFINE FILE_ATTRIBUTE_NORMAL 128
#DEFINE FILE_ATTRIBUTE_TEMPORARY 512
#DEFINE FILE_ATTRIBUTE_COMPRESSED 2048
#DEFINE FORMAT_MESSAGE_ALLOCATE_BUFFER 256
#DEFINE FORMAT_MESSAGE_FROM_SYSTEM 4096
#DEFINE FORMAT_MESSAGE_IGNORE_INSERTS 512
#DEFINE GENERIC_READ 0x80000000
#DEFINE GENERIC_WRITE 0x40000000

	LastErr=0
	ErrMsg=""
	host=""
	usr=""
	pswd=""
	IsConnected=.F.
	hInternet=0
	hConn=0

PROCEDURE  Init
	DECLARE INTEGER InternetOpen IN wininet.dll;
		STRING sAgent, INTEGER lAccessType, STRING sProxyName,;
		STRING sProxyBypass, STRING lFlags

	THIS.hInternet = InternetOpen ("VfpFtp 1.0",;
		INTERNET_OPEN_TYPE_DIRECT, 0,0,0)

	IF THIS.hInternet = 0
		RETURN .F.
	ENDIF
	THIS.declare

PROCEDURE Destroy
	THIS.FtpDisconnect
	= InternetCloseHandle(THIS.hInternet)

PROCEDURE ClearError
	THIS.LastErr = 0
	THIS.ErrMsg = ""

PROCEDURE SetError()
	DECLARE INTEGER GetLastError IN kernel32
	DECLARE INTEGER FormatMessage IN kernel32;
		INTEGER dwFlags, INTEGER lpSource, INTEGER dwMsgId,;
		INTEGER dwLangId, INTEGER @lpBuf, INTEGER nSz, INTEGER Args

	THIS.LastErr = GetLastError()

	LOCAL dwFlags, lpBuffer, lnLength
	* specifying format parameters
	dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER +;
		FORMAT_MESSAGE_FROM_SYSTEM + FORMAT_MESSAGE_IGNORE_INSERTS

	lpBuffer = 0
	lnLength = FormatMessage (dwFlags, 0, THIS.LastErr, 0, @lpBuffer, 0, 0)
	IF lnLength <> 0
		lpResult = REPLI (Chr(0), 500)
		= CopyMemory (@lpResult, lpBuffer, lnLength)
		THIS.ErrMsg = STRTRAN(LEFT(lpResult, lnLength), Chr(13)+Chr(10), "")
	ELSE
		THIS.ErrMsg = "#undefined#"
	ENDIF

FUNCTION FtpConnect(lcHost, lcUsr, lcPswd)
	THIS.FtpDisconnect

	DECLARE INTEGER InternetConnect IN wininet.dll;
		INTEGER hInetSession, STRING sSrvName, INTEGER nSrvPort,;
		STRING sUsrname, STRING sPwd, INTEGER lService,;
		INTEGER lFlags, INTEGER lContext

	THIS.hConn = InternetConnect (THIS.hInternet, lcHost,;
		INTERNET_INVALID_PORT_NUMBER, lcUsr, lcPswd,;
		INTERNET_SERVICE_FTP, 0, 0)

	IF THIS.hConn <> 0
		THIS.host = lcHost
		THIS.usr = lcUsr
		THIS.pswd = lcPswd
		THIS.IsConnected = .T.
	ELSE
		THIS.SetError()
	ENDIF

PROCEDURE FtpDisconnect
	IF THIS.IsConnected
		= InternetCloseHandle(THIS.hConn)
		STORE "" TO THIS.host, THIS.usr, THIS.pswd
		THIS.IsConnected = .F.
	ENDIF

FUNCTION GetCurrentDir
	if Not THIS.IsConnected
		THIS.SetError()
		RETURN "#not connected#"
	ELSE
		LOCAL lcDirectory, lnLen, lnResult
		lcDirectory = SPACE(250)
		lnLen = Len(lcDirectory)
		lnResult = FtpGetCurrentDirectory (THIS.hConn, @lcDirectory, @lnLen)
		RETURN Iif (lnResult=1, LEFT(lcDirectory, lnLen), "#error#")
	ENDIF

FUNCTION SetCurrentDir(lcDir)
	if Not THIS.IsConnected
		RETURN .F.
	ELSE
		IF (FtpSetCurrentDirectory (THIS.hConn, @lcDir) = 1)
			RETURN .T.
		ELSE
			THIS.SetError()
			RETURN .F.
		ENDIF
	ENDIF

FUNCTION RemoveDir(lcDir)
	IF Not DirExists(lcDir)
		RETURN .T.
	ENDIF
	DECLARE INTEGER FtpRemoveDirectory IN wininet;
		INTEGER hConnect, STRING lpszDirectory
RETURN (FtpRemoveDirectory(THIS.hConn, lcDir) = 1)

FUNCTION CreateDir(lcDir)
	IF DirExists(lcDir)
		RETURN .F.
	ENDIF
	DECLARE INTEGER FtpCreateDirectory IN wininet;
		INTEGER hFtpSession, STRING lpszDirectory
RETURN (FtpCreateDirectory(THIS.hConn, lcDir) = 1)

FUNCTION DirExists(lcDir)
	IF Not THIS.IsConnected
		RETURN .F.
	ELSE
		LOCAL lcCurrentDir
		lcCurrentDir = THIS.GetCurrentDir()

		IF THIS.SetCurrentDir(lcDir)
			THIS.SetCurrentDir(lcCurrentDir)
			RETURN .T.
		ELSE
			RETURN .F.
		ENDIF
	ENDIF

FUNCTION  Dir2Cursor(lcCursor)
	LOCAL lcFullPath, lcFullName, W32struct, lpFndFile, hFind, lnResult
	lcFullPath = THIS.GetCurrentDir()

	* create the cursor
	IF TYPE("lcCursor") <> "C"
		lcCursor = "ftpquery"
	ENDIF

	CREATE CURSOR (lcCursor) (;
		filename   C(250),;
		filesize   N(12),;
		created    T,;
		accessed   T,;
		modified   T,;
		fileattrib N(12),;
		rdonlyfile L,;
		hiddenfile L,;
		sysfile    L,;
		folder     L,;
		archfile   L,;
		normalfile L,;
		tempfile   L,;
		compressed L,;
		rootpath   M;
	)

	* object simulating the WIN32_FIND_DATA structure
	W32struct = CreateObject("struct_WIN32_FIND_DATA")

	* starting from the first file found
	lpFndFile = Repli(Chr(0), 320)
	hFind = FtpFindFirstFile (THIS.hConn, "*.*",;
		@lpFndFile, INTERNET_FLAG_NEED_FILE, 0)

	IF hFind = 0
		lnResult = -1
	ELSE
		DO WHILE .T.
			W32struct.setValue (lpFndFile)
			DO CASE
			CASE W32struct.filename == ".."
				lcFullName = SUBSTR (lcFullPath, 1, RAT("/", lcFullPath)-1)
			CASE W32struct.filename == "."
				lcFullName = lcFullPath
			OTHER
				lcFullName = lcFullPath + "/" + W32struct.filename
			ENDCASE
			lcFullName = STRTRAN(lcFullName, "//","/")

			WITH W32struct
				INSERT INTO (lcCursor) VALUES (;
					.filename, .filesizelo, .creationtime, .lastaccesstime,;
					.lastwritetime, .fileattributes, .IsRdOnly(), .IsHidden(),;
					.IsSystem(), .IsDirectory(), .IsArchive(), .IsNormal(),;
					.IsTemporary(), .IsCompressed(), lcFullName;
			)
			ENDWITH
			IF InternetFindNextFile(hFind, @lpFndFile) <> 1
				EXIT
			ENDIF
		ENDDO
		lnResult = RECCOUNT(lcCursor)
		GO TOP IN (lcCursor)
		= InternetCloseHandle(hFind)
	ENDIF
	RELEASE W32struct
RETURN lnResult

FUNCTION BeforePutFile(lcLocalFile, lcRemoteFile)
* virtual; return .F. to cancel file transfer
RETURN .T.

PROCEDURE AfterPutFile(lcLocalFile, lcRemoteFile, lResult)
* virtual

PROCEDURE OnPutChunk(lcLocalFile, lcRemoteFile, lnBytesWritten)
* virtual

FUNCTION PutToFtp(lcLocalFile, lcRemoteFile, lnChunkSize)
	IF Not (THIS.IsConnected And FILE(lcLocalFile))
		RETURN .F.
	ENDIF

	IF TYPE("lcRemoteFile") <> "C" Or EMPTY(lcRemoteFile)
		lcRemoteFile = SUBSTR(lcLocalFile, RAT("\",lcLocalFile)+1)
	ENDIF
		
	IF Not THIS.BeforePutFile(lcLocalFile, lcRemoteFile)
		RETURN .F.
	ENDIF

	LOCAL lResult
	IF TYPE("lnChunkSize") = "N" And lnChunkSize > 0
		lResult = THIS.PutToFtp1(lcLocalFile, lcRemoteFile, lnChunkSize)
	ELSE
		lResult = (FtpPutFile(THIS.hConn, lcLocalFile, lcRemoteFile,;
			FTP_TRANSFER_TYPE_BINARY, 0) = 1)
	ENDIF
	THIS.AfterPutFile(lcLocalFile, lcRemoteFile, lResult)
RETURN lResult

PROTECTED FUNCTION PutToFtp1(lcLocalFile, lcRemoteFile, lnChunkSize)
* copying local file to the remote target by small portions
	LOCAL hLocal, hRemote, lnBytesWritten, lcBuffer, lnLength
	lnBytesWritten = 0
	hLocal = FOPEN(lcLocalFile)
	IF (hLocal = -1)
		RETURN .F.
	ENDIF

	hRemote = FtpOpenFile(THIS.hConn, lcRemoteFile, GENERIC_WRITE,;
		FTP_TRANSFER_TYPE_BINARY, 0)

	IF hRemote = 0
		= FCLOSE(hLocal)
		RETURN .F.
	ENDIF

	DO WHILE Not FEOF(hLocal)
		lcBuffer = FREAD(hLocal, lnChunkSize)
		lnLength = Len(lcBuffer)
		IF lnLength > 0
			IF InternetWriteFile(hRemote, @lcBuffer, lnLength, @lnLength) = 1
				lnBytesWritten = lnBytesWritten + lnLength
				THIS.OnPutChunk(lcLocalFile, lcRemoteFile, lnBytesWritten)
			ELSE
				EXIT
			ENDIF
		ELSE
			EXIT
		ENDIF
	ENDDO
	= InternetCloseHandle(hRemote)
	= FCLOSE(hLocal)
RETURN .T.

FUNCTION BeforeGetFile(lcRemoteFile, lcLocalFile)
* virtual; return .F. to cancel file transfer
RETURN .T.

PROCEDURE AfterGetFile(lcRemoteFile, lcLocalFile, lResult)
* virtual

PROCEDURE OnGetChunk(lcRemoteFile, lcLocalFile, lnBytesWritten)
* virtual

FUNCTION GetFromFtp (lcRemoteFile, lcLocalFile, lnChunkSize)
	IF Not THIS.IsConnected
		RETURN .F.
	ENDIF

	IF TYPE("lcLocalFile") <> "C" Or EMPTY(lcLocalFile)
		lcLocalFile = SUBSTR(lcRemoteFile, RAT("/",lcRemoteFile)+1)
		lcLocalFile = FULLPATH(lcLocalFile)
	ENDIF

	IF Not THIS.BeforeGetFile(lcRemoteFile, lcLocalFile)
		RETURN .F.
	ENDIF

	LOCAL lResult
	IF TYPE("lnChunkSize") = "N" And lnChunkSize > 0
		lResult = THIS.GetFromFtp1(lcRemoteFile, lcLocalFile, lnChunkSize)
	ELSE
		lResult = (FtpGetFile(THIS.hConn, lcRemoteFile, lcLocalFile,;
			0, FILE_ATTRIBUTE_NORMAL, FTP_TRANSFER_TYPE_BINARY, 0) = 1)
	ENDIF
	THIS.AfterGetFile(lcRemoteFile, lcLocalFile, lResult)
RETURN lResult

PROTECTED FUNCTION GetFromFtp1(lcRemoteFile, lcLocalFile, lnChunkSize)
	LOCAL hRemote, hLocal, lnBytesRead, lcBuffer, lnBufSize
	hLocal = FCREATE (lcLocalFile)
	IF hLocal = -1
		RETURN .F.
	ENDIF
	
	hRemote = FtpOpenFile(THIS.hConn, lcRemoteFile, GENERIC_READ,;
		FTP_TRANSFER_TYPE_BINARY, 0)

	IF hRemote = 0
		FCLOSE(hLocal)
		RETURN .F.
	ENDIF
	
	lnBytesRead = 0
	DO WHILE .T.
		lcBuffer = Repli(Chr(0), lnChunkSize)
		lnBufSize = lnChunkSize
		IF InternetReadFile(hRemote, @lcBuffer, lnBufSize, @lnBufSize) = 1
			IF lnBufSize <> 0
				= FWRITE (hLocal, lcBuffer, lnBufSize)
				lnBytesRead = lnBytesRead + lnBufSize
				THIS.OnGetChunk(lcRemoteFile, lcLocalFile, lnBytesRead)
			ELSE
				EXIT
			ENDIF
		ELSE
			EXIT
		ENDIF
	ENDDO
	= InternetCloseHandle(hRemote)
	= FCLOSE(hLocal)
RETURN .T.

PROCEDURE declare
	DECLARE INTEGER InternetCloseHandle IN wininet.dll INTEGER hInet

	DECLARE RtlMoveMemory IN kernel32 As CopyMemory;
		STRING @Destination, INTEGER Source, INTEGER nLength

	DECLARE INTEGER FtpGetCurrentDirectory IN wininet.dll;
		INTEGER  hFtpSession, STRING @lpszDir, INTEGER @lpdwCurDir

	DECLARE INTEGER FtpSetCurrentDirectory IN wininet.dll;
		INTEGER  hFtpSession, STRING @lpszDir

	DECLARE INTEGER FtpFindFirstFile IN wininet.dll;
		INTEGER hFtpSession, STRING lpszSrchFile,;
		STRING @lpFindFileData, INTEGER dwFlags, INTEGER dwContent

	DECLARE INTEGER InternetFindNextFile IN wininet.dll;
		INTEGER hFind, STRING @lpvFindData

	DECLARE INTEGER FileTimeToSystemTime IN kernel32.dll;
		STRING @lpFileTime, STRING @lpSystemTime

	DECLARE INTEGER FtpOpenFile IN wininet;
		INTEGER hFtpSession, STRING sFileName, INTEGER lAccess,;
		INTEGER lFlags, INTEGER lContext

	DECLARE INTEGER FtpPutFile IN wininet.dll;
		INTEGER hConnect, STRING lpszLocalFile,;
		STRING lpszNewRmtFile, INTEGER dwFlags, INTEGER dwContext

	DECLARE INTEGER FtpGetFile IN wininet;
		INTEGER hConnect, STRING lpszRemoteFile,;
		STRING  lpszNewFile, INTEGER fFailIfExists,;
		INTEGER dwFlagsAndAttr, INTEGER dwFlags, INTEGER dwContext

	DECLARE INTEGER InternetWriteFile IN wininet;
		INTEGER hFile, STRING @sBuffer,;
		INTEGER lBytesToWrite, INTEGER @dwBytesWritten

	DECLARE INTEGER InternetReadFile IN wininet.dll;
		INTEGER hFile, STRING @lpBuffer,;
		INTEGER dwNumberOfBytesToRead, INTEGER @dwBytesRead

ENDDEFINE

DEFINE CLASS struct_WIN32_FIND_DATA As Custom
* this class emulates WIN32_FIND_DATA structure
	value			 = ""
	fileAttributes   = 0
	creationTimeLo   = 0
	creationTimeHi   = 0
	lastAccessTimeHi = 0
	lastAccessTimeLo = 0
	lastWriteTimeHi  = 0
	lastWriteTimeLo  = 0
	fileSizeLo		 = 0 && fileSizeHi omitted
	fileName		 = ""
	creationTime	 = CTOT ("")
	lastAccessTime   = CTOT ("")
	lastWriteTime	 = CTOT ("")

PROCEDURE  setValue (lcValue)	
* translates the buffer"s content into the object"s properties
	THIS.value			  = lcValue
	THIS.fileAttributes   = THIS.buf2num (THIS.value,  0, 4)
	THIS.creationTimeLo   = THIS.buf2num (THIS.value,  4, 4)
	THIS.creationTimeHi   = THIS.buf2num (THIS.value,  8, 4)
	THIS.lastAccessTimeHi = THIS.buf2num (THIS.value, 12, 4)
	THIS.lastAccessTimeLo = THIS.buf2num (THIS.value, 16, 4)
	THIS.lastWriteTimeHi  = THIS.buf2num (THIS.value, 20, 4)
	THIS.lastWriteTimeLo  = THIS.buf2num (THIS.value, 24, 4)
	THIS.fileSizeLo		  = THIS.buf2num (THIS.value, 32, 4)
	THIS.creationTime	  = THIS.ftime2dtime (SUBSTR(THIS.value,  5, 8))
	THIS.lastAccessTime   = THIS.ftime2dtime (SUBSTR(THIS.value, 13, 8))
	THIS.lastWriteTime	  = THIS.ftime2dtime (SUBSTR(THIS.value, 21, 8))
	THIS.fileName		  = ALLTRIM(SUBSTR(THIS.value, 45,250))
	IF AT(Chr(0), THIS.fileName) <> 0
		THIS.fileName = SUBSTR (THIS.fileName, 1, AT(Chr(0), THIS.fileName)-1)
	ENDIF

FUNCTION AttribSet (lnAttrib)
RETURN BitAnd(THIS.fileAttributes, lnAttrib) = lnAttrib

FUNCTION IsRdOnly
RETURN THIS.AttribSet(FILE_ATTRIBUTE_READONLY)

FUNCTION IsHidden
RETURN THIS.AttribSet(FILE_ATTRIBUTE_HIDDEN)

FUNCTION IsSystem
RETURN THIS.AttribSet(FILE_ATTRIBUTE_SYSTEM)

FUNCTION IsDirectory
RETURN THIS.AttribSet(FILE_ATTRIBUTE_DIRECTORY)

FUNCTION IsArchive
RETURN THIS.AttribSet(FILE_ATTRIBUTE_ARCHIVE)

FUNCTION IsNormal
RETURN THIS.AttribSet(FILE_ATTRIBUTE_NORMAL)

FUNCTION IsTemporary
RETURN THIS.AttribSet(FILE_ATTRIBUTE_TEMPORARY)

FUNCTION IsCompressed
RETURN THIS.AttribSet(FILE_ATTRIBUTE_COMPRESSED)

FUNCTION buf2num(lcBuffer, lnOffset, lnBytes)
* converts N bytes from the buffer into a numeric value
	lnResult = 0
	FOR ii=1 TO lnBytes
		lnResult = lnResult +;
			BitLShift(Asc(SUBSTR(lcBuffer, lnOffset+ii, 1)), (ii-1)*8)
	ENDFOR
RETURN lnResult

FUNCTION  ftime2dtime (lcFileTime)
	LOCAL lcSystemTime, ltResult, lcStoredSet
	IF lcFileTime = REPLI (Chr(0), 8)
		ltResult = CTOT ("")
	ELSE
		lcSystemTime = REPLI (Chr(0), 16)
		= FileTimeToSystemTime (@lcFileTime, @lcSystemTime)
		wYear   = THIS.buf2num (lcSystemTime,  0, 2)
		wMonth  = THIS.buf2num (lcSystemTime,  2, 2)
		wDay	= THIS.buf2num (lcSystemTime,  6, 2)
		wHour   = THIS.buf2num (lcSystemTime,  8, 2)
		wMinute = THIS.buf2num (lcSystemTime, 10, 2)
		wSecond = THIS.buf2num (lcSystemTime, 12, 2)

		lcStoredSet = SET ("DATE")
		SET DATE TO MDY
		lcDate = STRTRAN (STR(wMonth,2) + "/" +;
			STR(wDay,2) + "/" + STR(wYear,4), " ","0")
		lcTime = STRTRAN (STR(wHour,2) + ":" +;
			STR(wMinute,2) + ":" + STR(wSecond,2), " ","0")
		ltResult = CTOT (lcDate + " " + lcTime)
		SET DATE TO &lcStoredSet
	ENDIF
RETURN ltResult
ENDDEFINE  

Listed functions:

CopyMemory
FileTimeToSystemTime
FormatMessage
FtpCreateDirectory
FtpFindFirstFile
FtpGetCurrentDirectory
FtpGetFile
FtpOpenFile
FtpPutFile
FtpRemoveDirectory
FtpSetCurrentDirectory
GetLastError
InternetCloseHandle
InternetConnect
InternetFindNextFile
InternetOpen
InternetReadFile
InternetWriteFile

Comment:

Because of the VFP one-process and one-thread nature such FTP class -- under some circumstances -- is able to freeze the whole VFP application. With less reliable FTP connections I would choose an external library allowing to drop a frozen FTP connection.


To create a passive data connection to an FTP server, use INTERNET_FLAG_PASSIVE for dwFlags parameter in the InternetConnect call:

#DEFINE INTERNET_INVALID_PORT_NUMBER 0  
#DEFINE INTERNET_SERVICE_FTP         1  
#DEFINE INTERNET_FLAG_PASSIVE 0x08000000  
  
nFlags = INTERNET_FLAG_PASSIVE  </font><font color=#008000>&& use 0 for the active mode</font></font>  
  
hConnection = InternetConnect(m.hInternet, m.host,;  
	INTERNET_INVALID_PORT_NUMBER,;  
	m.usr, m.cPsw, INTERNET_SERVICE_FTP, m.nFlags, 0)