Home | Send Feedback

Uploading files from Ionic / Cordova to Minio / S3

Published: 16. February 2017  •  Updated: 6. December 2018  •  ionic, cordova, spring, s3, javascript, java

In the previous post we played with Minio, an S3 compatible server, and wrote a Java application that uploads a file to the server. And in an earlier post we had a look at an example that sends pictures from an Ionic / Cordova application to a Spring Boot server.

In this blog, we are going to do a combination of these two posts and create an example that sends pictures from an Ionic / Cordova application directly to a Minio server.

The developers of Minio wrote a JavaScript client library that works with Minio and S3 servers. The primary usage of this library is for Node.js applications, but it also works in the browser. But that's not the way you should use this, because a client needs to know the keys to send requests to an S3 compatible server, and storing these keys on the client is considered to be a security risk.

What an application should use instead are pre-signed URLs. This is not a Minio specific feature. Amazon S3 supports this as well. A pre-signed URL is like a one-time usage key that allows a client to upload or fetch files from an S3 compatible server without having security credentials with the service.


Flow of a connection with pre-signed URLs.

flow

The client sends a request (1) to a server that creates pre-signed URLs. This service can be implemented in any server-side framework. For our example, we're going to write this as a Spring Boot server. The service needs to know the security credentials for the Minio server (keys) to be able to create pre-signed URLs.
The service should first check if the client has the proper permissions to upload or fetch files from the server. Then it makes a connection (2) to Minio (or S3) to request a pre-signed URL and returns this URL to the client. A pre-signed URL can be either used for uploading or for fetching files. In our example, this URL allows the client to upload a file. The client can now directly connect (3) to the Minio server with this URL and either upload or fetch a file.

Client

First, we create the client application. For that we copy the project from this post (GitHub).

Then we open the TypeScript code of the home page and make the necessary changes.

    // @ts-ignore
    window.resolveLocalFileSystemURL(imageFileUri,
      (entry: any) => {
        entry.file((file: any) => this.readFile(file));
      });
  }

  private readFile(file: any): void {
    const fileName = file.name;
    const reader = new FileReader();
    reader.onloadend = () => {
      if (reader.result) {
        const imgBlob = new Blob([reader.result], {type: file.type});
        this.fetchPresignUrl(fileName).subscribe(url => this.postData(url, imgBlob));
      }
    };
    reader.readAsArrayBuffer(file);
  }

  private postData(url: string, blob: Blob): void {
    this.http.put(url, blob, {observe: 'response', responseType: 'text'})
      .pipe(catchError(e => this.handleError(e)),
        finalize(() => this.loading.dismiss())
      )
      .subscribe(resp => {
        this.showToast(resp.ok);
      });
  }

home.page.ts

Before the app can upload the picture, it needs to fetch the pre-signed URL. For that, the application calls the method fetchPresignUrl. This method sends an HTTP GET request to our Spring Boot server which is listening on the URL /getPreSignUrl and returns the pre-signed URL. After the app received the pre-signed URL, it initiates a PUT request to this URL with the binary data attached to the body. This PUT request directly connects to the Minio server and stores the file on the server.

Server

We implement the server with Spring Boot and the starter-web dependency. Also, we need to add the Minio Java client library to the pom.xml.

    <dependency>
      <groupId>io.minio</groupId>
      <artifactId>minio</artifactId>
      <version>8.5.7</version>
    </dependency>

pom.xml

I created a configuration class for the Minio keys and endpoint address, that allows me to externalize these configuration options in the application.properties or application.yml file.

@ConfigurationProperties(prefix = "minio")
@Component
public class MinioConfig {

  private String endpoint;
  private String accessKey;
  private String secretKey;
  private String region;

MinioConfig.java

Then the application instantiates a MinioClient and exposes it as a Spring-managed bean.

package ch.rasc.upload;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import io.minio.MinioClient;

@SpringBootApplication
public class Application {

  public final static Logger logger = LoggerFactory.getLogger(Application.class);

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  @Bean
  public MinioClient minioClient(MinioConfig config) {
    return MinioClient.builder().endpoint(config.getEndpoint())
        .credentials(config.getAccessKey(), config.getSecretKey()).build();
  }
}

Application.java

Next, we create a RestController with one handler that creates and returns pre-signed URLs. We inject the MinioClient bean and call the operation presignedPutObject to request a pre-signed URL from the Minio server.

public class PreSignController {
  private final static String BUCKET_NAME = "uploads";

  private final MinioClient minioClient;

  public PreSignController(MinioClient minioClient) {
    this.minioClient = minioClient;
  }

  @PostConstruct
  public void createBucket()
      throws InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException,
      ErrorResponseException, InternalException, IOException, InvalidResponseException,
      IllegalArgumentException, XmlParserException, ServerException {

    if (!this.minioClient
        .bucketExists(BucketExistsArgs.builder().bucket(BUCKET_NAME).build())) {
      this.minioClient.makeBucket(MakeBucketArgs.builder().bucket(BUCKET_NAME).build());
    }

  }

  @CrossOrigin
  @GetMapping("/getPreSignUrl")
  public String getPreSignUrl(@RequestParam("fileName") String fileName)
      throws InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException,
      ErrorResponseException, InternalException, IOException, InvalidResponseException,
      IllegalArgumentException, XmlParserException, ServerException {

    return this.minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
        .method(Method.PUT).bucket(BUCKET_NAME).expiry(1, TimeUnit.HOURS).build());

  }
}

PreSignController.java

In the @PostConstruct method, the application makes sure that the bucket with the name uploads exists. When the application requests a pre-signed URL, it must provide a bucket name, a file name, and the expiration time of the URL in seconds. In this example, the pre-sign URL is valid for 60 seconds. After that, the client is no longer able to use this URL.


To test the setup, we have to start the Minio and our Spring Boot server (mvn spring-boot:run). Then we have to deploy the Ionic app to a real device or start it on an emulator. You should now be able to upload pictures from the app to the Minio server. If there is a problem, check the security credentials and endpoint address for the Minio server in the application.yml file and make sure that the Spring Boot server and the Ionic app on the device/emulator can connect to the Minio server.

As mentioned before, this not only works with a Minio server. You can use the same Spring Boot application with Amazon S3 too. All you need to do is configuring the keys you get from Amazon and use the correct endpoint address (S3 Endpoints).


You find the complete source code for this project on GitHub.