169 lines
6.7 KiB
Java
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";
|
|
}
|
|
}
|
|
}
|