diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java index 5bc875f8..a991894d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/OssProperties.java @@ -14,6 +14,8 @@ public class OssProperties { private String imageVideoBucket; private String digitalHumanBucket; private String templateBucket; + /** 桌面客户端软件安装包桶(历史存公开 client 桶,勿用默认私桶)。 */ + private String softwareVersionBucket; private String accessKeyId; private String accessKeySecret; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java index b77fa248..3c0c3dae 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/oss/OssStorageService.java @@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.net.URI; import java.net.URLDecoder; @@ -29,6 +30,7 @@ public class OssStorageService { private static final String IMAGE_VIDEO_MODULE = "IMAGE_VIDEO"; private static final String DIGITAL_HUMAN_PREFIX = "digital-human/versions/"; + private static final String SOFTWARE_VERSION_PREFIX = "nanri-image/versions/"; private static final String LEGACY_MINIO_ENDPOINT = "http://47.110.241.161:9000"; private final OssProperties ossProperties; @@ -77,6 +79,26 @@ public class OssStorageService { return objectKey; } + /** + * 上传桌面客户端软件安装包(web_config 关联的 zip):写入**公开 client 桶**的 + * {@code nanri-image/versions/} 前缀(对齐 Flask 历史对象布局与公开直链, + * 勿用默认私有桶 nanri-ai-images,否则桌面端"检测更新"无法匿名下载)。 + * 桶可用 aiimage.oss.software-version-bucket(env AIIMAGE_OSS_SOFTWARE_VERSION_BUCKET)覆盖。 + */ + public String uploadSoftwareVersionPackage(File file, String objectKey) { + if (objectKey == null || !objectKey.startsWith(SOFTWARE_VERSION_PREFIX)) { + throw new IllegalArgumentException("software version objectKey must start with " + SOFTWARE_VERSION_PREFIX); + } + String bucket = softwareVersionBucket(); + uploadFile(file, bucket, objectKey); + return getPublicUrl(objectKey, bucket); + } + + private String softwareVersionBucket() { + String configured = ossProperties.getSoftwareVersionBucket(); + return (configured == null || configured.isBlank()) ? "client" : configured; + } + public String uploadText(String objectKey, String content) { if (objectKey == null || objectKey.isBlank()) { throw new IllegalArgumentException("objectKey must not be blank"); @@ -159,6 +181,41 @@ public class OssStorageService { return readObjectBytes(location.bucket(), location.objectKey()); } + /** + * Reads a managed result object with a hard cap on the total bytes read. + * Streaming 1 MiB chunks, aborting once {@code maxBytes} is exceeded. + * The caller is expected to translate the value result_file_size. + */ + public byte[] readObjectBytesBounded(String value, long maxBytes) { + StorageLocation location = resolveStorageLocation(value); + if (location == null) { + throw new IllegalArgumentException("object value must not be blank"); + } + String bucket = location.bucket(); + String objectKey = location.objectKey(); + try (var stream = buildClient().getObject(GetObjectArgs.builder() + .bucket(bucket) + .object(objectKey) + .build())) { + byte[] buffer = new byte[1024 * 1024]; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + long total = 0; + int read; + while ((read = stream.read(buffer)) != -1) { + total += read; + if (total > maxBytes) { + throw new IllegalArgumentException("结果文件过大,无法分析"); + } + out.write(buffer, 0, read); + } + return out.toByteArray(); + } catch (IllegalArgumentException ex) { + throw ex; + } catch (Exception ex) { + throw storageFailure("read", objectKey, ex); + } + } + public boolean objectExists(String bucket, String objectKey) { String normalizedBucket = requireStorageName(bucket, "bucket"); String normalizedObjectKey = requireStorageName(objectKey, "objectKey"); diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 5b105c93..160ed48b 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -110,6 +110,7 @@ aiimage: image-video-bucket: ${AIIMAGE_IMAGE_VIDEO_OSS_BUCKET:shufu-video} digital-human-bucket: ${AIIMAGE_DIGITAL_HUMAN_OSS_BUCKET:nanri-ai-digital-human} template-bucket: ${AIIMAGE_TEMPLATE_OSS_BUCKET:aiimage-templates} + software-version-bucket: ${AIIMAGE_OSS_SOFTWARE_VERSION_BUCKET:client} access-key-id: ${AIIMAGE_OSS_ACCESS_KEY_ID:appuser} access-key-secret: ${AIIMAGE_OSS_ACCESS_KEY_SECRET:AppUser@2024SecureKey} transient-storage: