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 combine 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 use of this library is for Node.js applications, but it also works in the browser. However, this is not the recommended way to use it. A client needs to know the keys to send requests to an S3 compatible server, and storing these keys on the client is considered a security risk.
Instead, an application should use pre-signed URLs. This is not a Minio-specific feature; Amazon S3 supports it as well. A pre-signed URL is like a one-time-use key that allows a client to upload or fetch files from an S3 compatible server without needing security credentials for the service.
Flow of a connection with pre-signed URLs.
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 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 used for uploading or 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);
});
}
Before the app can upload the picture, it needs to fetch the pre-signed URL. For that, the application calls the fetchPresignUrl
method. 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 receives 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. We also need to add the Minio Java client library to the pom.xml.
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.17</version>
</dependency>
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;
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();
}
}
Next, we create a RestController
with one handler that creates and returns pre-signed URLs. We inject the MinioClient
bean and call the presignedPutObject
operation 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());
}
}
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-signed 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 configure the keys you get from Amazon and use the correct endpoint address (S3 Endpoints).
You can find the complete source code for this project on GitHub.