A while back, I write a tutorial on how to upload a file using AngularJS' ngResource. Here it is. But recently, I found out it was a cool idea, but it was not a good idea. The problem is that, with Microsoft IIS, there is a limit of what the size of the request JSON. And if you use ASP.NET MVC (not the .NET Core one), it is really hard to increase the size of the JSON request. As a result, for my project, I had to use a HTML form to upload the file. As I have discovered, use HTML form to upload the file is not hard. And this tutorial will show you how. The sample application will upload the file, and the back end Spring REST based API controller will handle the receiving of uploaded file.
In addition to the file upload, I will also discuss how to add JSON request to the same form request so that it can be processed with the uploaded file. This is do-able because with HTML form, the file is a part of the multi-part form request. And anything you can add, can also be part of the request. This multi-part form data is like a big key-value collection. So one can put a file in it, and any string based value in it. Anyway, read on.
The sample application has two layer, the front end and the back end. The front end is a simple AngularJS application that will demo how to upload file, and how to add some extra data to the upload request. The back end will demo how to handle saving the file and how to handle the extra data with the uploaded file.
The front end is split into two JavaScript files. The first one (app.js) is the application for the front end. The other (uploadService.js) is the service which will handle the upload. It is pretty simple. Same with the back end, there is just one Rest controller (SampleFileUploadController.java) with one method that will take the uploaded file and save to disk. It also deserialize the string from the request to an object, then handle the object. In this case, I just write the object content to console.
Lets dig into the code.
In order to upload a file use FormData and AngularJS, I have to use $http, not ngResource. And I have to use HTTP method POST. These are the following steps to accomplish it:
Declare a FormData object, and attach a file object to it. And if needed I attach some more data to the FormData object:
var fd = new FormData();
fd.append('fileMetadata', JSON.stringify(imgMetadata));
fd.append('file', fileToUpload);
The second line of the above code sample is adding an extra object into the FormData object. The third line is adding the file to be uploaded. As I have mentioned before, FormData is a key-values collection. These two lines are:
Note these keys are very important. They will be used on the Java side so that the objects can be retrieved from the FormData collection. On the Java side, a FormData collection is represented by org.springframework.web.multipart.MultipartFile. We will discuss this later.
Next, I will use $http to do the upload. Or send the request data to the server side. Here is how I do it:
return $http.post(uploadUrl, fd, {
transformRequest: angular.identity,
headers: {
"Content-Type": undefined
}
});
There are a couple ideas I have to explain here:
$http.post()
. This method takes three parameters. The first is the URL I send the request, the second one is the FormData object. The third one is an object that would pass extra request data, such as the header and the cookie info.transformRequest: angular.identity
is to transform the user identity data and add to the request. This copies the user authentication data to the request so that it can be passed to backend server securely.$http.post()
will fill in the correct header data.After calling $http.post()
, the returned promise is send back to the caller so that the caller can handle the response sent back from the server. As you can see, this is only the first half of the front end UI code.
Before I go any further, I will show you the entire code of the file upload service. Here it is:
(function () {
"use strict";
var mod = angular.module("testSampleUploadModule", [ ]);
mod.factory("testSampleUploadService", [ "$http",
function ($http) {
var svc = {
uploadFile: uploadFile
};
function uploadFile(uploadUrl, fileToUpload) {
var imgMetadata = {
author: "Han Solo",
title: "Sample Upload Image",
description: "This is a simple upload with additional metadata attached.",
keywords: "keyword1,keyword2,keyword3,keyword4"
};
var fd = new FormData();
fd.append('fileMetadata', JSON.stringify(imgMetadata));
fd.append('file', fileToUpload);
return $http.post(uploadUrl, fd, {
transformRequest: angular.identity,
headers: {
"Content-Type": undefined
}
});
}
return svc;
}
]);
})();
Pretty simple, isn't it?
In previous section, we have seen the AngularJS service that can upload the file using FormData, and it takes advantages of the multi-part file upload capability, to send extra metadata along with the fie upload. This section shows how this service can be used.
Let's review how the front end does the file upload. On the HTML side, I still used the hidden file input field along with a bootstrap input group (a text field and a button stitched together as one control). Clicking the button, it will click the file input field, which will pop open the Open File dialog allowing user to choose a file. Once the user chooses the file, I don't have to extract the data as BASE64 encoded and added to a JSON request. Here is the HTML mark-up for the file input control:
<div class="input-group">
<input type=file id="chosenFile" style="display: none;">
<input type="text" class="form-control" ng-model="vm.fileName" ng-disabled="vm.uploadingFile">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="vm.clickBrowseFile()">Browse</button>
</span>
</div>
On the angular side, I have to setup the function of clicking the button to trigger the file input control to open the Open File dialog. This is how:
vm.clickBrowseFile = function () {
angular.element("#fileUploadForm #chosenFile").click();
};
After the button clicked and a file is selected, the file name will be displayed in the text field. This is done with the following:
angular.element("#fileUploadForm #chosenFile").bind("change", function(evt) {
if (evt) {
if (evt.target.files && evt.target.files.length > 0) {
vm.fileName = evt.target.files[0].name;
}
$scope.$apply();
}
});
These are all copied from my previous tutorial. Everything excepts the last part where I get the file name and display in the text field. I rewrote this part because the new way will be more efficient. And the new way make me look more professional (joking). If you are interested to review these, you can check it out. The next part is also part of the previous tutorial, and I have modified it to use the new upload service:
vm.clickUpload = function () {
vm.uploadingFile = true;
vm.uploadSuccess = false;
vm.uploadFailed = false;
var upoadFileField = angular.element("#fileUploadForm #chosenFile");
if (upoadFileField != null && upoadFileField.length > 0) {
if (upoadFileField[0].files && upoadFileField[0].files.length > 0) {
testSampleUploadService.uploadFile("./uploadFile", upoadFileField[0].files[0])
.then(function (result) {
if (result && result.status === 200) {
if (result.data && result.data.opSuccess === true) {
vm.uploadSuccess = true;
} else {
vm.uploadFailed = true;
}
} else {
vm.uploadFailed = true;
}
}, function (error) {
if (error) {
console.log(error);
}
vm.uploadFailed = true;
}).finally(function () {
upoadFileField[0].files = null;
vm.fileName = null;
vm.uploadingFile = false;
});
}
}
};
There are two parts about this code. The first is getting the selected file for upload. To do this, I query the element of the file input. Then I check whether this input has file attached. Once I am sure the file is selected, I would invoke the file upload service to upload the file. To check the attached file:
...
var upoadFileField = angular.element("#fileUploadForm #chosenFile");
if (upoadFileField != null && upoadFileField.length > 0) {
if (upoadFileField[0].files && upoadFileField[0].files.length > 0) {
...
}
...
}
...
To invoke the file upload service:
testSampleUploadService.uploadFile("./uploadFile", upoadFileField[0].files[0])
.then(function (result) {
if (result && result.status === 200) {
if (result.data && result.data.opSuccess === true) {
vm.uploadSuccess = true;
} else {
vm.uploadFailed = true;
}
} else {
vm.uploadFailed = true;
}
}, function (error) {
if (error) {
console.log(error);
}
vm.uploadFailed = true;
}).finally(function () {
upoadFileField[0].files = null;
vm.fileName = null;
vm.uploadingFile = false;
});
As shown, this is not a complicated design. Getting the file object from file input field is easy, just references upoadFileField[0].files[0]
. The rest is handling the return response. Before invoking the service, the input fields are all disabled using conditional variable vm.uploadingFile
. When it is set to true, the input fields and buttons will be disabled. When the upload operation with the back end completes, it is set to false and the input fields will be enabled.
When the upload is handled successfully by the backend, there will be a status message display at the very part. If it fails, the sample spot will display a red error message. This is controlled by the values of vm.uploadSuccess
and vm.uploadFailed
. These were set either by the success callback or the error callback. That is all for the front end.
The back end processing of the file upload is not complicated at all. The Multi-part file upload can contain one or more files, as well as other objects (identified by unique keys). The hardest part of the back end processing is to get the file and other objects in the same request. Once these were obtained, the processing would be relatively easy. For this tutorial, all I do is get the data, for the file, I save it to a pre-defined location. And for the other data object, which is a JSON formatted object, I just print out one of the properties in this object.
Setting up the RESTFul API controller for this is easy. Next it comes with the first hard part, how to declare a request handling method that can take care of the incoming request, here is the method declaration:
@RequestMapping(value="uploadFile", method=RequestMethod.POST)
public ResponseEntity<StatusResponse> uploadFile(
@RequestParam("file") MultipartFile srcFile,
@RequestParam("fileMetadata") String fileMetadata)
{
...
}
Spring Framework provided an object type called MultipartFile
. With this, handling the file upload becomes a lot easier. Here is the way I get the file info, which are the first 4 lines of the above method:
System.out.println("file received.");
System.out.println("file name: " + srcFile.getOriginalFilename());
System.out.println("file size: " + srcFile.getSize());
System.out.println("content type: " + srcFile.getContentType());
When the file is selected for the file upload, the file name (the name only, without the full file path with folders and such) can be retrieved from the object that represent the file, MultipartFile srcFile
. It is done through the method getOriginalFilename()
. Do not confuse this with the method called getName(). If you call getName(), guess what it returns back? The value returned is "file" This is the name which I have assigned to the file data in JavaScript code. And you can also find it as the value of @RequestParam("file")
. So, do not confused these two methods.
It is also possible to get the size of the file by calling getSize()
. This will return the integer size of the file. Another useful method to call is the getContentType()
method. It can return the mime type of the file. This is useful because if you save it, you can send it back along with the file itself. This saves you the trouble of finding a suitable system API that can find the mime type for the file later. It is do-able. but why waste the time and energy searching for a suitable system API or 3rd party jar when the file mime type can be retrieved and stored when they were first uploaded. It is just a thought.
Next, I want to show you how to save the file. The object type MultipartFile
has a method called getInputStream()
. The easiest way to utilize this is to copy the input stream into a FileOutputStream
. Then close the output stream and we are all done. Here it is:
FileOutputStream outFile = null;
...
try
{
outFile = new FileOutputStream("C:\\DevJunk\\" + srcFile.getOriginalFilename());
IOUtils.copy(srcFile.getInputStream(), outFile);
...
}
...
finally
{
if (outFile != null) {
try
{
outFile.close();
}
catch(Exception ex) {}
}
In the code snippet above, I hard code the file path where I save the file. I used the same file name that was passed in with the upload. There are other ways to save the file, get all the bytes as a byte array and save into database, or disk. I won't show how these are done. Just to give you some idea what other possibilities are.
The last concept I like to explain is how to convert the metadata object from string into an object. This is done with the Jackson framework. Jackson framework is used to convert JSON string to an Java object, and the other way around. In this case, I have a string based JSON object. I use Jackson framework to convert it to a Java object. Here is how I did it:
...
ObjectMapper objectMapper = new ObjectMapper();
FileMetadata fileMeta = objectMapper.readValue(fileMetadata, FileMetadata.class);
...
System.out.println("Additional Metadata: " + fileMeta.getDescription());
...
Once the string is successfully converted into an Java object, I use console output to display the description property of it. That is the last line of the above code snippet.
Here is the entire source code of the REST API controller.
package org.hanbo.boot.rest.controllers;
import java.io.FileOutputStream;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.hanbo.boot.rest.models.FileMetadata;
import org.hanbo.boot.rest.models.StatusResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
public class SampleFileUploadController
{
@RequestMapping(value="uploadFile", method=RequestMethod.POST)
public ResponseEntity<StatusResponse> uploadFile(
@RequestParam("file") MultipartFile srcFile,
@RequestParam("fileMetadata") String fileMetadata)
{
System.out.println("file received.");
System.out.println("file name: " + srcFile.getOriginalFilename());
System.out.println("file size: " + srcFile.getSize());
System.out.println("content type: " + srcFile.getContentType());
StatusResponse retResp = new StatusResponse();
FileOutputStream outFile = null;
try
{
outFile = new FileOutputStream("C:\\DevJunk\\" + srcFile.getOriginalFilename());
IOUtils.copy(srcFile.getInputStream(), outFile);
ObjectMapper objectMapper = new ObjectMapper();
FileMetadata fileMeta = objectMapper.readValue(fileMetadata, FileMetadata.class);
System.out.println("Additional Metadata: " + fileMeta.getDescription());
retResp.setSummary("Upload successful.");
retResp.setOpSuccess(true);
retResp.setDetailedMessage("Sample test file upload is succesful.");
}
catch(Exception ex)
{
ex.printStackTrace();
retResp.setSummary("Upload failed.");
retResp.setOpSuccess(false);
retResp.setDetailedMessage(String.format("Sample test file upload has failed. Exception [%s]", ex.getMessage()));
}
finally
{
if (outFile != null) {
try
{
outFile.close();
}
catch(Exception ex) {}
}
}
ResponseEntity<StatusResponse> retVal = ResponseEntity.ok(retResp);
return retVal;
}
}
This is all there is for the Java side. I know, the source codes (both front end and back end) are pretty horrible. It is for demonstration purposes. It should clearly show how the file upload works.
If I don't perform this step, the default max size of upload file will be 1MB, and the default form request size is also 1MB, this made the upload functionality almost useless. So what is the get-around? All I have to do is adding two lines to application.properties, and the max sizes would increase and make the functionality much more useful. Here they are:
spring.servlet.multipart.max-file-size=100MB spring.servlet.multipart.max-request-size=500MB
These two lines makes the file size to max of 100 MB, and the max request size is set to 500MB. This allows 5 files (at least 4 files with max allowed file size) to be attached to the request. You can reconfigure these two values to your liking. This is just a little extra information I like to add here.
To run the sample application, open a command line prompt. Then cd into the base directory where the source code of this application is located. Run the following command:
mvn clean install
Make sure the build succeeds. Then use the following command to start up the service:
java -jar target\hanbo-agularjs-uploadformdata-1.0.1.jar
Once the service starts up successfully, use the browser and navigate to:
http://localhost:8080/
The page will show as this:
Select a file use button "Browse", then click "Upload". You will see the file being stored at the location you specified. Although you might want to change the location of the file where it will be stored before you run the sample application.
This is not a brilliant tutorial, not a great tutorial. I hope it is a good one. In this tutorial, I want to show how to upload a file using AngularJS $http and JavaScript object type FormData. On the Java side, I used a RESTFul API controller to handle the upload.
In addition to the normal file upload operation, this tutorial also show how to include extra data as object in the upload request. The data represented as string can be converted to an object using the Jackson framework, turning JSON string into a Java object.
The back end service does not do much, only to save the file to disk and print out of data received at the back end. I purposely made this simple so that I can reference back on this tutorial so that I can build more complex functionality from all these. In essence, I wrote this for me so that one day I can check back when needed. Anyway, it will be helpful for me. I hope it will be helpful for you as well. Good luck.
There is no comments to this post/article.