|
|
@@ -0,0 +1,116 @@
|
|
|
+package space.anyi.serve.service.impl;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
|
|
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
|
|
+import software.amazon.awssdk.core.sync.RequestBody;
|
|
|
+import software.amazon.awssdk.regions.Region;
|
|
|
+import software.amazon.awssdk.services.s3.S3Client;
|
|
|
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
|
|
+import space.anyi.serve.entity.attachment.ImageValidationException;
|
|
|
+import space.anyi.serve.entity.meta.Meta;
|
|
|
+import space.anyi.serve.entity.meta.OssConfigMeta;
|
|
|
+import space.anyi.serve.service.MetaService;
|
|
|
+import space.anyi.serve.service.OssService;
|
|
|
+
|
|
|
+import java.net.URI;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class OssServiceImpl implements OssService {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(OssServiceImpl.class);
|
|
|
+ private static final Map<String, String> MIME_TO_EXT = Map.of(
|
|
|
+ "image/jpeg", ".jpg",
|
|
|
+ "image/png", ".png",
|
|
|
+ "image/gif", ".gif",
|
|
|
+ "image/webp", ".webp",
|
|
|
+ "image/bmp", ".bmp"
|
|
|
+ );
|
|
|
+ private static final String OBJECT_KEY_PATTERN = "forum/%s/%s/%s%s";
|
|
|
+
|
|
|
+ private final MetaService metaService;
|
|
|
+ private final ObjectMapper objectMapper;
|
|
|
+
|
|
|
+ public OssServiceImpl(MetaService metaService, ObjectMapper objectMapper) {
|
|
|
+ this.metaService = metaService;
|
|
|
+ this.objectMapper = objectMapper;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String upload(MultipartFile file, String type) {
|
|
|
+ OssConfigMeta config = getOssConfig();
|
|
|
+
|
|
|
+ String ext = MIME_TO_EXT.get(file.getContentType());
|
|
|
+ if (ext == null) {
|
|
|
+ throw new ImageValidationException("不支持的文件类型: " + file.getContentType());
|
|
|
+ }
|
|
|
+
|
|
|
+ String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
|
|
|
+ String uuid = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ String objectKey = String.format(OBJECT_KEY_PATTERN, type, dateDir, uuid, ext);
|
|
|
+
|
|
|
+ S3Client s3Client = buildS3Client(config);
|
|
|
+ try (s3Client) {
|
|
|
+ PutObjectRequest putRequest = PutObjectRequest.builder()
|
|
|
+ .bucket(config.getBucket())
|
|
|
+ .key(objectKey)
|
|
|
+ .contentType(file.getContentType())
|
|
|
+ .build();
|
|
|
+ s3Client.putObject(putRequest, RequestBody.fromBytes(file.getBytes()));
|
|
|
+ log.info("OSS upload success: bucket={}, key={}", config.getBucket(), objectKey);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OSS upload failed", e);
|
|
|
+ throw new RuntimeException("文件上传失败", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ String publicDomain = config.getPublicDomain();
|
|
|
+ if (publicDomain.endsWith("/")) {
|
|
|
+ return publicDomain + objectKey;
|
|
|
+ }
|
|
|
+ return publicDomain + "/" + objectKey;
|
|
|
+ }
|
|
|
+
|
|
|
+ private OssConfigMeta getOssConfig(){
|
|
|
+ Meta meta = metaService.getMeta(Meta.OSS_CONFIG_KEY);
|
|
|
+ String value = meta.getValue();
|
|
|
+ if (meta == null || value == null) {
|
|
|
+ throw new ImageValidationException("OSS 未配置");
|
|
|
+ }
|
|
|
+ OssConfigMeta config = null;
|
|
|
+ try {
|
|
|
+ config = objectMapper.readValue(value, OssConfigMeta.class);
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ if (config.getEndpoint() == null || config.getEndpoint().isBlank() ||
|
|
|
+ config.getRegion() == null || config.getRegion().isBlank() ||
|
|
|
+ config.getBucket() == null || config.getBucket().isBlank() ||
|
|
|
+ config.getAccessKey() == null || config.getAccessKey().isBlank() ||
|
|
|
+ config.getSecretKey() == null || config.getSecretKey().isBlank() ||
|
|
|
+ config.getPublicDomain() == null || config.getPublicDomain().isBlank()) {
|
|
|
+ throw new ImageValidationException("OSS 配置不完整");
|
|
|
+ }
|
|
|
+ return config;
|
|
|
+ }
|
|
|
+
|
|
|
+ // package-private for testability
|
|
|
+ S3Client buildS3Client(OssConfigMeta config) {
|
|
|
+ return S3Client.builder()
|
|
|
+ .endpointOverride(URI.create("https://"+config.getEndpoint()))
|
|
|
+ .region(Region.of(config.getRegion()))
|
|
|
+ .credentialsProvider(StaticCredentialsProvider.create(
|
|
|
+ AwsBasicCredentials.create(config.getAccessKey(), config.getSecretKey())
|
|
|
+ ))
|
|
|
+ .forcePathStyle(true)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+}
|