|
@@ -0,0 +1,140 @@
|
|
|
|
|
+package com.aitori.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.aitori.utils.GoogleStorageUtil;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.net.URI;
|
|
|
|
|
+import java.net.http.HttpClient;
|
|
|
|
|
+import java.net.http.HttpRequest;
|
|
|
|
|
+import java.net.http.HttpResponse;
|
|
|
|
|
+import java.time.Duration;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Locale;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+public class WorkflowResultStorageService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String GOOGLE_STORAGE_BASE_URL = "https://storage.googleapis.com/";
|
|
|
|
|
+ private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
|
|
|
|
|
+ .connectTimeout(Duration.ofSeconds(30))
|
|
|
|
|
+ .followRedirects(HttpClient.Redirect.NORMAL)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ public List<String> transferImageUrls(List<String> imageUrls) {
|
|
|
|
|
+ if (imageUrls == null || imageUrls.isEmpty()) {
|
|
|
|
|
+ return imageUrls;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> storedUrls = new ArrayList<>(imageUrls.size());
|
|
|
|
|
+ for (String imageUrl : imageUrls) {
|
|
|
|
|
+ storedUrls.add(transferUrl(imageUrl, "workflow/images"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return storedUrls;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public String transferVideoUrl(String videoUrl) {
|
|
|
|
|
+ return transferUrl(videoUrl, "workflow/videos");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String transferUrl(String sourceUrl, String prefix) {
|
|
|
|
|
+ if (sourceUrl == null || sourceUrl.isBlank()) {
|
|
|
|
|
+ return sourceUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isGoogleStorageUrl(sourceUrl)) {
|
|
|
|
|
+ return sourceUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ HttpRequest request = HttpRequest.newBuilder()
|
|
|
|
|
+ .uri(URI.create(sourceUrl))
|
|
|
|
|
+ .timeout(Duration.ofMinutes(5))
|
|
|
|
|
+ .GET()
|
|
|
|
|
+ .build();
|
|
|
|
|
+ HttpResponse<byte[]> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
|
|
|
|
+ if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
|
|
|
|
+ log.warn("转存远程文件失败, statusCode={}, sourceUrl={}", response.statusCode(), sourceUrl);
|
|
|
|
|
+ return sourceUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String contentType = normalizeContentType(response.headers().firstValue("Content-Type").orElse(null));
|
|
|
|
|
+ String extension = resolveExtension(sourceUrl, contentType);
|
|
|
|
|
+ String objectName = buildObjectName(prefix, extension);
|
|
|
|
|
+ String uploadedUrl = GoogleStorageUtil.upload(response.body(), objectName, contentType);
|
|
|
|
|
+ if (uploadedUrl == null) {
|
|
|
|
|
+ log.warn("上传到 Google Storage 失败, sourceUrl={}", sourceUrl);
|
|
|
|
|
+ return sourceUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("远程文件转存成功, sourceUrl={}, uploadedUrl={}", sourceUrl, uploadedUrl);
|
|
|
|
|
+ return uploadedUrl;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("转存远程文件异常, sourceUrl={}", sourceUrl, e);
|
|
|
|
|
+ return sourceUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isGoogleStorageUrl(String url) {
|
|
|
|
|
+ return url.startsWith(GOOGLE_STORAGE_BASE_URL);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String buildObjectName(String prefix, String extension) {
|
|
|
|
|
+ return prefix + "/" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "/"
|
|
|
|
|
+ + UUID.randomUUID() + "." + extension;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String normalizeContentType(String contentType) {
|
|
|
|
|
+ if (contentType == null || contentType.isBlank()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ int separatorIndex = contentType.indexOf(';');
|
|
|
|
|
+ String normalized = separatorIndex >= 0 ? contentType.substring(0, separatorIndex) : contentType;
|
|
|
|
|
+ return normalized.trim().toLowerCase(Locale.ROOT);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String resolveExtension(String sourceUrl, String contentType) {
|
|
|
|
|
+ String extensionFromUrl = extractExtensionFromUrl(sourceUrl);
|
|
|
|
|
+ if (extensionFromUrl != null) {
|
|
|
|
|
+ return extensionFromUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (contentType == null) {
|
|
|
|
|
+ return "bin";
|
|
|
|
|
+ }
|
|
|
|
|
+ return switch (contentType) {
|
|
|
|
|
+ case "image/jpeg" -> "jpg";
|
|
|
|
|
+ case "image/png" -> "png";
|
|
|
|
|
+ case "image/webp" -> "webp";
|
|
|
|
|
+ case "image/gif" -> "gif";
|
|
|
|
|
+ case "video/mp4" -> "mp4";
|
|
|
|
|
+ case "video/quicktime" -> "mov";
|
|
|
|
|
+ case "video/webm" -> "webm";
|
|
|
|
|
+ default -> "bin";
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String extractExtensionFromUrl(String sourceUrl) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String path = URI.create(sourceUrl).getPath();
|
|
|
|
|
+ if (path == null || path.isBlank()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ int dotIndex = path.lastIndexOf('.');
|
|
|
|
|
+ if (dotIndex < 0 || dotIndex == path.length() - 1) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String extension = path.substring(dotIndex + 1).toLowerCase(Locale.ROOT);
|
|
|
|
|
+ if (!extension.matches("[a-z0-9]{1,10}")) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return extension;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.debug("解析 URL 扩展名失败, sourceUrl={}", sourceUrl, e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|