4f08e93fe01eaefa764c54667ced69571ebad42a
[project/luci.git] / libs / luci-lib-httpclient / luasrc / httpclient / receiver.lua
1 --[[
2 LuCI - Lua Development Framework 
3
4 Copyright 2009 Steven Barth <steven@midlink.org>
5
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
9
10         http://www.apache.org/licenses/LICENSE-2.0
11
12 $Id$
13 ]]--
14
15 require "nixio.util"
16 local nixio = require "nixio"
17 local httpc = require "luci.httpclient"
18 local ltn12 = require "luci.ltn12"
19
20 local print, tonumber, require, unpack = print, tonumber, require, unpack
21
22 module "luci.httpclient.receiver"
23
24 local function prepare_fd(target)
25         -- Open fd for appending
26         local oflags = nixio.open_flags("wronly", "creat")
27         local file, code, msg = nixio.open(target, oflags)
28         if not file then
29                 return file, code, msg
30         end
31         
32         -- Acquire lock
33         local stat, code, msg = file:lock("tlock")
34         if not stat then
35                 return stat, code, msg
36         end
37         
38         file:seek(0, "end") 
39         
40         return file
41 end
42
43 local function splice_async(sock, pipeout, pipein, file, cb)
44         local ssize = 65536
45         local smode = nixio.splice_flags("move", "more", "nonblock")
46
47         -- Set pipe non-blocking otherwise we might end in a deadlock
48         local stat, code, msg = pipein:setblocking(false)
49         if stat then
50                 stat, code, msg = pipeout:setblocking(false)
51         end
52         if not stat then
53                 return stat, code, msg
54         end
55         
56         
57         local pollsock = {
58                 {fd=sock, events=nixio.poll_flags("in")}
59         }
60         
61         local pollfile = {
62                 {fd=file, events=nixio.poll_flags("out")}
63         }
64         
65         local done
66         local active -- Older splice implementations sometimes don't detect EOS
67         
68         repeat
69                 active = false
70                 
71                 -- Socket -> Pipe
72                 repeat
73                         nixio.poll(pollsock, 15000)
74                 
75                         stat, code, msg = nixio.splice(sock, pipeout, ssize, smode)
76                         if stat == nil then
77                                 return stat, code, msg
78                         elseif stat == 0 then
79                                 done = true
80                                 break
81                         elseif stat then
82                                 active = true
83                         end
84                 until stat == false
85                 
86                 -- Pipe -> File
87                 repeat
88                         nixio.poll(pollfile, 15000)
89                 
90                         stat, code, msg = nixio.splice(pipein, file, ssize, smode)
91                         if stat == nil then
92                                 return stat, code, msg
93                         elseif stat then
94                                 active = true
95                         end
96                 until stat == false
97                 
98                 if cb then
99                         cb(file)
100                 end
101                 
102                 if not active then
103                         -- We did not splice any data, maybe EOS, fallback to default
104                         return false
105                 end
106         until done
107         
108         pipein:close()
109         pipeout:close()
110         sock:close()
111         file:close()
112         return true
113 end
114
115 local function splice_sync(sock, pipeout, pipein, file, cb)
116         local os = require "os"
117         local ssize = 65536
118         local smode = nixio.splice_flags("move", "more")
119         local stat
120         
121         -- This is probably the only forking http-client ;-)
122         local pid, code, msg = nixio.fork()
123         if not pid then
124                 return pid, code, msg
125         elseif pid == 0 then
126                 pipein:close()
127                 file:close()
128
129                 repeat
130                         stat, code = nixio.splice(sock, pipeout, ssize, smode)
131                 until not stat or stat == 0
132         
133                 pipeout:close()
134                 sock:close()
135                 os.exit(stat or code)
136         else
137                 pipeout:close()
138                 sock:close()
139                 
140                 repeat
141                         stat, code, msg = nixio.splice(pipein, file, ssize, smode)
142                         if cb then
143                                 cb(file)
144                         end
145                 until not stat or stat == 0
146                 
147                 pipein:close()
148                 file:close()
149                 
150                 if not stat then
151                         nixio.kill(pid, 15)
152                         nixio.wait(pid)
153                         return stat, code, msg
154                 else
155                         pid, msg, code = nixio.wait(pid)
156                         if msg == "exited" then
157                                 if code == 0 then
158                                         return true
159                                 else
160                                         return nil, code, nixio.strerror(code)
161                                 end
162                         else
163                                 return nil, -0x11, "broken pump"
164                         end
165                 end
166         end
167 end
168
169 function request_to_file(uri, target, options, cbs)
170         options = options or {}
171         cbs = cbs or {}
172         options.headers = options.headers or {}
173         local hdr = options.headers
174         local file, code, msg
175         
176         if target then
177                 file, code, msg = prepare_fd(target)
178                 if not file then
179                         return file, code, msg
180                 end
181         
182                 local off = file:tell()
183                 
184                 -- Set content range
185                 if off > 0 then
186                         hdr.Range = hdr.Range or ("bytes=" .. off .. "-")  
187                 end
188         end
189         
190         local code, resp, buffer, sock = httpc.request_raw(uri, options)
191         if not code then
192                 -- No success
193                 if file then
194                         file:close()
195                 end
196                 return code, resp, buffer
197         elseif hdr.Range and code ~= 206 then
198                 -- We wanted a part but we got the while file
199                 sock:close()
200                 if file then
201                         file:close()
202                 end
203                 return nil, -4, code, resp
204         elseif not hdr.Range and code ~= 200 then
205                 -- We encountered an error
206                 sock:close()
207                 if file then
208                         file:close()
209                 end
210                 return nil, -4, code, resp
211         end
212         
213         if cbs.on_header then
214                 local stat = {cbs.on_header(file, code, resp)}
215                 if stat[1] == false then
216                         if file then
217                                 file:close()
218                         end
219                         sock:close()
220                         return unpack(stat)
221                 elseif stat[2] then
222                         file = file and stat[2]
223                 end
224         end
225         
226         if not file then
227                 return nil, -5, "no target given"
228         end
229
230         local chunked = resp.headers["Transfer-Encoding"] == "chunked"
231         local stat
232
233         -- Write the buffer to file
234         file:writeall(buffer)
235         
236         repeat
237                 if not options.splice or not sock:is_socket() or chunked then
238                         break
239                 end
240                 
241                 -- This is a plain TCP socket and there is no encoding so we can splice
242         
243                 local pipein, pipeout, msg = nixio.pipe()
244                 if not pipein then
245                         sock:close()
246                         file:close()
247                         return pipein, pipeout, msg
248                 end
249                 
250                 
251                 -- Adjust splice values
252                 local ssize = 65536
253                 local smode = nixio.splice_flags("move", "more")
254                 
255                 -- Splicing 512 bytes should never block on a fresh pipe
256                 local stat, code, msg = nixio.splice(sock, pipeout, 512, smode)
257                 if stat == nil then
258                         break
259                 end
260                 
261                 -- Now do the real splicing
262                 local cb = cbs.on_write
263                 if options.splice == "asynchronous" then
264                         stat, code, msg = splice_async(sock, pipeout, pipein, file, cb)
265                 elseif options.splice == "synchronous" then
266                         stat, code, msg = splice_sync(sock, pipeout, pipein, file, cb)
267                 else
268                         break
269                 end
270                 
271                 if stat == false then
272                         break
273                 end
274
275                 return stat, code, msg
276         until true
277         
278         local src = chunked and httpc.chunksource(sock) or sock:blocksource()
279         local snk = file:sink()
280         
281         if cbs.on_write then
282                 src = ltn12.source.chain(src, function(chunk)
283                         cbs.on_write(file)
284                         return chunk
285                 end)
286         end
287         
288         -- Fallback to read/write
289         stat, code, msg = ltn12.pump.all(src, snk)
290
291         file:close()
292         sock:close()
293         return stat and true, code, msg
294 end
295