fix(oss): 软件版本安装包显式上传公开 client 桶,对齐历史直链布局
Build Backend JAR / build (push) Has been cancelled

生产 Java 未设 AIIMAGE_OSS_BUCKET,默认桶为私有 nanri-ai-images;而 web_config
历史版本全部存于公开 client 桶(oss.aishufu.top/client/nanri-image/versions/...,
桌面端检测更新匿名下载)。原 uploadSoftwareVersionPackage 走默认桶会把新版本写入
私桶导致客户端无法下载。改为显式写 software-version-bucket(新增可配属性,默认
client,env AIIMAGE_OSS_SOFTWARE_VERSION_BUCKET)并保持 nanri-image/versions/ 前缀。

注:本提交含 OssStorageService 上"软件版本上传"方法与早前 readObjectBytesBounded
改动,为软件版本模块(见 ③)的配套存储能力。
This commit is contained in:
2026-09-04 10:21:47 +08:00
parent 64e4c19531
commit 3420c72c1d
3 changed files with 60 additions and 0 deletions
@@ -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;
}
@@ -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-bucketenv 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");
@@ -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: