284 lines
12 KiB
Java
284 lines
12 KiB
Java
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<KernelFileInfo> 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<UpdateItem> 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<java.nio.file.Path>) 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<java.nio.file.Path>) archStream::iterator) {
|
||
if (!Files.isDirectory(archDir)) {
|
||
continue;
|
||
}
|
||
String arch = archDir.getFileName().toString();
|
||
Optional<KernelFileInfo> 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<java.nio.file.Path>) 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<KernelFileInfo> 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) {
|
||
}
|
||
}
|