From ab74d8c28b5008bd614f015476bb2ad8b9f53ff6 Mon Sep 17 00:00:00 2001 From: Wh1te Date: Wed, 7 Oct 2020 10:52:20 +0800 Subject: [PATCH] feat: add MinIO support for attachment management. #1024 (#1097) --- build.gradle | 2 + .../app/handler/file/MinioFileHandler.java | 131 ++++++++++++++++++ .../halo/app/model/enums/AttachmentType.java | 7 +- .../app/model/properties/MinioProperties.java | 66 +++++++++ 4 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/main/java/run/halo/app/handler/file/MinioFileHandler.java create mode 100644 src/main/java/run/halo/app/model/properties/MinioProperties.java diff --git a/build.gradle b/build.gradle index b9b3c0eef..4a13d474e 100644 --- a/build.gradle +++ b/build.gradle @@ -49,6 +49,7 @@ ext { aliyunSdkVersion = "3.9.1" baiduSdkVersion = "0.10.36" qcloudSdkVersion = "5.6.25" + minioSdkVersion = "7.1.4" swaggerVersion = "2.9.2" commonsLangVersion = "3.10" httpclientVersion = "4.5.12" @@ -88,6 +89,7 @@ dependencies { implementation "com.baidubce:bce-java-sdk:$baiduSdkVersion" implementation "com.qcloud:cos_api:$qcloudSdkVersion" implementation "com.huaweicloud:esdk-obs-java:$huaweiObsVersion" + implementation "io.minio:minio:$minioSdkVersion" implementation "io.springfox:springfox-swagger2:$swaggerVersion" implementation "io.springfox:springfox-swagger-ui:$swaggerVersion" implementation "org.apache.commons:commons-lang3:$commonsLangVersion" diff --git a/src/main/java/run/halo/app/handler/file/MinioFileHandler.java b/src/main/java/run/halo/app/handler/file/MinioFileHandler.java new file mode 100644 index 000000000..4cc3ec8b9 --- /dev/null +++ b/src/main/java/run/halo/app/handler/file/MinioFileHandler.java @@ -0,0 +1,131 @@ +package run.halo.app.handler.file; + +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.multipart.MultipartFile; +import run.halo.app.exception.FileOperationException; +import run.halo.app.model.enums.AttachmentType; +import run.halo.app.model.properties.MinioProperties; +import run.halo.app.model.support.HaloConst; +import run.halo.app.model.support.UploadResult; +import run.halo.app.service.OptionService; +import run.halo.app.utils.FilenameUtils; +import run.halo.app.utils.ImageUtils; + +import javax.imageio.ImageReader; +import java.util.Objects; + + +/** + * MinIO file handler. + * + * @author Wh1te + * @date 2020-10-03 + */ +@Slf4j +@Component +public class MinioFileHandler implements FileHandler { + + private final OptionService optionService; + + public MinioFileHandler(OptionService optionService) { + this.optionService = optionService; + } + + @NotNull + @Override + public UploadResult upload(@NotNull MultipartFile file) { + Assert.notNull(file, "Multipart file must not be null"); + // Get config + String endpoint = optionService.getByPropertyOfNonNull(MinioProperties.ENDPOINT).toString(); + String accessKey = optionService.getByPropertyOfNonNull(MinioProperties.ACCESS_KEY).toString(); + String accessSecret = optionService.getByPropertyOfNonNull(MinioProperties.ACCESS_SECRET).toString(); + String bucketName = optionService.getByPropertyOfNonNull(MinioProperties.BUCKET_NAME).toString(); + String source = optionService.getByPropertyOrDefault(MinioProperties.SOURCE, String.class, ""); + + endpoint = StringUtils.appendIfMissing(endpoint, HaloConst.URL_SEPARATOR); + + MinioClient minioClient = MinioClient.builder() + .endpoint(endpoint) + .credentials(accessKey, accessSecret) + .build(); + + try { + String basename = FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + String timestamp = String.valueOf(System.currentTimeMillis()); + String upFilePath = StringUtils.join(StringUtils.isNotBlank(source) ? source + HaloConst.URL_SEPARATOR : "", + basename, "_", timestamp, ".", extension); + String filePath = StringUtils.join(endpoint, bucketName, HaloConst.URL_SEPARATOR, upFilePath); + + PutObjectArgs putObjectArgs = PutObjectArgs.builder() + .contentType(file.getContentType()) + .bucket(bucketName) + .stream(file.getInputStream(), file.getSize(), -1) + .object(upFilePath) + .build(); + minioClient.putObject(putObjectArgs); + + UploadResult uploadResult = new UploadResult(); + uploadResult.setFilename(basename); + uploadResult.setFilePath(filePath); + uploadResult.setKey(upFilePath); + uploadResult.setMediaType(MediaType.valueOf(Objects.requireNonNull(file.getContentType()))); + uploadResult.setSuffix(extension); + uploadResult.setSize(file.getSize()); + + // Handle thumbnail + if (FileHandler.isImageType(uploadResult.getMediaType())) { + ImageReader image = ImageUtils.getImageReaderFromFile(file.getInputStream(), extension); + assert image != null; + uploadResult.setWidth(image.getWidth(0)); + uploadResult.setHeight(image.getHeight(0)); + uploadResult.setThumbPath(filePath); + } + + return uploadResult; + } catch (Exception e) { + log.error("upload file to MINIO failed", e); + throw new FileOperationException("上传附件 " + file.getOriginalFilename() + " 到 MinIO 失败 ", e).setErrorData(e.getMessage()); + } + } + + @Override + public void delete(@NotNull String key) { + Assert.notNull(key, "File key must not be blank"); + + String endPoint = optionService.getByPropertyOfNonNull(MinioProperties.ENDPOINT).toString(); + endPoint = StringUtils.appendIfMissing(endPoint, HaloConst.URL_SEPARATOR); + + String accessKey = optionService.getByPropertyOfNonNull(MinioProperties.ACCESS_KEY).toString(); + String accessSecret = optionService.getByPropertyOfNonNull(MinioProperties.ACCESS_SECRET).toString(); + String bucketName = optionService.getByPropertyOfNonNull(MinioProperties.BUCKET_NAME).toString(); + + MinioClient minioClient = MinioClient.builder() + .endpoint(endPoint) + .credentials(accessKey, accessSecret) + .build(); + + try { + minioClient.removeObject(RemoveObjectArgs.builder() + .bucket(bucketName) + .object(key) + .build()); + } catch (Exception e) { + log.error("delete MINIO file: [{}] failed", key, e); + throw new FileOperationException("附件 " + key + " 从 MinIO 删除失败", e).setErrorData(e.getMessage()); + } + } + + @Override + public AttachmentType getAttachmentType() { + return AttachmentType.MINIO; + } +} diff --git a/src/main/java/run/halo/app/model/enums/AttachmentType.java b/src/main/java/run/halo/app/model/enums/AttachmentType.java index 48bd63908..35b00bb23 100644 --- a/src/main/java/run/halo/app/model/enums/AttachmentType.java +++ b/src/main/java/run/halo/app/model/enums/AttachmentType.java @@ -46,7 +46,12 @@ public enum AttachmentType implements ValueEnum { /** * 华为云 */ - HUAWEIOBS(7); + HUAWEIOBS(7), + + /** + * MINIO + */ + MINIO(8); private final Integer value; diff --git a/src/main/java/run/halo/app/model/properties/MinioProperties.java b/src/main/java/run/halo/app/model/properties/MinioProperties.java new file mode 100644 index 000000000..ed61988a2 --- /dev/null +++ b/src/main/java/run/halo/app/model/properties/MinioProperties.java @@ -0,0 +1,66 @@ +package run.halo.app.model.properties; + +/** + * Minio properties. + * + * @author Wh1te + * @date 2020-10-03 + */ +public enum MinioProperties implements PropertyEnum { + + /** + * Minio endpoint. + */ + ENDPOINT("minio_endpoint", String.class, ""), + + /** + * Minio bucket name. + */ + BUCKET_NAME("minio_bucket_name", String.class, ""), + + /** + * Minio access key. + */ + ACCESS_KEY("minio_access_key", String.class, ""), + + /** + * Minio access secret. + */ + ACCESS_SECRET("minio_access_secret", String.class, ""), + + /** + * Minio source + */ + SOURCE("minio_source", String.class, ""); + + private final String value; + + private final Class type; + + private final String defaultValue; + + MinioProperties(String value, Class type, String defaultValue) { + this.defaultValue = defaultValue; + if (!PropertyEnum.isSupportedType(type)) { + throw new IllegalArgumentException("Unsupported blog property type: " + type); + } + + this.value = value; + this.type = type; + } + + @Override + public Class getType() { + return type; + } + + @Override + public String defaultValue() { + return defaultValue; + } + + @Override + public String getValue() { + return value; + } +}