Search code examples
c#wcfkendo-upload

How to parse XHR multipart/form-data request with multiple files


I have seen multiple examples online of multipart/form-data parsers but none of them work on my site. I am using a Kendo Upload control set to async mode and with batch upload enabled, the generated request looks something like this:

BOUNDARY
Content-Disposition: form-data; name="Param_1"

Param_1 Value
BOUNDARY
Content-Disposition: form-data; name="Param_2"

Param_2 Value
BOUNDARY
Content-Disposition: form-data; name="files[]"; filename="A.docx"
Content-Type: application/octet-stream

[Binary Data Here]
BOUNDARY
Content-Disposition: form-data; name="files[]"; filename="B.docx"
Content-Type: application/octet-stream

[Binary Data Here]
BOUNDARY--

Every library I have found online successfully retrieves the parameters and the first file, but some never see the second and the ones that do save it incorrectly. There is a similar question on SO WCF multipart/form data with multiple files but that solution only works for text files not binary files.


Solution

  • The problem other solutions had with binary files was that they converted the response into a string to parse it and then converted that string back into the file, this does not work with binary data. I came up with a solution where instead of turning the response into a string to parse it, I left it as a byte[] and split it by the delimiter as a byte[] using the code found here. Once that's done convert each piece to a string to see if it is a parameter or a file, if it is a parameter then read it otherwise write it as a file. Here is the working code that assumes you have the Stream as a byte[] and the delimiter as a string:

    // given byte[] streamByte, String delimiterString, and Encoding encoding
    Regex regQuery;
    Match regMatch;
    string propertyType;
    
    byte[] delimiterBytes = encoding.GetBytes(delimiterString);
    byte[] delimiterWithNewLineBytes = encoding.GetBytes(delimiterString + "\r\n");
    // the request ends DELIMITER--\r\n
    byte[] delimiterEndBytes = encoding.GetBytes("\r\n" + delimiterString + "--\r\n");
    int lengthDifferenceWithEndBytes = (delimiterString + "--\r\n").Length;
    
    // seperate by delimiter + newline
    // ByteArraySplit code found at https://stackoverflow.com/a/9755250/4244411
    byte[][] separatedStream = ByteArraySplit(streamBytes, delimiterWithNewLineBytes);
    streamBytes = null;
    for (int i = 0; i < separatedStream.Length; i++)
    {
        // parse out whether this is a parameter or a file
        // get the first line of the byte[] as a string
        string thisPieceAsString = encoding.GetString(separatedStream[i]);
    
        if (string.IsNullOrWhiteSpace(thisPieceAsString)) { continue; }
    
        string firstLine = thisPieceAsString.Substring(0, thisPieceAsString.IndexOf("\r\n"));
    
        // Check the item to see what it is
        regQuery = new Regex(@"(?<=name\=\"")(.*?)(?=\"")");
        regMatch = regQuery.Match(firstLine);
        propertyType = regMatch.Value.Trim();
    
        // get the index of the start of the content and the end of the content
        int indexOfStartOfContent = thisPieceAsString.IndexOf("\r\n\r\n") + "\r\n\r\n".Length;
    
        // this line compares the name to the name of the html input control, 
        // this can be smarter by instead looking for the filename property
        if (propertyType != "files")
        {
            // this is a parameter!
            // if this is the last piece, chop off the final delimiter
            int lengthToRemove = (i == separatedStream.Length - 1) ? lengthDifferenceWithEndBytes : 0;
            string value = thisPieceAsString.Substring(indexOfStartOfContent, thisPieceAsString.Length - "\r\n".Length - indexOfStartOfContent - lengthToRemove);
            // do something with the parameter
        }
        else
        {
            // this is a file!
            regQuery = new Regex(@"(?<=filename\=\"")(.*?)(?=\"")");
            regMatch = regQuery.Match(firstLine);
            string fileName = regMatch.Value.Trim();
    
            // get the content byte[]
            // if this is the last piece, chop off the final delimiter
            int lengthToRemove = (i == separatedStream.Length - 1) ? delimiterEndBytes.Length : 0;
            int contentByteArrayStartIndex = encoding.GetBytes(thisPieceAsString.Substring(0, indexOfStartOfContent)).Length;
            byte[] fileData = new byte[separatedStream[i].Length - contentByteArrayStartIndex - lengthToRemove];
            Array.Copy(separatedStream[i], contentByteArrayStartIndex, fileData, 0, separatedStream[i].Length - contentByteArrayStartIndex - lengthToRemove);
            // save the fileData byte[] as the file
        }
    
    }