From 05e3796b856a6ab1e2dc489f28061e4011a6c118 Mon Sep 17 00:00:00 2001 From: nk Date: Sat, 6 Jun 2026 20:21:54 +0300 Subject: [PATCH] =?UTF-8?q?feat(app-version):=20=D1=8D=D0=BD=D0=B4=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D0=BD=D1=82=20/app-version=20+=20=D1=85=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BD=D0=B3=20APK=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE-=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /app-version читает манифест с диска (data/app-version.json, путь — env APP_VERSION_FILE) → {android:{version_name,version_code,download_url,force_update, sha256,notes}}. Релиз = заменить APK в /downloads + отредактировать json, без пересборки. При сбое файла отдаёт version_code:0 (апдейт не навязываем). Статика /downloads/ (DOWNLOADS_DIR) — раздаёт APK. --- app-version.example.json | 10 +++++ src/app-version/app-version.controller.ts | 49 +++++++++++++++++++++++ src/app-version/app-version.module.ts | 7 ++++ src/app.module.ts | 2 + src/main.ts | 5 +++ 5 files changed, 73 insertions(+) create mode 100644 app-version.example.json create mode 100644 src/app-version/app-version.controller.ts create mode 100644 src/app-version/app-version.module.ts diff --git a/app-version.example.json b/app-version.example.json new file mode 100644 index 0000000..74d15d0 --- /dev/null +++ b/app-version.example.json @@ -0,0 +1,10 @@ +{ + "android": { + "version_name": "1.1", + "version_code": 2, + "download_url": "http://121.127.37.212:3000/downloads/radiola-latest.apk", + "force_update": false, + "sha256": "", + "notes": "Тёмные цветовые темы, фикс таймера сна, авто-обновление." + } +} diff --git a/src/app-version/app-version.controller.ts b/src/app-version/app-version.controller.ts new file mode 100644 index 0000000..5a751c1 --- /dev/null +++ b/src/app-version/app-version.controller.ts @@ -0,0 +1,49 @@ +import { Controller, Get, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface PlatformVersion { + version_name: string; + version_code: number; + download_url: string; + force_update: boolean; + sha256?: string; + notes?: string; +} + +/** + * Манифест последней версии приложения для авто-обновления (как в nkVPN). + * Читается с диска при каждом запросе, поэтому релиз = заменить APK в /downloads + * и отредактировать data/app-version.json — без пересборки бэкенда. + * Путь к файлу — env APP_VERSION_FILE, иначе data/app-version.json. + */ +@ApiTags('app-version') +@Controller('app-version') +export class AppVersionController { + private readonly logger = new Logger(AppVersionController.name); + private readonly file = + process.env.APP_VERSION_FILE || + path.join(process.cwd(), 'data', 'app-version.json'); + + @Get() + @ApiOperation({ summary: 'Манифест последней версии приложения' }) + getVersion(): { android: PlatformVersion } { + try { + const raw = fs.readFileSync(this.file, 'utf-8'); + return JSON.parse(raw) as { android: PlatformVersion }; + } catch (e) { + this.logger.warn(`app-version.json недоступен: ${(e as Error).message}`); + // Безопасный фолбэк: version_code 0 ⇒ установленное приложение (code ≥ 1) + // никогда не увидит «обновление», т.е. при сбое файла апдейт не навязываем. + return { + android: { + version_name: '0', + version_code: 0, + download_url: '', + force_update: false, + }, + }; + } + } +} diff --git a/src/app-version/app-version.module.ts b/src/app-version/app-version.module.ts new file mode 100644 index 0000000..dfb432d --- /dev/null +++ b/src/app-version/app-version.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { AppVersionController } from './app-version.controller'; + +@Module({ + controllers: [AppVersionController], +}) +export class AppVersionModule {} diff --git a/src/app.module.ts b/src/app.module.ts index 73fbf61..90369f3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { UsersModule } from './users/users.module'; import { NowPlayingModule } from './now-playing/now-playing.module'; import { HealthCheckModule } from './health-check/health-check.module'; import { ChartsModule } from './charts/charts.module'; +import { AppVersionModule } from './app-version/app-version.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { ChartsModule } from './charts/charts.module'; NowPlayingModule, HealthCheckModule, ChartsModule, + AppVersionModule, ], }) export class AppModule {} diff --git a/src/main.ts b/src/main.ts index 44d296e..5b64f27 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,11 @@ async function bootstrap() { immutable: true, }); + // Раздача APK приложения для авто-обновления (/downloads/radiola-latest.apk). + const downloadsDir = + process.env.DOWNLOADS_DIR || join(process.cwd(), 'data', 'downloads'); + app.useStaticAssets(downloadsDir, { prefix: '/downloads/' }); + app.useGlobalPipes( new ValidationPipe({ whitelist: true,