init
This commit is contained in:
31
.dockerignore
Normal file
31
.dockerignore
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# .dockerignore
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Maven target
|
||||||
|
target/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Development
|
||||||
|
README.md
|
||||||
|
.env.local
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# Исходный код не нужен в образе (используется build/)
|
||||||
|
src/
|
||||||
|
pom.xml
|
||||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Maven build
|
||||||
|
target/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
|
*.jar
|
||||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Rosetta Server Updates
|
||||||
|
|
||||||
|
Это сервер обновлений для клиентских приложений. Для сборки нужно поставить Maven и использовать команду
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### GET /updates/get
|
||||||
|
|
||||||
|
Получить информацию о доступных обновлениях.
|
||||||
|
|
||||||
|
**Параметры запроса:**
|
||||||
|
- `platform` - платформа клиента (win32, darwin, linux)
|
||||||
|
- `arch` - архитектура (x64, arm64)
|
||||||
|
- `app` - текущая версия приложения (e.g., 0.5.0)
|
||||||
|
- `kernel` - текущая версия ядра (e.g., 1.4.4)
|
||||||
|
|
||||||
|
**Пример запроса:**
|
||||||
|
```
|
||||||
|
GET /updates/get?platform=win32&arch=x64&app=0.5.0&kernel=1.4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
**Формат ответа:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.6.0",
|
||||||
|
"platform": "win32",
|
||||||
|
"arch": "x64",
|
||||||
|
"kernel_update_required": false,
|
||||||
|
"sevice_pack_url": "/sp/sp-win32-x64-0.6.0-1.4.6.zip",
|
||||||
|
"kernel_url": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Поля ответа:**
|
||||||
|
- `version` - самая актуальная версия приложения на сервере
|
||||||
|
- `platform` - платформа
|
||||||
|
- `arch` - архитектура
|
||||||
|
- `kernel_update_required` - требуется ли обновление ядра
|
||||||
|
- `sevice_pack_url` - ссылка на пакет обновления приложения (если доступен)
|
||||||
|
- `kernel_url` - ссылка на обновление ядра (если требуется)
|
||||||
|
|
||||||
|
### GET /updates/all
|
||||||
|
|
||||||
|
Получить список всех доступных обновлений (заглушка).
|
||||||
15
build/Dockerfile
Normal file
15
build/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем готовый JAR со всеми зависимостями
|
||||||
|
COPY app.jar ./app.jar
|
||||||
|
|
||||||
|
# Создаём директории для обновлений (будут смонтированы из хоста)
|
||||||
|
RUN mkdir -p kernel packs
|
||||||
|
|
||||||
|
# Открываем порт (может быть переопределён через ENV)
|
||||||
|
EXPOSE ${PORT:-8080}
|
||||||
|
|
||||||
|
# Запускаем приложение с портом из окружения
|
||||||
|
CMD ["sh", "-c", "java -jar app.jar ${PORT:-8080}"]
|
||||||
18
build/docker-compose.yml
Normal file
18
build/docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
rosetta-updates:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: rosetta-updates
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8080}:${PORT:-8080}"
|
||||||
|
environment:
|
||||||
|
- PORT=${PORT:-8080}
|
||||||
|
volumes:
|
||||||
|
# Монтируем директории обновлений для автоматического подхвата изменений
|
||||||
|
# При загрузке на FTP докер автоматически подхватит обновления
|
||||||
|
- ./kernel:/app/kernel
|
||||||
|
- ./packs:/app/packs
|
||||||
|
restart: unless-stopped
|
||||||
118
pom.xml
Normal file
118
pom.xml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>im.rosetta</groupId>
|
||||||
|
<artifactId>rosetta-sdu</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<jersey.version>3.1.5</jersey.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.containers</groupId>
|
||||||
|
<artifactId>jersey-container-grizzly2-http</artifactId>
|
||||||
|
<version>${jersey.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.inject</groupId>
|
||||||
|
<artifactId>jersey-hk2</artifactId>
|
||||||
|
<version>${jersey.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.media</groupId>
|
||||||
|
<artifactId>jersey-media-json-jackson</artifactId>
|
||||||
|
<version>${jersey.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>2.0.12</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<!-- Сборка исполняемого JAR со всеми зависимостями -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>im.rosetta.Main</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
<finalName>app</finalName>
|
||||||
|
<appendAssemblyId>false</appendAssemblyId>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<!-- Удаляем папку build/classes перед сборкой -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-clean-plugin</artifactId>
|
||||||
|
<version>3.3.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>pre-clean</phase>
|
||||||
|
<configuration>
|
||||||
|
<excludeDefaultDirectories>true</excludeDefaultDirectories>
|
||||||
|
<filesets>
|
||||||
|
<fileset>
|
||||||
|
<directory>${project.basedir}/build</directory>
|
||||||
|
<includes>
|
||||||
|
<include>classes/</include>
|
||||||
|
</includes>
|
||||||
|
</fileset>
|
||||||
|
</filesets>
|
||||||
|
</configuration>
|
||||||
|
<goals>
|
||||||
|
<goal>clean</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<!-- Копирование готового JAR в папку build/ -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<configuration>
|
||||||
|
<target>
|
||||||
|
<mkdir dir="${project.basedir}/build"/>
|
||||||
|
<copy file="${project.build.directory}/app.jar" todir="${project.basedir}/build/" overwrite="true"/>
|
||||||
|
<echo message="> JAR в build/app.jar, готов к деполю на сервер"/>
|
||||||
|
</target>
|
||||||
|
</configuration>
|
||||||
|
<goals>
|
||||||
|
<goal>run</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
||||||
56
src/main/java/im/rosetta/Main.java
Normal file
56
src/main/java/im/rosetta/Main.java
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package im.rosetta;
|
||||||
|
|
||||||
|
import im.rosetta.config.AppConfig;
|
||||||
|
import org.glassfish.grizzly.http.server.HttpServer;
|
||||||
|
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Определяем порт запуска.
|
||||||
|
int port = resolvePort(args);
|
||||||
|
URI baseUri = URI.create("http://0.0.0.0:" + port + "/");
|
||||||
|
|
||||||
|
// Создаём и запускаем встроенный Grizzly с Jersey.
|
||||||
|
HttpServer server = GrizzlyHttpServerFactory.createHttpServer(baseUri, new AppConfig(), false);
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> stopServer(server)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Запуск и ожидание завершения.
|
||||||
|
server.start();
|
||||||
|
Thread.currentThread().join();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to start server", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resolvePort(String[] args) {
|
||||||
|
// Если порт указан аргументом — используем его.
|
||||||
|
if (args != null && args.length > 0) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(args[0]);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если порт задан в окружении — используем его.
|
||||||
|
String envPort = System.getenv("PORT");
|
||||||
|
if (envPort != null && !envPort.isBlank()) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(envPort);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Значение по умолчанию.
|
||||||
|
return 8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stopServer(HttpServer server) {
|
||||||
|
// Корректная остановка сервера.
|
||||||
|
if (server != null) {
|
||||||
|
server.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/java/im/rosetta/api/FilesResource.java
Normal file
55
src/main/java/im/rosetta/api/FilesResource.java
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package im.rosetta.api;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
// Ресурс для выдачи файлов обновлений.
|
||||||
|
@Path("/")
|
||||||
|
public class FilesResource {
|
||||||
|
|
||||||
|
// Базовые каталоги с файлами.
|
||||||
|
private static final java.nio.file.Path KERNEL_DIR = Paths.get("kernel").toAbsolutePath().normalize();
|
||||||
|
private static final java.nio.file.Path PACKS_DIR = Paths.get("packs").toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/kernel/{platform}/{arch}/{file}")
|
||||||
|
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
public Response getKernel(@PathParam("platform") String platform,
|
||||||
|
@PathParam("arch") String arch,
|
||||||
|
@PathParam("file") String file) {
|
||||||
|
// Формируем безопасный путь к файлу ядра.
|
||||||
|
java.nio.file.Path resolved = KERNEL_DIR.resolve(platform).resolve(arch).resolve(file).normalize();
|
||||||
|
if (!resolved.startsWith(KERNEL_DIR) || !Files.isRegularFile(resolved)) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
File target = resolved.toFile();
|
||||||
|
return Response.ok(target)
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"" + target.getName() + "\"")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/sp/{file}")
|
||||||
|
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
public Response getServicePack(@PathParam("file") String file) {
|
||||||
|
// Формируем безопасный путь к файлу пакета обновления.
|
||||||
|
java.nio.file.Path resolved = PACKS_DIR.resolve(file).normalize();
|
||||||
|
if (!resolved.startsWith(PACKS_DIR) || !Files.isRegularFile(resolved)) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
File target = resolved.toFile();
|
||||||
|
return Response.ok(target)
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"" + target.getName() + "\"")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
250
src/main/java/im/rosetta/api/UpdatesResource.java
Normal file
250
src/main/java/im/rosetta/api/UpdatesResource.java
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package im.rosetta.api;
|
||||||
|
|
||||||
|
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.Comparator;
|
||||||
|
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 UpdateResponse getAll() {
|
||||||
|
// По требованию возвращаем пустой ответ, если параметры не указаны.
|
||||||
|
return new UpdateResponse(null, null, null, false, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим самый новый пакет обновления и последнюю версию приложения на сервере.
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/java/im/rosetta/api/dto/UpdateItem.java
Normal file
9
src/main/java/im/rosetta/api/dto/UpdateItem.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package im.rosetta.api.dto;
|
||||||
|
|
||||||
|
public record UpdateItem(
|
||||||
|
String platform,
|
||||||
|
String arch,
|
||||||
|
String version,
|
||||||
|
String downloadUrl
|
||||||
|
) {
|
||||||
|
}
|
||||||
6
src/main/java/im/rosetta/api/dto/UpdateListResponse.java
Normal file
6
src/main/java/im/rosetta/api/dto/UpdateListResponse.java
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package im.rosetta.api.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record UpdateListResponse(List<UpdateItem> items) {
|
||||||
|
}
|
||||||
20
src/main/java/im/rosetta/api/dto/UpdateResponse.java
Normal file
20
src/main/java/im/rosetta/api/dto/UpdateResponse.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package im.rosetta.api.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
// Ответ сервера обновлений в формате, ожидаемом клиентом.
|
||||||
|
public record UpdateResponse(
|
||||||
|
// Версия приложения на сервере (самая актуальная доступная для платформы/архитектуры).
|
||||||
|
String version,
|
||||||
|
// Платформа клиента.
|
||||||
|
String platform,
|
||||||
|
// Архитектура клиента.
|
||||||
|
String arch,
|
||||||
|
// Требуется ли обновление ядра.
|
||||||
|
@JsonProperty("kernel_update_required") boolean kernelUpdateRequired,
|
||||||
|
// Ссылка на пакет обновления приложения.
|
||||||
|
@JsonProperty("sevice_pack_url") String servicePackUrl,
|
||||||
|
// Ссылка на обновление ядра (если требуется).
|
||||||
|
@JsonProperty("kernel_url") String kernelUrl
|
||||||
|
) {
|
||||||
|
}
|
||||||
13
src/main/java/im/rosetta/config/AppConfig.java
Normal file
13
src/main/java/im/rosetta/config/AppConfig.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package im.rosetta.config;
|
||||||
|
|
||||||
|
import org.glassfish.jersey.jackson.JacksonFeature;
|
||||||
|
import org.glassfish.jersey.server.ResourceConfig;
|
||||||
|
|
||||||
|
public class AppConfig extends ResourceConfig {
|
||||||
|
public AppConfig() {
|
||||||
|
// Регистрируем REST-ресурсы.
|
||||||
|
packages("im.rosetta.api");
|
||||||
|
// Включаем JSON-сериализацию.
|
||||||
|
register(JacksonFeature.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user