Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 106 - Allow filename metadata with other "parts" like bytearray or stream #127

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/FsHttp/CSharp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -343,14 +343,12 @@ type Multipart =
static member StringPart(context: IRequestContext<MultipartContext>, name, value) = Multipart.stringPart name value context.Self

[<Extension>]
static member FilePartWithName(context: IRequestContext<MultipartContext>, name, path) =
Multipart.filePartWithName name path context.Self
static member FilePart(context: IRequestContext<MultipartContext>, path, ?name, ?fileName) =
Multipart.filePart name fileName path context.Self

[<Extension>]
static member FilePart(context: IRequestContext<MultipartContext>, path) = Multipart.filePart path context.Self

[<Extension>]
static member ByteArrayPart(context: IRequestContext<MultipartContext>, name, value) = Multipart.byteArrayPart name value context.Self
static member ByteArrayPart(context: IRequestContext<MultipartContext>, name, value, ?fileName) =
Multipart.byteArrayPart name value fileName context.Self

[<Extension>]
static member StreamPart(context: IRequestContext<MultipartContext>, name, value) = Multipart.streamPart name value context.Self
Expand Down
19 changes: 16 additions & 3 deletions src/FsHttp/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,18 @@ type Header = {

type ContentData =
| StringContent of string
| ByteArrayContent of byte array
| ByteArrayContent of
{|
data: byte array
fileName: string option
|}
| StreamContent of System.IO.Stream
| FormUrlEncodedContent of Map<string, string>
| FileContent of string
| FileContent of
{|
path: string
fileName: string option
|}

type BodyContent = {
contentData: ContentData
Expand All @@ -91,6 +99,7 @@ type MultipartContent = {
contentData:
{|
name: string

contentType: string option
content: ContentData
|} list
Expand Down Expand Up @@ -157,7 +166,11 @@ and HeaderContext = {
member this.Transform() = {
header = this.header
content = {
BodyContent.contentData = ByteArrayContent [||]
BodyContent.contentData =
ByteArrayContent {|
data = [||]
fileName = None
|}
headers = Map.empty
contentType = None
}
Expand Down
9 changes: 4 additions & 5 deletions src/FsHttp/Dsl.CE.fs
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,13 @@ type IRequestContext<'self> with
[<CustomOperation("stringPart")>]
member this.StringPart(context: IRequestContext<MultipartContext>, name, value) = Multipart.stringPart name value context.Self

[<CustomOperation("filePartWithName")>]
member this.FilePartWithName(context: IRequestContext<MultipartContext>, name, path) = Multipart.filePartWithName name path context.Self

[<CustomOperation("filePart")>]
member this.FilePart(context: IRequestContext<MultipartContext>, path) = Multipart.filePart path context.Self
member this.FilePart(context: IRequestContext<MultipartContext>, path, ?name, ?fileName) =
Multipart.filePart name fileName path context.Self

[<CustomOperation("byteArrayPart")>]
member this.ByteArrayPart(context: IRequestContext<MultipartContext>, name, value) = Multipart.byteArrayPart name value context.Self
member this.ByteArrayPart(context: IRequestContext<MultipartContext>, name, value, ?fileName) =
Multipart.byteArrayPart name value fileName context.Self

[<CustomOperation("streamPart")>]
member this.StreamPart(context: IRequestContext<MultipartContext>, name, value) = Multipart.streamPart name value context.Self
Expand Down
47 changes: 39 additions & 8 deletions src/FsHttp/Dsl.fs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,14 @@ module Body =
}
}

let binary (data: byte array) (context: IToBodyContext) = content MimeTypes.octetStream (ByteArrayContent data) context
let binary (data: byte array) (context: IToBodyContext) =
content
MimeTypes.octetStream
(ByteArrayContent {|
data = data
fileName = None
|})
context

let stream (stream: System.IO.Stream) (context: IToBodyContext) = content MimeTypes.octetStream (StreamContent stream) context

Expand All @@ -303,7 +310,14 @@ module Body =
let formUrlEncoded (data: (string * string) list) (context: IToBodyContext) =
content "application/x-www-form-urlencoded" (FormUrlEncodedContent(Map.ofList data)) context

let file (path: string) (context: IToBodyContext) = content MimeTypes.octetStream (FileContent path) context
let file (path: string) (context: IToBodyContext) =
content
MimeTypes.octetStream
(FileContent {|
path = path
fileName = None
|})
context


module Multipart =
Expand Down Expand Up @@ -335,14 +349,31 @@ module Multipart =

let stringPart name (value: string) (context: IToMultipartContext) = part (StringContent value) None name context

let filePartWithName name (path: string) (context: IToMultipartContext) =
let filePart (name: string option) (fileName: string option) (path: string) (context: IToMultipartContext) =
let contentType = MimeTypes.guessMimeTypeFromPath path MimeTypes.defaultMimeType
part (FileContent path) (Some contentType) name context

let filePart (path: string) (context: IToMultipartContext) =
filePartWithName (System.IO.Path.GetFileNameWithoutExtension path) path context

let byteArrayPart name (value: byte[]) (context: IToMultipartContext) = part (ByteArrayContent value) None name context
let n =
name
|> Option.defaultWith (fun () -> System.IO.Path.GetFileNameWithoutExtension path)

part
(FileContent {|
path = path
fileName = fileName
|})
(Some contentType)
n
context

let byteArrayPart name (value: byte[]) (fileName: string option) (context: IToMultipartContext) =
part
(ByteArrayContent {|
data = value
fileName = fileName
|})
None
name
context

let streamPart name (value: System.IO.Stream) (context: IToMultipartContext) = part (StreamContent value) None name context

Expand Down
4 changes: 2 additions & 2 deletions src/FsHttp/Print.fs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ let private doPrintRequestOnly (httpVersion: string) (request: Request) (request
let formatContentData contentData =
match contentData with
| StringContent s -> s
| ByteArrayContent bytes -> sprintf "::ByteArray (length = %d)" bytes.Length
| ByteArrayContent baContent -> sprintf "::ByteArray (length = %d)" baContent.data.Length
| StreamContent stream -> sprintf "::Stream (length = %s)" (if stream.CanSeek then stream.Length.ToString() else "?")
| FormUrlEncodedContent formDataList ->
[
Expand All @@ -67,7 +67,7 @@ let private doPrintRequestOnly (httpVersion: string) (request: Request) (request
yield sprintf " %s = %s" kvp.Key kvp.Value
]
|> String.concat "\n"
| FileContent fileName -> sprintf "::File (name = %s)" fileName
| FileContent fsContent -> sprintf "::File (path = %s)" fsContent.path

let multipartIndicator =
match request.content with
Expand Down
44 changes: 30 additions & 14 deletions src/FsHttp/Request.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,46 @@ let toRequestAndMessage (request: IToRequest) : Request * HttpRequestMessage =
let requestMessage = new HttpRequestMessage(header.method, header.url.ToUriString())

let buildDotnetContent (part: ContentData) (contentType: string option) (name: string option) =

let addDispoHeader (content: HttpContent) (name: string option) (fileName: string option) =
let contentDispoHeaderValue = ContentDispositionHeaderValue("form-data")

match name with
| Some v -> contentDispoHeaderValue.Name <- v
| None -> ()

match fileName with
| Some v -> contentDispoHeaderValue.FileName <- v
| None -> ()

content.Headers.ContentDisposition <- contentDispoHeaderValue
()

let dotnetContent =
match part with
| StringContent s ->
// TODO: Encoding is set hard to UTF8 - but the HTTP request has it's own encoding header.
new StringContent(s) :> HttpContent
| ByteArrayContent data -> new ByteArrayContent(data) :> HttpContent
| ByteArrayContent baContent ->
let content = new ByteArrayContent(baContent.data) :> HttpContent

match request.content with
| Multi _ -> addDispoHeader content name baContent.fileName
| _ -> ()

content

| StreamContent s -> new StreamContent(s) :> HttpContent
| FormUrlEncodedContent data -> new FormUrlEncodedContent(data) :> HttpContent
| FileContent path ->
| FileContent c ->
let content =
let fs = System.IO.File.OpenRead path
let fs = System.IO.File.OpenRead c.path
new StreamContent(fs)

let contentDispoHeaderValue = ContentDispositionHeaderValue("form-data")

match name with
| Some v -> contentDispoHeaderValue.Name <- v
| None -> ()

do
contentDispoHeaderValue.FileName <- path
content.Headers.ContentDisposition <- contentDispoHeaderValue

content :> HttpContent
let path = c.fileName |> Option.defaultValue c.path
let path = System.IO.Path.GetFileNameWithoutExtension path
addDispoHeader content name (Some path)
content

if contentType.IsSome then
dotnetContent.Headers.ContentType <- MediaTypeHeaderValue.Parse contentType.Value
Expand Down
36 changes: 36 additions & 0 deletions src/Tests/Multipart.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,42 @@ let ``POST Multipart form data`` () =
])


[<TestCase>]
let ``POST Multipart bytearray with optional filename`` () =
let fileName1 = "fileName1"
let fileName2 = "fileName2"
let fileName3 = "fileName3"

use server =
POST
>=> request (fun r ->
let fileNames = r.files |> List.map (fun f -> f.fileName) |> joinLines

fileNames |> OK
)
|> serve

http {
POST(url @"")
multipart

ContentTypeForPart "application/json"
byteArrayPart "theFieldName" [| byte 0xff |] fileName1

ContentTypeForPart "application/json"
byteArrayPart "theFieldName" [| byte 0xff |] fileName2

ContentTypeForPart "application/json"
byteArrayPart "theFieldName" [| byte 0xff |] fileName3

ContentTypeForPart "application/json"
byteArrayPart "theFieldName" [| byte 0xff |]
}
|> Request.send
|> Response.toText
|> should equal (joinLines [ fileName1; fileName2; fileName3 ])


[<TestCase>]
let ``Explicitly specified content type part is dominant`` () =

Expand Down
Loading