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