1 -----------------------------------------------------------------------------
2 -- FTP support for the Lua language
5 -----------------------------------------------------------------------------
7 -----------------------------------------------------------------------------
8 -- Declare module and import dependencies
9 -----------------------------------------------------------------------------
11 local table = require("table")
12 local string = require("string")
13 local math
= require("math")
14 local socket
= require("socket")
15 local url
= require("socket.url")
16 local tp
= require("socket.tp")
17 local ltn12
= require("ltn12")
20 -----------------------------------------------------------------------------
22 -----------------------------------------------------------------------------
23 -- timeout in seconds before the program gives up on a connection
25 -- default port for ftp service
27 -- this is the default anonymous password. used when no password is
28 -- provided in url. should be changed to your e-mail.
30 PASSWORD
= "anonymous@anonymous.org"
32 -----------------------------------------------------------------------------
34 -----------------------------------------------------------------------------
35 local metat
= { __index
= {} }
37 function open(server
, port
, create
)
38 local tp
= socket
.try(tp
.connect(server
, port
or PORT
, TIMEOUT
, create
))
39 local f
= base
.setmetatable({ tp
= tp
}, metat
)
40 -- make sure everything gets closed in an exception
41 f
.try
= socket
.newtry(function() f
:close() end)
45 function metat
.__index
:portconnect()
46 self
.try(self
.server
:settimeout(TIMEOUT
))
47 self
.data
= self
.try(self
.server
:accept())
48 self
.try(self
.data
:settimeout(TIMEOUT
))
51 function metat
.__index
:pasvconnect()
52 self
.data
= self
.try(socket
.tcp())
53 self
.try(self
.data
:settimeout(TIMEOUT
))
54 self
.try(self
.data
:connect(self
.pasvt
.ip
, self
.pasvt
.port
))
57 function metat
.__index
:login(user
, password
)
58 self
.try(self
.tp
:command("user", user
or USER
))
59 local code
, reply
= self
.try(self
.tp
:check
{"2..", 331})
61 self
.try(self
.tp
:command("pass", password
or PASSWORD
))
62 self
.try(self
.tp
:check("2.."))
67 function metat
.__index
:pasv()
68 self
.try(self
.tp
:command("pasv"))
69 local code
, reply
= self
.try(self
.tp
:check("2.."))
70 local pattern
= "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)"
71 local a
, b
, c
, d
, p1
, p2
= socket
.skip(2, string.find(reply
, pattern
))
72 self
.try(a
and b
and c
and d
and p1
and p2
, reply
)
74 ip
= string.format("%d.%d.%d.%d", a
, b
, c
, d
),
81 return self
.pasvt
.ip
, self
.pasvt
.port
84 function metat
.__index
:port(ip
, port
)
87 ip
, port
= self
.try(self
.tp
:getcontrol():getsockname())
88 self
.server
= self
.try(socket
.bind(ip
, 0))
89 ip
, port
= self
.try(self
.server
:getsockname())
90 self
.try(self
.server
:settimeout(TIMEOUT
))
92 local pl
= math
.mod(port
, 256)
93 local ph
= (port
- pl
)/256
94 local arg
= string.gsub(string.format("%s,%d,%d", ip
, ph
, pl
), "%.", ",")
95 self
.try(self
.tp
:command("port", arg
))
96 self
.try(self
.tp
:check("2.."))
100 function metat
.__index
:send(sendt
)
101 self
.try(self
.pasvt
or self
.server
, "need port or pasv first")
102 -- if there is a pasvt table, we already sent a PASV command
103 -- we just get the data connection into self.data
104 if self
.pasvt
then self
:pasvconnect() end
105 -- get the transfer argument and command
106 local argument
= sendt
.argument
or
107 url
.unescape(string.gsub(sendt
.path
or "", "^[/\\]", ""))
108 if argument
== "" then argument
= nil end
109 local command
= sendt
.command
or "stor"
110 -- send the transfer command and check the reply
111 self
.try(self
.tp
:command(command
, argument
))
112 local code
, reply
= self
.try(self
.tp
:check
{"2..", "1.."})
113 -- if there is not a a pasvt table, then there is a server
114 -- and we already sent a PORT command
115 if not self
.pasvt
then self
:portconnect() end
116 -- get the sink, source and step for the transfer
117 local step
= sendt
.step
or ltn12
.pump
.step
118 local readt
= {self
.tp
.c
}
119 local checkstep
= function(src
, snk
)
120 -- check status in control connection while downloading
121 local readyt
= socket
.select(readt
, nil, 0)
122 if readyt
[tp
] then code
= self
.try(self
.tp
:check("2..")) end
123 return step(src
, snk
)
125 local sink
= socket
.sink("close-when-done", self
.data
)
126 -- transfer all data and check error
127 self
.try(ltn12
.pump
.all(sendt
.source
, sink
, checkstep
))
128 if string.find(code
, "1..") then self
.try(self
.tp
:check("2..")) end
129 -- done with data connection
131 -- find out how many bytes were sent
132 local sent
= socket
.skip(1, self
.data
:getstats())
137 function metat
.__index
:receive(recvt
)
138 self
.try(self
.pasvt
or self
.server
, "need port or pasv first")
139 if self
.pasvt
then self
:pasvconnect() end
140 local argument
= recvt
.argument
or
141 url
.unescape(string.gsub(recvt
.path
or "", "^[/\\]", ""))
142 if argument
== "" then argument
= nil end
143 local command
= recvt
.command
or "retr"
144 self
.try(self
.tp
:command(command
, argument
))
145 local code
= self
.try(self
.tp
:check
{"1..", "2.."})
146 if not self
.pasvt
then self
:portconnect() end
147 local source
= socket
.source("until-closed", self
.data
)
148 local step
= recvt
.step
or ltn12
.pump
.step
149 self
.try(ltn12
.pump
.all(source
, recvt
.sink
, step
))
150 if string.find(code
, "1..") then self
.try(self
.tp
:check("2..")) end
156 function metat
.__index
:cwd(dir
)
157 self
.try(self
.tp
:command("cwd", dir
))
158 self
.try(self
.tp
:check(250))
162 function metat
.__index
:type(type)
163 self
.try(self
.tp
:command("type", type))
164 self
.try(self
.tp
:check(200))
168 function metat
.__index
:greet()
169 local code
= self
.try(self
.tp
:check
{"1..", "2.."})
170 if string.find(code
, "1..") then self
.try(self
.tp
:check("2..")) end
174 function metat
.__index
:quit()
175 self
.try(self
.tp
:command("quit"))
176 self
.try(self
.tp
:check("2.."))
180 function metat
.__index
:close()
181 if self
.data
then self
.data
:close() end
182 if self
.server
then self
.server
:close() end
183 return self
.tp
:close()
186 -----------------------------------------------------------------------------
187 -- High level FTP API
188 -----------------------------------------------------------------------------
189 local function override(t
)
191 local u
= url
.parse(t
.url
)
192 for i
,v
in base
.pairs(t
) do
199 local function tput(putt
)
200 putt
= override(putt
)
201 socket
.try(putt
.host
, "missing hostname")
202 local f
= open(putt
.host
, putt
.port
, putt
.create
)
204 f
:login(putt
.user
, putt
.password
)
205 if putt
.type then f
:type(putt
.type) end
207 local sent
= f
:send(putt
)
218 local function parse(u
)
219 local t
= socket
.try(url
.parse(u
, default
))
220 socket
.try(t
.scheme
== "ftp", "wrong scheme '" .. t
.scheme
.. "'")
221 socket
.try(t
.host
, "missing hostname")
222 local pat
= "^type=(.)$"
224 t
.type = socket
.skip(2, string.find(t
.params
, pat
))
225 socket
.try(t
.type == "a" or t
.type == "i",
226 "invalid type '" .. t
.type .. "'")
231 local function sput(u
, body
)
232 local putt
= parse(u
)
233 putt
.source
= ltn12
.source
.string(body
)
237 put
= socket
.protect(function(putt
, body
)
238 if base
.type(putt
) == "string" then return sput(putt
, body
)
239 else return tput(putt
) end
242 local function tget(gett
)
243 gett
= override(gett
)
244 socket
.try(gett
.host
, "missing hostname")
245 local f
= open(gett
.host
, gett
.port
, gett
.create
)
247 f
:login(gett
.user
, gett
.password
)
248 if gett
.type then f
:type(gett
.type) end
255 local function sget(u
)
256 local gett
= parse(u
)
258 gett
.sink
= ltn12
.sink
.table(t
)
260 return table.concat(t
)
263 command
= socket
.protect(function(cmdt
)
264 cmdt
= override(cmdt
)
265 socket
.try(cmdt
.host
, "missing hostname")
266 socket
.try(cmdt
.command
, "missing command")
267 local f
= open(cmdt
.host
, cmdt
.port
, cmdt
.create
)
269 f
:login(cmdt
.user
, cmdt
.password
)
270 f
.try(f
.tp
:command(cmdt
.command
, cmdt
.argument
))
271 if cmdt
.check
then f
.try(f
.tp
:check(cmdt
.check
)) end
276 get
= socket
.protect(function(gett
)
277 if base
.type(gett
) == "string" then return sget(gett
)
278 else return tget(gett
) end