Uploading pictures from Ionic 3 to Spring Boot

Published: February 12, 2017  •  Updated: November 16, 2017  •  ionic3, cordova, spring, javascript, java

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 col-6>
       <button ion-button color="danger" type="button" full round large (click)="takePhoto()">
         <ion-icon name="camera"></ion-icon>
       </button>
     </ion-col>
     <ion-col col-6>
       <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>

src/pages/home/home.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 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
    }).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);
    });
  }

src/pages/home/home.ts

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

src/pages/home/home.ts

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

src/pages/home/home.ts

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 provider.

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

src/pages/home/home.ts

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.