From 4aef0ea65ca0f01336ffa83f3658df2fe1cad844 Mon Sep 17 00:00:00 2001 From: Giovanni Visciano Date: Sun, 1 Oct 2017 16:57:14 +0200 Subject: [PATCH] multipart add_file_content --- lib/maxwell/adapter/hackney.ex | 6 ++- lib/maxwell/multipart.ex | 95 +++++++++++++++++++++++++++------ test/maxwell/multipart_test.exs | 92 +++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 17 deletions(-) diff --git a/lib/maxwell/adapter/hackney.ex b/lib/maxwell/adapter/hackney.ex index faeb9be..3564d35 100644 --- a/lib/maxwell/adapter/hackney.ex +++ b/lib/maxwell/adapter/hackney.ex @@ -15,7 +15,11 @@ if Code.ensure_loaded?(:hackney) do format_response(result, conn) end - def send_multipart(conn), do: send_direct(conn) + def send_multipart(conn) do + %Conn{req_body: {:multipart, multiparts}} = conn + {req_headers, req_body} = Util.multipart_encode(conn, multiparts) + send_direct(%Conn{conn | req_headers: req_headers, req_body: req_body}) + end def send_file(conn), do: send_direct(conn) diff --git a/lib/maxwell/multipart.ex b/lib/maxwell/multipart.ex index 489d7a5..13e90ac 100644 --- a/lib/maxwell/multipart.ex +++ b/lib/maxwell/multipart.ex @@ -9,9 +9,13 @@ defmodule Maxwell.Multipart do @type disposition_t :: {String.t, params_t} @type boundary_t :: String.t @type name_t :: String.t + @type file_content_t :: binary @type part_t :: {:file, Path.t} | {:file, Path.t, headers_t} | {:file, Path.t, disposition_t, headers_t} + | {:file_content, file_content_t, String.t} + | {:file_content, file_content_t, String.t, headers_t} + | {:file_content, file_content_t, String.t, disposition_t, headers_t} | {:mp_mixed, String.t, boundary_t} | {:mp_mixed_eof, boundary_t} | {name_t, binary} @@ -50,6 +54,28 @@ defmodule Maxwell.Multipart do append_part(multipart, {:file, path, disposition, extra_headers}) end + @spec add_file_content(t, file_content_t, String.t) :: t + def add_file_content(multipart, file_content, filename) do + append_part(multipart, {:file_content, file_content, filename}) + end + + @spec add_file_content(t, file_content_t, String.t, headers_t) :: t + def add_file_content(multipart, file_content, filename, extra_headers) do + append_part(multipart, {:file_content, file_content, filename, extra_headers}) + end + + @spec add_file_content(t, file_content_t, String.t, disposition_t, headers_t) :: t + def add_file_content(multipart, file_content, filename, disposition, extra_headers) do + append_part(multipart, {:file_content, file_content, filename, disposition, extra_headers}) + end + + @spec add_file_content_with_name(t, file_content_t, String.t, String.t) :: t + @spec add_file_content_with_name(t, file_content_t, String.t, String.t, headers_t) :: t + def add_file_content_with_name(multipart, file_content, filename, name, extra_headers \\ []) do + disposition = {"form-data", [{"name", name}, {"filename", filename}]} + append_part(multipart, {:file_content, file_content, filename, disposition, extra_headers}) + end + @spec add_field(t, String.t, binary) :: t def add_field(multipart, name, value) when is_binary(name) and is_binary(value) do append_part(multipart, {name, value}) @@ -78,11 +104,14 @@ defmodule Maxwell.Multipart do 1. `{:file, path}` 2. `{:file, path, extra_headers}` 3. `{:file, path, disposition, extra_headers}` - 4. `{:mp_mixed, name, mixed_boundary}` - 5. `{:mp_mixed_eof, mixed_boundary}` - 6. `{name, bin_data}` - 7. `{name, bin_data, extra_headers}` - 8. `{name, bin_data, disposition, extra_headers}` + 4. `{:file_content, file_content, filename}` + 5. `{:file_content, file_content, filename, extra_headers}` + 6. `{:file_content, file_content, filename, disposition, extra_headers}` + 7. `{:mp_mixed, name, mixed_boundary}` + 8. `{:mp_mixed_eof, mixed_boundary}` + 9. `{name, bin_data}` + 10. `{name, bin_data, extra_headers}` + 11. `{name, bin_data, disposition, extra_headers}` Returns `{body_binary, size}` @@ -95,14 +124,17 @@ defmodule Maxwell.Multipart do * `boundary` - multipart boundary. * `parts` - receives lists list's member format: - 1. `{:file, path}` - 2. `{:file, path, extra_headers}` - 3. `{:file, path, disposition, extra_headers}` - 4. `{:mp_mixed, name, mixed_boundary}` - 5. `{:mp_mixed_eof, mixed_boundary}` - 6. `{name, bin_data}` - 7. `{name, bin_data, extra_headers}` - 8. `{name, bin_data, disposition, extra_headers}` + 1. `{:file, path}` + 2. `{:file, path, extra_headers}` + 3. `{:file, path, disposition, extra_headers}` + 4. `{:file_content, file_content, filename}` + 5. `{:file_content, file_content, filename, extra_headers}` + 6. `{:file_content, file_content, filename, disposition, extra_headers}` + 7. `{:mp_mixed, name, mixed_boundary}` + 8. `{:mp_mixed_eof, mixed_boundary}` + 9. `{name, bin_data}` + 10. `{name, bin_data, extra_headers}` + 11. `{name, bin_data, disposition, extra_headers}` """ @spec encode_form(boundary :: boundary_t, parts :: [part_t]) :: {boundary_t, integer} @@ -132,8 +164,8 @@ defmodule Maxwell.Multipart do """ @spec len_mp_stream(boundary :: boundary_t, parts :: [part_t]) :: integer def len_mp_stream(boundary, parts) do - size = Enum.reduce(parts, 0, fn( - {:file, path}, acc_size) -> + size = Enum.reduce(parts, 0, fn + ({:file, path}, acc_size) -> {mp_header, len} = mp_file_header(%{path: path}, boundary) acc_size + byte_size(mp_header) + len + @eof_size ({:file, path, extra_headers}, acc_size) -> @@ -143,6 +175,16 @@ defmodule Maxwell.Multipart do file = %{path: path, extra_headers: extra_headers, disposition: disposition} {mp_header, len} = mp_file_header(file, boundary) acc_size + byte_size(mp_header) + len + @eof_size + ({:file_content, file_content, filename}, acc_size) -> + {mp_header, len} = mp_file_header(%{path: filename, filesize: byte_size(file_content)}, boundary) + acc_size + byte_size(mp_header) + len + @eof_size + ({:file_content, file_content, filename, extra_headers}, acc_size) -> + {mp_header, len} = mp_file_header(%{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers}, boundary) + acc_size + byte_size(mp_header) + len + @eof_size + ({:file_content, file_content, filename, disposition, extra_headers}, acc_size) -> + file = %{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers, disposition: disposition} + {mp_header, len} = mp_file_header(file, boundary) + acc_size + byte_size(mp_header) + len + @eof_size ({:mp_mixed, name, mixed_boundary}, acc_size) -> {mp_header, _} = mp_mixed_header(name, mixed_boundary) acc_size + byte_size(mp_header) + @eof_size + byte_size(mp_eof(mixed_boundary)) @@ -190,6 +232,27 @@ defmodule Maxwell.Multipart do encode_form(parts, boundary, acc, acc_size) end + defp encode_form([{:file_content, file_content, filename}|parts], boundary, acc, acc_size) do + {mp_header, len} = mp_file_header(%{path: filename, filesize: byte_size(file_content)}, boundary) + acc_size = acc_size + byte_size(mp_header) + len + @eof_size + acc = acc <> mp_header <> file_content <> "\r\n" + encode_form(parts, boundary, acc, acc_size) + end + defp encode_form([{:file_content, file_content, filename, extra_headers}|parts], boundary, acc, acc_size) do + file = %{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers} + {mp_header, len} = mp_file_header(file, boundary) + acc_size = acc_size + byte_size(mp_header) + len + @eof_size + acc = acc <> mp_header <> file_content <> "\r\n" + encode_form(parts, boundary, acc, acc_size) + end + defp encode_form([{:file_content, file_content, filename, disposition, extra_headers}|parts], boundary, acc, acc_size) do + file = %{path: filename, filesize: byte_size(file_content), extra_headers: extra_headers, disposition: disposition} + {mp_header, len} = mp_file_header(file, boundary) + acc_size = acc_size + byte_size(mp_header) + len + @eof_size + acc = acc <> mp_header <> file_content <> "\r\n" + encode_form(parts, boundary, acc, acc_size) + end + defp encode_form([{:mp_mixed, name, mixed_boundary}|parts], boundary, acc, acc_size) do {mp_header, _} = mp_mixed_header(name, mixed_boundary) acc_size = acc_size + byte_size(mp_header) + @eof_size @@ -228,7 +291,7 @@ defmodule Maxwell.Multipart do file_name = path |> :filename.basename |> to_string {disposition, params} = file[:disposition] || {"form-data", [{"name", "\"file\""}, {"filename", "\"" <> file_name <> "\""}]} ctype = :mimerl.filename(path) - len = :filelib.file_size(path) + len = file[:filesize] || :filelib.file_size(path) extra_headers = file[:extra_headers] || [] extra_headers = extra_headers |> Enum.map(fn({k, v}) -> {String.downcase(k), v} end) diff --git a/test/maxwell/multipart_test.exs b/test/maxwell/multipart_test.exs index d9e1f81..e33ffe3 100644 --- a/test/maxwell/multipart_test.exs +++ b/test/maxwell/multipart_test.exs @@ -57,6 +57,42 @@ defmodule Maxwell.MultipartTest do assert String.replace(body, boundary, "") == "--\r\ncontent-length: 47\r\ncontent-disposition: form-data; name=content; filename=test/maxwell/multipart_test_file.sh\r\ncontent-type: image/jpeg\r\n\r\n#!/usr/bin/env bash\necho \"test multipart file\"\n\r\n----\r\n" end + test "File Content base" do + boundary = Multipart.new_boundary + filename = "test.sh" + file_content = "xxx" + {body, size} = Multipart.encode_form(boundary, [{:file_content, file_content, filename}]) + assert size == 219 + assert String.starts_with?(body, "--" <> boundary) == true + assert String.ends_with?(body, boundary <> "--\r\n") == true + assert String.replace(body, boundary, "") == "--\r\ncontent-length: 3\r\ncontent-disposition: form-data; name=\"file\"; filename=\"test.sh\"\r\ncontent-type: application/x-sh\r\n\r\nxxx\r\n----\r\n" + end + + test "File Content ExtraHeaders" do + boundary = Multipart.new_boundary + filename = "test.sh" + file_content = "xxx" + extra_headers = [{"Content-Type", "image/jpeg"}] + {body, size} = Multipart.encode_form(boundary, [{:file_content, file_content, filename, extra_headers}]) + assert size == 213 + assert String.starts_with?(body, "--" <> boundary) == true + assert String.ends_with?(body, boundary <> "--\r\n") == true + assert String.replace(body, boundary, "") == "--\r\ncontent-length: 3\r\ncontent-disposition: form-data; name=\"file\"; filename=\"test.sh\"\r\ncontent-type: image/jpeg\r\n\r\nxxx\r\n----\r\n" + end + + test "File Content Disposition" do + boundary = Multipart.new_boundary + filename = "test.sh" + file_content = "xxx" + extra_headers = [{"Content-Type", "image/jpeg"}] + disposition = {'form-data', [{"name", "content"}, {"filename", filename}]} + {body, size} = Multipart.encode_form(boundary, [{:file_content, file_content, filename, disposition, extra_headers}]) + assert size == 212 + assert String.starts_with?(body, "--" <> boundary) == true + assert String.ends_with?(body, boundary <> "--\r\n") == true + assert String.replace(body, boundary, "") == "--\r\ncontent-length: 3\r\ncontent-disposition: form-data; name=content; filename=test.sh\r\ncontent-type: image/jpeg\r\n\r\nxxx\r\n----\r\n" + end + test "mp_mixed name mixedboudnary" do boundary = Multipart.new_boundary name = "mp_mixed_test_name" @@ -149,6 +185,33 @@ defmodule Maxwell.MultipartTest do assert size == 239 end + test "file_content content filename stream len" do + boundary = Multipart.new_boundary + filename = "test.sh" + file_content = "xxx" + size = Multipart.len_mp_stream(boundary, [{:file_content, file_content, filename}]) + assert size == 219 + end + + test "file_content content filename extra_headers stream len" do + boundary = Multipart.new_boundary + extra_headers = [{"Content-Type", "image/jpeg"}] + filename = "test.sh" + file_content = "xxx" + size = Multipart.len_mp_stream(boundary, [{:file_content, file_content, filename, extra_headers}]) + assert size == 213 + end + + test "file_content content filename disposition extra_headers stream len" do + boundary = Multipart.new_boundary + extra_headers = [{"Content-Type", "image/jpeg"}] + filename = "test.sh" + file_content = "xxx" + disposition = {"form-data", [{"name", "content"}]} + size = Multipart.len_mp_stream(boundary, [{:file_content, file_content, filename, disposition, extra_headers}]) + assert size == 194 + end + test "mp_mixed stream len" do boundary = Multipart.new_boundary mixed_boundary = Multipart.new_boundary @@ -259,6 +322,35 @@ defmodule Maxwell.MultipartTest do == {:multipart, [{:file, "test.png", {"form-data", [{"name", "media"}, {"filename", "test.png"}]}, headers}]} end + test "add_file_content/3 should add a file_content part" do + assert Multipart.new |> Multipart.add_file_content("xxx", "test.txt") + == {:multipart, [{:file_content, "xxx", "test.txt"}]} + end + + test "add_file_content/4 should add a file_content part with headers" do + headers = [{"content-type", "image/png"}] + assert Multipart.new |> Multipart.add_file_content("xxx", "test.png", headers) + == {:multipart, [{:file_content, "xxx", "test.png", headers}]} + end + + test "add_file_content/5 should add a file part with disposition and headers" do + disposition = {"form-data", [{"name", "content"}, {"testname", "name"}]} + headers = [{"content-type", "image/png"}] + assert Multipart.new |> Multipart.add_file_content("xxx", "test.png", disposition, headers) + == {:multipart, [{:file_content, "xxx", "test.png", disposition, headers}]} + end + + test "add_file_content_with_name/4 should add a file_content with name" do + assert Multipart.new |> Multipart.add_file_content_with_name("xxx", "test.png", "media") + == {:multipart, [{:file_content, "xxx", "test.png", {"form-data", [{"name", "media"}, {"filename", "test.png"}]}, []}]} + end + + test "add_file_content_with_name/5 should add a file_content with name and headers" do + headers = [{"content-type", "image/png"}] + assert Multipart.new |> Multipart.add_file_content_with_name("xxx", "test.png", "media", headers) + == {:multipart, [{:file_content, "xxx", "test.png", {"form-data", [{"name", "media"}, {"filename", "test.png"}]}, headers}]} + end + test "add_field/3 should add a data part" do assert Multipart.new |> Multipart.add_field("key", "value") == {:multipart, [{"key", "value"}]}