Remote File Engine (.bmx)

Miscellaneous Forums/Blitz Showcase/Remote File Engine (.bmx)

This is the first thing i've ever done on BlitzMax. I've coded the same engine for B+ too and now I've done it for BlitzMax.

An easy way to read online/remote files. Supports FTP and HTTP protocols.

Functions:
file:TRemoteFile = OpenRemoteFile:TRemoteFile(url:String, port:Int = 80, HTTPpostdata/FTPusername, HTTPheader/FTPpassword)
RemoteReadAvail:Int(file:TRemoteFile)
ReadRemoteLine:String(file:TRemoteFile)
ReadRemoteString:String(file:TRemoteFile, length:Int)
ReadRemoteByte:Byte(file:TRemoteFile)
ReadRemoteInt:Int(file:TRemoteFile)
ReadRemoteLong:Long(file:TRemoteFile)
ReadRemoteDouble:Double(file:TRemoteFile)
ReadRemoteFloat:Float(file:TRemoteFile)
ReadRemoteShort:Short(file:TRemoteFile)
RemoteFileError:String(file:TRemoteFile) ' = "" if there was no error
RemoteFileName:String(file:TRemoteFile) ' sometimes the URL doesn't contain the name of the file and it's specified in HTTP header
RemoteFileSize:Int(file:TRemoteFile) ' -1 if there was no size sent by server
EORF(file:TRemoteFile) ' End Of Remote File
CloseRemoteFile(file:TRemoteFile)


Example:
Strict

Include "transporter.bmx"

'Local file:TRemoteFile = OpenRemoteFile("ftp://www.google.com/", 21, "username", "password")
Local file:TRemoteFile = OpenRemoteFile("http://www.google.com/")

' Check for errors
If RemoteFileError(file) = "" 

	'Wait until there's something to read
	While Not RemoteReadAvail(file)
	Wend
	
	' Read until the end of the file/stream
	While Not EORF(file)
		Print RemoteFileName(file) + " [" + RemoteReadAvail(file) + "] "+ " -> " + ReadRemoteLine(file)
	Wend
	
	' Close file/stream
	CloseRemoteFile(file)
Else
	Notify RemoteFileError(file)
EndIf
End


Engine:
Const RemoteFileCRLF:String = Chr(13) + Chr(10)
Const RemoteFileUserAgent:String = "Transporter"
Const RemoteFileTimeOut:Int = 5 * 1000
Const RemoteFileDebugMode:Int = True

Type TRemoteFile
	Field url:String, port:Int, protocol:String
	Field filename:String, size:Int
	Field socket:TSocket, stream:TSocketStream
	Field pasv_socket:TSocket, pasv_stream:TSocketStream
	Field error:String
	
	Function Open:TRemoteFile(url:String, port:Int = 80, postdata:String = "", header:String = "")
		Local file:TRemoteFile = New TRemoteFile
		
		Local protocol:String = Sector(url, ":", 0)
		Local host:String = Sector(url, "/", 2)
		Local path:String = "/" + Sector(url, "/", 3, True)
		
		' Check for port in server name
		If Instr(host, ":") Then
			port = Int(Sector(host, ":", 1))
			host = Sector(host, ":", 0)
		EndIf
		Local server:String = "www." + Sector$(host$, ".", Sectors%(host$, ".") - 1) + "." + Sector$(host$, ".", Sectors%(host$, "."))
		
		file.url = url
		file.port = port
		file.protocol = Lower(protocol)
		file.error = ""
		file.filename =  Sector(path, "/", Sectors(path, "/"))
		file.size = -1
		
		file.socket = CreateTCPSocket:TSocket()
		BindSocket(file.socket, 0)
		
		' Connect to server
		Local connection:Int = ConnectSocket(file.socket, HostIp(server), port)
		If Not connection Then connection:Int = ConnectSocket(file.socket, HostIp(host), port)
		
		If connection
			file.stream = CreateSocketStream:TSocketStream(file.socket)
			
			Select Lower(protocol)
				Case "http"
					Local txt:String = ""
					If Len(postdata) > 0 Then txt :+ "POST " + path + " HTTP/1.1" + RemoteFileCRLF Else txt :+ "GET " + path + " HTTP/1.1" + RemoteFileCRLF
					txt :+ "Host: " + host + RemoteFileCRLF
					txt :+ "User-Agent: " + RemoteFileUserAgent + RemoteFileCRLF
					If Len(header) > 0 Then txt :+ header + RemoteFileCRLF
					If Len(postdata) > 0 Then txt :+ "Content-Type: application/x-www-form-urlencoded" + RemoteFileCRLF + "Content-Length: " + Len(postdata) + RemoteFileCRLF
					txt :+ "Connection: Close" + RemoteFileCRLF
					txt :+ ""
					If Len(postdata) > 0 Then txt :+ RemoteFileCRLF + postdata
					WriteLine(file.stream, txt)
					
					If RemoteFileDebugMode Then Print(txt)
					
					' Check response
					Local tim:Int = MilliSecs()
					Repeat
						If RemoteReadAvail(file)
							txt = ReadRemoteLine(file)
							If Sector(txt, "/", 0) = "HTTP"
								If Int(Sector(txt, " ", 1)) => 303 Then file.error = "HTTP Error: " + txt
								Exit
							EndIf
						EndIf
						If (MilliSecs() - tim) => RemoteFileTimeOut Then file.error = "No response from server!"
					Forever
					
					Local cmd:String, value:String
					If file.error = ""
						Repeat
							If RemoteReadAvail(file)
								txt = ReadRemoteLine(file)
								If txt$ = "" Then Exit
								
								cmd = Sector(txt, ":", 0)
								value = Sector(txt, " ", 1)
								Select cmd
									Case ""
										Return file
									Case "Content-Disposition"
										Local i:Int, subtxt:String, subvalue:String
										For i = 0 To Sectors(value, ";")
											subtxt = Trim(Sector(value, ";", i))
											subvalue = Sector(subtxt, "=", 1)
											
											Select Lower(Sector(subtxt, "=", 0))
												Case "filename"
													file.filename = subvalue
											End Select
										Next
									Case "Content-Length"
										file.size = Int(value$)
								End Select
							EndIf
						Forever
					EndIf
				Case "ftp"
					' Check response
					If RemoteFileDebugMode Then Print("Waiting FTP response...")
					Local tim:Int = MilliSecs()
					Repeat
					Until (MilliSecs() - tim) => RemoteFileTimeOut  Or SocketReadAvail(file.socket)
					
					If RemoteFileDebugMode Then Print("Initializing PASV mode...")
					If SocketReadAvail(file.socket)
						Local txt:String, cmd:String, code:Int, value:String
						Local pasv_host:String, pasv_port:Int, pasv_connection:Int
						Repeat
							If SocketReadAvail(file.socket)
								txt = ReadLine(file.stream)
								If RemoteFileDebugMode Then Print("<<< " + txt)
								code = Int(Sector(txt, " ", 0))
								value = Mid$(Sector(txt, " ", 1, True), 1)
							Else
								txt = ""
								code = 0
								value = ""
								cmd = ""
							EndIf
							
							If Not SocketReadAvail(file.socket)
								Select code
									Case 220
										cmd = "USER " + postdata
									Case 331
										cmd = "PASS " + header
									Case 230
										cmd = "SIZE " + path
									Case 213
										file.size = Int(value)
										cmd = "PASV"
									Case 227
										value = Sector(value, "(", 1)
										pasv_host = Sector(value, ",", 0) + "." + Sector(value, ",", 1) + "." + Sector(value, ",", 2) + "." + Sector(value, ",", 3)
										pasv_port = Int(Sector(value, ",", 4)) * 256 + Int(Left$(Sector(value, ",", 5), Len(Sector(value, ",", 5)) - 1))
										
										file.pasv_socket = CreateTCPSocket()
										BindSocket(file.pasv_socket, 0)
										pasv_connection = ConnectSocket(file.pasv_socket, HostIp(pasv_host), pasv_port)
										
										If pasv_connection
											If RemoteFileDebugMode Then Print("PASV connection established")
											
											file.pasv_stream = CreateSocketStream(file.pasv_socket)
											cmd = "RETR " + path

										Else
											file.error = "Unable to connect to PASV stream on " + pasv_host + ":" + pasv_port
										EndIf
									Case 150
										Return file
								End Select
								
								' ERROR
								If code => 400
									file.error = "FTP error: " + txt
									Return file
								EndIf
							EndIf
							
							If Len(cmd) Then
								If RemoteFileDebugMode Then Print(">>> " + cmd)
								WriteLine(file.stream, cmd)
								cmd = ""
							EndIf
						Forever
					Else
						file.error = "No response from " + file.url + ":" + file.port
					EndIf
			End Select
		Else
			file.error = "Unable to connect to " + file.url + ":" + file.port
		EndIf
		
		Return file
	End Function
	
	Method ProtocolStream:TSocketStream()
		Local str:TSocketStream
		Select Lower(protocol)
			Case "ftp"
				Return pasv_stream
			Default
				Return stream
		End Select
	End Method

	Method ProtocolSocket:TSocket()
		Local sock:TSocket
		Select Lower(protocol)
			Case "ftp"
				Return pasv_socket
			Default
				Return socket
		End Select
	End Method
	
	Method ReadAvail:Int()
		Return SocketReadAvail(ProtocolSocket())
	End Method
	
	Method RemoteLine:String()
		Return ReadLine(ProtocolStream())
	End Method
	
	Method RemoteString:String(length:Int)
		Return ReadString(ProtocolStream(), length)
	End Method

	Method RemoteByte:Byte()
		Return ReadByte(ProtocolStream())
	End Method

	Method RemoteInt:Int()
		Return ReadInt(ProtocolStream())
	End Method

	Method RemoteLong:Long()
		Return ReadLong(ProtocolStream())
	End Method

	Method RemoteDouble:Double()
		Return ReadDouble(ProtocolStream())
	End Method

	Method RemoteFloat:Float()
		Return ReadFloat(ProtocolStream())
	End Method

	Method RemoteShort:Short()
		Return ReadShort(ProtocolStream())
	End Method
		
	Method LastError:String()
		Return error
	End Method
	
	Method Name:String()
		Return filename
	End Method
	
	Method FSize:Int()
		Return size
	End Method
	
	Method EndOFRemoteFile()
		Return Eof(ProtocolStream())
	End Method
	
	Method Close()
		Select Lower(protocol)
			Case "ftp"
				CloseStream(pasv_stream)
				CloseSocket(pasv_socket)
		End Select
		
		CloseStream(stream)
		CloseSocket(socket)
	End Method
End Type

Function OpenRemoteFile:TRemoteFile(url:String, port:Int = 80, postdata:String = "", header:String = "")
	Return TRemoteFile.Open(url, port, postdata, header)
End Function

Function RemoteReadAvail:Int(file:TRemoteFile)
	Return file.ReadAvail()
End Function

Function ReadRemoteLine:String(file:TRemoteFile)
	Return file.RemoteLine()
End Function

Function ReadRemoteString:String(file:TRemoteFile, length:Int)
	Return file.RemoteString(length)
End Function

Function ReadRemoteByte:Byte(file:TRemoteFile)
	Return file.RemoteByte()
End Function

Function ReadRemoteInt:Int(file:TRemoteFile)
	Return file.RemoteInt()
End Function

Function ReadRemoteLong:Long(file:TRemoteFile)
	Return file.RemoteLong()
End Function

Function ReadRemoteDouble:Double(file:TRemoteFile)
	Return file.RemoteDouble()
End Function

Function ReadRemoteFloat:Float(file:TRemoteFile)
	Return file.RemoteFloat()
End Function

Function ReadRemoteShort:Short(file:TRemoteFile)
	Return file.RemoteShort()
End Function

Function RemoteFileError:String(file:TRemoteFile)
	Return file.LastError()
End Function

Function RemoteFileName:String(file:TRemoteFile)
	Return file.Name()
End Function

Function RemoteFileSize:Int(file:TRemoteFile)
	Return file.FSize()
End Function

Function EORF(file:TRemoteFile)
	Return file.EndOFRemoteFile()
End Function

Function CloseRemoteFile(file:TRemoteFile)
	file.Close()
End Function

Function Sector:String(txt:String, separator:String, sector:Int, toend:Int = 0)
	Local result:String = "", occ:Int, i:Int
	For i:Int = 1 To Len(txt)
		If Mid(txt, i, 1) = separator
			occ :+ 1
			If toend And occ > sector Then result = result + Mid(txt, i, 1)
		Else
			If occ => sector Then result = result + Mid(txt, i, 1)
		EndIf
		If Not toend Then If occ > sector Then Exit
	Next
	Return result
End Function

Function Sectors:Int(txt:String, needle:String)
	Local occ:Int = 0, i:Int
	For i:Int = 1 To Len(txt)
		If Instr(txt, needle, i)
			occ = occ + 1
			i = Instr(txt, needle, i)
		Else
			Exit
		EndIf
	Next
	Return occ
End Function