public interface CloudStorageService {
/**
* 上傳文件並返回可訪問 URL
* @param bucket 存儲桶
* @param objectName 對象名(含路徑)
* @param inputStream 文件流
* @param contentType MIME 類型
* @return 公開訪問 URL(或內部路徑)
*/
String upload(String bucket, String objectName, InputStream inputStream, String contentType)
throws StorageException;
}
public class StorageException extends RuntimeException {
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}
import io.minio.*;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.concurrent.CompletableFuture;
@Service
public class MinioStorageService implements CloudStorageService {
private static final Logger log = LoggerFactory.getLogger(MinioStorageService.class);
private final MinioClient minioClient;
private final MinioProperties properties;
private final MeterRegistry meterRegistry;
// 指標:上傳成功/失敗計數、耗時分佈
private final Timer uploadTimer;
public MinioStorageService(
MinioClient minioClient,
MinioProperties properties,
MeterRegistry meterRegistry) {
this.minioClient = minioClient;
this.properties = properties;
this.meterRegistry = meterRegistry;
this.uploadTimer = Timer.builder("storage.upload.duration")
.description("MinIO upload latency")
.register(meterRegistry);
}
@Override
public String upload(String bucket, String objectName, InputStream inputStream, String contentType) {
return uploadTimer.recordCallable(() -> {
try {
// 確保存儲桶存在(冪等操作)
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
log.info("Created bucket: {}", bucket);
}
// 執行上傳
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(inputStream, -1, 10485760) // 10MB part size
.contentType(contentType)
.build());
String url = generatePublicUrl(bucket, objectName);
log.debug("File uploaded: {} -> {}", objectName, url);
meterRegistry.counter("storage.upload.success").increment();
return url;
} catch (Exception e) {
meterRegistry.counter("storage.upload.failure").increment();
log.error("MinIO upload failed for {}/{}", bucket, objectName, e);
throw new StorageException("文件上傳存儲失敗", e);
}
});
}
private String generatePublicUrl(String bucket, String objectName) {
// 注意:生產環境應使用預簽名 URL 或 CDN,而非直接暴露 MinIO
return String.format("%s/%s/%s", properties.getPublicUrl(), bucket, objectName);
}
}