123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- -- Copyright 2009 Steven Barth <steven@midlink.org>
- -- Licensed to the public under the Apache License 2.0.
- require "nixio.util"
- local nixio = require "nixio"
- local httpc = require "luci.httpclient"
- local ltn12 = require "luci.ltn12"
- local print, tonumber, require, unpack = print, tonumber, require, unpack
- module "luci.httpclient.receiver"
- local function prepare_fd(target)
- -- Open fd for appending
- 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("tlock")
- if not stat then
- return stat, code, msg
- end
-
- file:seek(0, "end")
-
- 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 ssize = 65536
- local smode = nixio.splice_flags("move", "more")
- local stat
-
- -- This is probably the only forking http-client ;-)
- local pid, code, msg = nixio.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
- nixio.kill(pid, 15)
- nixio.wait(pid)
- return stat, code, msg
- else
- pid, msg, code = nixio.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 {}
- cbs = cbs or {}
- options.headers = options.headers or {}
- local hdr = options.headers
- local file, code, msg
-
- if target then
- file, code, msg = prepare_fd(target)
- if not file then
- return file, code, msg
- end
-
- local off = file:tell()
-
- -- Set content range
- if off > 0 then
- hdr.Range = hdr.Range or ("bytes=" .. off .. "-")
- end
- end
-
- local code, resp, buffer, sock = httpc.request_raw(uri, options)
- if not code then
- -- No success
- if file then
- file:close()
- end
- return code, resp, buffer
- elseif hdr.Range and code ~= 206 then
- -- We wanted a part but we got the while file
- sock:close()
- if file then
- file:close()
- end
- return nil, -4, code, resp
- elseif not hdr.Range and code ~= 200 then
- -- We encountered an error
- sock:close()
- if file then
- file:close()
- end
- return nil, -4, code, resp
- end
-
- if cbs.on_header then
- local stat = {cbs.on_header(file, code, resp)}
- if stat[1] == false then
- if file then
- file:close()
- end
- sock:close()
- return unpack(stat)
- elseif stat[2] then
- file = file and stat[2]
- end
- end
-
- if not file then
- return nil, -5, "no target given"
- end
- local chunked = resp.headers["Transfer-Encoding"] == "chunked"
- local stat
- -- Write the buffer to file
- file:writeall(buffer)
-
- repeat
- if not options.splice or not sock:is_socket() or chunked then
- break
- end
-
- -- This is a plain TCP socket and there is no encoding so we can splice
-
- local pipein, pipeout, msg = nixio.pipe()
- if not pipein then
- sock:close()
- file:close()
- return pipein, pipeout, msg
- end
-
-
- -- Adjust splice values
- local ssize = 65536
- local smode = nixio.splice_flags("move", "more")
-
- -- 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
-
- -- 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
-
- if stat == false then
- break
- end
- return stat, code, msg
- until true
-
- local src = chunked and httpc.chunksource(sock) or sock:blocksource()
- local snk = file:sink()
-
- if cbs.on_write then
- src = ltn12.source.chain(src, function(chunk)
- cbs.on_write(file)
- return chunk
- end)
- end
-
- -- Fallback to read/write
- stat, code, msg = ltn12.pump.all(src, snk)
- file:close()
- sock:close()
- return stat and true, code, msg
- end
|