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:
10
app-version.example.json
Normal file
10
app-version.example.json
Normal file
@@ -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": "Тёмные цветовые темы, фикс таймера сна, авто-обновление."
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/app-version/app-version.controller.ts
Normal file
49
src/app-version/app-version.controller.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/app-version/app-version.module.ts
Normal file
7
src/app-version/app-version.module.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AppVersionController } from './app-version.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AppVersionController],
|
||||||
|
})
|
||||||
|
export class AppVersionModule {}
|
||||||
@@ -8,6 +8,7 @@ import { UsersModule } from './users/users.module';
|
|||||||
import { NowPlayingModule } from './now-playing/now-playing.module';
|
import { NowPlayingModule } from './now-playing/now-playing.module';
|
||||||
import { HealthCheckModule } from './health-check/health-check.module';
|
import { HealthCheckModule } from './health-check/health-check.module';
|
||||||
import { ChartsModule } from './charts/charts.module';
|
import { ChartsModule } from './charts/charts.module';
|
||||||
|
import { AppVersionModule } from './app-version/app-version.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -20,6 +21,7 @@ import { ChartsModule } from './charts/charts.module';
|
|||||||
NowPlayingModule,
|
NowPlayingModule,
|
||||||
HealthCheckModule,
|
HealthCheckModule,
|
||||||
ChartsModule,
|
ChartsModule,
|
||||||
|
AppVersionModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ async function bootstrap() {
|
|||||||
immutable: true,
|
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(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
whitelist: true,
|
whitelist: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user