From 946d801e97857360c3c2b4b7e8b63999646029c5 Mon Sep 17 00:00:00 2001 From: Vijay Nayar Date: Thu, 14 Mar 2024 13:20:58 +0100 Subject: [PATCH 1/7] Add a MultipartEntity definition for vibe.inet.webform. --- source/vibe/inet/webform.d | 181 ++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/source/vibe/inet/webform.d b/source/vibe/inet/webform.d index d9c4628..a14822e 100644 --- a/source/vibe/inet/webform.d +++ b/source/vibe/inet/webform.d @@ -13,6 +13,7 @@ import vibe.core.log; import vibe.core.path; import vibe.inet.message; import vibe.internal.string; +import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy; import vibe.stream.operations; import vibe.textfilter.urlencode; import std.range : isOutputRange; @@ -126,7 +127,7 @@ unittest files = The $(D FilePart)s mapped to the corresponding key in which details on transmitted files will be written to. content_type = The value of the Content-Type HTTP header. - body_reader = A valid $(D InputSteram) data stream consumed by the parser. + body_reader = A valid $(D InputStream) data stream consumed by the parser. max_line_length = The byte-sized maximum length of lines used as boundary delimitors in Multi-Part forms. */ void parseMultiPartForm(InputStream)(ref FormFields fields, ref FilePartFormFields files, @@ -640,3 +641,181 @@ unittest } } + +/** + * A MIME entity, typically sent in an HTTP request or e-mail with a "Content-Type" + * header value in the form "multipart/*", e.g. "multipart/form-data". Following RFC 2046, this + * entity represents one or more different data parts combined into a single body. + * + * A boundary value preceeded by "--" is used to separate the multipart body parts, and the last + * part is indicated by the boundary value also followed by "--". An example HTTP POST request + * is shown below: + * ``` + * POST /upload HTTP/1.1 + * Content-Length: 428 + * Content-Type: multipart/form-data; boundary=abcde12345 + * --abcde12345 + * Content-Disposition: form-data; name="id" + * Content-Type: text/plain + * 123e4567-e89b-12d3-a456-426655440000 + * --abcde12345 + * Content-Disposition: form-data; name="address" + * Content-Type: application/json + * { + * "street": "3, Garden St", + * "city": "Hillsbery, UT" + * } + * --abcde12345 + * Content-Disposition: form-data; name="profileImage "; filename="image1.png" + * Content-Type: application/octet-stream + * {…file content…} + * --abcde12345-- + * ``` + * + * See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 + * See_Also: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + * See_Also: https://datatracker.ietf.org/doc/html/rfc2388 + */ +final class MultipartEntity { + InetHeaderMap headers; + MultipartEntityPart[] parts; + string boundaryStr; + + private this() { } + + // Of the multipart subtypes, only "form-data" is used in HTTP. + static MultipartEntity ofFormData(MultipartEntityPart[] parts) { + import std.conv : to; + import std.ascii : letters, digits; + import std.range : chain; + import std.random : randomSample; + + auto entity = new MultipartEntity(); + + // Boundary delimiters can be up to 70 characters. + // https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1 + // > The only mandatory global parameter for the "multipart" media type is + // > the boundary parameter, which consists of 1 to 70 characters from a + // > set of characters known to be very robust through mail gateways + entity.boundaryStr = randomSample(chain(letters.to!(dchar[]), digits.to!(dchar[])), 50) + .to!string; + entity.headers["Content-Type"] = "multipart/form-data; boundary=" ~ entity.boundaryStr; + entity.parts = parts; + return entity; + } + /// + unittest { + import vibe.stream.memory; + + // TODO: Add proper testing. + auto entity = MultipartEntity.ofFormData([ + MultipartEntityPart.ofFormInput("name", "Bob Jones"), + MultipartEntityPart.ofFormFile("resume", "Resume-Bob.pdf", "application/pdf"), + MultipartEntityPart.ofFormFile( + "cover", "howdy.txt", "text/plain", createMemoryStream("hi there.")), + ]); + } + +} + +/** + * A single piece of a MultipartEntity, containing another MIME entity. For example, a single entity + * with the "multipart/form-data" might contain 3 parts, one with MIME type "application/json", + * another with "text/plain", and the last with "application/octet-stream". + * + * A part is like a normal MIME entity, composed of headers and a body, with the following rules: + * - There may be 0 headers, in such cases, the "Content-Type" defaults to "text/plain; charset=US-ASCII". + * - The boundary delimiter must NOT appear in the body. + * + * For 'multipart/form-data', additional rules apply from RFC2388: + * - Each MulipartEntityPart must contain a "Content-Disposition" header, e.g. + * ``` + * Content-Disposition: form-data; name="user" + * ``` + * - Each part's "Content-Disposition" header should have the type "form-data". + * - Each part's "Content-Disposition" header should have a parameter "name" matching the original + * HTML form input/select name. + * + * See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 + * See_Also: https://datatracker.ietf.org/doc/html/rfc2388 + */ +final class MultipartEntityPart { + import std.sumtype : SumType; + import vibe.core.file : FileStream; + + /** + * Each part of the MultipartEntity has headers, like the main HTTP Entity. + */ + InetHeaderMap headers; + + /** + * If the body is a string, it is directly written to the connection. + * If the body is an InputStream, that stream will be read and closed when written + * to the connection. + */ + SumType!(string, InterfaceProxy!InputStream) entityBody; + + private this() { } + + /** Returns the mime type part of the 'Content-Type' header. + + This function gets the pure mime type (e.g. "text/plain") + without any supplimentary parameters such as "charset=...". + Use contentTypeParameters to get any parameter string or + headers["Content-Type"] to get the raw value. + */ + @property string contentType() + const { + auto pv = "Content-Type" in headers; + if( !pv ) return "text/plain"; + auto idx = std.string.indexOf(*pv, ';'); + return idx >= 0 ? (*pv)[0 .. idx] : *pv; + } + /// ditto + @property void contentType(string ct) { headers["Content-Type"] = ct; } + + /** Returns any supplementary parameters of the 'Content-Type' header. + + This is a semicolon separated ist of key/value pairs. Usually, if set, + this contains the character set used for text based content types. + */ + @property string contentTypeParameters() + const { + auto pv = "Content-Type" in headers; + if( !pv ) return "charset=US-ASCII"; + auto idx = std.string.indexOf(*pv, ';'); + return idx >= 0 ? (*pv)[idx+1 .. $] : null; + } + + /// A builder method creating a part from a form item. + static MultipartEntityPart ofFormInput(T)(string name, T v) + if (__traits(compiles, to!string(T.init))) { + auto part = new MultipartEntityPart(); + part.headers["Content-Disposition"] = "form-data; name=" ~ name; + part.entityBody = v.to!string; + return part; + } + + /// A builder method creating a part by loading a file by its path. + static MultipartEntityPart ofFormFile(string name, string filePath, string contentType = "application/octet-stream") { + import std.path : baseName; + import vibe.core.file : openFile; + string fileName = baseName(filePath); + auto fileStream = interfaceProxy!InputStream(openFile(filePath)); + return MultipartEntityPart.ofFormFile(name, fileName, contentType, fileStream); + } + + /// A builder method creating a part by loading a file from a stream. + static MultipartEntityPart ofFormFile( + string name, string fileName, string contentType, InterfaceProxy!InputStream fileStream) { + auto part = new MultipartEntityPart(); + part.contentType = contentType; + part.headers["Content-Disposition"] = "form-data; name=" ~ name ~ "; filename=" ~ fileName; + // For now, take the file as-is using the "binary" encoding: + // https://datatracker.ietf.org/doc/html/rfc2045#section-2.9 + part.headers["Content-Transfer-Encoding"] = "binary"; + + part.entityBody = fileStream; + return part; + } +} From 63c7d2231acbfd172a0a98407f5db34021edfaa7 Mon Sep 17 00:00:00 2001 From: Vijay Nayar Date: Wed, 20 Mar 2024 21:36:55 +0100 Subject: [PATCH 2/7] Change MultipartEntity from class to struct, fix comment style. --- source/vibe/inet/webform.d | 136 +++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 65 deletions(-) diff --git a/source/vibe/inet/webform.d b/source/vibe/inet/webform.d index a14822e..c53e391 100644 --- a/source/vibe/inet/webform.d +++ b/source/vibe/inet/webform.d @@ -643,54 +643,59 @@ unittest } /** - * A MIME entity, typically sent in an HTTP request or e-mail with a "Content-Type" - * header value in the form "multipart/*", e.g. "multipart/form-data". Following RFC 2046, this - * entity represents one or more different data parts combined into a single body. - * - * A boundary value preceeded by "--" is used to separate the multipart body parts, and the last - * part is indicated by the boundary value also followed by "--". An example HTTP POST request - * is shown below: - * ``` - * POST /upload HTTP/1.1 - * Content-Length: 428 - * Content-Type: multipart/form-data; boundary=abcde12345 - * --abcde12345 - * Content-Disposition: form-data; name="id" - * Content-Type: text/plain - * 123e4567-e89b-12d3-a456-426655440000 - * --abcde12345 - * Content-Disposition: form-data; name="address" - * Content-Type: application/json - * { - * "street": "3, Garden St", - * "city": "Hillsbery, UT" - * } - * --abcde12345 - * Content-Disposition: form-data; name="profileImage "; filename="image1.png" - * Content-Type: application/octet-stream - * {…file content…} - * --abcde12345-- - * ``` - * - * See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 - * See_Also: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html - * See_Also: https://datatracker.ietf.org/doc/html/rfc2388 - */ -final class MultipartEntity { + An HTTP entity containing one or more HTTP entities of different types. + + Closely related to the MIME entity specification, this entity has a "Content-Type" header value + in the form "multipart/*", e.g. "multipart/form-data". Following RFC 2046, this entity represents + one or more different data parts combined into a single body. RFC 2388 describes the details of + the "multipart/form-data" content-type, which uses the "Content-Disposition" header to indicate + which form field each part describes. + + A boundary value preceeded by "--" is used to separate the multipart body parts, and the last + part is indicated by the boundary value also followed by "--". An example HTTP POST request + is shown below: + ``` + POST /upload HTTP/1.1 + Content-Length: 428 + Content-Type: multipart/form-data; boundary=abcde12345 + + --abcde12345 + Content-Disposition: form-data; name="id" + Content-Type: text/plain + 123e4567-e89b-12d3-a456-426655440000 + --abcde12345 + Content-Disposition: form-data; name="address" + Content-Type: application/json + { + "street": "3, Garden St", + "city": "Hillsbery, UT" + } + --abcde12345 + Content-Disposition: form-data; name="profileImage "; filename="image1.png" + Content-Type: application/octet-stream + {…file content…} + --abcde12345-- + ``` + + See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 + See_Also: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + See_Also: https://datatracker.ietf.org/doc/html/rfc2388 +*/ +struct MultipartEntity { InetHeaderMap headers; MultipartEntityPart[] parts; string boundaryStr; private this() { } - // Of the multipart subtypes, only "form-data" is used in HTTP. + /// Creates a "multipart/form-data" HTTP Entity. static MultipartEntity ofFormData(MultipartEntityPart[] parts) { import std.conv : to; import std.ascii : letters, digits; import std.range : chain; import std.random : randomSample; - auto entity = new MultipartEntity(); + auto entity = MultipartEntity(); // Boundary delimiters can be up to 70 characters. // https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1 @@ -719,27 +724,27 @@ final class MultipartEntity { } /** - * A single piece of a MultipartEntity, containing another MIME entity. For example, a single entity - * with the "multipart/form-data" might contain 3 parts, one with MIME type "application/json", - * another with "text/plain", and the last with "application/octet-stream". - * - * A part is like a normal MIME entity, composed of headers and a body, with the following rules: - * - There may be 0 headers, in such cases, the "Content-Type" defaults to "text/plain; charset=US-ASCII". - * - The boundary delimiter must NOT appear in the body. - * - * For 'multipart/form-data', additional rules apply from RFC2388: - * - Each MulipartEntityPart must contain a "Content-Disposition" header, e.g. - * ``` - * Content-Disposition: form-data; name="user" - * ``` - * - Each part's "Content-Disposition" header should have the type "form-data". - * - Each part's "Content-Disposition" header should have a parameter "name" matching the original - * HTML form input/select name. - * - * See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 - * See_Also: https://datatracker.ietf.org/doc/html/rfc2388 + A single piece of a MultipartEntity, containing another MIME entity. For example, a single entity + with the "multipart/form-data" might contain 3 parts, one with MIME type "application/json", + another with "text/plain", and the last with "application/octet-stream". + + A part is like a normal MIME entity, composed of headers and a body, with the following rules: + - There may be 0 headers, in such cases, the "Content-Type" defaults to "text/plain; charset=US-ASCII". + - The boundary delimiter must NOT appear in the body. + + For 'multipart/form-data', additional rules apply from RFC2388: + - Each MulipartEntityPart must contain a "Content-Disposition" header, e.g. + ``` + Content-Disposition: form-data; name="user" + ``` + - Each part's "Content-Disposition" header should have the type "form-data". + - Each part's "Content-Disposition" header should have a parameter "name" matching the original + HTML form input/select name. + + See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 + See_Also: https://datatracker.ietf.org/doc/html/rfc2388 */ -final class MultipartEntityPart { +struct MultipartEntityPart { import std.sumtype : SumType; import vibe.core.file : FileStream; @@ -759,10 +764,10 @@ final class MultipartEntityPart { /** Returns the mime type part of the 'Content-Type' header. - This function gets the pure mime type (e.g. "text/plain") - without any supplimentary parameters such as "charset=...". - Use contentTypeParameters to get any parameter string or - headers["Content-Type"] to get the raw value. + This function gets the pure mime type (e.g. "text/plain") + without any supplimentary parameters such as "charset=...". + Use contentTypeParameters to get any parameter string or + headers["Content-Type"] to get the raw value. */ @property string contentType() const { @@ -771,13 +776,14 @@ final class MultipartEntityPart { auto idx = std.string.indexOf(*pv, ';'); return idx >= 0 ? (*pv)[0 .. idx] : *pv; } + /// ditto @property void contentType(string ct) { headers["Content-Type"] = ct; } /** Returns any supplementary parameters of the 'Content-Type' header. - This is a semicolon separated ist of key/value pairs. Usually, if set, - this contains the character set used for text based content types. + This is a semicolon separated ist of key/value pairs. Usually, if set, + this contains the character set used for text based content types. */ @property string contentTypeParameters() const { @@ -787,7 +793,7 @@ final class MultipartEntityPart { return idx >= 0 ? (*pv)[idx+1 .. $] : null; } - /// A builder method creating a part from a form item. + /// Creates a multipart entity part as a named form item. static MultipartEntityPart ofFormInput(T)(string name, T v) if (__traits(compiles, to!string(T.init))) { auto part = new MultipartEntityPart(); @@ -797,12 +803,12 @@ final class MultipartEntityPart { } /// A builder method creating a part by loading a file by its path. - static MultipartEntityPart ofFormFile(string name, string filePath, string contentType = "application/octet-stream") { + static MultipartEntityPart ofFormFile(string name, string filePath) { import std.path : baseName; import vibe.core.file : openFile; string fileName = baseName(filePath); auto fileStream = interfaceProxy!InputStream(openFile(filePath)); - return MultipartEntityPart.ofFormFile(name, fileName, contentType, fileStream); + return MultipartEntityPart.ofFormFile(name, fileName, getMimeTypeForFile(filePath), fileStream); } /// A builder method creating a part by loading a file from a stream. From c151dcb9ad6cfdfbc6be4e416d42a6af0f4db5fc Mon Sep 17 00:00:00 2001 From: Vijay Nayar Date: Tue, 28 May 2024 13:47:10 +0200 Subject: [PATCH 3/7] WIP: Trying to figure out how to have a range of objects that include InputStream types. --- source/vibe/inet/webform.d | 305 +++++++++++++++++++++++-------------- 1 file changed, 194 insertions(+), 111 deletions(-) diff --git a/source/vibe/inet/webform.d b/source/vibe/inet/webform.d index c53e391..c037fff 100644 --- a/source/vibe/inet/webform.d +++ b/source/vibe/inet/webform.d @@ -16,12 +16,14 @@ import vibe.internal.string; import vibe.internal.interfaceproxy : InterfaceProxy, interfaceProxy; import vibe.stream.operations; import vibe.textfilter.urlencode; -import std.range : isOutputRange; -import std.traits : ValueType, KeyType; import std.array; +import std.typecons : tuple; import std.exception; +import std.range : isInputRange, isOutputRange, only, ElementType; import std.string; +import std.sumtype : SumType; +import std.traits : ValueType, KeyType; /** @@ -643,7 +645,30 @@ unittest } /** - An HTTP entity containing one or more HTTP entities of different types. + An HTTP multipart/form-data is like tree with up to 3 levels. + Tree-Nodes include: multipart/form-data for the form itself and multipart/mixed when multiple + files are attached to the same form input. + Leaf-nodes in the Multipart tree: simple-text and files. +*/ +bool isMultipartBodyType(T)() { + static if (isInputRange!(T) + && is(ElementType!(T) == MultipartEntity!(HeaderT, BodyT), HeaderT, BodyT)) { + return isStringMap!(HeaderT) && isMultipartBodyType!(BodyT); + } else { + return is(T : string) + || isInputStream!T; + } +} + +/** + A top-level multipart entity containing one or more HTTP entities of different types. + + In the context of HTTP, only the type "multipart/form-data" is used, which is composed of simple + values such as form text input or form checkbox input with default Content-Type of "text/plain", + single files with types like "application/pdf", or a set of files under a multipart entity of + type "multipart/mixed". In the context of email, it is common for "multipart/mixed" and + "multipart/alternative" to be used to form a tree of data, with each leaf node having a concrete + content-type such as "text/plain" or "application/pdf". Closely related to the MIME entity specification, this entity has a "Content-Type" header value in the form "multipart/*", e.g. "multipart/form-data". Following RFC 2046, this entity represents @@ -677,53 +702,6 @@ unittest --abcde12345-- ``` - See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 - See_Also: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html - See_Also: https://datatracker.ietf.org/doc/html/rfc2388 -*/ -struct MultipartEntity { - InetHeaderMap headers; - MultipartEntityPart[] parts; - string boundaryStr; - - private this() { } - - /// Creates a "multipart/form-data" HTTP Entity. - static MultipartEntity ofFormData(MultipartEntityPart[] parts) { - import std.conv : to; - import std.ascii : letters, digits; - import std.range : chain; - import std.random : randomSample; - - auto entity = MultipartEntity(); - - // Boundary delimiters can be up to 70 characters. - // https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1 - // > The only mandatory global parameter for the "multipart" media type is - // > the boundary parameter, which consists of 1 to 70 characters from a - // > set of characters known to be very robust through mail gateways - entity.boundaryStr = randomSample(chain(letters.to!(dchar[]), digits.to!(dchar[])), 50) - .to!string; - entity.headers["Content-Type"] = "multipart/form-data; boundary=" ~ entity.boundaryStr; - entity.parts = parts; - return entity; - } - /// - unittest { - import vibe.stream.memory; - - // TODO: Add proper testing. - auto entity = MultipartEntity.ofFormData([ - MultipartEntityPart.ofFormInput("name", "Bob Jones"), - MultipartEntityPart.ofFormFile("resume", "Resume-Bob.pdf", "application/pdf"), - MultipartEntityPart.ofFormFile( - "cover", "howdy.txt", "text/plain", createMemoryStream("hi there.")), - ]); - } - -} - -/** A single piece of a MultipartEntity, containing another MIME entity. For example, a single entity with the "multipart/form-data" might contain 3 parts, one with MIME type "application/json", another with "text/plain", and the last with "application/octet-stream". @@ -741,26 +719,31 @@ struct MultipartEntity { - Each part's "Content-Disposition" header should have a parameter "name" matching the original HTML form input/select name. + Params: + HeaderT = The type to use to represent headers. The default value is `InetHeaderMap`, which is + a struct using a static-array, and avoids dynamic memory allocations when there are fewer + than 32 headers. + See_Also: https://datatracker.ietf.org/doc/html/rfc2046#section-5.1 + See_Also: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html See_Also: https://datatracker.ietf.org/doc/html/rfc2388 - */ -struct MultipartEntityPart { - import std.sumtype : SumType; - import vibe.core.file : FileStream; - - /** - * Each part of the MultipartEntity has headers, like the main HTTP Entity. - */ - InetHeaderMap headers; - - /** - * If the body is a string, it is directly written to the connection. - * If the body is an InputStream, that stream will be read and closed when written - * to the connection. - */ - SumType!(string, InterfaceProxy!InputStream) entityBody; - - private this() { } +*/ +struct MultipartEntity(HeaderT = InetHeaderMap, BodyT) +if (isStringMap!HeaderT && isMultipartBodyType!BodyT) +{ + HeaderT headers; + BodyT bodyValue; + + /// If the entity is a multipart entity, the boundary string to use. It is kept here so that + /// it does not have to be read from the headers each time it is needed. + string boundary; + + /// MultipartEntities should be constructed using factory methods. + private this(HeaderT headers, BodyT bodyValue, string boundary = "") { + this.headers = headers; + this.bodyValue = bodyValue; + this.boundary = boundary; + } /** Returns the mime type part of the 'Content-Type' header. @@ -769,59 +752,159 @@ struct MultipartEntityPart { Use contentTypeParameters to get any parameter string or headers["Content-Type"] to get the raw value. */ - @property string contentType() - const { - auto pv = "Content-Type" in headers; - if( !pv ) return "text/plain"; - auto idx = std.string.indexOf(*pv, ';'); - return idx >= 0 ? (*pv)[0 .. idx] : *pv; - } + // @property string contentType() + // const { + // auto pv = "Content-Type" in headers; + // if( !pv ) return "text/plain"; + // auto idx = std.string.indexOf(*pv, ';'); + // return idx >= 0 ? (*pv)[0 .. idx] : *pv; + // } /// ditto - @property void contentType(string ct) { headers["Content-Type"] = ct; } + // @property void contentType(string ct) { headers["Content-Type"] = ct; } /** Returns any supplementary parameters of the 'Content-Type' header. This is a semicolon separated ist of key/value pairs. Usually, if set, this contains the character set used for text based content types. */ - @property string contentTypeParameters() - const { - auto pv = "Content-Type" in headers; - if( !pv ) return "charset=US-ASCII"; - auto idx = std.string.indexOf(*pv, ';'); - return idx >= 0 ? (*pv)[idx+1 .. $] : null; - } + // @property string contentTypeParameters() + // const { + // auto pv = "Content-Type" in headers; + // if( !pv ) return "charset=US-ASCII"; + // auto idx = std.string.indexOf(*pv, ';'); + // return idx >= 0 ? (*pv)[idx+1 .. $] : null; + // } +} - /// Creates a multipart entity part as a named form item. - static MultipartEntityPart ofFormInput(T)(string name, T v) - if (__traits(compiles, to!string(T.init))) { - auto part = new MultipartEntityPart(); - part.headers["Content-Disposition"] = "form-data; name=" ~ name; - part.entityBody = v.to!string; - return part; - } - /// A builder method creating a part by loading a file by its path. - static MultipartEntityPart ofFormFile(string name, string filePath) { - import std.path : baseName; - import vibe.core.file : openFile; - string fileName = baseName(filePath); - auto fileStream = interfaceProxy!InputStream(openFile(filePath)); - return MultipartEntityPart.ofFormFile(name, fileName, getMimeTypeForFile(filePath), fileStream); - } +//// +// Factory Methods +//// + +// /// Returns a MultipartEntity with Content-Type "multipart/form-data" from parts as a compile-time sequence. +// auto multipartFormData(MultipartR...)(MultipartR parts) { +// return multipartFormData(parts); +// } + +// /// +// unittest { +// import vibe.stream.memory; + +// // Build an entity using the var-args form. +// auto entity = multipartFormData( +// multipartFormInput("name", "Bob Jones"), +// multipartFormFile("resume", formFile("Resume-Bob.pdf", "application/pdf")), +// multipartFormFiles("photos", [ +// formFile("portrait1.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), +// formFile("portrait2.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), +// ]), +// ); +// } + +/// Returns a MultipartEntity with Content-Type "multipart/form-data" from parts as a range. +auto multipartFormData(MultipartR)(MultipartR parts) +if (isInputRange!MultipartR && isMultipartBodyType!(MultipartR)) { + string boundaryStr = createBoundaryString(); + auto headers = only( + tuple("Content-Type", "multipart/form-data; boundary=" ~ boundary)); + return MultipartEntity!(typeof(headers), typeof(parts))(headers: headers, bodyValue: parts, boundary: boundaryStr); +} + +/// +unittest { + import vibe.stream.memory; + + // Build an entity using the var-args form. + auto entity = multipartFormData(only( + multipartFormInput("name", "Bob Jones"), + multipartFormFile("resume", formFile("Resume-Bob.pdf", "application/pdf")), + multipartFormFiles("photos", [ + formFile("portrait1.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), + formFile("portrait2.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), + ]), + )); +} - /// A builder method creating a part by loading a file from a stream. - static MultipartEntityPart ofFormFile( - string name, string fileName, string contentType, InterfaceProxy!InputStream fileStream) { - auto part = new MultipartEntityPart(); - part.contentType = contentType; - part.headers["Content-Disposition"] = "form-data; name=" ~ name ~ "; filename=" ~ fileName; - // For now, take the file as-is using the "binary" encoding: - // https://datatracker.ietf.org/doc/html/rfc2045#section-2.9 - part.headers["Content-Transfer-Encoding"] = "binary"; - - part.entityBody = fileStream; - return part; +/// Creates a multipart entity part as a named form item. +static auto multipartFormInput(T)(string name, T v) { + import std.conv : to; + static assert(__traits(compiles, to!string(T.init)), "Type '" ~ T.stringof ~ "' must be convertible to a string!"); + auto headers = only( + tuple("Content-Disposition", "form-data; name=" ~ name)); + return MultipartEntity!(typeof(headers), string)(headers: headers, bodyValue: v.to!string); +} + +/// A convenience type to make it easier to group data about a file in a form. +struct FormFile(FileStreamT) { + string fileName; + FileStreamT fileStream; + string contentType; +} + +auto formFile(string filePath, string contentType = "") { + import std.path : baseName; + import vibe.core.file : openFile; + string fileName = baseName(filePath); + auto fileStream = interfaceProxy!InputStream(openFile(filePath)); + return formFile(fileName, fileStream, contentType); +} + +/// Creates a FormFile object which is used when attaching multiple files to a multipart form input field. +auto formFile(StreamT)(string fileName, StreamT fileStream, string contentType = "") +if (isInputStream!StreamT) { + import vibe.inet.mimetypes : getMimeTypeForFile; + if (contentType == "") { + contentType = getMimeTypeForFile(fileName); } + return FormFile!StreamT(fileName, fileStream, contentType); +} + +/// A builder method creating a part by loading a file from a stream. +auto multipartFormFile(StreamT)(string name, FormFile!StreamT formFile) +if (isInputStream!StreamT) { + auto headers = only( + tuple("Content-Type", formFile.contentType), + tuple("Content-Disposition", "form-data; name=" ~ name ~ "; filename=" ~ formFile.fileName), + // For now, take the file as-is using the "binary" encoding: + // https://datatracker.ietf.org/doc/html/rfc2045#section-2.9 + tuple("Content-Transfer-Encoding", "binary")); + return MultipartEntity!(typeof(headers), typeof(formFile.fileStream))( + headers: headers, bodyValue: formFile.fileStream); +} + +/// Creates a MultipartEntity consisting of several files for the same form input. +auto multipartFormFiles(FormFileR)(string name, FormFileR formFiles) { + static assert(isInputRange!FormFileR, "Type '" ~ FormFileR.stringof ~ "' is not an input range!"); + enum isFormFileElem = is(ElementType!FormFileR == FormFile!StreamT, StreamT); + static assert(isFormFileElem, "Type '" ~ (ElementType!FormFileR).stringof ~ "' must have elements of type FormFile!InputStream."); + static assert(isInputStream!StreamT, "Type '" ~ StreamT.stringof ~ "' is not InputStream-compatible!"); + import std.algorithm : map; + + string boundaryStr = createBoundaryString(); + auto headers = only( + tuple("Content-Type", "multipart/mixed; boundary=" ~ boundaryStr), + tuple("Content-Disposition", "form-data; name=" ~ name)); + auto multipartFormFileRange = formFiles.map!(formFile => multipartFormFile(name, formFile)); + return MultipartEntity!(typeof(headers), typeof(multipartFormFileRange))( + headers: headers, + bodyValue: multipartFormFileRange, + boundary: boundaryStr); + } + +/** + Boundary delimiters can be up to 70 characters. + https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1 + > The only mandatory global parameter for the "multipart" media type is + > the boundary parameter, which consists of 1 to 70 characters from a + > set of characters known to be very robust through mail gateways +*/ +private string createBoundaryString() { + import std.conv : to; + import std.ascii : letters, digits; + import std.range : chain; + import std.random : randomSample; + + return randomSample(chain(letters.to!(dchar[]), digits.to!(dchar[])), 50) + .to!string; } From 1d38ae2dbf17a8f5027fbab6d0288ccbd643787a Mon Sep 17 00:00:00 2001 From: Vijay Nayar Date: Tue, 28 May 2024 15:57:43 +0200 Subject: [PATCH 4/7] Included Variant to allow mixed range types. --- source/vibe/inet/webform.d | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/source/vibe/inet/webform.d b/source/vibe/inet/webform.d index c037fff..5eff221 100644 --- a/source/vibe/inet/webform.d +++ b/source/vibe/inet/webform.d @@ -24,6 +24,7 @@ import std.range : isInputRange, isOutputRange, only, ElementType; import std.string; import std.sumtype : SumType; import std.traits : ValueType, KeyType; +import std.variant : Variant, variantArray; /** @@ -651,9 +652,11 @@ unittest Leaf-nodes in the Multipart tree: simple-text and files. */ bool isMultipartBodyType(T)() { - static if (isInputRange!(T) - && is(ElementType!(T) == MultipartEntity!(HeaderT, BodyT), HeaderT, BodyT)) { + static if (isInputRange!(T) && is(ElementType!(T) == MultipartEntity!(HeaderT, BodyT), HeaderT, BodyT)) { return isStringMap!(HeaderT) && isMultipartBodyType!(BodyT); + } else static if (is(ElementType!(T) == Variant)) { + // TODO: Figure out a better alternative than a blanket allowance for Variant + return true; } else { return is(T : string) || isInputStream!T; @@ -732,7 +735,8 @@ struct MultipartEntity(HeaderT = InetHeaderMap, BodyT) if (isStringMap!HeaderT && isMultipartBodyType!BodyT) { HeaderT headers; - BodyT bodyValue; + //BodyT bodyValue; + Variant bodyValue; /// If the entity is a multipart entity, the boundary string to use. It is kept here so that /// it does not have to be read from the headers each time it is needed. @@ -807,7 +811,7 @@ auto multipartFormData(MultipartR)(MultipartR parts) if (isInputRange!MultipartR && isMultipartBodyType!(MultipartR)) { string boundaryStr = createBoundaryString(); auto headers = only( - tuple("Content-Type", "multipart/form-data; boundary=" ~ boundary)); + tuple("Content-Type", "multipart/form-data; boundary=" ~ boundaryStr)); return MultipartEntity!(typeof(headers), typeof(parts))(headers: headers, bodyValue: parts, boundary: boundaryStr); } @@ -816,9 +820,9 @@ unittest { import vibe.stream.memory; // Build an entity using the var-args form. - auto entity = multipartFormData(only( + auto entity = multipartFormData(variantArray( multipartFormInput("name", "Bob Jones"), - multipartFormFile("resume", formFile("Resume-Bob.pdf", "application/pdf")), + multipartFormFile("resume", formFile("Resume-Bob.pdf", createMemoryStream(cast(ubyte[]) "dummy data"), "application/pdf")), multipartFormFiles("photos", [ formFile("portrait1.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), formFile("portrait2.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), @@ -836,12 +840,14 @@ static auto multipartFormInput(T)(string name, T v) { } /// A convenience type to make it easier to group data about a file in a form. -struct FormFile(FileStreamT) { +struct FormFile(InputStreamT) +if (isInputStream!InputStreamT) { string fileName; - FileStreamT fileStream; + InputStreamT fileStream; string contentType; } +/// Creates a FormFile by opening a file specified by `filePath`. auto formFile(string filePath, string contentType = "") { import std.path : baseName; import vibe.core.file : openFile; @@ -857,7 +863,7 @@ if (isInputStream!StreamT) { if (contentType == "") { contentType = getMimeTypeForFile(fileName); } - return FormFile!StreamT(fileName, fileStream, contentType); + return FormFile!(StreamT)(fileName, fileStream, contentType); } /// A builder method creating a part by loading a file from a stream. From 3b76808beba62cb48a1b8095daa9ce29c3fca119 Mon Sep 17 00:00:00 2001 From: Vijay Nayar Date: Tue, 28 May 2024 16:53:52 +0200 Subject: [PATCH 5/7] Remove MultipartEntity BodyT parameter, as Variant is now being used. --- source/vibe/inet/webform.d | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/source/vibe/inet/webform.d b/source/vibe/inet/webform.d index 5eff221..c899faa 100644 --- a/source/vibe/inet/webform.d +++ b/source/vibe/inet/webform.d @@ -731,11 +731,10 @@ bool isMultipartBodyType(T)() { See_Also: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html See_Also: https://datatracker.ietf.org/doc/html/rfc2388 */ -struct MultipartEntity(HeaderT = InetHeaderMap, BodyT) -if (isStringMap!HeaderT && isMultipartBodyType!BodyT) +struct MultipartEntity(HeaderT = InetHeaderMap) +if (isStringMap!HeaderT) { HeaderT headers; - //BodyT bodyValue; Variant bodyValue; /// If the entity is a multipart entity, the boundary string to use. It is kept here so that @@ -743,7 +742,7 @@ if (isStringMap!HeaderT && isMultipartBodyType!BodyT) string boundary; /// MultipartEntities should be constructed using factory methods. - private this(HeaderT headers, BodyT bodyValue, string boundary = "") { + private this(BodyT)(HeaderT headers, BodyT bodyValue, string boundary = "") { this.headers = headers; this.bodyValue = bodyValue; this.boundary = boundary; @@ -783,7 +782,7 @@ if (isStringMap!HeaderT && isMultipartBodyType!BodyT) //// -// Factory Methods +// Multipart Factory Methods //// // /// Returns a MultipartEntity with Content-Type "multipart/form-data" from parts as a compile-time sequence. @@ -794,7 +793,6 @@ if (isStringMap!HeaderT && isMultipartBodyType!BodyT) // /// // unittest { // import vibe.stream.memory; - // // Build an entity using the var-args form. // auto entity = multipartFormData( // multipartFormInput("name", "Bob Jones"), @@ -812,7 +810,7 @@ if (isInputRange!MultipartR && isMultipartBodyType!(MultipartR)) { string boundaryStr = createBoundaryString(); auto headers = only( tuple("Content-Type", "multipart/form-data; boundary=" ~ boundaryStr)); - return MultipartEntity!(typeof(headers), typeof(parts))(headers: headers, bodyValue: parts, boundary: boundaryStr); + return MultipartEntity!(typeof(headers))(headers: headers, bodyValue: parts, boundary: boundaryStr); } /// @@ -836,7 +834,7 @@ static auto multipartFormInput(T)(string name, T v) { static assert(__traits(compiles, to!string(T.init)), "Type '" ~ T.stringof ~ "' must be convertible to a string!"); auto headers = only( tuple("Content-Disposition", "form-data; name=" ~ name)); - return MultipartEntity!(typeof(headers), string)(headers: headers, bodyValue: v.to!string); + return MultipartEntity!(typeof(headers))(headers: headers, bodyValue: v.to!string); } /// A convenience type to make it easier to group data about a file in a form. @@ -875,10 +873,18 @@ if (isInputStream!StreamT) { // For now, take the file as-is using the "binary" encoding: // https://datatracker.ietf.org/doc/html/rfc2045#section-2.9 tuple("Content-Transfer-Encoding", "binary")); - return MultipartEntity!(typeof(headers), typeof(formFile.fileStream))( + return MultipartEntity!(typeof(headers))( headers: headers, bodyValue: formFile.fileStream); } +// TODO: Using this causes an error. +// ``` +// Error: forward reference to inferred return type of function call `multipartFormFiles(name, __param_1, __param_2)` +// ``` +auto multipartFormFiles(FormFileR...)(string name, FormFileR formFiles) { + return multipartFormFiles(name, formFiles); +} + /// Creates a MultipartEntity consisting of several files for the same form input. auto multipartFormFiles(FormFileR)(string name, FormFileR formFiles) { static assert(isInputRange!FormFileR, "Type '" ~ FormFileR.stringof ~ "' is not an input range!"); @@ -892,11 +898,11 @@ auto multipartFormFiles(FormFileR)(string name, FormFileR formFiles) { tuple("Content-Type", "multipart/mixed; boundary=" ~ boundaryStr), tuple("Content-Disposition", "form-data; name=" ~ name)); auto multipartFormFileRange = formFiles.map!(formFile => multipartFormFile(name, formFile)); - return MultipartEntity!(typeof(headers), typeof(multipartFormFileRange))( + return MultipartEntity!(typeof(headers))( headers: headers, bodyValue: multipartFormFileRange, boundary: boundaryStr); - } +} /** Boundary delimiters can be up to 70 characters. From 03d6e59bda0dd1cd14a6e10a9b87812e2e9ab9c5 Mon Sep 17 00:00:00 2001 From: Vijay Nayar Date: Mon, 24 Jun 2024 13:07:33 +0200 Subject: [PATCH 6/7] Replace Variant with SumType, but remove template parameters to start discussion around parameterization. --- source/vibe/inet/webform.d | 145 +++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 63 deletions(-) diff --git a/source/vibe/inet/webform.d b/source/vibe/inet/webform.d index c899faa..e96a82f 100644 --- a/source/vibe/inet/webform.d +++ b/source/vibe/inet/webform.d @@ -20,7 +20,7 @@ import vibe.textfilter.urlencode; import std.array; import std.typecons : tuple; import std.exception; -import std.range : isInputRange, isOutputRange, only, ElementType; +import std.range : isInputRange, isOutputRange, only, ElementType, InputRangeObject; import std.string; import std.sumtype : SumType; import std.traits : ValueType, KeyType; @@ -731,21 +731,24 @@ bool isMultipartBodyType(T)() { See_Also: https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html See_Also: https://datatracker.ietf.org/doc/html/rfc2388 */ -struct MultipartEntity(HeaderT = InetHeaderMap) -if (isStringMap!HeaderT) +struct MultipartEntity //(HeaderT = InetHeaderMap) +//if (isStringMap!HeaderT) { - HeaderT headers; - Variant bodyValue; + //HeaderT headers; + InetHeaderMap headers; + //Variant bodyValue; + //SumType!(string, InterfaceProxy!InputStream, InputRangeObject!ubyte, MultipartEntity[]) bodyValue; + SumType!(string, InterfaceProxy!InputStream, MultipartEntity[]) bodyValue; /// If the entity is a multipart entity, the boundary string to use. It is kept here so that /// it does not have to be read from the headers each time it is needed. string boundary; - /// MultipartEntities should be constructed using factory methods. - private this(BodyT)(HeaderT headers, BodyT bodyValue, string boundary = "") { - this.headers = headers; - this.bodyValue = bodyValue; - this.boundary = boundary; + // /// MultipartEntities should be constructed using factory methods. + // private this(BodyT)(InetHeaderMap headers, BodyT bodyValue, string boundary = "") { + // this.headers = headers; + // this.bodyValue = bodyValue; + // this.boundary = boundary; } /** Returns the mime type part of the 'Content-Type' header. @@ -755,13 +758,13 @@ if (isStringMap!HeaderT) Use contentTypeParameters to get any parameter string or headers["Content-Type"] to get the raw value. */ - // @property string contentType() - // const { - // auto pv = "Content-Type" in headers; - // if( !pv ) return "text/plain"; - // auto idx = std.string.indexOf(*pv, ';'); - // return idx >= 0 ? (*pv)[0 .. idx] : *pv; - // } + @property string contentType() + const { + auto pv = "Content-Type" in headers; + if( !pv ) return "text/plain"; + auto idx = std.string.indexOf(*pv, ';'); + return idx >= 0 ? (*pv)[0 .. idx] : *pv; + } /// ditto // @property void contentType(string ct) { headers["Content-Type"] = ct; } @@ -771,13 +774,13 @@ if (isStringMap!HeaderT) This is a semicolon separated ist of key/value pairs. Usually, if set, this contains the character set used for text based content types. */ - // @property string contentTypeParameters() - // const { - // auto pv = "Content-Type" in headers; - // if( !pv ) return "charset=US-ASCII"; - // auto idx = std.string.indexOf(*pv, ';'); - // return idx >= 0 ? (*pv)[idx+1 .. $] : null; - // } + @property string contentTypeParameters() + const { + auto pv = "Content-Type" in headers; + if( !pv ) return "charset=US-ASCII"; + auto idx = std.string.indexOf(*pv, ';'); + return idx >= 0 ? (*pv)[idx+1 .. $] : null; + } } @@ -805,12 +808,17 @@ if (isStringMap!HeaderT) // } /// Returns a MultipartEntity with Content-Type "multipart/form-data" from parts as a range. -auto multipartFormData(MultipartR)(MultipartR parts) -if (isInputRange!MultipartR && isMultipartBodyType!(MultipartR)) { +// auto multipartFormData(MultipartR)(MultipartR parts) +// if (isInputRange!MultipartR && is(ElementType!MultipartR : MultipartEntity)) { +auto multipartFormData(MultipartEntity[] parts) { string boundaryStr = createBoundaryString(); - auto headers = only( - tuple("Content-Type", "multipart/form-data; boundary=" ~ boundaryStr)); - return MultipartEntity!(typeof(headers))(headers: headers, bodyValue: parts, boundary: boundaryStr); + auto headers = InetHeaderMap(); + headers["Content-Type"] = "multipart/form-data; boundary=" ~ boundaryStr; + auto entity = MultipartEntity(headers: headers, boundary: boundaryStr); + // Set the body separately to avoid compiler error: + // `Error: cannot implicitly convert expression `parts` of type `MultipartEntity[]` to `SumType!(...` + entity.bodyValue = parts; + return entity; } /// @@ -818,30 +826,31 @@ unittest { import vibe.stream.memory; // Build an entity using the var-args form. - auto entity = multipartFormData(variantArray( + auto entity = multipartFormData([ multipartFormInput("name", "Bob Jones"), multipartFormFile("resume", formFile("Resume-Bob.pdf", createMemoryStream(cast(ubyte[]) "dummy data"), "application/pdf")), multipartFormFiles("photos", [ formFile("portrait1.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), formFile("portrait2.jpg", createMemoryStream(cast(ubyte[]) "dummy data"), "image/png"), ]), - )); + ]); } /// Creates a multipart entity part as a named form item. static auto multipartFormInput(T)(string name, T v) { import std.conv : to; static assert(__traits(compiles, to!string(T.init)), "Type '" ~ T.stringof ~ "' must be convertible to a string!"); - auto headers = only( - tuple("Content-Disposition", "form-data; name=" ~ name)); - return MultipartEntity!(typeof(headers))(headers: headers, bodyValue: v.to!string); + auto headers = InetHeaderMap(); + headers["Content-Disposition"] = "form-data; name=" ~ name; + auto entity = MultipartEntity(headers: headers); + entity.bodyValue = v.to!string; + return entity; } /// A convenience type to make it easier to group data about a file in a form. -struct FormFile(InputStreamT) -if (isInputStream!InputStreamT) { +struct FormFile { string fileName; - InputStreamT fileStream; + InterfaceProxy!InputStream fileStream; string contentType; } @@ -855,53 +864,63 @@ auto formFile(string filePath, string contentType = "") { } /// Creates a FormFile object which is used when attaching multiple files to a multipart form input field. -auto formFile(StreamT)(string fileName, StreamT fileStream, string contentType = "") +auto formFile(StreamT)(string fileName, StreamT inputStream, string contentType = "") if (isInputStream!StreamT) { import vibe.inet.mimetypes : getMimeTypeForFile; if (contentType == "") { contentType = getMimeTypeForFile(fileName); } - return FormFile!(StreamT)(fileName, fileStream, contentType); + auto fileStream = interfaceProxy!InputStream(inputStream); + return FormFile(fileName, fileStream, contentType); } /// A builder method creating a part by loading a file from a stream. -auto multipartFormFile(StreamT)(string name, FormFile!StreamT formFile) -if (isInputStream!StreamT) { - auto headers = only( - tuple("Content-Type", formFile.contentType), - tuple("Content-Disposition", "form-data; name=" ~ name ~ "; filename=" ~ formFile.fileName), - // For now, take the file as-is using the "binary" encoding: - // https://datatracker.ietf.org/doc/html/rfc2045#section-2.9 - tuple("Content-Transfer-Encoding", "binary")); - return MultipartEntity!(typeof(headers))( - headers: headers, bodyValue: formFile.fileStream); +auto multipartFormFile(string name, FormFile formFile) { + auto headers = InetHeaderMap(); + headers["Content-Type"] = formFile.contentType; + headers["Content-Disposition"] = "form-data; name=" ~ name ~ "; filename=" ~ formFile.fileName; + // For now, take the file as-is using the "binary" encoding: + // https://datatracker.ietf.org/doc/html/rfc2045#section-2.9 + headers["Content-Transfer-Encoding"] = "binary"; + auto entity = MultipartEntity(headers: headers); + entity.bodyValue = formFile.fileStream; + return entity; } // TODO: Using this causes an error. // ``` // Error: forward reference to inferred return type of function call `multipartFormFiles(name, __param_1, __param_2)` // ``` -auto multipartFormFiles(FormFileR...)(string name, FormFileR formFiles) { - return multipartFormFiles(name, formFiles); -} +//auto multipartFormFiles(FormFileR...)(string name, FormFileR formFiles) { +// return multipartFormFiles(name, formFiles); +//} /// Creates a MultipartEntity consisting of several files for the same form input. -auto multipartFormFiles(FormFileR)(string name, FormFileR formFiles) { - static assert(isInputRange!FormFileR, "Type '" ~ FormFileR.stringof ~ "' is not an input range!"); - enum isFormFileElem = is(ElementType!FormFileR == FormFile!StreamT, StreamT); - static assert(isFormFileElem, "Type '" ~ (ElementType!FormFileR).stringof ~ "' must have elements of type FormFile!InputStream."); - static assert(isInputStream!StreamT, "Type '" ~ StreamT.stringof ~ "' is not InputStream-compatible!"); +auto multipartFormFiles(string name, FormFile[] formFiles) { + // static assert(isInputRange!FormFileR, "Type '" ~ FormFileR.stringof ~ "' is not an input range!"); + // enum isFormFileElem = is(ElementType!FormFileR == FormFile); + // static assert(isFormFileElem, "Type '" ~ (ElementType!FormFileR).stringof ~ "' must have elements of type FormFile."); import std.algorithm : map; string boundaryStr = createBoundaryString(); - auto headers = only( - tuple("Content-Type", "multipart/mixed; boundary=" ~ boundaryStr), - tuple("Content-Disposition", "form-data; name=" ~ name)); - auto multipartFormFileRange = formFiles.map!(formFile => multipartFormFile(name, formFile)); - return MultipartEntity!(typeof(headers))( + auto headers = InetHeaderMap(); + headers["Content-Type"] = "multipart/mixed; boundary=" ~ boundaryStr; + headers["Content-Disposition"] = "form-data; name=" ~ name; + //auto multipartFormFileRange = formFiles.map!(formFile => multipartFormFile(name, formFile)); + //auto multipartFormFiles = formFiles.map!(formFile => multipartFormFile(name, formFile)).array; + // This loop is required because SumType disables certain copy constructors, an using a map above will result in: + // std/array.d(113,23): Error: generating an `inout` copy constructor for `struct vibe.inet.webform.MultipartEntity` failed, therefore instances of it are uncopyable + // core/internal/lifetime.d(69,9): Error: static assert: "Cannot emplace a MultipartEntity because MultipartEntity.this(this) is annotated with @disable." + + MultipartEntity[] parts = new MultipartEntity[](formFiles.length); + foreach (i, formFile; formFiles) { + parts[i] = multipartFormFile(name, formFile); + } + auto entity = MultipartEntity( headers: headers, - bodyValue: multipartFormFileRange, boundary: boundaryStr); + entity.bodyValue = parts; + return entity; } /** From 5aba3d8fd27ddbefc9dd587389f8a956e56b4309 Mon Sep 17 00:00:00 2001 From: Vijay Nayar Date: Mon, 24 Jun 2024 13:44:53 +0200 Subject: [PATCH 7/7] Typo. --- source/vibe/inet/webform.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/vibe/inet/webform.d b/source/vibe/inet/webform.d index e96a82f..87b029e 100644 --- a/source/vibe/inet/webform.d +++ b/source/vibe/inet/webform.d @@ -749,7 +749,7 @@ struct MultipartEntity //(HeaderT = InetHeaderMap) // this.headers = headers; // this.bodyValue = bodyValue; // this.boundary = boundary; - } + // } /** Returns the mime type part of the 'Content-Type' header.