Implements onProgressChanged callback for httpclient.

This commit is contained in:
Dominik Picheta 2016-09-24 22:36:19 +02:00
commit cff6ec2155
2 changed files with 73 additions and 14 deletions

View file

@ -80,7 +80,7 @@
## currently only basic authentication is supported. ## currently only basic authentication is supported.
import net, strutils, uri, parseutils, strtabs, base64, os, mimetypes, import net, strutils, uri, parseutils, strtabs, base64, os, mimetypes,
math, random, httpcore math, random, httpcore, times
import asyncnet, asyncdispatch import asyncnet, asyncdispatch
import nativesockets import nativesockets
@ -669,17 +669,30 @@ proc generateHeaders(requestUrl: Uri, httpMethod: string,
add(result, "\c\L") add(result, "\c\L")
type type
ProgressChangedProc*[ReturnType] =
proc (total, progress, speed: BiggestInt):
ReturnType {.closure, gcsafe.}
HttpClientBase*[SocketType] = ref object HttpClientBase*[SocketType] = ref object
socket: SocketType socket: SocketType
connected: bool connected: bool
currentURL: Uri ## Where we are currently connected. currentURL: Uri ## Where we are currently connected.
headers*: HttpHeaders headers*: HttpHeaders ## Headers to send in requests.
maxRedirects: int maxRedirects: int
userAgent: string userAgent: string
timeout: int ## Only used for blocking HttpClient for now. timeout: int ## Only used for blocking HttpClient for now.
proxy: Proxy proxy: Proxy
## ``nil`` or the callback to call when request progress changes.
when SocketType is Socket:
onProgressChanged*: ProgressChangedProc[void]
else:
onProgressChanged*: ProgressChangedProc[Future[void]]
when defined(ssl): when defined(ssl):
sslContext: net.SslContext sslContext: net.SslContext
contentTotal: BiggestInt
contentProgress: BiggestInt
oneSecondProgress: BiggestInt
lastProgressReport: float
type type
HttpClient* = HttpClientBase[Socket] HttpClient* = HttpClientBase[Socket]
@ -708,6 +721,7 @@ proc newHttpClient*(userAgent = defUserAgent,
result.maxRedirects = maxRedirects result.maxRedirects = maxRedirects
result.proxy = proxy result.proxy = proxy
result.timeout = timeout result.timeout = timeout
result.onProgressChanged = nil
when defined(ssl): when defined(ssl):
result.sslContext = sslContext result.sslContext = sslContext
@ -737,6 +751,7 @@ proc newAsyncHttpClient*(userAgent = defUserAgent,
result.maxRedirects = maxRedirects result.maxRedirects = maxRedirects
result.proxy = proxy result.proxy = proxy
result.timeout = -1 # TODO result.timeout = -1 # TODO
result.onProgressChanged = nil
when defined(ssl): when defined(ssl):
result.sslContext = sslContext result.sslContext = sslContext
@ -746,19 +761,37 @@ proc close*(client: HttpClient | AsyncHttpClient) =
client.socket.close() client.socket.close()
client.connected = false client.connected = false
proc recvFull(socket: Socket | AsyncSocket, proc reportProgress(client: HttpClient | AsyncHttpClient,
progress: BiggestInt) {.multisync.} =
client.contentProgress += progress
client.oneSecondProgress += progress
if epochTime() - client.lastProgressReport >= 1.0:
if not client.onProgressChanged.isNil:
await client.onProgressChanged(client.contentTotal,
client.contentProgress,
client.oneSecondProgress)
client.oneSecondProgress = 0
client.lastProgressReport = epochTime()
proc recvFull(client: HttpClient | AsyncHttpClient,
size: int, timeout: int): Future[string] {.multisync.} = size: int, timeout: int): Future[string] {.multisync.} =
## Ensures that all the data requested is read and returned. ## Ensures that all the data requested is read and returned.
result = "" result = ""
while true: while true:
if size == result.len: break if size == result.len: break
when socket is Socket:
let data = socket.recv(size - result.len, timeout) let remainingSize = size - result.len
let sizeToRecv = min(remainingSize, net.BufferSize)
when client.socket is Socket:
let data = client.socket.recv(sizeToRecv, timeout)
else: else:
let data = await socket.recv(size - result.len) let data = await client.socket.recv(sizeToRecv)
if data == "": break # We've been disconnected. if data == "": break # We've been disconnected.
result.add data result.add data
await reportProgress(client, data.len)
proc parseChunks(client: HttpClient | AsyncHttpClient): Future[string] proc parseChunks(client: HttpClient | AsyncHttpClient): Future[string]
{.multisync.} = {.multisync.} =
result = "" result = ""
@ -786,10 +819,10 @@ proc parseChunks(client: HttpClient | AsyncHttpClient): Future[string]
httpError("Invalid chunk size: " & chunkSizeStr) httpError("Invalid chunk size: " & chunkSizeStr)
inc(i) inc(i)
if chunkSize <= 0: if chunkSize <= 0:
discard await recvFull(client.socket, 2, client.timeout) # Skip \c\L discard await recvFull(client, 2, client.timeout) # Skip \c\L
break break
result.add await recvFull(client.socket, chunkSize, client.timeout) result.add await recvFull(client, chunkSize, client.timeout)
discard await recvFull(client.socket, 2, client.timeout) # Skip \c\L discard await recvFull(client, 2, client.timeout) # Skip \c\L
# Trailer headers will only be sent if the request specifies that we want # Trailer headers will only be sent if the request specifies that we want
# them: http://tools.ietf.org/html/rfc2616#section-3.6.1 # them: http://tools.ietf.org/html/rfc2616#section-3.6.1
@ -797,6 +830,12 @@ proc parseBody(client: HttpClient | AsyncHttpClient,
headers: HttpHeaders, headers: HttpHeaders,
httpVersion: string): Future[string] {.multisync.} = httpVersion: string): Future[string] {.multisync.} =
result = "" result = ""
# Reset progress from previous requests.
client.contentTotal = 0
client.contentProgress = 0
client.oneSecondProgress = 0
client.lastProgressReport = 0
if headers.getOrDefault"Transfer-Encoding" == "chunked": if headers.getOrDefault"Transfer-Encoding" == "chunked":
result = await parseChunks(client) result = await parseChunks(client)
else: else:
@ -805,8 +844,9 @@ proc parseBody(client: HttpClient | AsyncHttpClient,
var contentLengthHeader = headers.getOrDefault"Content-Length" var contentLengthHeader = headers.getOrDefault"Content-Length"
if contentLengthHeader != "": if contentLengthHeader != "":
var length = contentLengthHeader.parseint() var length = contentLengthHeader.parseint()
client.contentTotal = length
if length > 0: if length > 0:
result = await client.socket.recvFull(length, client.timeout) result = await client.recvFull(length, client.timeout)
if result == "": if result == "":
httpError("Got disconnected while trying to read body.") httpError("Got disconnected while trying to read body.")
if result.len != length: if result.len != length:
@ -820,7 +860,7 @@ proc parseBody(client: HttpClient | AsyncHttpClient,
if headers.getOrDefault"Connection" == "close" or httpVersion == "1.0": if headers.getOrDefault"Connection" == "close" or httpVersion == "1.0":
var buf = "" var buf = ""
while true: while true:
buf = await client.socket.recvFull(4000, client.timeout) buf = await client.recvFull(4000, client.timeout)
if buf == "": break if buf == "": break
result.add(buf) result.add(buf)

View file

@ -7,6 +7,8 @@ from net import TimeoutError
import httpclient, asyncdispatch import httpclient, asyncdispatch
const manualTests = false
proc asyncTest() {.async.} = proc asyncTest() {.async.} =
var client = newAsyncHttpClient() var client = newAsyncHttpClient()
var resp = await client.request("http://example.com/") var resp = await client.request("http://example.com/")
@ -39,9 +41,18 @@ proc asyncTest() {.async.} =
resp = await client.post("http://validator.w3.org/check", multipart=data) resp = await client.post("http://validator.w3.org/check", multipart=data)
doAssert(resp.code.is2xx) doAssert(resp.code.is2xx)
# onProgressChanged
when manualTests:
proc onProgressChanged(total, progress, speed: BiggestInt) {.async.} =
echo("Downloaded ", progress, " of ", total)
echo("Current rate: ", speed div 1000, "kb/s")
client.onProgressChanged = onProgressChanged
discard await client.getContent("http://speedtest-ams2.digitalocean.com/100mb.test")
client.close() client.close()
# Proxy test # Proxy test
#when manualTests:
# client = newAsyncHttpClient(proxy = newProxy("http://51.254.106.76:80/")) # client = newAsyncHttpClient(proxy = newProxy("http://51.254.106.76:80/"))
# var resp = await client.request("https://github.com") # var resp = await client.request("https://github.com")
# echo resp # echo resp
@ -77,6 +88,14 @@ proc syncTest() =
resp = client.post("http://validator.w3.org/check", multipart=data) resp = client.post("http://validator.w3.org/check", multipart=data)
doAssert(resp.code.is2xx) doAssert(resp.code.is2xx)
# onProgressChanged
when manualTests:
proc onProgressChanged(total, progress, speed: BiggestInt) =
echo("Downloaded ", progress, " of ", total)
echo("Current rate: ", speed div 1000, "kb/s")
client.onProgressChanged = onProgressChanged
discard client.getContent("http://speedtest-ams2.digitalocean.com/100mb.test")
client.close() client.close()
# Timeout test. # Timeout test.