package im.rosetta.api; import im.rosetta.api.dto.UpdateItem; import im.rosetta.api.dto.UpdateListResponse; import im.rosetta.api.dto.UpdateResponse; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @Path("/updates") @Produces(MediaType.APPLICATION_JSON) public class UpdatesResource { // Каталоги с обновлениями относительно корня запуска приложения. private static final java.nio.file.Path KERNEL_DIR = Paths.get("kernel"); private static final java.nio.file.Path PACKS_DIR = Paths.get("packs"); // Регулярка для извлечения версии ядра из имени файла. private static final Pattern KERNEL_VERSION_PATTERN = Pattern.compile(".*-(\\d+(?:\\.\\d+)*)\\.[^.]+$"); @GET @Path("/get") public UpdateResponse getUpdate(@QueryParam("platform") String platform, @QueryParam("arch") String arch, @QueryParam("app") String appVersion, @QueryParam("kernel") String kernelVersion) { // Нормализуем входные параметры. String normalizedPlatform = normalize(platform); String normalizedArch = normalize(arch); String normalizedApp = normalize(appVersion); String normalizedKernel = normalize(kernelVersion); // Приводим платформу и архитектуру к нижнему регистру для сопоставления с // именами файлов. if (normalizedPlatform != null) { normalizedPlatform = normalizedPlatform.toLowerCase(Locale.ROOT); } if (normalizedArch != null) { normalizedArch = normalizedArch.toLowerCase(Locale.ROOT); } // Если параметры не заданы — возвращаем пустой ответ. if (normalizedPlatform == null || normalizedArch == null || normalizedApp == null || normalizedKernel == null) { return new UpdateResponse(null, normalizedPlatform, normalizedArch, false, null, null); } // Ищем лучший пакет обновления и последнюю версию приложения на сервере. PackSelection packSelection = findPackSelection(normalizedPlatform, normalizedArch, normalizedApp); String serverAppVersion = packSelection.serverVersion() != null ? packSelection.serverVersion() : normalizedApp; String servicePackUrl = packSelection.updateFileName() != null ? "/sp/" + packSelection.updateFileName() : null; boolean kernelUpdateRequired = false; String kernelUrl = null; // Проверяем требование к ядру, только если пакет обновления найден. if (packSelection.updateFileName() != null && compareVersions(normalizedKernel, packSelection.minKernelRequired()) < 0) { kernelUpdateRequired = true; // Ищем актуальную версию ядра на сервере. Optional latestKernel = findLatestKernel(normalizedPlatform, normalizedArch); if (latestKernel.isPresent()) { KernelFileInfo info = latestKernel.get(); kernelUrl = "/kernel/" + normalizedPlatform + "/" + normalizedArch + "/" + info.fileName(); } } return new UpdateResponse(serverAppVersion, normalizedPlatform, normalizedArch, kernelUpdateRequired, servicePackUrl, kernelUrl); } @GET @Path("/all") public UpdateListResponse getAll() { List items = new ArrayList<>(); if (!Files.isDirectory(KERNEL_DIR)) { return new UpdateListResponse(items); } try (var stream = Files.list(KERNEL_DIR)) { for (java.nio.file.Path platformDir : (Iterable) stream::iterator) { if (!Files.isDirectory(platformDir)) { continue; } String platform = platformDir.getFileName().toString(); try (var archStream = Files.list(platformDir)) { for (java.nio.file.Path archDir : (Iterable) archStream::iterator) { if (!Files.isDirectory(archDir)) { continue; } String arch = archDir.getFileName().toString(); Optional latestKernel = findLatestKernel(platform, arch); if (latestKernel.isPresent()) { KernelFileInfo info = latestKernel.get(); items.add(new UpdateItem(platform, arch, info.version(), "/kernel/" + platform + "/" + arch + "/" + info.fileName())); } } } catch (IOException ignored) { } } } catch (IOException ignored) { } return new UpdateListResponse(items); } // Находим самый новый пакет обновления и последнюю версию приложения на // сервере. private PackSelection findPackSelection(String platform, String arch, String clientAppVersion) { if (!Files.isDirectory(PACKS_DIR)) { return new PackSelection(null, null, null); } String bestUpdateFile = null; String bestUpdateVersion = null; String minKernelRequired = null; String maxServerVersion = null; try (var stream = Files.list(PACKS_DIR)) { for (java.nio.file.Path path : (Iterable) stream::iterator) { if (!Files.isRegularFile(path)) { continue; } PackFileInfo info = parsePackFileName(path.getFileName().toString()); if (info == null) { continue; } if (!platform.equals(info.platform()) || !arch.equals(info.arch())) { continue; } // Обновляем информацию о максимальной версии на сервере. if (maxServerVersion == null || compareVersions(info.appVersion(), maxServerVersion) > 0) { maxServerVersion = info.appVersion(); } // Проверяем, что версия клиента меньше версии пакета. if (compareVersions(clientAppVersion, info.appVersion()) >= 0) { continue; } // Выбираем самый новый доступный пакет. if (bestUpdateVersion == null || compareVersions(info.appVersion(), bestUpdateVersion) > 0) { bestUpdateVersion = info.appVersion(); bestUpdateFile = info.fileName(); minKernelRequired = info.minKernelRequired(); } } } catch (IOException ignored) { // Если каталог недоступен, возвращаем пустой результат. return new PackSelection(null, null, null); } return new PackSelection(bestUpdateFile, minKernelRequired, maxServerVersion); } // Ищем последний доступный файл ядра. private Optional findLatestKernel(String platform, String arch) { java.nio.file.Path targetDir = KERNEL_DIR.resolve(platform).resolve(arch); if (!Files.isDirectory(targetDir)) { return Optional.empty(); } try (var stream = Files.list(targetDir)) { return stream.filter(Files::isRegularFile) .map(java.nio.file.Path::getFileName) .map(java.nio.file.Path::toString) .map(this::parseKernelFileName) .filter(info -> info != null) .max(Comparator.comparing(KernelFileInfo::version, this::compareVersions)); } catch (IOException ignored) { return Optional.empty(); } } // Парсим имя файла ядра и извлекаем версию. private KernelFileInfo parseKernelFileName(String fileName) { Matcher matcher = KERNEL_VERSION_PATTERN.matcher(fileName); if (!matcher.matches()) { return null; } return new KernelFileInfo(fileName, matcher.group(1)); } // Парсим имя файла пакета обновления приложения. private PackFileInfo parsePackFileName(String fileName) { String lower = fileName.toLowerCase(Locale.ROOT); if (!lower.startsWith("sp-") || !lower.endsWith(".zip")) { return null; } String baseName = fileName.substring(0, fileName.length() - 4); String[] parts = baseName.split("-"); if (parts.length != 5) { return null; } return new PackFileInfo(parts[1], parts[2], parts[3], parts[4], fileName); } // Сравнение версий вида 1.4.5. private int compareVersions(String left, String right) { if (left == null && right == null) { return 0; } if (left == null) { return -1; } if (right == null) { return 1; } String[] leftParts = left.split("\\."); String[] rightParts = right.split("\\."); int max = Math.max(leftParts.length, rightParts.length); for (int i = 0; i < max; i++) { int l = i < leftParts.length ? parseVersionPart(leftParts[i]) : 0; int r = i < rightParts.length ? parseVersionPart(rightParts[i]) : 0; if (l != r) { return Integer.compare(l, r); } } return 0; } // Безопасный парсинг части версии. private int parseVersionPart(String part) { try { return Integer.parseInt(part); } catch (NumberFormatException ignored) { return 0; } } // Нормализуем строку (убираем пробелы и null). private String normalize(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } // Вспомогательная структура для файла ядра. private record KernelFileInfo(String fileName, String version) { } // Вспомогательная структура для файла пакета. private record PackFileInfo(String platform, String arch, String appVersion, String minKernelRequired, String fileName) { } // Результат выбора пакета обновления. private record PackSelection(String updateFileName, String minKernelRequired, String serverVersion) { } }