package org.gcube.keycloak.avatar.storage.s3; import java.io.InputStream; import org.gcube.keycloak.avatar.storage.AvatarStorageProvider; import org.jboss.logging.Logger; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import io.minio.GetObjectArgs; import io.minio.MinioClient; import io.minio.PutObjectArgs; import io.minio.RemoveObjectArgs; import io.minio.errors.ErrorResponseException; public class MinioAvatarStorageProvider implements AvatarStorageProvider { private static final Logger logger = Logger.getLogger(MinioAvatarStorageProvider.class); private static final String AVATAR_FOLDER = "avatar"; private static final int CHUNK_SIZE = 5242880; private final Configuration configuration; public MinioAvatarStorageProvider(Configuration configuration) { this.configuration = configuration; } @Override public void saveAvatarImage(RealmModel realmModel, UserModel userModel, InputStream input) { logger.debugf("Saving avatar to S3 for user: %s", userModel.getUsername()); execute(minioClient -> { String avatarFileName = getAvatarFilePath(realmModel, userModel); logger.tracef("Saving avatar file object as: %s", avatarFileName); return minioClient .putObject(PutObjectArgs.builder().bucket(configuration.rootBucket).object(avatarFileName) .stream(input, -1, CHUNK_SIZE).build()); }); } @Override public InputStream loadAvatarImage(RealmModel realmModel, UserModel userModel) { logger.debugf("Loading avatar from S3 for user: %s", userModel.getUsername()); return execute(new Executor() { public InputStream execute(MinioClient minioClient) throws Exception { try { String avatarFileName = getAvatarFilePath(realmModel, userModel); logger.tracef("Getting avatar file object: %s", avatarFileName); return minioClient .getObject( GetObjectArgs.builder().bucket(configuration.rootBucket).object(avatarFileName) .build()); } catch (ErrorResponseException e) { if (e.response().code() == 404) { logger.debugf("Avatar file not found for user '%s' in realm '%s'", userModel.getUsername(), realmModel.getName()); return null; } else { throw e; } } }; }); } @Override public void deleteAvatarImage(RealmModel realmModel, UserModel userModel) { logger.debugf("Deleting avatar from S3 for user: %s", userModel.getUsername()); execute(minioClient -> { String avatarFileName = getAvatarFilePath(realmModel, userModel); logger.tracef("Deleting avatar file object: %s", avatarFileName); minioClient.removeObject(RemoveObjectArgs.builder().bucket(configuration.rootBucket) .object(avatarFileName).build()); return true; }); } @Override public void close() { // NOOP } public String getAvatarFilePath(RealmModel realmModel, UserModel userModel) { return AVATAR_FOLDER + "/" + realmModel.getName() + "/" + userModel.getUsername(); } public T execute(Executor executor) { try { MinioClient minioClient = MinioClient.builder() .endpoint(configuration.getServerUrl()) .credentials(configuration.getAccessKey(), configuration.getSecretKey()) .build(); return executor.execute(minioClient); } catch (Exception e) { throw new RuntimeException("Executing operation on S3 persistnce", e); } } public static class Configuration { private final String serverUrl; private final String accessKey; private final String secretKey; private final String rootBucket; public Configuration(String serverUrl, String accessKey, String secretKey, String rootBucket) { this.serverUrl = serverUrl; this.accessKey = accessKey; this.secretKey = secretKey; this.rootBucket = rootBucket; } public String getServerUrl() { return serverUrl; } public String getAccessKey() { return accessKey; } public String getSecretKey() { return secretKey; } public String getRootBucket() { return rootBucket; } } public interface Executor { T execute(MinioClient minioClient) throws Exception; } }