Drop support for luaposix and bitlib (obsoleted by nixio)
[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 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         
175         local file, code, msg = prepare_fd(target)
176         if not file then
177                 return file, code, msg
178         end
179         
180         local off = file:tell()
181         
182         -- Set content range
183         if off > 0 then
184                 hdr.Range = hdr.Range or ("bytes=" .. off .. "-")  
185         end
186         
187         local code, resp, buffer, sock = httpc.request_raw(uri, options)
188         if not code then
189                 -- No success
190                 file:close()
191                 return code, resp, buffer
192         elseif hdr.Range and code ~= 206 then
193                 -- We wanted a part but we got the while file
194                 sock:close()
195                 file:close()
196                 return nil, -4, code, resp
197         elseif not hdr.Range and code ~= 200 then
198                 -- We encountered an error
199                 sock:close()
200                 file:close()
201                 return nil, -4, code, resp
202         end
203         
204         if cbs.on_header then
205                 local stat = {cbs.on_header(file, code, resp)}
206                 if stat[1] == false then
207                         file:close()
208                         sock:close()
209                         return unpack(stat)
210                 end
211         end
212
213         local chunked = resp.headers["Transfer-Encoding"] == "chunked"
214         local stat
215
216         -- Write the buffer to file
217         file:writeall(buffer)
218         
219         repeat
220                 if not options.splice or not sock:is_socket() or chunked then
221                         break
222                 end
223                 
224                 -- This is a plain TCP socket and there is no encoding so we can splice
225         
226                 local pipein, pipeout, msg = nixio.pipe()
227                 if not pipein then
228                         sock:close()
229                         file:close()
230                         return pipein, pipeout, msg
231                 end
232                 
233                 
234                 -- Adjust splice values
235                 local ssize = 65536
236                 local smode = nixio.splice_flags("move", "more")
237                 
238                 -- Splicing 512 bytes should never block on a fresh pipe
239                 local stat, code, msg = nixio.splice(sock, pipeout, 512, smode)
240                 if stat == nil then
241                         break
242                 end
243                 
244                 -- Now do the real splicing
245                 local cb = cbs.on_write
246                 if options.splice == "asynchronous" then
247                         stat, code, msg = splice_async(sock, pipeout, pipein, file, cb)
248                 elseif options.splice == "synchronous" then
249                         stat, code, msg = splice_sync(sock, pipeout, pipein, file, cb)
250                 else
251                         break
252                 end
253                 
254                 if stat == false then
255                         break
256                 end
257
258                 return stat, code, msg
259         until true
260         
261         local src = chunked and httpc.chunksource(sock) or sock:blocksource()
262         local snk = file:sink()
263         
264         if cbs.on_write then
265                 src = ltn12.source.chain(src, function(chunk)
266                         cbs.on_write(file)
267                         return chunk
268                 end)
269         end
270         
271         -- Fallback to read/write
272         stat, code, msg = ltn12.pump.all(src, snk)
273
274         file:close()
275         sock:close()
276         return stat and true, code, msg
277 end
278