Home | Send Feedback

Uploading pictures from Ionic / Cordova to Spring Boot

Published: 12. February 2017  •  Updated: 6. December 2018  •  ionic, 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 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;
    }

  }
}

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

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

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(): 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,
    });
  }

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

home.page.ts

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

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

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.