feat(app-version): эндпоинт /app-version + хостинг APK для авто-обновления

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.
This commit is contained in:
nk
2026-06-06 20:21:54 +03:00
parent 4aa3b55b5e
commit 05e3796b85
5 changed files with 73 additions and 0 deletions

View File

@@ -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,
},
};
}
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { AppVersionController } from './app-version.controller';
@Module({
controllers: [AppVersionController],
})
export class AppVersionModule {}

View File

@@ -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 {}

View File

@@ -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,