In this post, we are going to 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 Spring Web
as the only dependency.
Next, we create a RestController
with a method that handles the picture upload. The handler listens for POST requests from 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("pictureupload", e);
return false;
}
}
}
The method first tests if a file with the same name already exists and deletes it. After that, it copies the file content from MultiPartFile
into a file on the filesystem. The method returns true if the transfer and copy process were 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
file in the folder src/main/resources
. In this file, we have to configure the maximum request file size parameters.
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
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 sends one file per request, and the pictures on my phone have a size between 7 and 12 MB.
That's all we have to write and configure for the server-side. Spring Boot automatically configures everything else. You can start the server from the command line with ./mvnw spring-boot:run
or in an IDE by launching the main class.
Client ¶
We start building the client app with the Ionic command-line tool and base our app on the blank starter template.
ionic start upload blank
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.
ionic cordova plugin add cordova-plugin-camera
ionic cordova plugin add cordova-plugin-file
Open the home page template and insert this code.
<ion-header>
<ion-toolbar>
<ion-title>
Upload
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label>
<ion-row>
<ion-col size="6">
<ion-button (click)="takePhoto()" color="danger" expand="full" shape="round" size="large" type="button">
<ion-icon name="camera"></ion-icon>
</ion-button>
</ion-col>
<ion-col size="6">
<ion-button (click)="selectPhoto()" color="secondary" expand="full" shape="round" size="large" type="button">
<ion-icon name="image"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-label>
</ion-item>
<ion-item *ngIf="error">
<ion-label>
<strong>{{error}}</strong>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
<img *ngIf="myPhoto" [src]="myPhoto"/>
</ion-label>
</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 of the home page, we implement the two-click handlers
takePhoto(): void {
// @ts-ignore
const camera: any = navigator.camera;
camera.getPicture((imageData: any) => {
this.myPhoto = this.convertFileSrc(imageData);
this.changeDetectorRef.detectChanges();
this.changeDetectorRef.markForCheck();
this.uploadPhoto(imageData);
}, (error: any) => this.error = JSON.stringify(error), {
quality: 100,
destinationType: camera.DestinationType.FILE_URI,
sourceType: camera.PictureSourceType.CAMERA,
encodingType: camera.EncodingType.JPEG
});
}
selectPhoto(): void {
// @ts-ignore
const camera: any = navigator.camera;
camera.getPicture((imageData: any) => {
this.myPhoto = this.convertFileSrc(imageData);
this.uploadPhoto(imageData);
}, (error: any) => this.error = JSON.stringify(error), {
sourceType: camera.PictureSourceType.PHOTOLIBRARY,
destinationType: camera.DestinationType.FILE_URI,
quality: 100,
encodingType: camera.EncodingType.JPEG,
});
}
Both functions are very similar, takePhoto
opens the camera and selectPhoto
opens the photo library explorer. The code in the success handler assigns the picture to the myPhoto
instance variable, which shows the picture on the home page. Then it calls the uploadPhoto
method that starts the upload process to the server.
private async uploadPhoto(imageFileUri: any): Promise<void> {
this.error = null;
this.loading = await this.loadingCtrl.create({
message: 'Uploading...'
});
this.loading.present();
// @ts-ignore
window.resolveLocalFileSystemURL(imageFileUri,
(entry: any) => {
entry.file((file: any) => this.readFile(file));
});
}
Because the Camera plugin returns the picture as a URI and not the raw data, we have to load the picture content into our app to be able to send the data with the HTTP service to the server. Alternatively, we could set the destination type to data URL (Camera.DestinationType.DATA_URL
), but this returns the file contents as a base64 encoded string and that increases the file size about 30%.
To be able to load the file, the app first needs to convert the URI it gets from the camera to an object. It does that by calling the resolveLocalFilesystemUrl
function that returns either a FileEntry
or DirectoryEntry
. In this case, it's always a FileEntry
object.
The program next calls 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): void {
const reader = new FileReader();
reader.onloadend = () => {
const formData = new FormData();
if (reader.result) {
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 File API to read the file into an ArrayBuffer
. The onloadend
event is called as soon as 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 a request parameter. The app then creates and sends a POST request with Angular's HTTP client.
private postData(formData: FormData): void {
this.http.post<boolean>(`${environment.serverURL}/upload`, formData)
.pipe(
catchError(e => this.handleError(e)),
finalize(() => 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 does 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 root of the server application. The entire source code for this example is hosted on GitHub.