Commit 9f0c79f8 authored by nanahira's avatar nanahira

complete package

parent f6959604
import { Body, Controller, Get, Param, Post, Query, Req, UploadedFile, UseGuards, UseInterceptors, ValidationPipe } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
Req,
UploadedFile,
UseGuards,
UseInterceptors,
ValidationPipe,
} from '@nestjs/common';
import { AppService } from './app.service';
import { ApiBody, ApiConsumes, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
import { BlankReturnMessageDto, GetAppReturnMessageDto, ReturnMessageDto, StringReturnMessageDto } from './dto/ReturnMessage.dto';
import {
BlankReturnMessageDto,
BuildReturnMessageDto,
GetAppReturnMessageDto,
ReturnMessageDto,
StringReturnMessageDto,
} from './dto/ReturnMessage.dto';
import { FetchMyCardUser, MyCardUser } from './utility/mycard-auth';
import { AppsJson } from './utility/apps-json-type';
import { MyCardAppMaintainerGuard } from './my-card-app-maintainer.guard';
......@@ -14,6 +33,7 @@ import { Stream } from 'stream';
import { PackageResult } from './dto/PackageResult.dto';
import { platform } from 'os';
import AppClass = AppsJson.AppClass;
import { DepotDto } from './dto/Depot.dto';
@Controller('api')
export class AppController {
......@@ -70,27 +90,41 @@ export class AppController {
}
}
@Post('build/:id/:platform/:locale/:version')
@Delete('build/:id/:version')
@ApiOperation({
summary: '删除打包',
description: '删除的打包会被彻底删除',
})
@ApiParam({ name: 'id', description: 'APP 的 id' })
@ApiParam({ name: 'version', description: 'APP 的版本号' })
@ApiQuery({ type: DepotDto, description: 'APP 的类型' })
async removeBuild(
@FetchMyCardUser() user: MyCardUser,
@Param('id') id: string,
@Query(new ValidationPipe({ transform: true })) depot: DepotDto,
@Param('version') version: string
): Promise<BlankReturnMessageDto> {
return this.appService.removeBuild(user, id, depot, version);
}
@Post('build/:id/:version')
@ApiOperation({
summary: '打包文件',
description: '必须登录用户且必须是管理员或者拥有1个 app 才能上传',
})
@ApiConsumes('multipart/form-data')
@ApiParam({ name: 'id', description: 'APP 的 id' })
@ApiParam({ name: 'platform', description: 'APP 的 版本号', enum: AppsJson.Platform })
@ApiParam({ name: 'locale', description: 'APP 的 版本号', enum: AppsJson.Locale })
@ApiParam({ name: 'version', description: 'APP 的 版本号' })
@ApiParam({ name: 'version', description: 'APP 的版本号' })
@ApiQuery({ type: DepotDto, description: 'APP 的类型' })
@ApiBody({
description: 'app 的 tar.gz 文件',
type: FileUploadDto,
})
@ApiCreatedResponse({ type: BlankReturnMessageDto })
@ApiCreatedResponse({ type: BuildReturnMessageDto })
async makeBuild(
@FetchMyCardUser() user: MyCardUser,
//@UploadedFile() file: Express.Multer.File,
@Param('id') id: string,
@Param('platform') platform: AppsJson.Platform,
@Param('locale') locale: AppsJson.Locale,
@Query(new ValidationPipe({ transform: true })) depot: DepotDto,
@Param('version') version: string,
@Req() req: Request
) {
......@@ -111,7 +145,7 @@ export class AppController {
// console.log(`got file ${fieldname}`);
const stream = new Stream.Readable().wrap(fileStream);
try {
resolve(await this.appService.makeBuild(user, stream, id, platform, locale, version));
resolve(await this.appService.makeBuild(user, stream, id, depot, version));
} catch (e) {
stream.destroy();
reject(e);
......
......@@ -7,15 +7,23 @@ import { BlankReturnMessageDto, ReturnMessageDto } from './dto/ReturnMessage.dto
import { MyCardUser } from './utility/mycard-auth';
import { PackagerService } from './packager/packager.service';
import internal from 'stream';
import { Depot } from './entities/Depot.entity';
import { DepotDto } from './dto/Depot.dto';
import { Build } from './entities/Build.entity';
import { ConfigService } from '@nestjs/config';
import { Archive } from './entities/Archive.entity';
@Injectable()
export class AppService extends ConsoleLogger {
private readonly packageVersionTraceCount: number;
constructor(
@InjectConnection('app')
private db: Connection,
private packager: PackagerService
private packager: PackagerService,
config: ConfigService
) {
super('app');
this.packageVersionTraceCount = parseInt(config.get('PACKAGE_VERSION_TRACE_COUNT')) || 5;
}
async getAppsJson() {
......@@ -132,14 +140,26 @@ export class AppService extends ConsoleLogger {
}, 201);
}
async makeBuild(
user: MyCardUser,
stream: internal.Readable,
id: string,
platform: AppsJson.Platform,
locale: AppsJson.Locale,
version: string
) {
async getOrCreateDepot(app: App, depotDto: DepotDto) {
const depotOption = depotDto.toActual;
let depot = await this.db.getRepository(Depot).findOne({ where: { app, ...depotOption } });
if (!depot) {
depot = new Depot();
depot.app = app;
depot.platform = depotOption.platform;
depot.locale = depotOption.locale;
depot.arch = depotOption.arch;
depot.builds = [];
depot = await this.db.getRepository(Depot).save(depot);
}
return depot;
}
async checkExistingBuild(depot: Depot, version: string) {
return this.db.getRepository(Build).findOne({ where: { depot, version }, select: ['id'] });
}
async makeBuild(user: MyCardUser, stream: internal.Readable, id: string, depotDto: DepotDto, version: string) {
if (!user) {
throw new BlankReturnMessageDto(401, 'Needs login').toException();
}
......@@ -154,9 +174,54 @@ export class AppService extends ConsoleLogger {
if (!app.isUserCanEditApp(user)) {
throw new BlankReturnMessageDto(403, 'Permission denied').toException();
}
const depot = await this.getOrCreateDepot(app, depotDto);
if (await this.checkExistingBuild(depot, version)) {
throw new BlankReturnMessageDto(404, 'Build exists').toException();
}
const build = new Build();
build.depot = depot;
build.version = version;
this.log(`Start packaging ${app.id}.`);
const result = await this.packager.build(stream, app.packagePrefix);
return new ReturnMessageDto(201, 'success', result);
try {
const previousTracingBuildChecksums = (
await this.db
.getRepository(Build)
.find({ where: { depot }, order: { id: 'DESC' }, select: ['id', 'checksum'], take: this.packageVersionTraceCount })
).map((b) => b.checksum);
const result = await this.packager.build(stream, app.packagePrefix, previousTracingBuildChecksums);
build.checksum = result.checksum;
build.archives = result.archives;
return new ReturnMessageDto(201, 'success', await this.db.getRepository(Build).save(build));
} catch (e) {
this.error(`Build ${app.id} ${JSON.stringify(depotDto.toActual)} ${build.version} failed: ${e.toString()}`);
throw new BlankReturnMessageDto(500, 'Build failed').toException();
}
}
async removeBuild(user: MyCardUser, id: string, depotDto: DepotDto, version: string) {
if (!user) {
throw new BlankReturnMessageDto(401, 'Needs login').toException();
}
const app = await this.db.getRepository(App).findOne({
where: { id },
select: ['id', 'author'],
});
if (!app) {
throw new BlankReturnMessageDto(404, 'App not found').toException();
}
if (!app.isUserCanEditApp(user)) {
throw new BlankReturnMessageDto(403, 'Permission denied').toException();
}
const depot = await this.getOrCreateDepot(app, depotDto);
const build = await this.checkExistingBuild(depot, version);
if (!build) {
throw new BlankReturnMessageDto(404, 'Build not found').toException();
}
await this.db.transaction(async (edb) => {
await edb.getRepository(Archive).delete({ build });
await edb.getRepository(Build).delete(build);
});
return new BlankReturnMessageDto(200, 'success');
}
async deleteApp(id: string) {
......
import { AppsJson } from '../utility/apps-json-type';
import Platform = AppsJson.Platform;
import Locale = AppsJson.Locale;
import { ApiParam, ApiProperty } from '@nestjs/swagger';
export interface DepotLike {
platform?: string;
arch?: string;
locale?: string;
}
export class DepotDto implements DepotLike {
@ApiProperty({ description: 'APP 的平台', enum: AppsJson.Platform })
platform?: string;
@ApiProperty({ description: 'APP 的 arch' })
arch?: string;
@ApiProperty({ description: 'APP 的语言', enum: AppsJson.Locale })
locale?: string;
get toActual(): DepotLike {
return {
platform: this.platform || 'generic',
arch: this.arch || 'generic',
locale: this.locale || 'generic',
};
}
}
import { ApiProperty } from '@nestjs/swagger';
import { HttpException } from '@nestjs/common';
import { AppsJson } from '../utility/apps-json-type';
import { Build } from '../entities/Build.entity';
export class BlankReturnMessageDto {
@ApiProperty({ description: '返回状态' })
......@@ -55,3 +56,8 @@ export class UploadAssignInfoReturnMessageDto extends BlankReturnMessageDto {
@ApiProperty({ description: '返回内容' })
data?: UploadAssignInfo;
}
export class BuildReturnMessageDto extends BlankReturnMessageDto {
@ApiProperty({ description: '返回内容' })
data?: Build;
}
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { Depot } from './Depot.entity';
import { Archive } from './Archive.entity';
import { Index } from 'typeorm';
import { TimeBase } from './TimeBase.entity';
@Entity()
@Index((b) => [b.depot, b.version])
export class Build extends TimeBase {
@PrimaryGeneratedColumn()
id: number;
......@@ -13,10 +14,10 @@ export class Build extends TimeBase {
@Column()
version: string;
@ManyToOne((type) => Depot, (depot) => depot.builds)
@ManyToOne(() => Depot, (depot) => depot.builds)
depot: Depot;
@OneToMany((type) => Archive, (archive) => archive.build)
@OneToMany(() => Archive, (archive) => archive.build, { cascade: true })
archives: Archive[];
@Column({ type: 'hstore', hstoreType: 'object' })
......
......@@ -2,10 +2,11 @@ import { Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } f
import { App } from './App.entity';
import { Build } from './Build.entity';
import { TimeBase } from './TimeBase.entity';
import { DepotLike } from '../dto/Depot.dto';
@Index((d) => [d.app, d.locale, d.platform, d.arch], { unique: true })
@Entity()
export class Depot extends TimeBase {
export class Depot extends TimeBase implements DepotLike {
@PrimaryGeneratedColumn()
id: number;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment