Home | Send Feedback

Uploading pictures from Ionic 4 / Cordova to Spring Boot

Published: February 12, 2017  •  Updated: December 06, 2018  •  ionic4, cordova, spring, javascript, java

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 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;
    }

  }

UploadController.java

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 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 request file size parameters.

spring:
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 20MB

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 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 padding>
  <ion-item>
    <ion-label>
      <ion-row>
        <ion-col size="6">
          <ion-button color="danger" type="button" expand="full" shape="round" size="large" (click)="takePhoto()">
            <ion-icon name="camera"></ion-icon>
          </ion-button>
        </ion-col>
        <ion-col size="6">
          <ion-button color="secondary" type="button" expand="full" shape="round" size="large" (click)="selectPhoto()">
            <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>

home.page.html

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() {
    const camera: any = navigator['camera'];
    camera.getPicture(imageData => {
      this.myPhoto = this.convertFileSrc(imageData);
      this.uploadPhoto(imageData);
    }, error => this.error = JSON.stringify(error), {
      quality: 100,
      destinationType: camera.DestinationType.FILE_URI,
      sourceType: camera.PictureSourceType.CAMERA,
      encodingType: camera.EncodingType.JPEG
    });
  }

  selectPhoto(): void {
    const camera: any = navigator['camera'];
    camera.getPicture(imageData => {
      this.myPhoto = this.convertFileSrc(imageData);
      this.uploadPhoto(imageData);
    }, error => this.error = JSON.stringify(error), {
      sourceType: camera.PictureSourceType.PHOTOLIBRARY,
      destinationType: camera.DestinationType.FILE_URI,
      quality: 100,
      encodingType: camera.EncodingType.JPEG,
    });
  }

home.page.ts

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, that 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) {
    this.error = null;
    this.loading = await this.loadingCtrl.create({
      message: 'Uploading...'
    });

    this.loading.present();

    window['resolveLocalFileSystemURL'](imageFileUri,
      entry => {
        entry['file'](file => this.readFile(file));
      });
  }

home.page.ts

Because the Camera plugin returns the picture as an 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 load the file the app first resolves the URI it gets from the camera plugin. 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) {
    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);
  }

home.page.ts

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 request parameter. The app then creates and sends a POST request with Angular's HTTP client.

  private postData(formData: FormData) {
    this.http.post<boolean>(`${environment.serverURL}/upload`, formData)
      .pipe(
        catchError(e => this.handleError(e)),
        finalize(() => this.loading.dismiss())
      )
      .subscribe(ok => this.showToast(ok));
  }

home.page.ts

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.