feat: ship portal and android release updates
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.yoyuzh;
|
||||
|
||||
import com.yoyuzh.config.AdminProperties;
|
||||
import com.yoyuzh.config.AndroidReleaseProperties;
|
||||
import com.yoyuzh.config.CorsProperties;
|
||||
import com.yoyuzh.config.FileStorageProperties;
|
||||
import com.yoyuzh.config.JwtProperties;
|
||||
@@ -15,7 +16,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
FileStorageProperties.class,
|
||||
CorsProperties.class,
|
||||
AdminProperties.class,
|
||||
RegistrationProperties.class
|
||||
RegistrationProperties.class,
|
||||
AndroidReleaseProperties.class
|
||||
})
|
||||
public class PortalBackendApplication {
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import com.yoyuzh.common.ApiResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/app/android")
|
||||
@RequiredArgsConstructor
|
||||
public class AndroidReleaseController {
|
||||
|
||||
private final AndroidReleaseService androidReleaseService;
|
||||
|
||||
@GetMapping("/latest")
|
||||
public ApiResponse<AndroidReleaseResponse> getLatestRelease() {
|
||||
return ApiResponse.success(androidReleaseService.getLatestRelease());
|
||||
}
|
||||
|
||||
@GetMapping("/download")
|
||||
public ResponseEntity<byte[]> downloadLatestRelease() {
|
||||
return buildDownloadResponse(androidReleaseService.downloadLatestRelease());
|
||||
}
|
||||
|
||||
@GetMapping("/download/{fileName:.+}")
|
||||
public ResponseEntity<byte[]> downloadVersionedRelease(@PathVariable String fileName) {
|
||||
AndroidReleaseDownload download = androidReleaseService.downloadLatestRelease();
|
||||
if (!download.fileName().equals(fileName)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return buildDownloadResponse(download);
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> buildDownloadResponse(AndroidReleaseDownload download) {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType("application/vnd.android.package-archive"))
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename(download.fileName(), StandardCharsets.UTF_8)
|
||||
.build()
|
||||
.toString())
|
||||
.contentLength(download.content().length)
|
||||
.body(download.content());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
public record AndroidReleaseDownload(
|
||||
String fileName,
|
||||
byte[] content
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
public record AndroidReleaseMetadata(
|
||||
String objectKey,
|
||||
String fileName,
|
||||
String versionCode,
|
||||
String versionName,
|
||||
String publishedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "app.android")
|
||||
public class AndroidReleaseProperties {
|
||||
|
||||
private String metadataObjectKey = "android/releases/latest.json";
|
||||
private String downloadPublicUrl = "https://api.yoyuzh.xyz/api/app/android/download";
|
||||
|
||||
public String getMetadataObjectKey() {
|
||||
return metadataObjectKey;
|
||||
}
|
||||
|
||||
public void setMetadataObjectKey(String metadataObjectKey) {
|
||||
this.metadataObjectKey = metadataObjectKey;
|
||||
}
|
||||
|
||||
public String getDownloadPublicUrl() {
|
||||
return downloadPublicUrl;
|
||||
}
|
||||
|
||||
public void setDownloadPublicUrl(String downloadPublicUrl) {
|
||||
this.downloadPublicUrl = downloadPublicUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
public record AndroidReleaseResponse(
|
||||
String downloadUrl,
|
||||
String fileName,
|
||||
String versionCode,
|
||||
String versionName,
|
||||
String publishedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.common.ErrorCode;
|
||||
import com.yoyuzh.files.storage.FileContentStorage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AndroidReleaseService {
|
||||
|
||||
private final FileContentStorage fileContentStorage;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AndroidReleaseProperties androidReleaseProperties;
|
||||
|
||||
public AndroidReleaseResponse getLatestRelease() {
|
||||
AndroidReleaseMetadata metadata = loadReleaseMetadata();
|
||||
return new AndroidReleaseResponse(
|
||||
buildVersionedDownloadUrl(metadata),
|
||||
metadata.fileName(),
|
||||
metadata.versionCode(),
|
||||
metadata.versionName(),
|
||||
metadata.publishedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public String buildLatestDownloadUrl() {
|
||||
return androidReleaseProperties.getDownloadPublicUrl();
|
||||
}
|
||||
|
||||
public AndroidReleaseDownload downloadLatestRelease() {
|
||||
AndroidReleaseMetadata metadata = loadReleaseMetadata();
|
||||
String objectKey = metadata.objectKey();
|
||||
String fileName = metadata.fileName();
|
||||
if (objectKey == null || objectKey.isBlank() || fileName == null || fileName.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "Android 安装包暂未发布");
|
||||
}
|
||||
|
||||
return new AndroidReleaseDownload(
|
||||
fileName,
|
||||
fileContentStorage.readBlob(objectKey)
|
||||
);
|
||||
}
|
||||
|
||||
private String buildVersionedDownloadUrl(AndroidReleaseMetadata metadata) {
|
||||
String fileName = metadata.fileName();
|
||||
if (fileName == null || fileName.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "Android 安装包暂未发布");
|
||||
}
|
||||
|
||||
String baseUrl = androidReleaseProperties.getDownloadPublicUrl();
|
||||
String separator = baseUrl.endsWith("/") ? "" : "/";
|
||||
return baseUrl + separator + URI.create("https://placeholder/" + fileName).getPath().substring(1);
|
||||
}
|
||||
|
||||
private void validateReleaseMetadata(AndroidReleaseMetadata metadata) {
|
||||
String objectKey = metadata.objectKey();
|
||||
String fileName = metadata.fileName();
|
||||
if (objectKey == null || objectKey.isBlank() || fileName == null || fileName.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "Android 安装包暂未发布");
|
||||
}
|
||||
}
|
||||
|
||||
private AndroidReleaseMetadata loadReleaseMetadata() {
|
||||
String metadataObjectKey = androidReleaseProperties.getMetadataObjectKey();
|
||||
if (metadataObjectKey == null || metadataObjectKey.isBlank()) {
|
||||
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "Android 安装包暂未发布");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] content = fileContentStorage.readBlob(metadataObjectKey);
|
||||
AndroidReleaseMetadata metadata = objectMapper.readValue(content, AndroidReleaseMetadata.class);
|
||||
if (metadata == null) {
|
||||
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "Android 安装包暂未发布");
|
||||
}
|
||||
validateReleaseMetadata(metadata);
|
||||
return metadata;
|
||||
} catch (BusinessException ex) {
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "Android 安装包元数据读取失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,10 @@ public class FileStorageProperties {
|
||||
private String scope;
|
||||
private int ttlSeconds = 3600;
|
||||
private String region = "automatic";
|
||||
private String publicDownloadBaseUrl;
|
||||
private String packageDownloadBaseUrl;
|
||||
private String packageDownloadSecret;
|
||||
private int packageDownloadTtlSeconds = 300;
|
||||
|
||||
public String getApiBaseUrl() {
|
||||
return apiBaseUrl;
|
||||
@@ -110,5 +114,37 @@ public class FileStorageProperties {
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getPublicDownloadBaseUrl() {
|
||||
return publicDownloadBaseUrl;
|
||||
}
|
||||
|
||||
public void setPublicDownloadBaseUrl(String publicDownloadBaseUrl) {
|
||||
this.publicDownloadBaseUrl = publicDownloadBaseUrl;
|
||||
}
|
||||
|
||||
public String getPackageDownloadBaseUrl() {
|
||||
return packageDownloadBaseUrl;
|
||||
}
|
||||
|
||||
public void setPackageDownloadBaseUrl(String packageDownloadBaseUrl) {
|
||||
this.packageDownloadBaseUrl = packageDownloadBaseUrl;
|
||||
}
|
||||
|
||||
public String getPackageDownloadSecret() {
|
||||
return packageDownloadSecret;
|
||||
}
|
||||
|
||||
public void setPackageDownloadSecret(String packageDownloadSecret) {
|
||||
this.packageDownloadSecret = packageDownloadSecret;
|
||||
}
|
||||
|
||||
public int getPackageDownloadTtlSeconds() {
|
||||
return packageDownloadTtlSeconds;
|
||||
}
|
||||
|
||||
public void setPackageDownloadTtlSeconds(int packageDownloadTtlSeconds) {
|
||||
this.packageDownloadTtlSeconds = packageDownloadTtlSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.yoyuzh.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@Configuration
|
||||
@@ -9,6 +10,9 @@ public class RestClientConfig {
|
||||
|
||||
@Bean
|
||||
public RestClient restClient(RestClient.Builder builder) {
|
||||
return builder.build();
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(3000);
|
||||
requestFactory.setReadTimeout(5000);
|
||||
return builder.requestFactory(requestFactory).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ public class SecurityConfig {
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/auth/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html")
|
||||
.permitAll()
|
||||
.requestMatchers("/api/app/android/latest", "/api/app/android/download", "/api/app/android/download/*")
|
||||
.permitAll()
|
||||
.requestMatchers("/api/transfer/**")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/files/share-links/*")
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.yoyuzh.common.PageResponse;
|
||||
import com.yoyuzh.config.FileStorageProperties;
|
||||
import com.yoyuzh.files.storage.FileContentStorage;
|
||||
import com.yoyuzh.files.storage.PreparedUpload;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -24,8 +25,12 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -34,6 +39,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@@ -49,19 +55,42 @@ public class FileService {
|
||||
private final FileShareLinkRepository fileShareLinkRepository;
|
||||
private final AdminMetricsService adminMetricsService;
|
||||
private final long maxFileSize;
|
||||
private final String packageDownloadBaseUrl;
|
||||
private final String packageDownloadSecret;
|
||||
private final long packageDownloadTtlSeconds;
|
||||
private final Clock clock;
|
||||
|
||||
@Autowired
|
||||
public FileService(StoredFileRepository storedFileRepository,
|
||||
FileBlobRepository fileBlobRepository,
|
||||
FileContentStorage fileContentStorage,
|
||||
FileShareLinkRepository fileShareLinkRepository,
|
||||
AdminMetricsService adminMetricsService,
|
||||
FileStorageProperties properties) {
|
||||
this(storedFileRepository, fileBlobRepository, fileContentStorage, fileShareLinkRepository, adminMetricsService, properties, Clock.systemUTC());
|
||||
}
|
||||
|
||||
FileService(StoredFileRepository storedFileRepository,
|
||||
FileBlobRepository fileBlobRepository,
|
||||
FileContentStorage fileContentStorage,
|
||||
FileShareLinkRepository fileShareLinkRepository,
|
||||
AdminMetricsService adminMetricsService,
|
||||
FileStorageProperties properties,
|
||||
Clock clock) {
|
||||
this.storedFileRepository = storedFileRepository;
|
||||
this.fileBlobRepository = fileBlobRepository;
|
||||
this.fileContentStorage = fileContentStorage;
|
||||
this.fileShareLinkRepository = fileShareLinkRepository;
|
||||
this.adminMetricsService = adminMetricsService;
|
||||
this.maxFileSize = properties.getMaxFileSize();
|
||||
this.packageDownloadBaseUrl = StringUtils.hasText(properties.getS3().getPackageDownloadBaseUrl())
|
||||
? properties.getS3().getPackageDownloadBaseUrl().trim()
|
||||
: null;
|
||||
this.packageDownloadSecret = StringUtils.hasText(properties.getS3().getPackageDownloadSecret())
|
||||
? properties.getS3().getPackageDownloadSecret().trim()
|
||||
: null;
|
||||
this.packageDownloadTtlSeconds = Math.max(1, properties.getS3().getPackageDownloadTtlSeconds());
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -370,6 +399,12 @@ public class FileService {
|
||||
return downloadDirectory(user, storedFile);
|
||||
}
|
||||
|
||||
if (shouldUsePublicPackageDownload(storedFile)) {
|
||||
return ResponseEntity.status(302)
|
||||
.location(URI.create(buildPublicPackageDownloadUrl(storedFile)))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (fileContentStorage.supportsDirectDownload()) {
|
||||
return ResponseEntity.status(302)
|
||||
.location(URI.create(fileContentStorage.createBlobDownloadUrl(
|
||||
@@ -393,6 +428,10 @@ public class FileService {
|
||||
}
|
||||
adminMetricsService.recordDownloadTraffic(storedFile.getSize());
|
||||
|
||||
if (shouldUsePublicPackageDownload(storedFile)) {
|
||||
return new DownloadUrlResponse(buildPublicPackageDownloadUrl(storedFile));
|
||||
}
|
||||
|
||||
if (fileContentStorage.supportsDirectDownload()) {
|
||||
return new DownloadUrlResponse(fileContentStorage.createBlobDownloadUrl(
|
||||
getRequiredBlob(storedFile).getObjectKey(),
|
||||
@@ -521,6 +560,117 @@ public class FileService {
|
||||
.body(archiveBytes);
|
||||
}
|
||||
|
||||
private boolean shouldUsePublicPackageDownload(StoredFile storedFile) {
|
||||
return fileContentStorage.supportsDirectDownload()
|
||||
&& StringUtils.hasText(packageDownloadBaseUrl)
|
||||
&& StringUtils.hasText(packageDownloadSecret)
|
||||
&& isAppPackage(storedFile);
|
||||
}
|
||||
|
||||
private boolean isAppPackage(StoredFile storedFile) {
|
||||
String filename = storedFile.getFilename() == null ? "" : storedFile.getFilename().toLowerCase(Locale.ROOT);
|
||||
String contentType = storedFile.getContentType() == null ? "" : storedFile.getContentType().toLowerCase(Locale.ROOT);
|
||||
return filename.endsWith(".apk")
|
||||
|| filename.endsWith(".ipa")
|
||||
|| "application/vnd.android.package-archive".equals(contentType)
|
||||
|| "application/octet-stream".equals(contentType) && (filename.endsWith(".apk") || filename.endsWith(".ipa"));
|
||||
}
|
||||
|
||||
private String buildPublicPackageDownloadUrl(StoredFile storedFile) {
|
||||
FileBlob blob = getRequiredBlob(storedFile);
|
||||
String base = packageDownloadBaseUrl.endsWith("/")
|
||||
? packageDownloadBaseUrl.substring(0, packageDownloadBaseUrl.length() - 1)
|
||||
: packageDownloadBaseUrl;
|
||||
String path = "/" + trimLeadingSlash(blob.getObjectKey());
|
||||
if (base.endsWith("/_dl")) {
|
||||
path = "/_dl" + path;
|
||||
}
|
||||
long expires = clock.instant().getEpochSecond() + packageDownloadTtlSeconds;
|
||||
String signature = buildSecureLinkSignature(path, expires);
|
||||
return base
|
||||
+ "/"
|
||||
+ trimLeadingSlash(blob.getObjectKey())
|
||||
+ "?md5="
|
||||
+ encodeQueryParam(signature)
|
||||
+ "&expires="
|
||||
+ expires
|
||||
+ "&response-content-disposition="
|
||||
+ encodeQueryParam(buildAsciiContentDisposition(storedFile.getFilename()));
|
||||
}
|
||||
|
||||
private String buildAsciiContentDisposition(String filename) {
|
||||
String sanitized = sanitizeDownloadFilename(filename);
|
||||
StringBuilder disposition = new StringBuilder("attachment; filename=\"")
|
||||
.append(escapeContentDispositionFilename(buildAsciiDownloadFilename(sanitized)))
|
||||
.append("\"");
|
||||
if (StringUtils.hasText(sanitized)) {
|
||||
disposition.append("; filename*=UTF-8''")
|
||||
.append(sanitized);
|
||||
}
|
||||
return disposition.toString();
|
||||
}
|
||||
|
||||
private String buildAsciiDownloadFilename(String filename) {
|
||||
String normalized = sanitizeDownloadFilename(filename);
|
||||
if (!StringUtils.hasText(normalized)) {
|
||||
return "download";
|
||||
}
|
||||
|
||||
String sanitized = normalized.replaceAll("[\\r\\n]", "_");
|
||||
StringBuilder ascii = new StringBuilder(sanitized.length());
|
||||
for (int i = 0; i < sanitized.length(); i++) {
|
||||
char current = sanitized.charAt(i);
|
||||
if (current >= 32 && current <= 126 && current != '"' && current != '\\') {
|
||||
ascii.append(current);
|
||||
} else {
|
||||
ascii.append('_');
|
||||
}
|
||||
}
|
||||
|
||||
String fallback = ascii.toString().trim();
|
||||
String extension = extractAsciiExtension(normalized);
|
||||
String baseName = extension.isEmpty() ? fallback : fallback.substring(0, Math.max(0, fallback.length() - extension.length()));
|
||||
if (baseName.replace("_", "").isBlank()) {
|
||||
return extension.isEmpty() ? "download" : "download" + extension;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private String sanitizeDownloadFilename(String filename) {
|
||||
return StringUtils.hasText(filename) ? filename.trim().replaceAll("[\\r\\n]", "_") : "";
|
||||
}
|
||||
|
||||
private String extractAsciiExtension(String filename) {
|
||||
int extensionIndex = filename.lastIndexOf('.');
|
||||
if (extensionIndex > 0 && extensionIndex < filename.length() - 1) {
|
||||
String extension = filename.substring(extensionIndex).replaceAll("[^A-Za-z0-9.]", "");
|
||||
return StringUtils.hasText(extension) ? extension : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String escapeContentDispositionFilename(String filename) {
|
||||
return filename.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private String trimLeadingSlash(String value) {
|
||||
return value.startsWith("/") ? value.substring(1) : value;
|
||||
}
|
||||
|
||||
private String buildSecureLinkSignature(String path, long expires) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
byte[] hash = digest.digest((expires + path + " " + packageDownloadSecret).getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("生成下载签名失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String encodeQueryParam(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
}
|
||||
|
||||
private FileMetadataResponse saveFileMetadata(User user,
|
||||
String normalizedPath,
|
||||
String filename,
|
||||
|
||||
@@ -31,6 +31,9 @@ app:
|
||||
usernames: ${APP_ADMIN_USERNAMES:}
|
||||
registration:
|
||||
invite-code: ${APP_AUTH_REGISTRATION_INVITE_CODE:}
|
||||
android:
|
||||
metadata-object-key: ${YOYUZH_ANDROID_RELEASE_METADATA_OBJECT_KEY:android/releases/latest.json}
|
||||
download-public-url: ${YOYUZH_ANDROID_DOWNLOAD_PUBLIC_URL:https://api.yoyuzh.xyz/api/app/android/download}
|
||||
storage:
|
||||
provider: ${YOYUZH_STORAGE_PROVIDER:local}
|
||||
max-file-size: ${YOYUZH_STORAGE_MAX_FILE_SIZE:524288000}
|
||||
@@ -43,6 +46,10 @@ app:
|
||||
scope: ${YOYUZH_DOGECLOUD_STORAGE_SCOPE:}
|
||||
ttl-seconds: ${YOYUZH_DOGECLOUD_STORAGE_TTL_SECONDS:3600}
|
||||
region: ${YOYUZH_DOGECLOUD_S3_REGION:automatic}
|
||||
public-download-base-url: ${YOYUZH_DOGECLOUD_PUBLIC_DOWNLOAD_BASE_URL:}
|
||||
package-download-base-url: ${YOYUZH_PACKAGE_DOWNLOAD_BASE_URL:https://api.yoyuzh.xyz/_dl}
|
||||
package-download-secret: ${YOYUZH_PACKAGE_DOWNLOAD_SECRET:}
|
||||
package-download-ttl-seconds: ${YOYUZH_PACKAGE_DOWNLOAD_TTL_SECONDS:300}
|
||||
cors:
|
||||
allowed-origins:
|
||||
- http://localhost:3000
|
||||
|
||||
Reference in New Issue
Block a user