How does ReadAsMultipartAsync actually work? - c#

I have a canvas element in a web site that I'm uploading using the FormData API. Here's how I do it:
upload: function (e) {
var file = this.imagePreview.ui.canvas.get(0).toDataURL("image/jpeg")
.replace('data:image/jpeg;base64,', '');
if (file) {
var formData = new FormData();
formData.append('file', file);
$.ajax({
url: app.getApiRoot + 'UserFiles/',
type: "post",
data: formData,
processData: false,
contentType: false,
error: function () {
$("#file_upload_result").html('there was an error while submitting');
}
});
}
}
where I'm replacing the whole data:image/jpeg;base64, business as per this post.
On the backend I have the following multipart controller:
public async Task<IHttpActionResult> PostUserFile()
{
string imageDir = ConfigurationManager.AppSettings["UploadedImageDir"];
var PATH = HttpContext.Current.Server.MapPath("~/" + imageDir);
var rootUrl = Request.RequestUri.AbsoluteUri.Replace(Request.RequestUri.AbsolutePath, String.Empty);
if (Request.Content.IsMimeMultipartContent())
{
var streamProvider = new CustomMultipartFormDataStreamProvider(PATH);
await Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(t =>
{
if (t.IsFaulted || t.IsCanceled)
{
throw new HttpResponseException(HttpStatusCode.InternalServerError);
}
var files = streamProvider.FileData.Select(i =>
{
var info = new FileInfo(i.LocalFileName);
return new UserFile(User.Identity.GetUserId(), info.Name, rootUrl + "/" + imageDir + "/" + info.Name, info.Length / 1024);
});
db.UserFiles.AddRange(files);
db.SaveChangesAsync();
});
return Ok();
}
else
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotAcceptable, "This request is not properly formatted"));
}
}
Where I implement a CustomMultipartFormDataStreamProvider as follows:
public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
{
public CustomMultipartFormDataStreamProvider(string path)
: base(path)
{ }
public override string GetLocalFileName(Headers.HttpContentHeaders headers)
{
var name = !string.IsNullOrWhiteSpace(headers.ContentDisposition.FileName) ?
headers.ContentDisposition.FileName :
"NoName";
// This is here because Chrome submits files in quotation
// marks which get treated as part of the filename and get escaped
return name.Replace("\"\"", string.Empty);
}
}
Sadly, the FileData on the streamProvider fed into the ReadAsMultipartAsync method is empty:
And another thing I find interesting is that the uploaded binary file is escaped, as I notice from the watch window:
The %2f is the escape sequence for the / character.
I simply can't pinpoint the problem. Anyone have any suggestions?

Mixing ContinueWith and async/await leads to problems, just do await Request.Content.ReadAsMultipartAsync(streamProvider); and everything in the .ContinueWith would just go after the await like normal code.
Or remove async/await and add:
return Request.Content.ReadAsMultipartAsync(streamProvider).Continu‌​eWith...
Also you can look this link: Sending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME

Related

Passing potential huge files in chunks to Web API

I have to pass potential huge files from an ASP.NET Core middle Server to an ASP.NET backend.
I can’t use the ASP.NET backend web API directly, I have to go over a MVC Controller.
Currently my middle Server gets the file in Chunks (and does some verification), saves it to disk and after it completes it rereads it in chunks to pass it forward.
Is there an easy way to pass the chunks without buffering the file?
I currently got this:
MVC Controler:
[HttpPost]
public ActionResult UploadChunk(IFormFile fileChunk, string chunkMetadata)
{
if (!string.IsNullOrEmpty(chunkMetadata))
{
var metaDataObject = JsonConvert.DeserializeObject<ChunkMetadata>(chunkMetadata);
...
AppendContentToFile(tempFilePath, fileChunk); //writes file with FileMode.Append,
}
}
my upload to back end [Edit]:
public IHttpActionResult FileUpload(string fileUri)
{
try
{
if (Request.Content.IsMimeMultipartContent())
{
var configProvider = Resolve<IApplicationConfigurationProvider>();
var uploadRootDir = configProvider.TemporaryFileUploadPath;
var streamProvider = new MultipartStreamProvider(uploadRootDir);
// If the file is huge and is not split into chunks, the 'ReadAsMultipartAsync' call
// takes as long as full file has been copied
var readResult = Request.Content.ReadAsMultipartAsync(streamProvider).Result;
var fileSvc = Resolve<IFileService>();
string targetFilePath = string.Empty;
foreach (MultipartFileData fileData in streamProvider.FileData)
{
ContentDispositionHeaderValue contentDisposition = fileData.Headers.ContentDisposition;
string fileName = contentDisposition.FileName;
if (!GetFileName(fileName, out var targetFileName))
{
return BadRequest($"ContentDisposition.FileName must match 'file' of URI-query! Actual: {targetFileName}");
}
var rawSourceFileInfo = new FileInfo(targetFileName);
if (contentDisposition.Size.HasValue && contentDisposition.Size.Value > 0)
{
if (!fileSvc.CreateNewFilePath(fileUri, new PathOptions(true), out var validFileFullPath))
{
return BadRequest($"Unable to create physical-path from fileId='{fileUri}'");
}
targetFilePath = validFileFullPath.FullName;
fileSvc.AddChunk(validFileFullPath.FullName, contentDisposition.Size.Value, fileData.LocalFileName);
}
else
{
return BadRequest("File upload must set a valid file-length in ContentDisposition");
}
}
return Ok(targetFilePath);
}
else
{
return BadRequest("File upload must be a 'IsMimeMultipartContent'");
}
}
catch (Exception error)
{
LogError(error, "FileUpload");
return InternalServerError(error);
}
}
Thanks in advance for any help!
[Edit]
my not working call from client to back end:
<script>
$(document).ready(function (e) {
$("#uploadImage").on('change', (function (e) {
// append file input to form data
var fileInput = document.getElementById('uploadImage');
var file = fileInput.files[0];
var formData = new FormData();
formData.append('uploadImage', file);
$.ajax({
url: "http://localhost/service/filestore/v1/upload?fileUri=someText",
type: "POST",
data: formData,
contentType: false,
cache: false,
processData: false,
success: function (data) {
if (data == 'invalid') {
// invalid file format.
$("#err").html("Invalid File !").fadeIn();
}
else {
// view uploaded file.
$("#preview").html(data).fadeIn();
$("#form")[0].reset();
}
},
error: function (e) {
$("#err").html(e).fadeIn();
}
});
}));
});
</script>

Having trouble uploading image from Angular 8 to ASP.Netcore Web API

I have spent the whole day trying to get this to work, I have looked at the following:
https://www.talkingdotnet.com/upload-file-angular-5-asp-net-core-2-1-web-api/
https://code-maze.com/upload-files-dot-net-core-angular/
and many more than I can count.
All I want is to send an a form and an image. The errors am getting are:
Missing content-type boundary
Incorrect Content-Type
The function evaluation requires all threads to run
In angular
register(user: UserViewModel, logo: File) {
// We use formData because we can't send file as an object
const formData = new FormData();
formData.append("user", JSON.stringify(user));
formData.append("logo", logo);
console.log(formData);
return this.http.post<UserRegisterViewModel>(`${UserAPI.API_User}/${"register"}`, formData).pipe(map(user => {
return user;
}));
}
Than my c# code looks like so
[HttpPost, DisableRequestSizeLimit]
[AllowAnonymous]
[Route("register")]
//[Consumes("application/json", "multipart/form-data")]
public async Task<IActionResult> RegisterAsync()
{
IFormFile logo = null;
try
{
// Get the logo
logo = Request.Form.Files[0];
// Get the user json string
var userJson = Request.Form["user"];
If you want to fetch the file from Request.Form . you can follow below code sample :
Client side :
const formData = new FormData();
formData.append('file', fileToUpload, fileToUpload.name);
this.http.post('https://localhost:5001/api/upload', formData, {reportProgress: true, observe: 'events'})
.subscribe(event => {
if (event.type === HttpEventType.UploadProgress)
this.progress = Math.round(100 * event.loaded / event.total);
else if (event.type === HttpEventType.Response) {
this.message = 'Upload success.';
this.onUploadFinished.emit(event.body);
}
});
Server side :
[HttpPost, DisableRequestSizeLimit]
public IActionResult Upload()
{
try
{
var file = Request.Form.Files[0];
var folderName = Path.Combine("StaticFiles", "Images");
var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName);
.....
}
catch (Exception ex)
{
....
}
}
Or you can get file with FromForm:
Client side :
let fileToUpload = <File>files[0];
const formData = new FormData();
formData.append('file', fileToUpload, fileToUpload.name);
this.http.post('YourURL', formData, {headers: {
'Accept': 'application/json',
'Content-Disposition' : 'multipart/form-data'
},reportProgress: true, observe: 'events'})
.subscribe(event => {
....
});
Then server side will be :
[HttpPost, DisableRequestSizeLimit]
public IActionResult Upload([FromForm(Name = "file")] IFormFile file)
{
}
This fixed it, I had a break point on
logo = Request.Form.Files[0];
For some reason there is a bug in VS2017 and 2019.
https://developercommunity.visualstudio.com/content/problem/146887/vs2017-v4-requestform-debug.html

invoke api from ajax asp.net

I have a web Api that allow me to add a multiple Image with with another parameter
(place_Id , is_Main)
I use this code bellow to upload the image
[Route("api/Image")]
[HttpPost]
public async Task<IHttpActionResult> PostImage()
{
// Check if the request contains multipart/form-data.
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
string root = HttpContext.Current.Server.MapPath("~/Images/Places");
var provider = new CustomMultipartFormDataStreamProvider(root);
try
{
// Read the form data.
var task = await Request.Content.ReadAsMultipartAsync(provider).ContinueWith<IEnumerable<FileDesc>>(t =>
{
if (t.IsFaulted || t.IsCanceled)
{
throw new HttpResponseException(HttpStatusCode.InternalServerError);
}
var fileInfo = provider.FileData.Select(d =>
{
var info = new FileInfo(d.LocalFileName);
//return new FileDesc(info.Name);
return new FileDesc(info.Name);
});
return fileInfo;
});
int placeId = int.Parse(provider.FormData["placeId"]);
bool isMain = Convert.ToBoolean(provider.FormData["isMain"]);
var listOfAttchments = task.ToList();
string attachmentsPath = Request.RequestUri.Scheme +
System.Uri.SchemeDelimiter +
Request.RequestUri.Host +
(Request.RequestUri.IsDefaultPort ? "" : ":" + Request.RequestUri.Port) +
"/Images/Places/";
Images i = new Images();
if (listOfAttchments.Count > 0)
{
foreach (var item in listOfAttchments)
{
i.FileLocation = item.name;
i.FromUser = true;
i.TableName = "Places";
i.IsMain = isMain;
i.TableId = placeId;
db.Images.Add(i);
}
}
await db.SaveChangesAsync();
return Ok(new
{
result = true,
listAttachmment = listOfAttchments
}
);
}
catch (System.Exception e)
{
return BadRequest(e.StackTrace + "\nTest" + e.Data + "\nTest" + e.InnerException + "\nTest" + e.Message + "\nTest" + e.Source + "\nTest" + e.TargetSite);
}
}
The previous api is in another domain,
and I have a web forms application , that want to upload image from it, using the previous api
var data = new FormData();
jQuery.each(jQuery('#file')[0].files, function (i, file) {
data.append(" placeId: 7, isMain: 1");
data.append('image1' + i, file);
});
$("#btn2").click(function () {
jQuery.ajax({
url: '{url}/api/api/Image',
data: data,
contentType: false,
processData: false,
method: 'POST',
type: 'POST', // For jQuery < 1.9
success: function (data) {
alert(data);
}
});
});
I used the above code to invoke it, but I have a problem,
can you help me
Please ensure that you are not receiving XSS Error message (normally other domains are configured that you will not be able to trigger calls from a different domain addresses).
In the below code, i am not sure why do you have /api/api
url: '{url}/api/api/Image',
Please post us the error message you are receiving

Why am I getting a 500 Internal Error, no caught exceptions?

Controller:
[HttpPost]
public ActionResult HtmlToPdf ( String html )
{
try
{
var filename = DateTime.Now.Ticks.ToString() + ".pdf";
using (MemoryStream ms = new MemoryStream())
{
var pdf = TheArtOfDev.HtmlRenderer.PdfSharp.PdfGenerator.GeneratePdf(html, PdfSharp.PageSize.A4);
pdf.Save(Server.MapPath("~/Dumps/") + filename);
}
return Json(new { filename = filename });
}
catch ( Exception e )
{
return Json(new { msg = e.Message });
}
}
AJAX:
$('#download-as-pdf').click(function () {
//var resultsHtml = $(this).closest('html').html();
var resultsHtml = "<html><head></head><body><p>Hey there</p></body></html>";
$.ajax({
url: '/Answers/HtmlToPdf',
method: 'POST',
data: { html: resultsHtml },
success: function (retobj) {
console.log(retobj);
},
error: function (retobj) {
console.log("The error callback was called");//TEST
}
});
})
I know the controller is getting called because if I set resultsHtml = undefined then I get back an error from the controller
{msg: "Cannot save a PDF document with no pages."}
So what is going on here? Any ideas?
Have you checked if the folder you specified has R/W permissions for IUSR/IIS?
I would put a breakpoint and debug precisely this line:
pdf.Save(Server.MapPath("~/Dumps/") + filename);
It seems you are saving a empty pdf and i thinks it is your problem, your html parameter must has some content to generate pdf in below code:

Stop Execution of HTTPPOST ActionMethod in MVC

I have webApplication in MVC Framework..
i have situation where i have to provide user to export some data to csv file
for that i have written following code ..
[HttpPost]
public ActionResult ExportReportToFile(ReportCriteriaViewModels posdata, string name)
{
string strQuery = GetReportQuery(posdata, name);
IEnumerable<REP_MM_DEMOGRAPHIC_CC> lstDemographics = ReportDataAccess.GetReportData<REP_MM_DEMOGRAPHIC_CC>(strQuery);
if (lstDemographics.Count() > 0)
return new CsvActionResult<REP_MM_DEMOGRAPHIC_CC>(lstDemographics.ToList(), "LISDataExport.csv");
else
return view(posdata);
}
Above code works fine... if in listResult Count is Greater than zero then i returns File to download.. but if i dont get any records in lstDemographics then i returns view..
My problem is when i dont get any result in lstDemographics, i dont want to return view coz it refreshes whole view.. so is there any way where we can stop execution of Action Method and browser doesn't refresh the view and stay as it is..
Thanks..
You will have to make an AJAX call to stop page refresh.
To achieve file export, we actually broke the process in two AJAX calls. First call sends a request to server, server prepare a file and store it in temp table. Server return the file name to AJAX call if there is data. If no data or error, it return a JSON result to indicate a failure.
On success, view make another AJAX request to download the file passing file name.
Something like this:
[Audit(ActionName = "ExportDriverFile")]
public ActionResult ExportDriverFile(int searchId, string exportType, string exportFormat)
{
var user = GetUser();
var driverSearchCriteria = driverSearchCriteriaService.GetDriverSearchCriteria(searchId);
var fileName = exportType + "_" + driverSearchCriteria.SearchType + "_" + User.Identity.Name.Split('#')[0] + "." + exportFormat;
//TempData["ExportBytes_" + fileName] = null;
_searchService.DeleteTempStore(searchId);
var exportBytes = exportService.ExportDriverFileStream(driverSearchCriteria, searchId, exportType, exportFormat, user.DownloadCode, user.OrganizationId);
if (exportBytes != null)
{
var tempStore = new TempStore
{
SearchId = searchId,
FileName = fileName,
ExportFormat = exportFormat,
ExportType = exportType,
DataAsBlob = exportBytes
};
var obj = _searchService.AddTempStore(tempStore);
return Json(fileName);
}
else
{
return Json("failed");
}
}
[HttpGet]
public ActionResult DownloadStream(string fileName, int searchId)
{
var tempStore = _searchService.GetTempStore(searchId);
var bytes = tempStore.DataAsBlob;
if (bytes != null)
{
var stream = new MemoryStream(bytes);
// TempData["ExportBytes_" + fileName] = null;
_searchService.DeleteTempStore(searchId);
return File(stream, "application/vnd.ms-excel", fileName);
}
_logger.Log("Export/DownloadStream request failed", LogCategory.Error);
return Json("Failed");
}
At client side, we do something like:
function ExportData(exportType, exportFormat) {
var searchId = document.getElementById('hfSelectedDriverId').value;
var model = { searchId: searchId, exportType: exportType, exportFormat: exportFormat };
//$('div[class=ajax_overlay]').remove();
//alert("The file will be downloaded in few minutes..");
$.ajax({
url: '#Url.Action("ExportDriverFile", "Export")',
contentType: 'application/json; charset=utf-8',
type: 'POST',
dataType: 'html',
data: JSON.stringify(model)
})
.success(function (result) {
result = result.toString().replace(/"/gi, "");
if (result == "" || result == "failed")
{
alert("File download failed. Please try again!");
}
else
{
window.location = '/Export/DownloadStream?fileName=' + result+"&searchId="+searchId;
}
})
.error(function (xhr, status) {
//
//alert(status);
});
//$('div[class=ajax_overlay]').remove();
}
You should create javascript function with $.getJSON method.
On controller side you just have to check, if you get data from database then return file path else return message.
Your JS code should be something like this:
$.getJSON(url)
.done(function (data) {
if (data.filePath) // If get data, fill filePath
window.location = data.filePath;
else
alert(data.msg);
});
And from controller you can create a HTTPGET Action method that return JSON data like:
return Json(new { msg = "No data found" }, JsonRequestBehavior.AllowGet);
Based on condition you can simple change msg with filePath.

Categories