diff options
author | Diego Nehab <diego@tecgraf.puc-rio.br> | 2001-09-12 18:27:55 +0000 |
---|---|---|
committer | Diego Nehab <diego@tecgraf.puc-rio.br> | 2001-09-12 18:27:55 +0000 |
commit | 9546cd10ab0f9e51123e3111548549d36f10d473 (patch) | |
tree | 331c78a46fecebece37a1bdc5a3fa96fc0146344 /src | |
parent | 5c622071f95b833727767219cdee31b1419d6cc4 (diff) | |
download | luasocket-9546cd10ab0f9e51123e3111548549d36f10d473.tar.gz luasocket-9546cd10ab0f9e51123e3111548549d36f10d473.tar.bz2 luasocket-9546cd10ab0f9e51123e3111548549d36f10d473.zip |
Updated for LuaSocket 1.4.
Better tested, some name changes.
Diffstat (limited to 'src')
-rw-r--r-- | src/ftp.lua | 551 |
1 files changed, 324 insertions, 227 deletions
diff --git a/src/ftp.lua b/src/ftp.lua index bbb672b..98b08eb 100644 --- a/src/ftp.lua +++ b/src/ftp.lua | |||
@@ -1,60 +1,62 @@ | |||
1 | ----------------------------------------------------------------------------- | 1 | ----------------------------------------------------------------------------- |
2 | -- Simple FTP support for the Lua language using the LuaSocket 1.3b toolkit. | 2 | -- FTP support for the Lua language |
3 | -- LuaSocket 1.4 toolkit. | ||
3 | -- Author: Diego Nehab | 4 | -- Author: Diego Nehab |
4 | -- Date: 26/12/2000 | 5 | -- Date: 26/12/2000 |
5 | -- Conforming to: RFC 959 | 6 | -- Conforming to: RFC 959, LTN7 |
7 | -- RCS ID: $Id$ | ||
6 | ----------------------------------------------------------------------------- | 8 | ----------------------------------------------------------------------------- |
7 | 9 | ||
10 | local Public, Private = {}, {} | ||
11 | FTP = Public | ||
12 | |||
8 | ----------------------------------------------------------------------------- | 13 | ----------------------------------------------------------------------------- |
9 | -- Program constants | 14 | -- Program constants |
10 | ----------------------------------------------------------------------------- | 15 | ----------------------------------------------------------------------------- |
11 | -- timeout in seconds before the program gives up on a connection | 16 | -- timeout in seconds before the program gives up on a connection |
12 | local TIMEOUT = 60 | 17 | Public.TIMEOUT = 60 |
13 | -- default port for ftp service | 18 | -- default port for ftp service |
14 | local PORT = 21 | 19 | Public.PORT = 21 |
15 | -- this is the default anonymous password. used when no password is | 20 | -- this is the default anonymous password. used when no password is |
16 | -- provided in url. should be changed for your e-mail. | 21 | -- provided in url. should be changed to your e-mail. |
17 | local EMAIL = "anonymous@anonymous.org" | 22 | Public.EMAIL = "anonymous@anonymous.org" |
18 | -- block size used in transfers | 23 | -- block size used in transfers |
19 | local BLOCKSIZE = 4096 | 24 | Public.BLOCKSIZE = 8192 |
25 | |||
26 | ----------------------------------------------------------------------------- | ||
27 | -- Required libraries | ||
28 | ----------------------------------------------------------------------------- | ||
29 | dofile "concat.lua" | ||
30 | dofile "url.lua" | ||
31 | dofile "code.lua" | ||
20 | 32 | ||
21 | ----------------------------------------------------------------------------- | 33 | ----------------------------------------------------------------------------- |
22 | -- Parses a url and returns its scheme, user, password, host, port | 34 | -- Tries to send DOS mode lines. Closes socket on error. |
23 | -- and path components, according to RFC 1738, Uniform Resource Locators (URL), | ||
24 | -- of December 1994 | ||
25 | -- Input | 35 | -- Input |
26 | -- url: unique resource locator desired | 36 | -- sock: server socket |
27 | -- default: table containing default values to be returned | 37 | -- line: string to be sent |
28 | -- Returns | 38 | -- Returns |
29 | -- table with the following fields: | 39 | -- err: message in case of error, nil if successfull |
30 | -- host: host to connect | 40 | ----------------------------------------------------------------------------- |
31 | -- path: url path | 41 | function Private.try_sendline(sock, line) |
32 | -- port: host port to connect | 42 | local err = sock:send(line .. "\r\n") |
33 | -- user: user name | 43 | if err then sock:close() end |
34 | -- pass: password | 44 | return err |
35 | -- scheme: protocol | 45 | end |
36 | ----------------------------------------------------------------------------- | 46 | |
37 | local split_url = function(url, default) | 47 | ----------------------------------------------------------------------------- |
38 | -- initialize default parameters | 48 | -- Tries to get a pattern from the server and closes socket on error |
39 | local parsed = default or {} | 49 | -- sock: socket connected to the server |
40 | -- get scheme | 50 | -- ...: pattern to receive |
41 | url = gsub(url, "^(.+)://", function (s) %parsed.scheme = s end) | 51 | -- Returns |
42 | -- get user name and password. both can be empty! | 52 | -- ...: received pattern |
43 | -- moreover, password can be ommited | 53 | -- err: error message if any |
44 | url = gsub(url, "^([^@:/]*)(:?)([^:@/]-)@", function (u, c, p) | 54 | ----------------------------------------------------------------------------- |
45 | %parsed.user = u | 55 | function Private.try_receive(...) |
46 | -- there can be an empty password, but the ':' has to be there | 56 | local sock = arg[1] |
47 | -- or else there is no password | 57 | local data, err = call(sock.receive, arg) |
48 | %parsed.pass = nil -- kill default password | 58 | if err then sock:close() end |
49 | if c == ":" then %parsed.pass = p end | 59 | return data, err |
50 | end) | ||
51 | -- get host | ||
52 | url = gsub(url, "^([%w%.%-]+)", function (h) %parsed.host = h end) | ||
53 | -- get port if any | ||
54 | url = gsub(url, "^:(%d+)", function (p) %parsed.port = p end) | ||
55 | -- whatever is left is the path | ||
56 | if url ~= "" then parsed.path = url end | ||
57 | return parsed | ||
58 | end | 60 | end |
59 | 61 | ||
60 | ----------------------------------------------------------------------------- | 62 | ----------------------------------------------------------------------------- |
@@ -65,14 +67,12 @@ end | |||
65 | -- ip: string containing ip for data connection | 67 | -- ip: string containing ip for data connection |
66 | -- port: port for data connection | 68 | -- port: port for data connection |
67 | ----------------------------------------------------------------------------- | 69 | ----------------------------------------------------------------------------- |
68 | local get_pasv = function(pasv) | 70 | function Private.get_pasv(pasv) |
69 | local a,b,c,d,p1,p2 | 71 | local a, b, c, d, p1, p2, _ |
70 | local ip, port | 72 | local ip, port |
71 | _,_, a, b, c, d, p1, p2 = | 73 | _,_, a, b, c, d, p1, p2 = |
72 | strfind(pasv, "(%d*),(%d*),(%d*),(%d*),(%d*),(%d*)") | 74 | strfind(pasv, "(%d*),(%d*),(%d*),(%d*),(%d*),(%d*)") |
73 | if not (a and b and c and d and p1 and p2) then | 75 | if not (a and b and c and d and p1 and p2) then return nil, nil end |
74 | return nil, nil | ||
75 | end | ||
76 | ip = format("%d.%d.%d.%d", a, b, c, d) | 76 | ip = format("%d.%d.%d.%d", a, b, c, d) |
77 | port = tonumber(p1)*256 + tonumber(p2) | 77 | port = tonumber(p1)*256 + tonumber(p2) |
78 | return ip, port | 78 | return ip, port |
@@ -87,12 +87,11 @@ end | |||
87 | -- Returns | 87 | -- Returns |
88 | -- error message in case of error, nil otherwise | 88 | -- error message in case of error, nil otherwise |
89 | ----------------------------------------------------------------------------- | 89 | ----------------------------------------------------------------------------- |
90 | local send_command = function(control, cmd, arg) | 90 | function Private.send_command(control, cmd, arg) |
91 | local line, err | 91 | local line |
92 | if arg then line = cmd .. " " .. arg .. "\r\n" | 92 | if arg then line = cmd .. " " .. arg |
93 | else line = cmd .. "\r\n" end | 93 | else line = cmd end |
94 | err = control:send(line) | 94 | return %Private.try_sendline(control, line) |
95 | return err | ||
96 | end | 95 | end |
97 | 96 | ||
98 | ----------------------------------------------------------------------------- | 97 | ----------------------------------------------------------------------------- |
@@ -103,16 +102,16 @@ end | |||
103 | -- answer: whole server reply, nil if error | 102 | -- answer: whole server reply, nil if error |
104 | -- code: answer status code or error message | 103 | -- code: answer status code or error message |
105 | ----------------------------------------------------------------------------- | 104 | ----------------------------------------------------------------------------- |
106 | local get_answer = function(control) | 105 | function Private.get_answer(control) |
107 | local code, lastcode, sep | 106 | local code, lastcode, sep, _ |
108 | local line, err = control:receive() | 107 | local line, err = %Private.try_receive(control) |
109 | local answer = line | 108 | local answer = line |
110 | if err then return nil, err end | 109 | if err then return nil, err end |
111 | _,_, code, sep = strfind(line, "^(%d%d%d)(.)") | 110 | _,_, code, sep = strfind(line, "^(%d%d%d)(.)") |
112 | if not code or not sep then return nil, answer end | 111 | if not code or not sep then return nil, answer end |
113 | if sep == "-" then -- answer is multiline | 112 | if sep == "-" then -- answer is multiline |
114 | repeat | 113 | repeat |
115 | line, err = control:receive() | 114 | line, err = %Private.try_receive(control) |
116 | if err then return nil, err end | 115 | if err then return nil, err end |
117 | _,_, lastcode, sep = strfind(line, "^(%d%d%d)(.)") | 116 | _,_, lastcode, sep = strfind(line, "^(%d%d%d)(.)") |
118 | answer = answer .. "\n" .. line | 117 | answer = answer .. "\n" .. line |
@@ -130,12 +129,9 @@ end | |||
130 | -- code: reply code or nil in case of error | 129 | -- code: reply code or nil in case of error |
131 | -- answer: server complete answer or system error message | 130 | -- answer: server complete answer or system error message |
132 | ----------------------------------------------------------------------------- | 131 | ----------------------------------------------------------------------------- |
133 | local check_answer = function(control, success) | 132 | function Private.check_answer(control, success) |
134 | local answer, code = %get_answer(control) | 133 | local answer, code = %Private.get_answer(control) |
135 | if not answer then | 134 | if not answer then return nil, code end |
136 | control:close() | ||
137 | return nil, code | ||
138 | end | ||
139 | if type(success) ~= "table" then success = {success} end | 135 | if type(success) ~= "table" then success = {success} end |
140 | for i = 1, getn(success) do | 136 | for i = 1, getn(success) do |
141 | if code == success[i] then | 137 | if code == success[i] then |
@@ -158,38 +154,10 @@ end | |||
158 | -- code: reply code or nil in case of error | 154 | -- code: reply code or nil in case of error |
159 | -- answer: server complete answer or system error message | 155 | -- answer: server complete answer or system error message |
160 | ----------------------------------------------------------------------------- | 156 | ----------------------------------------------------------------------------- |
161 | local try_command = function(control, cmd, arg, success) | 157 | function Private.command(control, cmd, arg, success) |
162 | local err = %send_command(control, cmd, arg) | 158 | local err = %Private.send_command(control, cmd, arg) |
163 | if err then | 159 | if err then return nil, err end |
164 | control:close() | 160 | return %Private.check_answer(control, success) |
165 | return nil, err | ||
166 | end | ||
167 | local code, answer = %check_answer(control, success) | ||
168 | if not code then return nil, answer end | ||
169 | return code, answer | ||
170 | end | ||
171 | |||
172 | ----------------------------------------------------------------------------- | ||
173 | -- Creates a table with all directories in path | ||
174 | -- Input | ||
175 | -- file: abolute path to file | ||
176 | -- Returns | ||
177 | -- a table with the following fields | ||
178 | -- name: filename | ||
179 | -- path: directory to file | ||
180 | -- isdir: is it a directory? | ||
181 | ----------------------------------------------------------------------------- | ||
182 | local split_path = function(file) | ||
183 | local parsed = {} | ||
184 | file = gsub(file, "(/)$", function(i) %parsed.isdir = i end) | ||
185 | if not parsed.isdir then | ||
186 | file = gsub(file, "([^/]+)$", function(n) %parsed.name = n end) | ||
187 | end | ||
188 | file = gsub(file, "/$", "") | ||
189 | file = gsub(file, "^/", "") | ||
190 | if file == "" then file = nil end | ||
191 | parsed.path = file | ||
192 | if parsed.path or parsed.name or parsed.isdir then return parsed end | ||
193 | end | 161 | end |
194 | 162 | ||
195 | ----------------------------------------------------------------------------- | 163 | ----------------------------------------------------------------------------- |
@@ -200,11 +168,10 @@ end | |||
200 | -- code: nil if error | 168 | -- code: nil if error |
201 | -- answer: server answer or error message | 169 | -- answer: server answer or error message |
202 | ----------------------------------------------------------------------------- | 170 | ----------------------------------------------------------------------------- |
203 | local check_greeting = function(control) | 171 | function Private.greet(control) |
204 | local code, answer = %check_answer(control, {120, 220}) | 172 | local code, answer = %Private.check_answer(control, {120, 220}) |
205 | if not code then return nil, answer end | ||
206 | if code == 120 then -- please try again, somewhat busy now... | 173 | if code == 120 then -- please try again, somewhat busy now... |
207 | code, answer = %check_answer(control, {220}) | 174 | return %Private.check_answer(control, {220}) |
208 | end | 175 | end |
209 | return code, answer | 176 | return code, answer |
210 | end | 177 | end |
@@ -214,16 +181,15 @@ end | |||
214 | -- Input | 181 | -- Input |
215 | -- control: control connection with server | 182 | -- control: control connection with server |
216 | -- user: user name | 183 | -- user: user name |
217 | -- pass: user password if any | 184 | -- password: user password if any |
218 | -- Returns | 185 | -- Returns |
219 | -- code: nil if error | 186 | -- code: nil if error |
220 | -- answer: server answer or error message | 187 | -- answer: server answer or error message |
221 | ----------------------------------------------------------------------------- | 188 | ----------------------------------------------------------------------------- |
222 | local login = function(control, user, pass) | 189 | function Private.login(control, user, password) |
223 | local code, answer = %try_command(control, "user", parsed.user, {230, 331}) | 190 | local code, answer = %Private.command(control, "user", user, {230, 331}) |
224 | if not code then return nil, answer end | 191 | if code == 331 and password then -- need pass and we have pass |
225 | if code == 331 and parsed.pass then -- need pass and we have pass | 192 | return %Private.command(control, "pass", password, {230, 202}) |
226 | code, answer = %try_command(control, "pass", parsed.pass, {230, 202}) | ||
227 | end | 193 | end |
228 | return code, answer | 194 | return code, answer |
229 | end | 195 | end |
@@ -237,12 +203,9 @@ end | |||
237 | -- code: nil if error | 203 | -- code: nil if error |
238 | -- answer: server answer or error message | 204 | -- answer: server answer or error message |
239 | ----------------------------------------------------------------------------- | 205 | ----------------------------------------------------------------------------- |
240 | local cwd = function(control, path) | 206 | function Private.cwd(control, path) |
241 | local code, answer = 250, "Home directory used" | 207 | if path then return %Private.command(control, "cwd", path, {250}) |
242 | if path then | 208 | else return 250, nil end |
243 | code, answer = %try_command(control, "cwd", path, {250}) | ||
244 | end | ||
245 | return code, answer | ||
246 | end | 209 | end |
247 | 210 | ||
248 | ----------------------------------------------------------------------------- | 211 | ----------------------------------------------------------------------------- |
@@ -253,20 +216,19 @@ end | |||
253 | -- server: server socket bound to local address, nil if error | 216 | -- server: server socket bound to local address, nil if error |
254 | -- answer: error message if any | 217 | -- answer: error message if any |
255 | ----------------------------------------------------------------------------- | 218 | ----------------------------------------------------------------------------- |
256 | local port = function(control) | 219 | function Private.port(control) |
257 | local code, answer | 220 | local code, answer |
258 | local server, ctl_ip | 221 | local server, ctl_ip |
259 | ctl_ip, answer = control:getsockname() | 222 | ctl_ip, answer = control:getsockname() |
260 | server, answer = bind(ctl_ip, 0) | 223 | server, answer = bind(ctl_ip, 0) |
261 | server:timeout(%TIMEOUT) | 224 | server:timeout(%Public.TIMEOUT) |
262 | local ip, p, ph, pl | 225 | local ip, p, ph, pl |
263 | ip, p = server:getsockname() | 226 | ip, p = server:getsockname() |
264 | pl = mod(p, 256) | 227 | pl = mod(p, 256) |
265 | ph = (p - pl)/256 | 228 | ph = (p - pl)/256 |
266 | local arg = gsub(format("%s,%d,%d", ip, ph, pl), "%.", ",") | 229 | local arg = gsub(format("%s,%d,%d", ip, ph, pl), "%.", ",") |
267 | code, answer = %try_command(control, "port", arg, {200}) | 230 | code, answer = %Private.command(control, "port", arg, {200}) |
268 | if not code then | 231 | if not code then |
269 | control:close() | ||
270 | server:close() | 232 | server:close() |
271 | return nil, answer | 233 | return nil, answer |
272 | else return server end | 234 | else return server end |
@@ -280,10 +242,9 @@ end | |||
280 | -- code: nil if error | 242 | -- code: nil if error |
281 | -- answer: server answer or error message | 243 | -- answer: server answer or error message |
282 | ----------------------------------------------------------------------------- | 244 | ----------------------------------------------------------------------------- |
283 | local logout = function(control) | 245 | function Private.logout(control) |
284 | local code, answer = %try_command(control, "quit", nil, {221}) | 246 | local code, answer = %Private.command(control, "quit", nil, {221}) |
285 | if not code then return nil, answer end | 247 | if code then control:close() end |
286 | control:close() | ||
287 | return code, answer | 248 | return code, answer |
288 | end | 249 | end |
289 | 250 | ||
@@ -295,10 +256,10 @@ end | |||
295 | -- Returns | 256 | -- Returns |
296 | -- nil if successfull, or an error message in case of error | 257 | -- nil if successfull, or an error message in case of error |
297 | ----------------------------------------------------------------------------- | 258 | ----------------------------------------------------------------------------- |
298 | local receive_indirect = function(data, callback) | 259 | function Private.receive_indirect(data, callback) |
299 | local chunk, err, res | 260 | local chunk, err, res |
300 | while not err do | 261 | while not err do |
301 | chunk, err = data:receive(%BLOCKSIZE) | 262 | chunk, err = %Private.try_receive(data, %Public.BLOCKSIZE) |
302 | if err == "closed" then err = "done" end | 263 | if err == "closed" then err = "done" end |
303 | res = callback(chunk, err) | 264 | res = callback(chunk, err) |
304 | if not res then break end | 265 | if not res then break end |
@@ -310,33 +271,38 @@ end | |||
310 | -- Input | 271 | -- Input |
311 | -- control: control connection with server | 272 | -- control: control connection with server |
312 | -- server: server socket bound to local address | 273 | -- server: server socket bound to local address |
313 | -- file: file name under current directory | 274 | -- name: file name |
314 | -- isdir: is file a directory name? | 275 | -- is_directory: is file a directory name? |
315 | -- callback: callback to receive file contents | 276 | -- download_cb: callback to receive file contents |
316 | -- Returns | 277 | -- Returns |
317 | -- err: error message in case of error, nil otherwise | 278 | -- err: error message in case of error, nil otherwise |
318 | ----------------------------------------------------------------------------- | 279 | ----------------------------------------------------------------------------- |
319 | local retrieve = function(control, server, file, isdir, callback) | 280 | function Private.retrieve(control, server, name, is_directory, download_cb) |
320 | local code, answer | 281 | local code, answer |
321 | local data | 282 | local data |
322 | -- ask server for file or directory listing accordingly | 283 | -- ask server for file or directory listing accordingly |
323 | if isdir then code, answer = %try_command(control, "nlst", file, {150, 125}) | 284 | if is_directory then |
324 | else code, answer = %try_command(control, "retr", file, {150, 125}) end | 285 | code, answer = %Private.cwd(control, name) |
286 | if not code then return answer end | ||
287 | code, answer = %Private.command(control, "nlst", nil, {150, 125}) | ||
288 | else | ||
289 | code, answer = %Private.command(control, "retr", name, {150, 125}) | ||
290 | end | ||
291 | if not code then return nil, answer end | ||
325 | data, answer = server:accept() | 292 | data, answer = server:accept() |
326 | server:close() | 293 | server:close() |
327 | if not data then | 294 | if not data then |
328 | control:close() | 295 | control:close() |
329 | return answer | 296 | return answer |
330 | end | 297 | end |
331 | answer = %receive_indirect(data, callback) | 298 | answer = %Private.receive_indirect(data, download_cb) |
332 | if answer then | 299 | if answer then |
333 | control:close() | 300 | control:close() |
334 | return answer | 301 | return answer |
335 | end | 302 | end |
336 | data:close() | 303 | data:close() |
337 | -- make sure file transfered ok | 304 | -- make sure file transfered ok |
338 | code, answer = %check_answer(control, {226, 250}) | 305 | return %Private.check_answer(control, {226, 250}) |
339 | if not code then return answer end | ||
340 | end | 306 | end |
341 | 307 | ||
342 | ----------------------------------------------------------------------------- | 308 | ----------------------------------------------------------------------------- |
@@ -344,11 +310,11 @@ end | |||
344 | -- Input | 310 | -- Input |
345 | -- data: data connection | 311 | -- data: data connection |
346 | -- send_cb: callback to produce file contents | 312 | -- send_cb: callback to produce file contents |
347 | -- chunk, size: first callback results | 313 | -- chunk, size: first callback return values |
348 | -- Returns | 314 | -- Returns |
349 | -- nil if successfull, or an error message in case of error | 315 | -- nil if successfull, or an error message in case of error |
350 | ----------------------------------------------------------------------------- | 316 | ----------------------------------------------------------------------------- |
351 | local try_sendindirect = function(data, send_cb, chunk, size) | 317 | function Private.send_indirect(data, send_cb, chunk, size) |
352 | local sent, err | 318 | local sent, err |
353 | sent = 0 | 319 | sent = 0 |
354 | while 1 do | 320 | while 1 do |
@@ -358,9 +324,9 @@ local try_sendindirect = function(data, send_cb, chunk, size) | |||
358 | else return "invalid callback return" end | 324 | else return "invalid callback return" end |
359 | end | 325 | end |
360 | err = data:send(chunk) | 326 | err = data:send(chunk) |
361 | if err then | 327 | if err then |
362 | data:close() | 328 | data:close() |
363 | return err | 329 | return err |
364 | end | 330 | end |
365 | sent = sent + strlen(chunk) | 331 | sent = sent + strlen(chunk) |
366 | if sent >= size then break end | 332 | if sent >= size then break end |
@@ -379,9 +345,9 @@ end | |||
379 | -- code: return code, nil if error | 345 | -- code: return code, nil if error |
380 | -- answer: server answer or error message | 346 | -- answer: server answer or error message |
381 | ----------------------------------------------------------------------------- | 347 | ----------------------------------------------------------------------------- |
382 | local store = function(control, server, file, send_cb) | 348 | function Private.store(control, server, file, send_cb) |
383 | local data | 349 | local data, err |
384 | local code, answer = %try_command(control, "stor", file, {150, 125}) | 350 | local code, answer = %Private.command(control, "stor", file, {150, 125}) |
385 | if not code then | 351 | if not code then |
386 | control:close() | 352 | control:close() |
387 | return nil, answer | 353 | return nil, answer |
@@ -394,7 +360,7 @@ local store = function(control, server, file, send_cb) | |||
394 | return nil, answer | 360 | return nil, answer |
395 | end | 361 | end |
396 | -- send whole file | 362 | -- send whole file |
397 | err = %try_sendindirect(data, send_cb, send_cb()) | 363 | err = %Private.send_indirect(data, send_cb, send_cb()) |
398 | if err then | 364 | if err then |
399 | control:close() | 365 | control:close() |
400 | return nil, err | 366 | return nil, err |
@@ -402,144 +368,275 @@ local store = function(control, server, file, send_cb) | |||
402 | -- close connection to inform that file transmission is complete | 368 | -- close connection to inform that file transmission is complete |
403 | data:close() | 369 | data:close() |
404 | -- check if file was received correctly | 370 | -- check if file was received correctly |
405 | return %check_answer(control, {226, 250}) | 371 | return %Private.check_answer(control, {226, 250}) |
406 | end | 372 | end |
407 | 373 | ||
408 | ----------------------------------------------------------------------------- | 374 | ----------------------------------------------------------------------------- |
409 | -- Change transfer type | 375 | -- Change transfer type |
410 | -- Input | 376 | -- Input |
411 | -- control: control connection with server | 377 | -- control: control connection with server |
412 | -- type: new transfer type | 378 | -- params: "type=i" for binary or "type=a" for ascii |
413 | -- Returns | 379 | -- Returns |
414 | -- code: nil if error | 380 | -- err: error message if any |
415 | -- answer: server answer or error message | ||
416 | ----------------------------------------------------------------------------- | 381 | ----------------------------------------------------------------------------- |
417 | local change_type = function(control, type) | 382 | function Private.change_type(control, params) |
418 | if type == "b" then type = "i" else type = "a" end | 383 | local type |
419 | return %try_command(control, "type", type, {200}) | 384 | if params == "type=i" then type = "i" |
385 | elseif params == "type=a" then type = "a" end | ||
386 | if type then | ||
387 | local code, err = %Private.command(control, "type", type, {200}) | ||
388 | if not code then return err end | ||
389 | end | ||
420 | end | 390 | end |
421 | 391 | ||
422 | ----------------------------------------------------------------------------- | 392 | ----------------------------------------------------------------------------- |
423 | -- Retrieve a file from a ftp server | 393 | -- Starts a control connection, checks the greeting and log on |
424 | -- Input | 394 | -- Input |
425 | -- url: file location | 395 | -- parsed: parsed URL components |
426 | -- receive_cb: callback to receive file contents | ||
427 | -- type: "binary" or "ascii" | ||
428 | -- Returns | 396 | -- Returns |
397 | -- control: control connection with server, or nil if error | ||
429 | -- err: error message if any | 398 | -- err: error message if any |
430 | ----------------------------------------------------------------------------- | 399 | ----------------------------------------------------------------------------- |
431 | function ftp_getindirect(url, receive_cb, type) | 400 | function Private.open(parsed) |
432 | local control, server, data, err | ||
433 | local answer, code, server, pfile, file | ||
434 | parsed = %split_url(url, {user = "anonymous", port = 21, pass = %EMAIL}) | ||
435 | -- start control connection | 401 | -- start control connection |
436 | control, err = connect(parsed.host, parsed.port) | 402 | local control, err = connect(parsed.host, parsed.port) |
437 | if not control then return err end | 403 | if not control then return nil, err end |
438 | control:timeout(%TIMEOUT) | 404 | -- make sure we don't block forever |
439 | -- get and check greeting | 405 | control:timeout(%Public.TIMEOUT) |
440 | code, answer = %check_greeting(control) | 406 | -- check greeting |
441 | if not code then return answer end | 407 | local code, answer = %Private.greet(control) |
408 | if not code then return nil, answer end | ||
442 | -- try to log in | 409 | -- try to log in |
443 | code, answer = %login(control, parsed.user, parsed.pass) | 410 | code, err = %Private.login(control, parsed.user, parsed.password) |
444 | if not code then return answer end | 411 | if not code then return nil, err |
445 | -- go to directory | 412 | else return control end |
446 | pfile = %split_path(parsed.path) | 413 | end |
447 | if not pfile then return "invalid path" end | 414 | |
448 | code, answer = %cwd(control, pfile.path) | 415 | ----------------------------------------------------------------------------- |
449 | if not code then return answer end | 416 | -- Closes the connection with the server |
450 | -- change to binary type? | 417 | -- Input |
451 | code, answer = %change_type(control, type) | 418 | -- control: control connection with server |
452 | if not code then return answer end | 419 | ----------------------------------------------------------------------------- |
453 | -- setup passive connection | 420 | function Private.close(control) |
454 | server, answer = %port(control) | ||
455 | if not server then return answer end | ||
456 | -- ask server to send file or directory listing | ||
457 | err = %retrieve(control, server, pfile.name, pfile.isdir, receive_cb) | ||
458 | if err then return err end | ||
459 | -- disconnect | 421 | -- disconnect |
460 | %logout(control) | 422 | %Private.logout(control) |
461 | end | 423 | end |
462 | 424 | ||
463 | ----------------------------------------------------------------------------- | 425 | ----------------------------------------------------------------------------- |
464 | -- Uploads a file to a FTP server | 426 | -- Changes to the directory pointed to by URL |
465 | -- Input | 427 | -- Input |
466 | -- url: file location | 428 | -- control: control connection with server |
467 | -- send_cb: callback to produce the file contents | 429 | -- segment: parsed URL path segments |
468 | -- type: "binary" or "ascii" | ||
469 | -- Returns | 430 | -- Returns |
470 | -- err: error message if any | 431 | -- err: error message if any |
471 | ----------------------------------------------------------------------------- | 432 | ----------------------------------------------------------------------------- |
472 | function ftp_putindirect(url, send_cb, type) | 433 | function Private.change_dir(control, segment) |
473 | local control, data | 434 | local n = getn(segment) |
474 | local answer, code, server, file, pfile | 435 | for i = 1, n-1 do |
475 | parsed = %split_url(url, {user = "anonymous", port = 21, pass = %EMAIL}) | 436 | local code, answer = %Private.cwd(control, segment[i]) |
476 | -- start control connection | 437 | if not code then return answer end |
477 | control, answer = connect(parsed.host, parsed.port) | 438 | end |
478 | if not control then return answer end | 439 | end |
479 | control:timeout(%TIMEOUT) | 440 | |
480 | -- get and check greeting | 441 | ----------------------------------------------------------------------------- |
481 | code, answer = %check_greeting(control) | 442 | -- Stores a file in current directory |
482 | if not code then return answer end | 443 | -- Input |
483 | -- try to log in | 444 | -- control: control connection with server |
484 | code, answer = %login(control, parsed.user, parsed.pass) | 445 | -- request: a table with the fields: |
485 | if not code then return answer end | 446 | -- upload_cb: send callback to send file contents |
486 | -- go to directory | 447 | -- segment: parsed URL path segments |
487 | pfile = %split_path(parsed.path) | 448 | -- Returns |
488 | if not pfile or pfile.isdir then return "invalid path" end | 449 | -- err: error message if any |
489 | code, answer = %cwd(control, pfile.path) | 450 | ----------------------------------------------------------------------------- |
490 | if not code then return answer end | 451 | function Private.upload(control, request, segment) |
491 | -- change to binary type? | 452 | local code, name, upload_cb |
492 | code, answer = %change_type(control, type) | 453 | -- get remote file name |
454 | name = segment[getn(segment)] | ||
455 | if not name then | ||
456 | control:close() | ||
457 | return "Invalid file path" | ||
458 | end | ||
459 | upload_cb = request.upload_cb | ||
460 | -- setup passive connection | ||
461 | local server, answer = %Private.port(control) | ||
462 | if not server then return answer end | ||
463 | -- ask server to receive file | ||
464 | code, answer = %Private.store(control, server, name, upload_cb) | ||
493 | if not code then return answer end | 465 | if not code then return answer end |
466 | end | ||
467 | |||
468 | ----------------------------------------------------------------------------- | ||
469 | -- Download a file from current directory | ||
470 | -- Input | ||
471 | -- control: control connection with server | ||
472 | -- request: a table with the fields: | ||
473 | -- download_cb: receive callback to receive file contents | ||
474 | -- segment: parsed URL path segments | ||
475 | -- Returns | ||
476 | -- err: error message if any | ||
477 | ----------------------------------------------------------------------------- | ||
478 | function Private.download(control, request, segment) | ||
479 | local code, name, is_directory, download_cb | ||
480 | is_directory = segment.is_directory | ||
481 | download_cb = request.download_cb | ||
482 | -- get remote file name | ||
483 | name = segment[getn(segment)] | ||
484 | if not name and not is_directory then | ||
485 | control:close() | ||
486 | return "Invalid file path" | ||
487 | end | ||
494 | -- setup passive connection | 488 | -- setup passive connection |
495 | server, answer = %port(control) | 489 | local server, answer = %Private.port(control) |
496 | if not server then return answer end | 490 | if not server then return answer end |
497 | -- ask server to send file | 491 | -- ask server to send file or directory listing |
498 | code, answer = %store(control, server, pfile.name, send_cb) | 492 | code, answer = %Private.retrieve(control, server, name, |
493 | is_directory, download_cb) | ||
499 | if not code then return answer end | 494 | if not code then return answer end |
500 | -- disconnect | 495 | end |
501 | %logout(control) | 496 | |
502 | -- no errors | 497 | ----------------------------------------------------------------------------- |
503 | return nil | 498 | -- Parses the FTP URL setting default values |
499 | -- Input | ||
500 | -- request: a table with the fields: | ||
501 | -- url: the target URL | ||
502 | -- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory | ||
503 | -- user: account user name | ||
504 | -- password: account password | ||
505 | -- Returns | ||
506 | -- parsed: a table with parsed components | ||
507 | ----------------------------------------------------------------------------- | ||
508 | function Private.parse_url(request) | ||
509 | local parsed = URL.parse_url(request.url, { | ||
510 | host = "", | ||
511 | user = "anonymous", | ||
512 | port = 21, | ||
513 | path = "/", | ||
514 | password = %Public.EMAIL | ||
515 | }) | ||
516 | -- explicit login information overrides that given by URL | ||
517 | parsed.user = request.user or parsed.user | ||
518 | parsed.password = request.password or parsed.password | ||
519 | -- explicit representation type overrides that given by URL | ||
520 | if request.type then parsed.params = "type=" .. request.type end | ||
521 | return parsed | ||
522 | end | ||
523 | |||
524 | ----------------------------------------------------------------------------- | ||
525 | -- Parses the FTP URL path setting default values | ||
526 | -- Input | ||
527 | -- parsed: a table with the parsed URL components | ||
528 | -- Returns | ||
529 | -- dirs: a table with parsed directory components | ||
530 | ----------------------------------------------------------------------------- | ||
531 | function Private.parse_path(parsed) | ||
532 | local segment = URL.parse_path(parsed.path) | ||
533 | segment.is_directory = segment.is_directory or (parsed.params == "type=d") | ||
534 | return segment | ||
535 | end | ||
536 | |||
537 | ----------------------------------------------------------------------------- | ||
538 | -- Builds a request table from a URL or request table | ||
539 | -- Input | ||
540 | -- url_or_request: target url or request table (a table with the fields: | ||
541 | -- url: the target URL | ||
542 | -- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory | ||
543 | -- user: account user name | ||
544 | -- password: account password) | ||
545 | -- Returns | ||
546 | -- request: request table | ||
547 | ----------------------------------------------------------------------------- | ||
548 | function Private.build_request(data) | ||
549 | local request = {} | ||
550 | if type(data) == "table" then for i, v in data do request[i] = v end | ||
551 | else request.url = data end | ||
552 | return request | ||
553 | end | ||
554 | |||
555 | ----------------------------------------------------------------------------- | ||
556 | -- Downloads a file from a FTP server | ||
557 | -- Input | ||
558 | -- request: a table with the fields: | ||
559 | -- url: the target URL | ||
560 | -- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory | ||
561 | -- user: account user name | ||
562 | -- password: account password | ||
563 | -- download_cb: receive callback to receive file contents | ||
564 | -- Returns | ||
565 | -- err: error message if any | ||
566 | ----------------------------------------------------------------------------- | ||
567 | function Public.get_cb(request) | ||
568 | local parsed = %Private.parse_url(request) | ||
569 | local control, err = %Private.open(parsed) | ||
570 | if not control then return err end | ||
571 | local segment = %Private.parse_path(parsed) | ||
572 | return %Private.change_dir(control, segment) or | ||
573 | %Private.change_type(control, parsed.params) or | ||
574 | %Private.download(control, request, segment) or | ||
575 | %Private.close(control) | ||
504 | end | 576 | end |
505 | 577 | ||
506 | ----------------------------------------------------------------------------- | 578 | ----------------------------------------------------------------------------- |
507 | -- Uploads a file to a FTP server | 579 | -- Uploads a file to a FTP server |
508 | -- Input | 580 | -- Input |
509 | -- url: file location | 581 | -- request: a table with the fields: |
510 | -- bytes: file contents | 582 | -- url: the target URL |
511 | -- type: "binary" or "ascii" | 583 | -- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory |
584 | -- user: account user name | ||
585 | -- password: account password | ||
586 | -- upload_cb: send callback to send file contents | ||
512 | -- Returns | 587 | -- Returns |
513 | -- err: error message if any | 588 | -- err: error message if any |
514 | ----------------------------------------------------------------------------- | 589 | ----------------------------------------------------------------------------- |
515 | function ftp_put(url, bytes, type) | 590 | function Public.put_cb(request) |
516 | local send_cb = function() | 591 | local parsed = %Private.parse_url(request) |
517 | return %bytes, strlen(%bytes) | 592 | local control, err = %Private.open(parsed) |
518 | end | 593 | if not control then return err end |
519 | return ftp_putindirect(url, send_cb, type) | 594 | local segment = %Private.parse_path(parsed) |
595 | return %Private.change_dir(control, segment) or | ||
596 | %Private.change_type(control, parsed.params) or | ||
597 | %Private.upload(control, request, segment) or | ||
598 | %Private.close(control) | ||
520 | end | 599 | end |
521 | 600 | ||
522 | ----------------------------------------------------------------------------- | 601 | ----------------------------------------------------------------------------- |
523 | -- We need fast concatenation routines for direct requests | 602 | -- Uploads a file to a FTP server |
603 | -- Input | ||
604 | -- url_or_request: target url or request table (a table with the fields: | ||
605 | -- url: the target URL | ||
606 | -- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory | ||
607 | -- user: account user name | ||
608 | -- password: account password) | ||
609 | -- content: file contents | ||
610 | -- Returns | ||
611 | -- err: error message if any | ||
524 | ----------------------------------------------------------------------------- | 612 | ----------------------------------------------------------------------------- |
525 | dofile("buffer.lua") | 613 | function Public.put(url_or_request, content) |
614 | local request = %Private.build_request(url_or_request) | ||
615 | request.upload_cb = function() | ||
616 | return %content, strlen(%content) | ||
617 | end | ||
618 | return %Public.put_cb(request) | ||
619 | end | ||
526 | 620 | ||
527 | ----------------------------------------------------------------------------- | 621 | ----------------------------------------------------------------------------- |
528 | -- Retrieve a file from a ftp server | 622 | -- Retrieve a file from a ftp server |
529 | -- Input | 623 | -- Input |
530 | -- url: file location | 624 | -- url_or_request: target url or request table (a table with the fields: |
531 | -- type: "binary" or "ascii" | 625 | -- url: the target URL |
626 | -- type: "i" for "image" mode, "a" for "ascii" mode or "d" for directory | ||
627 | -- user: account user name | ||
628 | -- password: account password) | ||
532 | -- Returns | 629 | -- Returns |
533 | -- data: file contents as a string | 630 | -- data: file contents as a string |
534 | -- err: error message in case of error, nil otherwise | 631 | -- err: error message in case of error, nil otherwise |
535 | ----------------------------------------------------------------------------- | 632 | ----------------------------------------------------------------------------- |
536 | function ftp_get(url, type) | 633 | function Public.get(url_or_request) |
537 | local bytes = { buf = buf_create() } | 634 | local cat = Concat.create() |
538 | local receive_cb = function(chunk, err) | 635 | local request = %Private.build_request(url_or_request) |
539 | if not chunk then %bytes.buf = nil end | 636 | request.download_cb = function(chunk, err) |
540 | buf_addstring(%bytes.buf, chunk) | 637 | if chunk then %cat:addstring(chunk) end |
541 | return 1 | 638 | return 1 |
542 | end | 639 | end |
543 | err = ftp_getindirect(url, receive_cb, type) | 640 | local err = %Public.get_cb(request) |
544 | return buf_getresult(bytes.buf), err | 641 | return cat:getresult(), err |
545 | end | 642 | end |