summary refs log tree commit diff stats
path: root/lib/pure/httpclient.nim
diff options
context:
space:
mode:
authordef <dennis@felsin9.de>2015-01-04 05:36:43 +0100
committerdef <dennis@felsin9.de>2015-01-05 00:14:53 +0100
commit38392d2d037c32e3b5773af82f28a2950858f816 (patch)
treef5246553ccef7dbcdf779923b08e60606bd02b04 /lib/pure/httpclient.nim
parent5ee4806aa322a0943efdd0fb89b5c2d1b1c1ce40 (diff)
downloadNim-38392d2d037c32e3b5773af82f28a2950858f816.tar.gz
Add multipart parameter to httpclient's post and postContent
Diffstat (limited to 'lib/pure/httpclient.nim')
-rw-r--r--lib/pure/httpclient.nim163
1 files changed, 128 insertions, 35 deletions
diff --git a/lib/pure/httpclient.nim b/lib/pure/httpclient.nim
index a4b8bbedb..b6ff62bdc 100644
--- a/lib/pure/httpclient.nim
+++ b/lib/pure/httpclient.nim
@@ -32,21 +32,12 @@
 ## the server.
 ##
 ## .. code-block:: Nim
-##   var headers: string = "Content-Type: multipart/form-data; boundary=xyz\c\L"
-##   var body: string = "--xyz\c\L"
-##   # soap 1.2 output
-##   body.add("Content-Disposition: form-data; name=\"output\"\c\L")
-##   body.add("\c\Lsoap12\c\L")
+##   var data = newData()
+##   data["output"] = "soap12"
+##   data["uploaded_file"] = ("test.html", "text/html",
+##     "<html><head></head><body><p>test</p></body></html>")
 ##
-##   # html
-##   body.add("--xyz\c\L")
-##   body.add("Content-Disposition: form-data; name=\"uploaded_file\";" &
-##            " filename=\"test.html\"\c\L")
-##   body.add("Content-Type: text/html\c\L")
-##   body.add("\c\L<html><head></head><body><p>test</p></body></html>\c\L")
-##   body.add("--xyz--")
-##
-##   echo(postContent("http://validator.w3.org/check", headers, body))
+##   echo postContent("http://validator.w3.org/check", multipart=data)
 ##
 ## Asynchronous HTTP requests
 ## ==========================
@@ -88,7 +79,7 @@
 ## constructor should be used for this purpose. However,
 ## currently only basic authentication is supported.
 
-import net, strutils, uri, parseutils, strtabs, base64, os
+import net, strutils, uri, parseutils, strtabs, base64, os, mimetypes, math
 import asyncnet, asyncdispatch
 import rawsockets
 
@@ -103,6 +94,10 @@ type
     url*: Uri
     auth*: string
 
+  MultipartEntries* = openarray[tuple[name, content: string]]
+  MultipartData* = ref object
+    content: seq[string]
+
   ProtocolError* = object of IOError   ## exception that is raised when server
                                        ## does not conform to the implemented
                                        ## protocol
@@ -282,6 +277,104 @@ proc newProxy*(url: string, auth = ""): Proxy =
   ## Constructs a new ``TProxy`` object.
   result = Proxy(url: parseUri(url), auth: auth)
 
+proc newData*: MultipartData =
+  ## Constructs a new ``MultipartData`` object.
+  MultipartData(content: @[])
+
+proc add*(p: var MultipartData, name, content: string, filename: string = nil, contentType: string = nil) =
+  ## Add a value to the multipart data. Raises a `ValueError` exception if
+  ## `name`, `filename` or `contentType` contain newline characters.
+
+  if {'\c','\L'} in name:
+    raise newException(ValueError, "name contains a newline character")
+  if filename != nil and {'\c','\L'} in filename:
+    raise newException(ValueError, "filename contains a newline character")
+  if contentType != nil and {'\c','\L'} in contentType:
+    raise newException(ValueError, "contentType contains a newline character")
+
+  var str = "Content-Disposition: form-data; name=\"" & name & "\""
+  if filename != nil:
+    str.add("; filename=\"" & filename & "\"")
+  str.add("\c\L")
+  if contentType != nil:
+    str.add("Content-Type: " & contentType & "\c\L")
+  str.add("\c\L" & content & "\c\L")
+
+  p.content.add(str)
+
+proc add*(p: var MultipartData, xs: MultipartEntries): MultipartData {.discardable.} =
+  ## Add a list of multipart entries to the multipart data `p`. All values are
+  ## added without a filename and without a content type.
+  ##
+  ## .. code-block:: Nim
+  ##   data.add({"action": "login", "format": "json"})
+  for name, content in xs.items:
+    p.add(name, content)
+  result = p
+
+proc newData*(xs: MultipartEntries): MultipartData =
+  ## Create a new multipart data object and fill it with the entries `xs`
+  ## directly.
+  ##
+  ## .. code-block:: Nim
+  ##   var data = newData({"action": "login", "format": "json"})
+  result = MultipartData(content: @[])
+  result.add(xs)
+
+proc addFiles*(p: var MultipartData, xs: openarray[tuple[name, file: string]]) {.discardable.} =
+  ## Add files to a multipart data object. The file will be opened from your
+  ## disk, read and sent with the automatically determined MIME type. Raises an
+  ## `IOError` if the file cannot be opened or reading fails. To manually
+  ## specify file content, filename and MIME type, use `[]=` instead.
+  ##
+  ## .. code-block:: Nim
+  ##   data.addFiles({"uploaded_file": "public/test.html"})
+  var m = newMimetypes()
+  for name, file in xs.items:
+    var contentType: string
+    let (dir, fName, ext) = splitFile(file)
+    if ext.len > 0:
+      contentType = m.getMimetype(ext[1..ext.high], nil)
+    p.add(name, readFile(file), fName & ext, contentType)
+
+proc `[]=`*(p: var MultipartData, name, content: string) =
+  ## Add a multipart entry to the multipart data `p`. The value is added
+  ## without a filename and without a content type.
+  ##
+  ## .. code-block:: Nim
+  ##   data["username"] = "NimUser"
+  p.add(name, content)
+
+proc `[]=`*(p: var MultipartData, name: string, file: tuple[name, contentType, content: string]) =
+  ## Add a file to the multipart data `p`, specifying filename, contentType and
+  ## content manually.
+  ##
+  ## .. code-block:: Nim
+  ##   data["uploaded_file"] = ("test.html", "text/html",
+  ##     "<html><head></head><body><p>test</p></body></html>")
+  p.add(name, file.content, file.name, file.contentType)
+
+proc format(p: MultipartData): tuple[header, body: string] =
+  if p.content == nil or p.content.len == 0:
+    return ("", "")
+
+  # Create boundary that is not in the data to be formatted
+  var bound: string
+  while true:
+    bound = $random(int.high)
+    var found = false
+    for s in p.content:
+      if bound in s:
+        found = true
+    if not found:
+      break
+
+  result.header = "Content-Type: multipart/form-data; boundary=" & bound & "\c\L"
+  result.body = ""
+  for s in p.content:
+    result.body.add("--" & bound & "\c\L" & s)
+  result.body.add("--" & bound & "--\c\L")
+
 proc request*(url: string, httpMethod = httpGET, extraHeaders = "",
               body = "",
               sslContext: SSLContext = defaultSSLContext,
@@ -389,15 +482,21 @@ proc post*(url: string, extraHeaders = "", body = "",
            maxRedirects = 5,
            sslContext: SSLContext = defaultSSLContext,
            timeout = -1, userAgent = defUserAgent,
-           proxy: Proxy = nil): Response =
+           proxy: Proxy = nil,
+           multipart: MultipartData = nil): Response =
   ## | POSTs ``body`` to the ``url`` and returns a ``Response`` object.
   ## | This proc adds the necessary Content-Length header.
   ## | This proc also handles redirection.
   ## | Extra headers can be specified and must be separated by ``\c\L``.
   ## | An optional timeout can be specified in miliseconds, if reading from the
   ## server takes longer than specified an ETimeout exception will be raised.
-  var xh = extraHeaders & "Content-Length: " & $len(body) & "\c\L"
-  result = request(url, httpPOST, xh, body, sslContext, timeout, userAgent,
+  ## | The optional ``multipart`` parameter can be used to create
+  ## ``multipart/form-data`` POSTs comfortably.
+  let (mpHeaders, mpBody) = format(multipart)
+
+  var xb = mpBody & body
+  var xh = extraHeaders & mpHeaders & "Content-Length: " & $len(xb) & "\c\L"
+  result = request(url, httpPOST, xh, xb, sslContext, timeout, userAgent,
                    proxy)
   var lastUrl = ""
   for i in 1..maxRedirects:
@@ -412,14 +511,17 @@ proc postContent*(url: string, extraHeaders = "", body = "",
                   maxRedirects = 5,
                   sslContext: SSLContext = defaultSSLContext,
                   timeout = -1, userAgent = defUserAgent,
-                  proxy: Proxy = nil): string =
+                  proxy: Proxy = nil,
+                  multipart: MultipartData = nil): string =
   ## | POSTs ``body`` to ``url`` and returns the response's body as a string
   ## | Raises exceptions for the status codes ``4xx`` and ``5xx``
   ## | Extra headers can be specified and must be separated by ``\c\L``.
   ## | An optional timeout can be specified in miliseconds, if reading from the
   ## server takes longer than specified an ETimeout exception will be raised.
+  ## | The optional ``multipart`` parameter can be used to create
+  ## ``multipart/form-data`` POSTs comfortably.
   var r = post(url, extraHeaders, body, maxRedirects, sslContext, timeout,
-               userAgent, proxy)
+               userAgent, proxy, multipart)
   if r.status[0] in {'4','5'}:
     raise newException(HttpRequestError, r.status)
   else:
@@ -710,18 +812,9 @@ when isMainModule:
     #var r = get("http://validator.w3.org/check?uri=http%3A%2F%2Fgoogle.com&
     #  charset=%28detect+automatically%29&doctype=Inline&group=0")
 
-    var headers: string = "Content-Type: multipart/form-data; boundary=xyz\c\L"
-    var body: string = "--xyz\c\L"
-    # soap 1.2 output
-    body.add("Content-Disposition: form-data; name=\"output\"\c\L")
-    body.add("\c\Lsoap12\c\L")
-
-    # html
-    body.add("--xyz\c\L")
-    body.add("Content-Disposition: form-data; name=\"uploaded_file\";" &
-             " filename=\"test.html\"\c\L")
-    body.add("Content-Type: text/html\c\L")
-    body.add("\c\L<html><head></head><body><p>test</p></body></html>\c\L")
-    body.add("--xyz--")
-
-    echo(postContent("http://validator.w3.org/check", headers, body))
+    var data = newData()
+    data["output"] = "soap12"
+    data["uploaded_file"] = ("test.html", "text/html",
+      "<html><head></head><body><p>test</p></body></html>")
+
+    echo postContent("http://validator.w3.org/check", multipart=data)