it0/packages/services/version-service/src/interfaces/rest/controllers/version.controller.ts

215 lines
6.7 KiB
TypeScript

import {
Controller,
Get,
Post,
Put,
Delete,
Patch,
Param,
Query,
Body,
UploadedFile,
UseInterceptors,
NotFoundException,
Logger,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ConfigService } from '@nestjs/config';
import { diskStorage } from 'multer';
import * as fs from 'fs';
import * as path from 'path';
import { AppVersionRepository } from '../../../infrastructure/repositories/app-version.repository';
import { Platform } from '../../../domain/entities/app-version.entity';
import { CreateVersionDto } from '../../../application/dtos/create-version.dto';
import { UpdateVersionDto } from '../../../application/dtos/update-version.dto';
const UPLOAD_DIR = '/data/versions';
@Controller('api/v1/versions')
export class VersionController {
private readonly logger = new Logger(VersionController.name);
private readonly downloadBaseUrl: string;
constructor(
private readonly versionRepo: AppVersionRepository,
private readonly config: ConfigService,
) {
this.downloadBaseUrl = this.config.get<string>(
'DOWNLOAD_BASE_URL',
'https://it0api.szaiai.com/downloads/versions',
);
// Ensure upload directories exist
for (const p of [Platform.ANDROID, Platform.IOS]) {
const dir = path.join(UPLOAD_DIR, p.toLowerCase());
fs.mkdirSync(dir, { recursive: true });
}
}
@Get()
async list(
@Query('platform') platform?: string,
@Query('includeDisabled') includeDisabled?: string,
) {
const filter: { platform?: Platform; includeDisabled?: boolean } = {};
if (platform) {
filter.platform = platform.toUpperCase() as Platform;
}
if (includeDisabled === 'true') {
filter.includeDisabled = true;
}
return this.versionRepo.findAll(filter);
}
@Get(':id')
async getById(@Param('id') id: string) {
const version = await this.versionRepo.findById(id);
if (!version) throw new NotFoundException('Version not found');
return version;
}
@Post()
async create(@Body() dto: CreateVersionDto) {
return this.versionRepo.create({
...dto,
releaseDate: dto.releaseDate ? new Date(dto.releaseDate) : undefined,
});
}
@Put(':id')
async update(@Param('id') id: string, @Body() dto: UpdateVersionDto) {
const existing = await this.versionRepo.findById(id);
if (!existing) throw new NotFoundException('Version not found');
return this.versionRepo.update(id, {
...dto,
releaseDate: dto.releaseDate ? new Date(dto.releaseDate) : undefined,
});
}
@Delete(':id')
async delete(@Param('id') id: string) {
const version = await this.versionRepo.findById(id);
if (!version) throw new NotFoundException('Version not found');
// Delete the associated file if it exists
if (version.downloadUrl) {
const platformDir = version.platform.toLowerCase();
const filename = path.basename(new URL(version.downloadUrl).pathname);
const filePath = path.join(UPLOAD_DIR, platformDir, filename);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
this.logger.log(`Deleted file: ${filePath}`);
}
}
await this.versionRepo.delete(id);
return { deleted: true };
}
@Patch(':id/toggle')
async toggle(@Param('id') id: string, @Body('isEnabled') isEnabled: boolean) {
const existing = await this.versionRepo.findById(id);
if (!existing) throw new NotFoundException('Version not found');
return this.versionRepo.update(id, { isEnabled });
}
@Post('upload')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: (req, _file, cb) => {
const platform = (req.body.platform || 'ANDROID').toLowerCase();
const dir = path.join(UPLOAD_DIR, platform);
fs.mkdirSync(dir, { recursive: true });
cb(null, dir);
},
filename: (_req, file, cb) => {
// Preserve original filename with timestamp prefix to avoid collisions
const timestamp = Date.now();
const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
cb(null, `${timestamp}_${safeName}`);
},
}),
limits: { fileSize: 500 * 1024 * 1024 }, // 500 MB
}),
)
async upload(
@UploadedFile() file: Express.Multer.File,
@Body('platform') platform: string,
@Body('versionName') versionName: string,
@Body('buildNumber') buildNumber: string,
@Body('changelog') changelog?: string,
@Body('isForceUpdate') isForceUpdate?: string,
@Body('minOsVersion') minOsVersion?: string,
@Body('releaseDate') releaseDate?: string,
) {
const platformEnum = (platform || 'ANDROID').toUpperCase() as Platform;
const platformDir = platformEnum.toLowerCase();
const downloadUrl = `${this.downloadBaseUrl}/${platformDir}/${file.filename}`;
return this.versionRepo.create({
platform: platformEnum,
versionName,
buildNumber,
changelog: changelog || undefined,
downloadUrl,
fileSize: file.size,
isForceUpdate: isForceUpdate === 'true',
isEnabled: true,
minOsVersion: minOsVersion || undefined,
releaseDate: releaseDate ? new Date(releaseDate) : undefined,
});
}
@Post('parse')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: '/tmp',
filename: (_req, file, cb) => {
cb(null, `parse_${Date.now()}_${file.originalname}`);
},
}),
limits: { fileSize: 500 * 1024 * 1024 },
}),
)
async parsePackage(
@UploadedFile() file: Express.Multer.File,
@Body('platform') platform: string,
) {
try {
// Dynamic import for app-info-parser (CommonJS module)
const AppInfoParser = (await import('app-info-parser')).default;
const parser = new AppInfoParser(file.path);
const info = await parser.parse();
let result: { versionName?: string; versionCode?: string; minSdkVersion?: string } = {};
if (platform?.toUpperCase() === 'IOS') {
result = {
versionName: info.CFBundleShortVersionString,
versionCode: info.CFBundleVersion,
minSdkVersion: info.MinimumOSVersion,
};
} else {
result = {
versionName: info.versionName,
versionCode: String(info.versionCode),
minSdkVersion: info.usesSdk?.minSdkVersion
? String(info.usesSdk.minSdkVersion)
: undefined,
};
}
return result;
} catch (err) {
this.logger.warn(`Failed to parse package: ${(err as Error).message}`);
return { versionName: null, versionCode: null, minSdkVersion: null };
} finally {
// Clean up temp file
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
}
}
}