Files
goldenChart/backend/src/main/java/com/goldenchart/service/MobileAppReleaseService.java
T

169 lines
6.7 KiB
Java

package com.goldenchart.service;
import com.goldenchart.dto.MobileAppReleaseInfoDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.*;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.Optional;
import java.util.stream.Stream;
@Service
@Slf4j
public class MobileAppReleaseService {
private static final String DEFAULT_APK_NAME = "goldenchart-android.apk";
private static final DateTimeFormatter DT_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.of("Asia/Seoul"));
@Value("${goldenchart.mobile-app.release-dir:data/mobile-releases}")
private String releaseDir;
@Value("${goldenchart.mobile-app.version:}")
private String configuredVersion;
@Value("${goldenchart.mobile-app.public-base-url:}")
private String configuredPublicBaseUrl;
@Value("${goldenchart.mobile-app.ios-testflight-url:}")
private String iosTestFlightUrl;
public MobileAppReleaseInfoDto getInfo(String requestBaseUrl) {
String publicBaseUrl = resolvePublicBaseUrl(requestBaseUrl);
boolean iosAvailable = iosTestFlightUrl != null && !iosTestFlightUrl.isBlank();
Optional<Path> apk = resolveApkPath();
if (apk.isEmpty()) {
return MobileAppReleaseInfoDto.builder()
.available(false)
.installPageUrl(buildInstallPageUrl(publicBaseUrl))
.iosAvailable(iosAvailable)
.iosTestFlightUrl(iosAvailable ? iosTestFlightUrl.trim() : null)
.build();
}
Path file = apk.get();
try {
long size = Files.size(file);
Instant updated = Files.getLastModifiedTime(file).toInstant();
String base = normalizeBase(publicBaseUrl);
String downloadUrl = base + "/api/mobile-app/download/android";
String version = resolveApkVersion(file);
return MobileAppReleaseInfoDto.builder()
.available(true)
.fileName(file.getFileName().toString())
.version(version)
.sizeBytes(size)
.updatedAt(DT_FMT.format(updated))
.downloadUrl(downloadUrl)
.installPageUrl(buildInstallPageUrl(publicBaseUrl))
.iosAvailable(iosAvailable)
.iosTestFlightUrl(iosAvailable ? iosTestFlightUrl.trim() : null)
.build();
} catch (IOException e) {
log.warn("[mobile-app] stat failed: {}", e.getMessage());
return MobileAppReleaseInfoDto.builder()
.available(false)
.installPageUrl(buildInstallPageUrl(publicBaseUrl))
.iosAvailable(iosAvailable)
.iosTestFlightUrl(iosAvailable ? iosTestFlightUrl.trim() : null)
.build();
}
}
public ResponseEntity<Resource> downloadAndroid() {
Optional<Path> apk = resolveApkPath();
if (apk.isEmpty()) {
return ResponseEntity.notFound().build();
}
Path file = apk.get();
FileSystemResource resource = new FileSystemResource(file);
String filename = file.getFileName().toString();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.contentType(MediaType.parseMediaType("application/vnd.android.package-archive"))
.body(resource);
}
private Optional<Path> resolveApkPath() {
Path dir = Paths.get(releaseDir).toAbsolutePath().normalize();
if (!Files.isDirectory(dir)) {
log.debug("[mobile-app] release dir missing: {}", dir);
return Optional.empty();
}
Path preferred = dir.resolve(DEFAULT_APK_NAME);
if (Files.isRegularFile(preferred)) {
return Optional.of(preferred);
}
try (Stream<Path> stream = Files.list(dir)) {
return stream
.filter(p -> Files.isRegularFile(p) && p.getFileName().toString().toLowerCase().endsWith(".apk"))
.max(Comparator.comparing(p -> {
try {
return Files.getLastModifiedTime(p).toMillis();
} catch (IOException e) {
return 0L;
}
}));
} catch (IOException e) {
log.warn("[mobile-app] list dir failed: {}", e.getMessage());
return Optional.empty();
}
}
private static String buildInstallPageUrl(String publicBaseUrl) {
return normalizeBase(publicBaseUrl) + "/install.html";
}
private String resolvePublicBaseUrl(String requestBaseUrl) {
if (configuredPublicBaseUrl != null && !configuredPublicBaseUrl.isBlank()) {
return configuredPublicBaseUrl.trim();
}
if (requestBaseUrl != null && !requestBaseUrl.isBlank()) {
return requestBaseUrl.trim();
}
return "http://exdev.co.kr";
}
private static String normalizeBase(String publicBaseUrl) {
if (publicBaseUrl == null || publicBaseUrl.isBlank()) {
return "http://exdev.co.kr";
}
return publicBaseUrl.replaceAll("/+$", "");
}
/** APK와 함께 기록된 .version sidecar → .env configuredVersion → 파일 수정 시각 */
private String resolveApkVersion(Path apkFile) {
String apkName = apkFile.getFileName().toString();
Path sidecar = apkFile.getParent().resolve(
apkName.replaceAll("(?i)\\.apk$", "") + ".version");
try {
if (Files.isRegularFile(sidecar)) {
String fromFile = Files.readString(sidecar).trim();
if (!fromFile.isBlank()) {
return fromFile;
}
}
} catch (IOException e) {
log.warn("[mobile-app] read version sidecar failed: {}", e.getMessage());
}
if (configuredVersion != null && !configuredVersion.isBlank()) {
return configuredVersion.trim();
}
try {
return DT_FMT.format(Files.getLastModifiedTime(apkFile).toInstant());
} catch (IOException e) {
return "unknown";
}
}
}