Home | Send Feedback

Uploading pictures from Capacitor app to Spring Boot

Published: 11. June 2019  •  Updated: 18. November 2024  •  java, spring, javascript, ionic, capacitor

In an older blog post, I wrote about uploading pictures from a Cordova app to a Spring Boot server.

With the release of Ionic Capacitor 1.0 just a few days ago (May 22, 2019) it's time to revisit this topic and write an upload example with a Capacitor app.

Capacitor is Ionic's solution to wrap applications written in JavaScript, CSS, and HTML into a native container for the iOS and Android platforms. Capacitor works very similarly to Cordova, and the native container exposes the capabilities of the device to the JavaScript application.

There are a few differences to Cordova. A big difference is that Cordova recreates the build folder for Android and iOS each time it builds a new app version. Whereas with Capacitor, you create the build folder for Android and iOS once and also commit it into the source code management system. During development, you then only copy the build output of your JavaScript application into the native app folders (with npx cap copy or npx cap sync)

Another difference is that the core platform of Capacitor already provides often used plugins without installing any additional library. You find a list of the plugins that are part of the core here:
https://capacitorjs.com/docs/apis

Capacitor not only supports their own plugins it also supports most of the Cordova plugins, so you have access to a wide variety of plugins that should work on Capacitor.

Read more about Capacitor on the official homepage:
https://capacitorjs.com/


In this blog post, we are going to create a trivial app where the user can take a photo, and the application automatically uploads it over HTTP to a Spring Boot backend. The app implements the upload in two different ways. One is using a "normal" upload with Angular's HttpClient, and the other upload uses the JavaScript tus library, that splits the file into multiple chunks and uploads them one by one.

I cover the tus functionality only briefly in this blog post because I wrote a blog post about tus that covers this topic in detail: https://golb.hplar.ch/2019/06/upload-with-tus.html

Here a screenshot of the app. With the toggle, you can switch between the "normal" and tus.io upload.

app screenshot

Client

Setup

The JavaScript client application is based on the blank Ionic template. First, add the JavaScript tus library.

npm install tus-js-client

and the Capacitor library and CLI

npm install @capacitor/core
npm install -D @capacitor/cli

Initialize Capacitor

npx cap init

To use the Camera plugin, we need to import the Plugins object of @capacitor/core in TypeScript.

import {Plugins, .... } from '@capacitor/core';

We can then access the Camera API via the Plugins object: Plugins.Camera.


Camera API

The app presents two buttons, one to take a photo from the camera and one to select a picture from the image gallery. The code behind these buttons is very similar. The only difference is the source property that we pass in the configuration object to the Camera.getPhoto() method

  async takePhoto(): Promise<void> {
    const ab = await this.getPhoto(CameraSource.Camera);
    if (ab) {
      if (this.tus) {
        await this.uploadTus(ab);
      } else {
        await this.uploadAll(ab);
      }
    }
  }

  async selectPhoto(): Promise<void> {
    const ab = await this.getPhoto(CameraSource.Photos);
    if (ab) {
      if (this.tus) {
        await this.uploadTus(ab);
      } else {
        await this.uploadAll(ab);
      }
    }
  }

  private async getPhoto(source: CameraSource): Promise<string | undefined> {
    const image = await Camera.getPhoto({
      quality: 90,
      allowEditing: false,
      resultType: CameraResultType.Uri,
      source
    });

    if (image.webPath) {
      this.photo = this.sanitizer.bypassSecurityTrustResourceUrl(image.webPath);
    }
    return image.webPath;
  }

home.page.ts

The Camera.getPhoto() method currently only returns jpeg images, and with the quality configuration, we indicate the quality of the picture (0 = worst, 100 = best). Greater numbers mean bigger file sizes.

There are currently three supported returnType: CameraResultType.Base64, CameraResultType.DataUrl and CameraResultType.Uri

The result type Uri returns a path that can be used to set the src attribute of an image for efficient loading and rendering. We run the path first through Angular's DomSanitizer to prevent any XSS issues before we assign it to the variable photo.

This variable is used as the value for the src attribute in an img tag

        <img *ngIf="photo" [src]="photo"/>

home.page.html

See the documentation page for more information about the Camera API:
https://capacitorjs.com/docs/apis/camera


normal upload

The uploadAll() method implements a simple upload with Angular's HttpClient. First, we need to convert the web path we get from the Camera plugin into a Blob. A straightforward solution is to use the Fetch API and "fetch" the web path, which returns the desired blob object.

Next, we create a FormData object and append a new value with the blob object. The important thing here is that the name file needs to match the name of the request parameter on the server we are going to implement in the next section.

  private async uploadAll(webPath: string): Promise<void> {
    this.loading = await this.loadingCtrl.create({
      message: 'Uploading...'
    });
    await this.loading.present();

    const blob = await fetch(webPath).then(r => r.blob());

    const formData = new FormData();
    formData.append('file', blob, `file-${this.counter++}.jpg`);
    this.http.post<boolean>(`${environment.serverUrl}/uploadAll`, formData)
      .pipe(
        catchError(e => this.handleError(e)),
        finalize(() => this.loading?.dismiss())
      )
      .subscribe(ok => this.showToast(ok));
  }

home.page.ts

Lastly, we send the file with a regular POST request to the Spring Boot back end.


tus.io upload

The upload with the JavaScript tus library works very similarly. We also first need to convert the web path into a blob object and then pass this blob to Upload. I won't go much further into detail about tus.io because the code is essentially the same as the code in my blog post about tus.io.

  private async uploadTus(webPath: string): Promise<void> {

    this.loading = await this.loadingCtrl.create({
      message: 'Uploading...'
    });
    await this.loading.present();

    const blob = await fetch(webPath).then(r => r.blob());
    const upload = new Upload(blob, {
      endpoint: `${environment.serverUrl}/upload`,
      retryDelays: [0, 3000, 6000, 12000, 24000],
      chunkSize: 512 * 1024,
      metadata: {
        filename: `file-${this.counter++}.jpg`
      },
      onError: () => {
        this.showToast(false);
        this.loading?.dismiss();
      },
      onSuccess: () => {
        this.showToast(true);
        this.loading?.dismiss();
      }
    });

    upload.start();
  }

home.page.ts


Build

With the program now finished, we can build the project and add iOS and Android support.

First, make sure that all required dependencies are installed on your computer:
https://capacitorjs.com/docs/getting-started/environment-setup

ng build --configuration production
npx cap add android
npx cap add ios

Next, open Android Studio and/or Xcode.

npx cap run android
npx cap run ios

From there you can build the native apps and deploy them to a real device or run them inside an emulator.

Server

The server is a Spring Boot application including the Web dependency (spring-boot-starter-web) and is essentially a copy of the application that I wrote for my previous blog post about tus.io.

I add one additional HTTP endpoint that handles the "normal" upload request. The important part here is that the request parameter name matches the name in the FormData of the JavaScript client (file).

  @CrossOrigin
  @PostMapping("/uploadAll")
  @ResponseBody
  public boolean uploadAll(@RequestParam("file") MultipartFile file) {

    try {
      Path downloadedFile = this.uploadDirectory
          .resolve(Paths.get(file.getOriginalFilename()));
      Files.deleteIfExists(downloadedFile);
      Files.copy(file.getInputStream(), downloadedFile);
      return true;
    }
    catch (IOException e) {
      LoggerFactory.getLogger(this.getClass()).error("uploadAll", e);
      return false;
    }

  }

UploadController.java

The handling of the file is very straightforward. The code extracts the bytes with the InputStream from the MultipartFile and writes them into a file in the upload directory.

This also supports sending multiple files in one request. On the client you add multiple blobs the FormData object. Each with a different name.

const formData = new FormData();
formData.append('file1', blob1, `file-1.jpg`);
formData.append('file2', blob2, `file-2.jpg`);

and on the HTTP endpoint you define multiple parameters matching the FormData values.

public boolean uploadAll(@RequestParam("file1") MultipartFile file1, @RequestParam("file2") MultipartFile file2) {

And finally, as always, when you write a Spring Boot application that supports file uploads, check and adjust the max size settings.

spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB

application.properties

The defaults are 1MB for max-file-size and 10MB for max-request-size.


For testing, I use ngrok. I start the Spring Boot application on my development machine, then create a tunnel with ngrok

ngrok http 8080

And set the path that ngrok gives me in the client application environment file.

export const environment = {
  production: true,
  serverUrl: 'https://a946be920a04.ngrok.io'
};

environment.prod.ts

Then build the JavaScript application, run npx cap copy and build the native app in Xcode and/or Android Studio and deploy it to a real device or emulator.

Web

The nice thing about the Capacitor Camera plugin is that it also works on the web. Not out of the box, but with minimal changes to the existing code.

First, we add one additional package

npm install @ionic/pwa-elements

then we open src/main.ts and add code that calls defineCustomElements after the Angular app has been loaded.

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import { defineCustomElements } from '@ionic/pwa-elements/loader';
import {AppModule} from './app/app.module';

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err))
  .then(() => defineCustomElements(window));


main.ts

The @ionic/pwa-elements package implements web-based functionality and user interfaces for some Capacitor plugins. This package will be ignored when you run the application on a device wrapped in Capacitor, but when you run this in a browser, you get a nice camera user interface.

For more information about Capacitor visit the official homepage:
https://capacitorjs.com/

Also, read this guide about writing a Camera app:
https://capacitorjs.com/docs/guides/ionic-framework-app

You find the source code for the example presented in this blog post on GitHub:
https://github.com/ralscha/blog2019/tree/master/capupload