ad2ac8db7bdabdf9a61bc37d574d43d2d1c4a796
[project/luci.git] / libs / 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 posix = require "posix"
118         local ssize = 65536
119         local smode = nixio.splice_flags("move", "more")
120         local stat
121         
122         -- This is probably the only forking http-client ;-)
123         local pid, code, msg = posix.fork()
124         if not pid then
125                 return pid, code, msg
126         elseif pid == 0 then
127                 pipein:close()
128                 file:close()
129
130                 repeat
131                         stat, code = nixio.splice(sock, pipeout, ssize, smode)
132                 until not stat or stat == 0
133         
134                 pipeout:close()
135                 sock:close()
136                 os.exit(stat or code)
137         else
138                 pipeout:close()
139                 sock:close()
140                 
141                 repeat
142                         stat, code, msg = nixio.splice(pipein, file, ssize, smode)
143                         if cb then
144                                 cb(file)
145                         end
146                 until not stat or stat == 0
147                 
148                 pipein:close()
149                 file:close()
150                 
151                 if not stat then
152                         posix.kill(pid)
153                         posix.wait(pid)
154                         return stat, code, msg
155                 else
156                         pid, msg, code = posix.wait(pid)
157                         if msg == "exited" then
158                                 if code == 0 then
159                                         return true
160                                 else
161                                         return nil, code, nixio.strerror(code)
162                                 end
163                         else
164                                 return nil, -0x11, "broken pump"
165                         end
166                 end
167         end
168 end
169
170 function request_to_file(uri, target, options, cbs)
171         options = options or {}
172         cbs = cbs or {}
173         options.headers = options.headers or {}
174         local hdr = options.headers
175         
176         local file, code, msg = prepare_fd(target)
177         if not file then
178                 return file, code, msg
179         end
180         
181         local off = file:tell()
182         
183         -- Set content range
184         if off > 0 then
185                 hdr.Range = hdr.Range or ("bytes=" .. off .. "-")  
186         end
187         
188         local code, resp, buffer, sock = httpc.request_raw(uri, options)
189         if not code then
190                 -- No success
191                 file:close()
192                 return code, resp, buffer
193         elseif hdr.Range and code ~= 206 then
194                 -- We wanted a part but we got the while file
195                 sock:close()
196                 file:close()
197                 return nil, -4, code, resp
198         elseif not hdr.Range and code ~= 200 then
199                 -- We encountered an error
200                 sock:close()
201                 file:close()
202                 return nil, -4, code, resp
203         end
204         
205         if cbs.on_header then
206                 local stat = {cbs.on_header(file, code, resp)}
207                 if stat[1] == false then
208                         file:close()
209                         sock:close()
210                         return unpack(stat)
211                 end
212         end
213
214         local chunked = resp.headers["Transfer-Encoding"] == "chunked"
215         local stat
216
217         -- Write the buffer to file
218         file:writeall(buffer)
219         
220         repeat
221                 if not options.splice or not sock:is_socket() or chunked then
222                         break
223                 end
224                 
225                 -- This is a plain TCP socket and there is no encoding so we can splice
226         
227                 local pipein, pipeout, msg = nixio.pipe()
228                 if not pipein then
229                         sock:close()
230                         file:close()
231                         return pipein, pipeout, msg
232                 end
233                 
234                 
235                 -- Adjust splice values
236                 local ssize = 65536
237                 local smode = nixio.splice_flags("move", "more")
238                 
239                 -- Splicing 512 bytes should never block on a fresh pipe
240                 local stat, code, msg = nixio.splice(sock, pipeout, 512, smode)
241                 if stat == nil then
242                         break
243                 end
244                 
245                 -- Now do the real splicing
246                 local cb = cbs.on_write
247                 if options.splice == "asynchronous" then
248                         stat, code, msg = splice_async(sock, pipeout, pipein, file, cb)
249                 elseif options.splice == "synchronous" then
250                         stat, code, msg = splice_sync(sock, pipeout, pipein, file, cb)
251                 else
252                         break
253                 end
254                 
255                 if stat == false then
256                         break
257                 end
258
259                 return stat, code, msg
260         until true
261         
262         local src = chunked and httpc.chunksource(sock) or sock:blocksource()
263         local snk = file:sink()
264         
265         if cbs.on_write then
266                 src = ltn12.source.chain(src, function(chunk)
267                         cbs.on_write(file)
268                         return chunk
269                 end)
270         end
271         
272         -- Fallback to read/write
273         stat, code, msg = ltn12.pump.all(src, snk)
274
275         file:close()
276         sock:close()
277         return stat and true, code, msg
278 end
279