More splicing stuff
[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 = print, tonumber, require
21
22 module "luci.httpclient.receiver"
23
24 local function prepare_fd(target)
25         -- Open fd for appending
26         local file, code, msg = nixio.open(target, "r+")
27         if not file and code == nixio.const.ENOENT then
28                 file, code, msg = nixio.open(target, "w")
29                 if file then
30                         file:flush()
31                 end
32         end
33         if not file then
34                 return file, code, msg
35         end
36         
37         -- Acquire lock
38         local stat, code, msg = file:lock("ex", "nb")
39         if not stat then
40                 return stat, code, msg
41         end
42         
43         file:seek(0, "end") 
44         
45         return file
46 end
47
48 local function splice_async(sock, pipeout, pipein, file, cb)
49         local ssize = 65536
50         local smode = nixio.splice_flags("move", "more", "nonblock")
51
52         -- Set pipe non-blocking otherwise we might end in a deadlock
53         local stat, code, msg = pipein:setblocking(false)
54         if stat then
55                 stat, code, msg = pipeout:setblocking(false)
56         end
57         if not stat then
58                 return stat, code, msg
59         end
60         
61         
62         local pollsock = {
63                 {fd=sock, events=nixio.poll_flags("in")}
64         }
65         
66         local pollfile = {
67                 {fd=file, events=nixio.poll_flags("out")}
68         }
69         
70         local done
71         local active -- Older splice implementations sometimes don't detect EOS
72         
73         repeat
74                 active = false
75                 
76                 -- Socket -> Pipe
77                 repeat
78                         nixio.poll(pollsock, 15000)
79                 
80                         stat, code, msg = nixio.splice(sock, pipeout, ssize, smode)
81                         if stat == nil then
82                                 return stat, code, msg
83                         elseif stat == 0 then
84                                 done = true
85                                 break
86                         elseif stat then
87                                 active = true
88                         end
89                 until stat == false
90                 
91                 -- Pipe -> File
92                 repeat
93                         nixio.poll(pollfile, 15000)
94                 
95                         stat, code, msg = nixio.splice(pipein, file, ssize, smode)
96                         if stat == nil then
97                                 return stat, code, msg
98                         elseif stat then
99                                 active = true
100                         end
101                 until stat == false
102                 
103                 if cb then
104                         cb(file)
105                 end
106                 
107                 if not active then
108                         -- We did not splice any data, maybe EOS, fallback to default
109                         return false
110                 end
111         until done
112         
113         pipein:close()
114         pipeout:close()
115         sock:close()
116         file:close()
117         return true
118 end
119
120 local function splice_sync(sock, pipeout, pipein, file, cb)
121         local os = require "os"
122         local posix = require "posix"
123         local ssize = 65536
124         local smode = nixio.splice_flags("move", "more")
125         local stat
126         
127         -- This is probably the only forking http-client ;-)
128         local pid, code, msg = posix.fork()
129         if not pid then
130                 return pid, code, msg
131         elseif pid == 0 then
132                 pipein:close()
133                 file:close()
134
135                 repeat
136                         stat, code = nixio.splice(sock, pipeout, ssize, smode)
137                 until not stat or stat == 0
138         
139                 pipeout:close()
140                 sock:close()
141                 os.exit(stat or code)
142         else
143                 pipeout:close()
144                 sock:close()
145                 
146                 repeat
147                         stat, code, msg = nixio.splice(pipein, file, ssize, smode)
148                         if cb then
149                                 cb(file)
150                         end
151                 until not stat or stat == 0
152                 
153                 pipein:close()
154                 file:close()
155                 
156                 if not stat then
157                         posix.kill(pid)
158                         posix.wait(pid)
159                         return stat, code, msg
160                 else
161                         pid, msg, code = posix.wait(pid)
162                         if msg == "exited" then
163                                 if code == 0 then
164                                         return true
165                                 else
166                                         return nil, code, nixio.strerror(code)
167                                 end
168                         else
169                                 return nil, -0x11, "broken pump"
170                         end
171                 end
172         end
173 end
174
175 function request_to_file(uri, target, options, cbs)
176         options = options or {}
177         cbs = cbs or {}
178         options.headers = options.headers or {}
179         local hdr = options.headers
180         
181         local file, code, msg = prepare_fd(target)
182         if not file then
183                 return file, code, msg
184         end
185         
186         local off = file:tell()
187         
188         -- Set content range
189         if off > 0 then
190                 hdr.Range = hdr.Range or ("bytes=" .. off .. "-")  
191         end
192         
193         local code, resp, buffer, sock = httpc.request_raw(uri, options)
194         if not code then
195                 -- No success
196                 file:close()
197                 return code, resp, buffer
198         elseif hdr.Range and code ~= 206 then
199                 -- We wanted a part but we got the while file
200                 sock:close()
201                 file:close()
202                 return nil, -4, code, resp
203         elseif not hdr.Range and code ~= 200 then
204                 -- We encountered an error
205                 sock:close()
206                 file:close()
207                 return nil, -4, code, resp
208         end
209         
210         if cbs.on_header then
211                 cbs.on_header(file, code, resp)
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