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 { AppService } from './app.service';
import { ApiBody, ApiConsumes, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger'; 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 { FetchMyCardUser, MyCardUser } from './utility/mycard-auth';
import { AppsJson } from './utility/apps-json-type'; import { AppsJson } from './utility/apps-json-type';
import { MyCardAppMaintainerGuard } from './my-card-app-maintainer.guard'; import { MyCardAppMaintainerGuard } from './my-card-app-maintainer.guard';
...@@ -14,6 +33,7 @@ import { Stream } from 'stream'; ...@@ -14,6 +33,7 @@ import { Stream } from 'stream';
import { PackageResult } from './dto/PackageResult.dto'; import { PackageResult } from './dto/PackageResult.dto';
import { platform } from 'os'; import { platform } from 'os';
import AppClass = AppsJson.AppClass; import AppClass = AppsJson.AppClass;
import { DepotDto } from './dto/Depot.dto';
@Controller('api') @Controller('api')
export class AppController { export class AppController {
...@@ -70,27 +90,41 @@ 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({ @ApiOperation({
summary: '打包文件', summary: '打包文件',
description: '必须登录用户且必须是管理员或者拥有1个 app 才能上传', description: '必须登录用户且必须是管理员或者拥有1个 app 才能上传',
}) })
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiParam({ name: 'id', description: 'APP 的 id' }) @ApiParam({ name: 'id', description: 'APP 的 id' })
@ApiParam({ name: 'platform', description: 'APP 的 版本号', enum: AppsJson.Platform }) @ApiParam({ name: 'version', description: 'APP 的版本号' })
@ApiParam({ name: 'locale', description: 'APP 的 版本号', enum: AppsJson.Locale }) @ApiQuery({ type: DepotDto, description: 'APP 的类型' })
@ApiParam({ name: 'version', description: 'APP 的 版本号' })
@ApiBody({ @ApiBody({
description: 'app 的 tar.gz 文件', description: 'app 的 tar.gz 文件',
type: FileUploadDto, type: FileUploadDto,
}) })
@ApiCreatedResponse({ type: BlankReturnMessageDto }) @ApiCreatedResponse({ type: BuildReturnMessageDto })
async makeBuild( async makeBuild(
@FetchMyCardUser() user: MyCardUser, @FetchMyCardUser() user: MyCardUser,
//@UploadedFile() file: Express.Multer.File,
@Param('id') id: string, @Param('id') id: string,
@Param('platform') platform: AppsJson.Platform, @Query(new ValidationPipe({ transform: true })) depot: DepotDto,
@Param('locale') locale: AppsJson.Locale,
@Param('version') version: string, @Param('version') version: string,
@Req() req: Request @Req() req: Request
) { ) {
...@@ -111,7 +145,7 @@ export class AppController { ...@@ -111,7 +145,7 @@ export class AppController {
// console.log(`got file ${fieldname}`); // console.log(`got file ${fieldname}`);
const stream = new Stream.Readable().wrap(fileStream); const stream = new Stream.Readable().wrap(fileStream);
try { try {
resolve(await this.appService.makeBuild(user, stream, id, platform, locale, version)); resolve(await this.appService.makeBuild(user, stream, id, depot, version));
} catch (e) { } catch (e) {
stream.destroy(); stream.destroy();
reject(e); reject(e);
......
...@@ -7,15 +7,23 @@ import { BlankReturnMessageDto, ReturnMessageDto } from './dto/ReturnMessage.dto ...@@ -7,15 +7,23 @@ import { BlankReturnMessageDto, ReturnMessageDto } from './dto/ReturnMessage.dto
import { MyCardUser } from './utility/mycard-auth'; import { MyCardUser } from './utility/mycard-auth';
import { PackagerService } from './packager/packager.service'; import { PackagerService } from './packager/packager.service';
import internal from 'stream'; 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() @Injectable()
export class AppService extends ConsoleLogger { export class AppService extends ConsoleLogger {
private readonly packageVersionTraceCount: number;
constructor( constructor(
@InjectConnection('app') @InjectConnection('app')
private db: Connection, private db: Connection,
private packager: PackagerService private packager: PackagerService,
config: ConfigService
) { ) {
super('app'); super('app');
this.packageVersionTraceCount = parseInt(config.get('PACKAGE_VERSION_TRACE_COUNT')) || 5;
} }
async getAppsJson() { async getAppsJson() {
...@@ -132,14 +140,26 @@ export class AppService extends ConsoleLogger { ...@@ -132,14 +140,26 @@ export class AppService extends ConsoleLogger {
}, 201); }, 201);
} }
async makeBuild( async getOrCreateDepot(app: App, depotDto: DepotDto) {
user: MyCardUser, const depotOption = depotDto.toActual;
stream: internal.Readable, let depot = await this.db.getRepository(Depot).findOne({ where: { app, ...depotOption } });
id: string, if (!depot) {
platform: AppsJson.Platform, depot = new Depot();
locale: AppsJson.Locale, depot.app = app;
version: string 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) { if (!user) {
throw new BlankReturnMessageDto(401, 'Needs login').toException(); throw new BlankReturnMessageDto(401, 'Needs login').toException();
} }
...@@ -154,9 +174,54 @@ export class AppService extends ConsoleLogger { ...@@ -154,9 +174,54 @@ export class AppService extends ConsoleLogger {
if (!app.isUserCanEditApp(user)) { if (!app.isUserCanEditApp(user)) {
throw new BlankReturnMessageDto(403, 'Permission denied').toException(); 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}.`); this.log(`Start packaging ${app.id}.`);
const result = await this.packager.build(stream, app.packagePrefix); try {
return new ReturnMessageDto(201, 'success', result); 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) { 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 { ApiProperty } from '@nestjs/swagger';
import { HttpException } from '@nestjs/common'; import { HttpException } from '@nestjs/common';
import { AppsJson } from '../utility/apps-json-type'; import { AppsJson } from '../utility/apps-json-type';
import { Build } from '../entities/Build.entity';
export class BlankReturnMessageDto { export class BlankReturnMessageDto {
@ApiProperty({ description: '返回状态' }) @ApiProperty({ description: '返回状态' })
...@@ -55,3 +56,8 @@ export class UploadAssignInfoReturnMessageDto extends BlankReturnMessageDto { ...@@ -55,3 +56,8 @@ export class UploadAssignInfoReturnMessageDto extends BlankReturnMessageDto {
@ApiProperty({ description: '返回内容' }) @ApiProperty({ description: '返回内容' })
data?: UploadAssignInfo; 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 { Depot } from './Depot.entity';
import { Archive } from './Archive.entity'; import { Archive } from './Archive.entity';
import { Index } from 'typeorm'; import { Index } from 'typeorm';
import { TimeBase } from './TimeBase.entity'; import { TimeBase } from './TimeBase.entity';
@Entity() @Entity()
@Index((b) => [b.depot, b.version])
export class Build extends TimeBase { export class Build extends TimeBase {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
...@@ -13,10 +14,10 @@ export class Build extends TimeBase { ...@@ -13,10 +14,10 @@ export class Build extends TimeBase {
@Column() @Column()
version: string; version: string;
@ManyToOne((type) => Depot, (depot) => depot.builds) @ManyToOne(() => Depot, (depot) => depot.builds)
depot: Depot; depot: Depot;
@OneToMany((type) => Archive, (archive) => archive.build) @OneToMany(() => Archive, (archive) => archive.build, { cascade: true })
archives: Archive[]; archives: Archive[];
@Column({ type: 'hstore', hstoreType: 'object' }) @Column({ type: 'hstore', hstoreType: 'object' })
......
...@@ -2,10 +2,11 @@ import { Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } f ...@@ -2,10 +2,11 @@ import { Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } f
import { App } from './App.entity'; import { App } from './App.entity';
import { Build } from './Build.entity'; import { Build } from './Build.entity';
import { TimeBase } from './TimeBase.entity'; import { TimeBase } from './TimeBase.entity';
import { DepotLike } from '../dto/Depot.dto';
@Index((d) => [d.app, d.locale, d.platform, d.arch], { unique: true }) @Index((d) => [d.app, d.locale, d.platform, d.arch], { unique: true })
@Entity() @Entity()
export class Depot extends TimeBase { export class Depot extends TimeBase implements DepotLike {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; 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