0

I have been having an issue trying to upload files to an existing ASMX service that we have (see C# below). We currently have a Silverlight app which uses the service without any issues, but I have not been successful in getting our new app which uses AJAX to leverage the service.

I have pulled advice from these in for reference for much of this:

Have an of type="file" which triggers this function:

function ArrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array(buffer);

    for (var xx = 0, len = bytes.byteLength; xx < len; ++xx) {
        binary += String.fromCharCode(bytes[xx]);
    }
    return window.btoa(binary);

    // return btoa(String.fromCharCode.apply(null, bytes)); // Note: tried this but always get an error "Maximum call stack size exceeded"
}

function DoUpload(files) {
    var reader = new FileReader();
    reader.onload = function (e) {
        var data = {
            file: ArrayBufferToBase64(e.target.result),
            extension: file.name.substr(file.name.lastIndexOf("."))
        };

        $.ajax({
            headers: {
                "X-RequestDigest": $("#__REQUESTDIGEST").val()
            },
            url: "{baseurl}" + "FileInfo.asmx/UploadFile",
            contentType: "application/json",
            data: JSON.stringify(data),
            dataType: 'json',
            type: "POST",
            success: function (response: ITempDocumentInfo) {
                dfd.resolve();
            },
            error: function (e) {
                console.error(e);
            }
        });
    };
    reader.readAsArrayBuffer(file);
}

The service lives in SharePoint and looks like this:

[WebService(Namespace = "http://www.blah.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[System.Web.Script.Services.ScriptService]
public class FileInfo : System.Web.Services.WebService
{
    [WebMethod(EnableSession = true)]
    public TempDocumentInfo AddScannedItem(string instanceId, string uri, string itemKey, string data, string extension)
    {
        byte[] bytes = new byte[data.Length * sizeof(char)];
        System.Buffer.BlockCopy(data.ToCharArray(), 0, bytes, 0, bytes.Length);
        var stream = new MemoryStream(bytes);
        .
        .
        .
    }
}

Once I have the data in C#, I cannot seem to be able to be able to do anything with it. For example. Uploading and processing a bmp image throws an exception:

var blah = System.Windows
                 .Media
                 .Imaging
                 .BmpBitmapDecoder
                 .Create(stream,
                         BitmapCreateOptions.PreservePixelFormat,            
                         BitmapCacheOption.None);

Am I not generating the AJAX request correctly?

Community
  • 1
  • 1
franzcatch
  • 169
  • 2
  • 15

2 Answers2

2

So it turns out I was not converting the the data to a byte[] correctly.
Instead of:

new byte[data.Length * sizeof(char)];
System.Buffer.BlockCopy(data.ToCharArray(), 0, bytes, 0, bytes.Length);

I did this:

byte[] bytes = Convert.FromBase64String(data);

All works fine now.

franzcatch
  • 169
  • 2
  • 15
-1

Try attempting to POST the file data directly using the request body. The way you can achieve this is by placing the file input into a form tag and grabbing the FormData in your script. Note: this is only supported in modern browsers.

<form enctype="multipart/form-data" id="FileUploadForm">
     <input type="file" name="MyFile" />
     <button type="button" id="UploadButton">Upload</button>
</form>

$('body').on('click', '#UploadButton', function () {
     var formData = new FormData($('#FileUploadForm')[0]);

     $.ajax({
        type: 'POST',
        url: '{baseurl}' + 'FileInfo.asmx/UploadFile',
        data: formData,
        cache: false,
        contentType: false,
        processData: false
    })
});

The cache, contentType and processData attributes of that AJAX call must be set in order for this to work. The server side call to receive the file contents would look like this:

 public async Task<bool> UploadFile()
 {
      try
      {
           if (!Request.Content.IsMimeMultipartContent())
           {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
           }

            var provider = new CustomMultipartFormDataStreamProvider(_tempPath);

            await Request.Content.ReadAsMultipartAsync(provider);

            return true; // you can create a custom model and return that instead. bool is used as an example.
        }
        catch (Exception ex)
        {
            throw;
        }
    }

The contents of the file are sent via the Request and the ReadAsMultipartAsync function is what actually performs the upload. The FormDataStreamProvider accepts the path of where you would like to upload the file and writes it to that location. This will primarily be the App_Data folder, as it is the only folder by default with write access.

Here is the CustomMultipartFormDataStreamProvider I am using, for reference. The reason I am using this is so that the file is saved using the same file name as the one selected by the user. ASP.NET will replace this with a Guid for security reasons.

 public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
 {
      public CustomMultipartFormDataStreamProvider(string path) : base(path)
      {
      }

      public override string GetLocalFileName(HttpContentHeaders headers)
      {
           return headers.ContentDisposition.FileName.Replace("\"", string.Empty);
      }
 }
Chris Bohatka
  • 363
  • 1
  • 4
  • 14
  • Thanks for the info Chris. Posting FormData was actually the very first thing that I did and it worked great. But the higher ups want me to reuse the existing service. – franzcatch Mar 17 '15 at 13:43
  • Ah, my apologies then. I'll take another look over your original post and see if I can come up with anything to assist you. – Chris Bohatka Mar 17 '15 at 13:53