Far from complete, but it shows it's possible. I think I'm gonna try to build a full GUI just for the sake of it. What the code below can do is provide a working framework for using FTP. It will connect, authenticate, establish the secondary data connection, then finally retrieve a directory listing of files. This uses passive mode (as opposed to active), which I chose to do so because it's simpler to implement and test because the client requests connection details from the server rather than telling the server how to connect to the client. Also, there is no TLS here, so passwords are sent in plain text. Because of that, I would not recommended using this in any production application where the connection relies on your own personal credentials as it's simple to intercept. So if anyone would like to attempt to implement TLS......
One last note, the server address has to be an IP and not "ftp.myserver.com".
Commands:
ftp_connect( ip, user, pass, logging[0,1] )
ftp_SetLogFile( path )
ftp_disconnect()
ftp_setLocalDir( path )
ftp_listener()
ftp_list()
ftp_getFileList()
ftp_sendFile( filename )
ftp_getFile( filename )
ftp_SetDir( path )
ftp.agc
#CONSTANT FTP_STATE_LOGIN_USER 1
#CONSTANT FTP_STATE_LOGIN_PASS 2
#CONSTANT FTP_STATE_READY 3
#CONSTANT FTP_STATE_GET_FILE 4
#CONSTANT FTP_STATE_SEND_FILE 5
#CONSTANT FTP_STATE_LIST 6
#CONSTANT FTP_STATE_CWD 7
Global private_months$ as string[12] = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
Type FTP_File
name as string
date as string
isDirectory as integer
symlink as string
permissions as string
EndType
/*
* FTP operates over 2 sockets; a command socket and a data socket.
* All commands are sent over the command socket and persists over
* the duration of the session. The data socket handles sending
* and receiving data on each command request. A new data socket
* is used for each file transfer and closed upon completion. You
* should not have more than 2 sockets open during an ftp session.
* Because of this, a job queue is implemented.
*/
Type Queue_Object
serverFile as string
localFile as string
state as integer
EndType
Type FTP_Handler
user as string
pass as string
cmdConn as integer //
dataConn as integer //
flag as integer // current or last job type
bytes as integer[] // temporarily stores bytes from command socket
grantedAccess as integer // 1 if user was granted access to server
await as integer // 1 while a job is being processed
remoteFile as string // a filename on the remote server
localFile as string // a filename on local user system
fileId as integer // file ID for reading and writing
datalog as string[] // stores all messages on the command socket
logMode as integer // 1 to write to datalog, 0 don't log anything
jobQueue as Queue_Object[] // FTP handles 1 connection per file at a time
allDataReceived as integer // 1 when all data has been read during a file download
allDataSent as integer // 1 when all data has been sent during a file upload
localDir as string // Local absolute path of current working directory
sizeEst as integer // An estimate in total bytes of the file being downloaded
bytesRead as integer // How many bytes have been read during current download (may be higher than sizeEst)
`bytesSent as integer // Number of bytes sent
files as FTP_File[] // Array of files, this is emptied and repopulated each time ftp_list() is called
chunkLimit as integer // Default 5120. Increase for faster downloads. Too high may impact performance
logPath as string // Absolute path of log file. If not set, log is not written and lost when app closes
getFiles as integer // Ready to read file listing
EndType
/* The main ftp object */
global _ftp as FTP_Handler
/**
* Absolute file path. If this is not set, a log
* will not be written, even if log mode is on.
*/
function ftp_SetLogFile(path as string)
_ftp.logPath = path
endfunction
/**
* Initiates the connection sequence to the FTP server
* Only IP address is acceptable, no domain (ftp.mysite.com)
*/
function ftp_connect(ip as string, user as string, pass as string, logMode as integer)
`if _ftp.logMode = 1 then opentowrite("raw:D:\AGK\ftp\media\dump.txt", 1)
_ftp.cmdConn = connectSocket(ip, 21, 3000)
_ftp.user = user
_ftp.pass = pass
_ftp.chunkLimit = 5120
_ftp.logMode = logMode
ftp_setLocalDir(getCurrentDir())
endfunction
/**
* Disconnect from FTP server properly
*
*/
function ftp_disconnect()
private_ftp_SendString(_ftp.cmdConn, "QUIT")
if _ftp.dataConn > 0
if getSocketConnected(_ftp.dataConn) = 1 then deleteSocket(_ftp.dataConn)
_ftp.dataConn = 0
endif
if _ftp.logMode = 1 and _ftp.logPath <> ""
f = openToWrite("raw:"+_ftp.logPath)
for i = 0 to _ftp.datalog.length
writeLine(f, _ftp.datalog[i])
next i
closeFile(f)
endif
endfunction
/**
* Sets the local working directory. The path where downloaded
* files are written.
*/
function ftp_setLocalDir(dir as string)
FILE_SEPARATOR$ = "\"
if findString(dir, "/") > 0 then FILE_SEPARATOR$ = "/"
if mid(dir, len(dir), 1) <> "/" and mid(dir, len(dir), 1) <> "\" then dir = dir + FILE_SEPARATOR$
_ftp.localDir = dir
endfunction
/**
*
*
*/
function ftp_listener()
// data socket
if _ftp.dataConn > 0
if getSocketConnected(_ftp.dataConn) = 1
/* Handles file downloads from server */
if _ftp.flag = FTP_STATE_GET_FILE and _ftp.fileId > 0 // make sure output file is ready
if GetSocketBytesAvailable(_ftp.dataConn) > 0
chunk = 0
while getSocketBytesAvailable(_ftp.dataConn) > 0 and chunk < _ftp.chunkLimit
inc chunk
inc _ftp.bytesRead
b = getSocketByte(_ftp.dataConn)
if _ftp.flag = FTP_STATE_GET_FILE
writeByte(_ftp.fileId, b)
endif
endwhile
else
if _ftp.flag = FTP_STATE_GET_FILE and _ftp.allDataReceived = 1
if _ftp.fileId > 0
closeFile(_ftp.fileId)
_ftp.fileId = 0
_ftp.remoteFile = ""
_ftp.localFile = ""
_ftp.await = 0
_ftp.allDataReceived = 0
private_ftp_resetDataSocket()
timething2 = GetMilliseconds()
endif
endif
endif
endif // writeFile
/* Handles file upload to server */
if _ftp.flag = FTP_STATE_SEND_FILE and _ftp.fileId > 0
/*
* todo:
* Needs chunking implemented for
* larger files
*/
while fileEOF(_ftp.fileId) = 0
b = readByte(_ftp.fileId)
sendSocketByte(_ftp.dataConn, b)
endwhile
flushSocket(_ftp.dataConn)
closeFile(_ftp.fileId)
_ftp.fileId = 0
_ftp.remoteFile = ""
_ftp.localFile = ""
_ftp.allDataSent = 1
private_ftp_resetDataSocket()
endif
/* Retrieves data containing list of files */
if _ftp.flag = FTP_STATE_LIST and _ftp.getFiles = 1
// Read all bytes first because we need to look ahead for end of line
local bytes as integer[]
while getSocketBytesAvailable(_ftp.dataConn) > 0
b = getSocketByte(_ftp.dataConn)
bytes.insert(b)
endwhile
s$ = ""
_ftp.files.length = -1
for i = 0 to bytes.length-1
if bytes[i] = 13 and bytes[i+1] = 10
if _ftp.logMode > 0 then _ftp.datalog.insert(s$)
_ftp.files.insert(private_ftp_ParseFileString(s$))
s$ = "" : inc i
else
s$ = s$ + chr(bytes[i])
endif
next i
_ftp.await = 0
_ftp.getFiles = 0
private_ftp_resetDataSocket()
endif
endif // if dataConn connected
endif // if dataConn
// listen for data from server
if _ftp.cmdConn > 0
if getSocketConnected(_ftp.cmdConn) = 1
if GetSocketBytesAvailable(_ftp.cmdConn) > 0
while getSocketBytesAvailable(_ftp.cmdConn)
b = getSocketByte(_ftp.cmdConn)
_ftp.bytes.insert(b)
`if _ftp.logMode > 0 then writeByte(_ftp.logFile, b)
endwhile
endif
endif
endif
// process the bytes
if _ftp.bytes.length > 0
start = 0
s$ = ""
for i = 0 to _ftp.bytes.length-1
// Lines are terminated with 0d0a (carriage return followed by newline feed)
if _ftp.bytes[i] = 13 and _ftp.bytes[i+1] = 10
if _ftp.logMode > 0 then _ftp.datalog.insert(s$)
code$ = trimString(left(s$, 4), " ")
if code$ = "220" // ready to send commands to server
private_ftp_SendString(_ftp.cmdConn, "USER "+_ftp.user)
endif
if code$ = "331" // username ok, need password
private_ftp_SendString(_ftp.cmdConn, "PASS "+_ftp.pass)
endif
if code$ = "230" // you've been granted access
_ftp.grantedAccess = 1
endif
if code$ = "530" // access denied (not logged in, wrong password, etc...)
endif
if code$ = "425" // no data connection
endif
if code$ = "221" // command connection disconnected (user logged out)
if _ftp.cmdConn > 0
if getSocketConnected(_ftp.cmdConn) = 1 then deleteSocket(_ftp.cmdConn)
_ftp.cmdConn = 0
endif
endif
if code$ = "227" // response to PASV
// server is telling us where to connect for the data connection
i1 = findString(s$, "(")+1
i2 = findString(s$, ")")
t$ = mid(s$, i1, i2-i1)
data_ip$ = getStringToken2(t$, ',', 1)+"."+getStringToken2(t$, ',', 2)+"."+getStringToken2(t$, ',', 3)+"."+getStringToken2(t$, ',', 4)
data_port = val(getStringToken2(t$, ',', 5))*256 + val(getStringToken2(t$, ',', 6))
_ftp.dataConn = connectSocket(data_ip$, data_port, 3000)
if _ftp.flag = FTP_STATE_LIST
private_ftp_SendString(_ftp.cmdConn, "TYPE A") // ASCII-mode
else
private_ftp_SendString(_ftp.cmdConn, "TYPE I") // binary-mode
endif
endif
if code$ = "200"
if _ftp.flag = FTP_STATE_GET_FILE
private_ftp_SendString(_ftp.cmdConn, "RETR "+_ftp.remoteFile)
endif
if _ftp.flag = FTP_STATE_SEND_FILE
private_ftp_SendString(_ftp.cmdConn, "STOR "+_ftp.remoteFile)
endif
if _ftp.flag = FTP_STATE_LIST
private_ftp_SendString(_ftp.cmdConn, "LIST")
endif
endif
if code$ = "150" // data connection established
if _ftp.flag = FTP_STATE_GET_FILE // getting file from server
//_ftp.writeFile = openToWrite(_ftp.localFile, 0)
_ftp.fileId = openToWrite(_ftp.localFile, 0)
_ftp.bytesRead = 0
if findString(s$,"download", 1, 4) > 0
size$ = getStringToken2(s$, " ", 2)
term$ = getStringToken2(s$, " ", 3)
if term$ = "kbytes"
_ftp.sizeEst = floor(valFloat(size$)) * 1024
endif
endif
endif
if _ftp.flag = FTP_STATE_SEND_FILE // sending file to server
//_ftp.readFile = openToRead(_ftp.localFile)
_ftp.fileId = openToRead(_ftp.localFile)
_ftp.allDataSent = 0
endif
if _ftp.flag = FTP_STATE_LIST
`private_ftp_SendString(_ftp.cmdConn, "LIST")
endif
endif
if code$ = "226" // data connection closed
if _ftp.flag = FTP_STATE_GET_FILE
_ftp.allDataReceived = 1
endif
if _ftp.flag = FTP_STATE_SEND_FILE
_ftp.await = 0
endif
if _ftp.flag = FTP_STATE_LIST
_ftp.getFiles = 1
endif
endif
if code$ = "250" // file action completed ok
_ftp.await = 0
endif
if code$ = "550" // file operation not performed
_ftp.await = 0
_ftp.flag = 0
endif
s$ = ""
inc i
else
s$ = s$ + chr(_ftp.bytes[i])
endif
next i
_ftp.bytes.length = -1
endif
if _ftp.jobQueue.length > -1
if _ftp.grantedAccess = 1 and _ftp.await = 0
if _ftp.jobQueue[0].state = FTP_STATE_GET_FILE
_ftp.fileId = 0
_ftp.remoteFile = _ftp.jobQueue[0].serverFile
_ftp.localFile = _ftp.jobQueue[0].localFile
_ftp.jobQueue.remove(0)
_ftp.await = 1 // ftp only likes one command at a time
private_ftp_resetDataSocket() // free up any previous data socket
_ftp.flag = FTP_STATE_GET_FILE // track what the ftp connection is currently doing
private_ftp_SendString(_ftp.cmdConn, "PASV") // initiate new passive data socket
elseif _ftp.jobQueue[0].state = FTP_STATE_SEND_FILE
_ftp.remoteFile = _ftp.jobQueue[0].serverFile
_ftp.localFile = _ftp.jobQueue[0].localFile
_ftp.jobQueue.remove(0)
_ftp.fileId = 0
_ftp.await = 1
private_ftp_resetDataSocket()
_ftp.flag = FTP_STATE_SEND_FILE
private_ftp_SendString(_ftp.cmdConn, "PASV")
elseif _ftp.jobQueue[0].state = FTP_STATE_LIST
_ftp.getFiles = 0
_ftp.jobQueue.remove(0)
_ftp.await = 1
private_ftp_resetDataSocket()
_ftp.flag = FTP_STATE_LIST
private_ftp_SendString(_ftp.cmdConn, "PASV")
elseif _ftp.jobQueue[0].state = FTP_STATE_CWD
_ftp.await = 1
private_ftp_SendString(_ftp.cmdConn, "CWD "+_ftp.jobQueue[0].serverFile)
_ftp.jobQueue.remove(0)
endif
endif
endif
endfunction
/**
* Requests a directory listing
*
*/
function ftp_list()
local q as Queue_Object
q.state = FTP_STATE_LIST
_ftp.jobQueue.insert(q)
endfunction
/**
* Returns an array of FTP_File
* Array is empty until ftp_list() has been
* called and completes processing
*/
function ftp_getFileList()
endfunction _ftp.files
/**
* Just the file name, not an absolute path.
* Path is determined by the currently set local dir
*/
function ftp_sendFile(localFilename as string)
local q as Queue_Object
q.state = FTP_STATE_SEND_FILE
q.localFile = "raw:"+_ftp.localDir+localFilename
q.serverFile = localFilename
_ftp.jobQueue.insert(q)
endfunction
/**
* Downloads a file from the server and writes
* to local path as set by ftp_setLocalDir()
*/
function ftp_getFile(serverPath as string)
local q as Queue_Object
q.state = FTP_STATE_GET_FILE
i = findStringReverse(serverPath, "\")
if i = 0 then i = findStringReverse(serverPath, "/")
filename$ = mid(serverPath, i+1, len(serverPath))
q.serverFile = filename$
q.localFile = "raw:"+_ftp.localDir+filename$
_ftp.jobQueue.insert(q)
endfunction
function ftp_SetDir(path as string)
local q as Queue_Object
q.state = FTP_STATE_CWD
q.serverFile = path
_ftp.jobQueue.insert(q)
endfunction
/**
* Cleans up data socket
*
*/
function private_ftp_resetDataSocket()
if _ftp.dataConn > 0
if getSocketConnected(_ftp.dataConn) = 1 then deleteSocket(_ftp.dataConn)
_ftp.dataConn = 0
endif
endfunction
/**
* Converts month abbreviation into number.
* ie: 'Mar' = 3
*
*/
function private_ftp_GetMonth(m$)
for i = 0 to private_months$.length
if m$ = private_months$[i] then exitfunction i+1
next i
endfunction 0
/**
* Parses the response strings after requesting a list of files
* from the current directory.
*
* Sample string may look something like this:
* -rwx--x--x 5 ndc92f5 ndc92f5 34075 Dec 5 2022 test stuff.dat
*
*/
function private_ftp_ParseFileString(s$ as string)
local f as FTP_File
p$ = getStringToken(s$, " ", 1)
m$ = str(private_ftp_GetMonth(getStringToken(s$, " ", 6)))
d$ = getStringToken(s$, " ", 7)
y$ = getStringToken(s$, " ", 8)
if findString(y$, ":") > 0 then y$ = str(getYearFromUnix(getUnixTime())) // current year will show time instead of year
f.date = m$+"/"+d$+"/"+y$
i = findString(s$, getStringToken(s$, " ", 9))
f.name = right(s$, len(s$)-i+1)
c$ = mid(p$, 1, 1)
if c$ = "l" // link
i = findString(f.name, " ->")
fname$ = left(f.name, i-1)
f.symlink = mid(f.name, i+4, len(f.name))
f.name = fname$
f.isDirectory = 1
endif
if c$ = "d" then f.isDirectory = 1
o = 0
g = 0
e = 0
if mid(s$, 2, 1) = "r" then o = 4
if mid(s$, 3, 1) = "w" then o = o+2
if mid(s$, 4, 1) = "x" then o = o+1
if mid(s$, 5, 1) = "r" then g = 4
if mid(s$, 6, 1) = "w" then g = g+2
if mid(s$, 7, 1) = "x" then g = g+1
if mid(s$, 8, 1) = "r" then e = 4
if mid(s$, 9, 1) = "w" then e = e+2
if mid(s$, 10, 1) = "x" then e = e+1
f.permissions = str(o)+str(g)+str(e)
endfunction f
/**
*
*
*/
function private_ftp_SendString(c as integer, s as string)
for i = 1 to len(s)
sendSocketByte(c, asc(mid(s, i, 1)))
next i
sendSocketByte(c, 13)
sendSocketByte(c, 10)
flushSocket(c)
endfunction