앱 버전 갱신문제

This commit is contained in:
Macbook
2026-06-14 19:49:40 +09:00
parent ad6347f2a2
commit ac2b0eb7f1
7 changed files with 180 additions and 30 deletions
@@ -1,7 +1,10 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.dto.DesktopAppPlatformReleaseDto;
import com.goldenchart.dto.DesktopAppReleaseInfoDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -14,18 +17,29 @@ import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.Locale;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@Service
@Slf4j
@RequiredArgsConstructor
public class DesktopAppReleaseService {
private static final DateTimeFormatter DT_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.of("Asia/Seoul"));
private static final Pattern VERSION_IN_NAME =
Pattern.compile("GoldenChart[_-](\\d+\\.\\d+\\.\\d+)", Pattern.CASE_INSENSITIVE);
private final ObjectMapper objectMapper;
@Value("${goldenchart.desktop-app.release-dir:data/desktop-releases}")
private String releaseDir;
@Value("${goldenchart.desktop-app.updates-dir:}")
private String updatesDir;
@Value("${goldenchart.desktop-app.version:}")
private String configuredVersion;
@@ -40,9 +54,18 @@ public class DesktopAppReleaseService {
Optional<Path> macFile = resolveMacInstaller();
Optional<Path> winFile = resolveWindowsInstaller();
String version = configuredVersion != null && !configuredVersion.isBlank()
? configuredVersion.trim()
: resolveVersion(macFile, winFile);
String macVer = macFile.map(p -> extractVersionFromFileName(p.getFileName().toString())).orElse(null);
String winVer = winFile.map(p -> extractVersionFromFileName(p.getFileName().toString())).orElse(null);
String publishedVer = readLatestJsonVersion();
String buildInfoVer = readBuildInfoVersion(releaseDirectory());
String version = maxSemver(
nullIfBlank(configuredVersion),
publishedVer,
buildInfoVer,
macVer,
winVer
);
Instant latestUpdated = Stream.of(macFile, winFile)
.flatMap(Optional::stream)
@@ -53,8 +76,8 @@ public class DesktopAppReleaseService {
return DesktopAppReleaseInfoDto.builder()
.version(version)
.updatedAt(latestUpdated.equals(Instant.EPOCH) ? null : DT_FMT.format(latestUpdated))
.mac(buildPlatform(macFile, staticBase, version))
.windows(buildPlatform(winFile, staticBase, version))
.mac(buildPlatform(macFile, staticBase, macVer != null ? macVer : version))
.windows(buildPlatform(winFile, staticBase, winVer != null ? winVer : version))
.build();
}
@@ -93,27 +116,97 @@ public class DesktopAppReleaseService {
}
}
private String resolveVersion(Optional<Path> macFile, Optional<Path> winFile) {
return Stream.of(macFile, winFile)
.flatMap(Optional::stream)
.map(p -> p.getFileName().toString())
.map(this::extractVersionFromFileName)
.filter(v -> v != null && !v.isBlank())
.findFirst()
.orElse(null);
private String readLatestJsonVersion() {
Path latest = resolveUpdatesDirectory() != null
? resolveUpdatesDirectory().resolve("latest.json")
: null;
if (latest == null || !Files.isRegularFile(latest)) {
return null;
}
try {
JsonNode root = objectMapper.readTree(Files.readString(latest));
JsonNode ver = root.path("version");
return ver.isTextual() ? ver.asText().trim() : null;
} catch (Exception e) {
log.debug("[desktop-app] latest.json read failed: {}", e.getMessage());
return null;
}
}
private String readBuildInfoVersion(Path release) {
if (release == null) {
return null;
}
Path info = release.resolve("build-info.json");
if (!Files.isRegularFile(info)) {
return null;
}
try {
JsonNode root = objectMapper.readTree(Files.readString(info));
JsonNode ver = root.path("version");
return ver.isTextual() ? ver.asText().trim() : null;
} catch (Exception e) {
log.debug("[desktop-app] build-info read failed: {}", e.getMessage());
return null;
}
}
private String extractVersionFromFileName(String fileName) {
// GoldenChart_0.1.0_aarch64.dmg, GoldenChart_0.1.0_x64-setup.exe
var m = java.util.regex.Pattern
.compile("GoldenChart[_-](\\d+\\.\\d+\\.\\d+)", java.util.regex.Pattern.CASE_INSENSITIVE)
.matcher(fileName);
Matcher m = VERSION_IN_NAME.matcher(fileName);
return m.find() ? m.group(1) : null;
}
private String maxSemver(String... candidates) {
String best = null;
for (String c : candidates) {
if (c == null || c.isBlank()) {
continue;
}
if (best == null || compareSemver(c, best) > 0) {
best = c.trim();
}
}
return best;
}
private int compareSemver(String a, String b) {
int[] pa = parseSemverParts(a);
int[] pb = parseSemverParts(b);
for (int i = 0; i < 3; i++) {
int d = pa[i] - pb[i];
if (d != 0) {
return d;
}
}
return 0;
}
private int[] parseSemverParts(String v) {
String[] parts = v.replaceAll("^[vV]", "").split("\\.");
int[] out = new int[3];
for (int i = 0; i < 3; i++) {
out[i] = i < parts.length ? parseIntSafe(parts[i]) : 0;
}
return out;
}
private int parseIntSafe(String s) {
try {
return Integer.parseInt(s.replaceAll("[^0-9].*", ""));
} catch (NumberFormatException e) {
return 0;
}
}
private static String nullIfBlank(String s) {
return s == null || s.isBlank() ? null : s.trim();
}
private Optional<Path> resolveMacInstaller() {
Path dir = releaseDirectory();
if (dir == null) return Optional.empty();
if (dir == null) {
return Optional.empty();
}
try (Stream<Path> stream = Files.list(dir)) {
return stream
.filter(p -> Files.isRegularFile(p) && p.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".dmg"))
@@ -126,7 +219,9 @@ public class DesktopAppReleaseService {
private Optional<Path> resolveWindowsInstaller() {
Path dir = releaseDirectory();
if (dir == null) return Optional.empty();
if (dir == null) {
return Optional.empty();
}
try (Stream<Path> stream = Files.list(dir)) {
return stream
.filter(p -> {
@@ -144,26 +239,44 @@ public class DesktopAppReleaseService {
private Comparator<Path> macComparator() {
return Comparator
.comparingInt((Path p) -> scoreMac(p.getFileName().toString()))
.thenComparing((Path p) -> semverScore(extractVersionFromFileName(p.getFileName().toString())))
.thenComparing(p -> lastModified(p).toEpochMilli());
}
private Comparator<Path> windowsComparator() {
return Comparator
.comparingInt((Path p) -> scoreWindows(p.getFileName().toString()))
.thenComparing((Path p) -> semverScore(extractVersionFromFileName(p.getFileName().toString())))
.thenComparing(p -> lastModified(p).toEpochMilli());
}
private long semverScore(String version) {
if (version == null) {
return 0;
}
int[] p = parseSemverParts(version);
return p[0] * 1_000_000L + p[1] * 1_000L + p[2];
}
private static int scoreMac(String name) {
String lower = name.toLowerCase(Locale.ROOT);
if (lower.contains("aarch64") || lower.contains("arm64")) return 3;
if (lower.contains("universal")) return 2;
if (lower.contains("x64") || lower.contains("x86_64")) return 1;
if (lower.contains("aarch64") || lower.contains("arm64")) {
return 3;
}
if (lower.contains("universal")) {
return 2;
}
if (lower.contains("x64") || lower.contains("x86_64")) {
return 1;
}
return 0;
}
private static int scoreWindows(String name) {
String lower = name.toLowerCase(Locale.ROOT);
if (lower.contains("setup")) return 2;
if (lower.contains("setup")) {
return 2;
}
return 1;
}
@@ -176,6 +289,22 @@ public class DesktopAppReleaseService {
return dir;
}
private Path resolveUpdatesDirectory() {
if (updatesDir != null && !updatesDir.isBlank()) {
Path dir = Paths.get(updatesDir).toAbsolutePath().normalize();
if (Files.isDirectory(dir)) {
return dir;
}
}
Path fallback = releaseDirectory() != null
? releaseDirectory().getParent().resolve("desktop-updates")
: null;
if (fallback != null && Files.isDirectory(fallback)) {
return fallback;
}
return null;
}
private static String encodeFileName(String fileName) {
return fileName.replace(" ", "%20");
}