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 { 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 {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user