httpclient: Improved callback support
[project/luci.git] / libs / httpclient / luasrc / httpclient / receiver.lua
index f478fe8..55503f1 100644 (file)
@@ -14,28 +14,23 @@ $Id$
 
 require "nixio.util"
 local nixio = require "nixio"
-local httpclient = require "luci.httpclient"
+local httpc = require "luci.httpclient"
 local ltn12 = require "luci.ltn12"
 
-local print = print
+local print, tonumber, require = print, tonumber, require
 
 module "luci.httpclient.receiver"
 
 local function prepare_fd(target)
        -- Open fd for appending
-       local file, code, msg = nixio.open(target, "r+")
-       if not file and code == nixio.const.ENOENT then
-               file, code, msg = nixio.open(target, "w")
-               if file then
-                       file:flush()
-               end
-       end
+       local oflags = nixio.open_flags("wronly", "creat")
+       local file, code, msg = nixio.open(target, oflags)
        if not file then
                return file, code, msg
        end
        
        -- Acquire lock
-       local stat, code, msg = file:lock("ex", "nb")
+       local stat, code, msg = file:lock("tlock")
        if not stat then
                return stat, code, msg
        end
@@ -45,6 +40,132 @@ local function prepare_fd(target)
        return file
 end
 
+local function splice_async(sock, pipeout, pipein, file, cb)
+       local ssize = 65536
+       local smode = nixio.splice_flags("move", "more", "nonblock")
+
+       -- Set pipe non-blocking otherwise we might end in a deadlock
+       local stat, code, msg = pipein:setblocking(false)
+       if stat then
+               stat, code, msg = pipeout:setblocking(false)
+       end
+       if not stat then
+               return stat, code, msg
+       end
+       
+       
+       local pollsock = {
+               {fd=sock, events=nixio.poll_flags("in")}
+       }
+       
+       local pollfile = {
+               {fd=file, events=nixio.poll_flags("out")}
+       }
+       
+       local done
+       local active -- Older splice implementations sometimes don't detect EOS
+       
+       repeat
+               active = false
+               
+               -- Socket -> Pipe
+               repeat
+                       nixio.poll(pollsock, 15000)
+               
+                       stat, code, msg = nixio.splice(sock, pipeout, ssize, smode)
+                       if stat == nil then
+                               return stat, code, msg
+                       elseif stat == 0 then
+                               done = true
+                               break
+                       elseif stat then
+                               active = true
+                       end
+               until stat == false
+               
+               -- Pipe -> File
+               repeat
+                       nixio.poll(pollfile, 15000)
+               
+                       stat, code, msg = nixio.splice(pipein, file, ssize, smode)
+                       if stat == nil then
+                               return stat, code, msg
+                       elseif stat then
+                               active = true
+                       end
+               until stat == false
+               
+               if cb then
+                       cb(file)
+               end
+               
+               if not active then
+                       -- We did not splice any data, maybe EOS, fallback to default
+                       return false
+               end
+       until done
+       
+       pipein:close()
+       pipeout:close()
+       sock:close()
+       file:close()
+       return true
+end
+
+local function splice_sync(sock, pipeout, pipein, file, cb)
+       local os = require "os"
+       local posix = require "posix"
+       local ssize = 65536
+       local smode = nixio.splice_flags("move", "more")
+       local stat
+       
+       -- This is probably the only forking http-client ;-)
+       local pid, code, msg = posix.fork()
+       if not pid then
+               return pid, code, msg
+       elseif pid == 0 then
+               pipein:close()
+               file:close()
+
+               repeat
+                       stat, code = nixio.splice(sock, pipeout, ssize, smode)
+               until not stat or stat == 0
+       
+               pipeout:close()
+               sock:close()
+               os.exit(stat or code)
+       else
+               pipeout:close()
+               sock:close()
+               
+               repeat
+                       stat, code, msg = nixio.splice(pipein, file, ssize, smode)
+                       if cb then
+                               cb(file)
+                       end
+               until not stat or stat == 0
+               
+               pipein:close()
+               file:close()
+               
+               if not stat then
+                       posix.kill(pid)
+                       posix.wait(pid)
+                       return stat, code, msg
+               else
+                       pid, msg, code = posix.wait(pid)
+                       if msg == "exited" then
+                               if code == 0 then
+                                       return true
+                               else
+                                       return nil, code, nixio.strerror(code)
+                               end
+                       else
+                               return nil, -0x11, "broken pump"
+                       end
+               end
+       end
+end
 
 function request_to_file(uri, target, options, cbs)
        options = options or {}
@@ -64,7 +185,7 @@ function request_to_file(uri, target, options, cbs)
                hdr.Range = hdr.Range or ("bytes=" .. off .. "-")  
        end
        
-       local code, resp, buffer, sock = httpclient.request_raw(uri, options)
+       local code, resp, buffer, sock = httpc.request_raw(uri, options)
        if not code then
                -- No success
                file:close()
@@ -82,17 +203,22 @@ function request_to_file(uri, target, options, cbs)
        end
        
        if cbs.on_header then
-               cbs.on_header(file, code, resp)
+               local stat = {cbs.on_header(file, code, resp)}
+               if stat[1] == false then
+                       file:close()
+                       sock:close()
+                       return true, nil, nil, unpack(stat, 2)
+               end
        end
 
        local chunked = resp.headers["Transfer-Encoding"] == "chunked"
+       local stat
 
        -- Write the buffer to file
        file:writeall(buffer)
-       print ("Buffered data: " .. #buffer .. " Byte")
        
        repeat
-               if not sock:is_socket() or chunked then
+               if not options.splice or not sock:is_socket() or chunked then
                        break
                end
                
@@ -106,78 +232,34 @@ function request_to_file(uri, target, options, cbs)
                end
                
                
-               -- Disable blocking for the pipe otherwise we might end in a deadlock
-               local stat, code, msg = pipein:setblocking(false)
-               if stat then
-                       stat, code, msg = pipeout:setblocking(false)
-               end
-               if not stat then
-                       sock:close()
-                       file:close()
-                       return stat, code, msg
-               end
-               
-               
                -- Adjust splice values
                local ssize = 65536
-               local smode = nixio.splice_flags("move", "more", "nonblock")
+               local smode = nixio.splice_flags("move", "more")
                
-               local stat, code, msg = nixio.splice(sock, pipeout, ssize, smode)
+               -- Splicing 512 bytes should never block on a fresh pipe
+               local stat, code, msg = nixio.splice(sock, pipeout, 512, smode)
                if stat == nil then
                        break
                end
                
-               local pollsock = {
-                       {fd=sock, events=nixio.poll_flags("in")}
-               }
-               
-               local pollfile = {
-                       {fd=file, events=nixio.poll_flags("out")}
-               }
-               
-               local done
-               
-               repeat
-                       -- Socket -> Pipe
-                       repeat
-                               nixio.poll(pollsock, 15000)
-                       
-                               stat, code, msg = nixio.splice(sock, pipeout, ssize, smode)
-                               if stat == nil then
-                                       sock:close()
-                                       file:close()
-                                       return stat, code, msg
-                               elseif stat == 0 then
-                                       done = true
-                                       break
-                               end
-                       until stat == false
-                       
-                       -- Pipe -> File
-                       repeat
-                               nixio.poll(pollfile, 15000)
-                       
-                               stat, code, msg = nixio.splice(pipein, file, ssize, smode)
-                               if stat == nil then
-                                       sock:close()
-                                       file:close()
-                                       return stat, code, msg
-                               end
-                       until stat == false
-                       
-                       if cbs.on_write then
-                               cbs.on_write(file)
-                       end
-               until done
+               -- Now do the real splicing
+               local cb = cbs.on_write
+               if options.splice == "asynchronous" then
+                       stat, code, msg = splice_async(sock, pipeout, pipein, file, cb)
+               elseif options.splice == "synchronous" then
+                       stat, code, msg = splice_sync(sock, pipeout, pipein, file, cb)
+               else
+                       break
+               end
                
-               file:close()
-               sock:close()
-               return true
+               if stat == false then
+                       break
+               end
+
+               return stat, code, msg
        until true
        
-       print "Warning: splice() failed, falling back to read/write mode"
-       
-       local src = chunked and httpclient.chunksource(sock) or sock:blocksource()
+       local src = chunked and httpc.chunksource(sock) or sock:blocksource()
        local snk = file:sink()
        
        if cbs.on_write then
@@ -188,10 +270,10 @@ function request_to_file(uri, target, options, cbs)
        end
        
        -- Fallback to read/write
-       local stat, code, msg = ltn12.pump.all(src, snk)
-       if stat then
-               file:close()
-               sock:close()
-       end
-       return stat, code, msg
-end
\ No newline at end of file
+       stat, code, msg = ltn12.pump.all(src, snk)
+
+       file:close()
+       sock:close()
+       return stat and true, code, msg
+end
+