February 12, 2017 • ionic, cordova, spring
In this post we will build a file upload example with Ionic and Spring Boot. In the client app the user takes a picture with the camera or selects a picture from the photo gallery and the program sends the file to a Spring Boot application over HTTP.
Server
We generate the Spring Boot application with the https://start.spring.io/ website and select Web
as the only dependency.
Next we create a RestController
with a method that handles the upload of the picture. The handler listens for POST requests to the url /upload
. The client sends HTTP multipart requests, Spring MVC automatically decodes these requests and calls our method with an instance of the MultipartFile interface. This class encapsulates information about the uploaded file, like filename, size in bytes and the binary content of the file.
@RestController
public class UploadController {
@CrossOrigin
@PostMapping("/upload")
public boolean pictureupload(@RequestParam("file") MultipartFile file) {
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
try {
Path downloadedFile = Paths.get(file.getOriginalFilename());
Files.deleteIfExists(downloadedFile);
Files.copy(file.getInputStream(), downloadedFile);
return true;
}
catch (IOException e) {
LoggerFactory.getLogger(this.getClass()).error("picture upload", e);
return false;
}
}
}
src/main/java/ch/rasc/upload/UploadController.java
The method checks if a file with the same name already exists and deletes it. After that it copies the file content from the MultiPartFile into a file on the filesystem. The method returns true if the transfer and copy process was successful. The client app uses this response as a simple error handling mechanism and presents a corresponding message to the user.
Next we need to create an application.yml
or application.properties
file in the folder src/main/resources
. In this file we have to configure the maximum file size parameters.
spring:
http:
multipart:
max-file-size: 20MB
max-request-size: 20MB
src/main/resources/application.yml
The config option max-file-size
specifies the maximum size in bytes of a file (default 1MB). The max-request-size
specifies the maximum size in bytes of a http request (default 10MB). There are two options because a request could contain more than one file. I set both options to 20MB because the client app only send one file per request and the pictures on my phone have a size between 7 and 12 MB.
This is all we have to write and configure for the server side. Everything else is automatically configured by Spring Boot. You can start the server on the command line with ./mvnw spring-boot:run
or in an IDE by running the main class.
Client
We start building the client app with the Ionic command line tool and base our app on the blank template.
ionic start upload blank
Next we add a platform. I use Android here, but the example works with iOS too.
cordova platform add android
Then we need to install two Cordova plugins. The camera plugin allows the app to take pictures with the camera and to select pictures from the photo library. The file plugin implements a File API that allows an application to read files from the filesystem.
cordova plugin add cordova-plugin-camera
cordova plugin add cordova-plugin-file
npm install @ionic-native/camera
npm install @ionic-native/file
Open the file src/pages/home.html
and add the following code.
<ion-header>
<ion-navbar>
<ion-title>
Upload
</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-item>
<ion-row>
<ion-col width-50>
<button ion-button color="danger" type="button" full round large (click)="takePhoto()">
<ion-icon name="camera"></ion-icon>
</button>
</ion-col>
<ion-col width-50>
<button ion-button color="secondary" type="button" full round large (click)="selectPhoto()">
<ion-icon name="image"></ion-icon>
</button>
</ion-col>
</ion-row>
</ion-item>
<ion-item *ngIf="error">
<strong>{{error}}</strong>
</ion-item>
<ion-item>
<img *ngIf="myPhoto" class="img-responsive" [src]="myPhoto"/>
</ion-item>
</ion-content>
The app displays two buttons. The button on the left opens the camera, the button on the right opens a photo gallery explorer. Below the two buttons we add an ion-item
element that shows error messages and another ion-item
that displays the selected picture.
In the TypeScript class src/pages/home.ts
we implement the two click handlers
takePhoto() {
this.camera.getPicture({
quality: 100,
destinationType: this.camera.DestinationType.FILE_URI,
sourceType: this.camera.PictureSourceType.CAMERA,
encodingType: this.camera.EncodingType.PNG,
saveToPhotoAlbum: true
}).then(imageData => {
this.myPhoto = imageData;
this.uploadPhoto(imageData);
}, error => {
this.error = JSON.stringify(error);
});
}
selectPhoto(): void {
this.camera.getPicture({
sourceType: this.camera.PictureSourceType.PHOTOLIBRARY,
destinationType: this.camera.DestinationType.FILE_URI,
quality: 100,
encodingType: this.camera.EncodingType.PNG,
}).then(imageData => {
this.myPhoto = imageData;
this.uploadPhoto(imageData);
}, error => {
this.error = JSON.stringify(error);
});
}
Both functions are very similar, takePhoto
opens the camera and selectPhoto
opens the photo library explorer. The code in the then
handler assigns the picture to the myPhoto
instance variable, that shows the picture on the home page. Then it calls the uploadPhoto
function that starts the upload process to the server.
private uploadPhoto(imageFileUri: any): void {
this.error = null;
this.loading = this.loadingCtrl.create({
content: 'Uploading...'
});
this.loading.present();
this.file.resolveLocalFilesystemUrl(imageFileUri)
.then(entry => (<FileEntry>entry).file(file => this.readFile(file)))
.catch(err => console.log(err));
}
Because the Camera plugin returns the picture as an URI we have to load the contents into our app to be able to send the file with the http service to the server. Alternatively we could set the destination type to data url (Camera.DestinationType.DATA_URL
), but this would convert the file contents to a base64 encoded string and that makes the file about 30% larger.
To load the file the app first has to resolve the URI it gets from the camera plugin. It does that by calling the resolveLocalFilesystemUrl
function that returns either a FileEntry
or DirectoryEntry
. We know that in this case it's always a file so we can safely cast it to a FileEntry
. Ionic Native wraps the resolveLocalFilesystemUrl
function in a Promise
so the app can handle the result in the then
handler.
The program calls next the file
function on the FileEntry
object. This function creates a File
object and expects a callback handler. In this callback the app calls the readFile
function.
private readFile(file: any) {
const reader = new FileReader();
reader.onloadend = () => {
const formData = new FormData();
const imgBlob = new Blob([reader.result], {type: file.type});
formData.append('file', imgBlob, file.name);
this.postData(formData);
};
reader.readAsArrayBuffer(file);
}
In the readFile
function the program utilizes the FileReader
from the html file api to read the file into an ArrayBuffer
. The onloadend
event is called when the file is successfully read. The app then creates a FormData
object, wraps the array buffer in a Blob
and adds it to the FormData
object with the name 'file
'. This is the same name the server expects as request parameter. The app then creates a POST request with Angular's http service
private postData(formData: FormData) {
this.http.post("http://192.168.178.20:8080/upload", formData)
.catch((e) => this.handleError(e))
.map(response => response.text())
.finally(() => this.loading.dismiss())
.subscribe(ok => this.showToast(ok));
}
To test the app you have to run it in an emulator or on a real device. It will not work in the browser because of the Cordova plugins.
ionic cordova run android
When everything works you should find the uploaded files in the project directory of the server application. The entire source code for this example is hosted on GitHub.