Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(store-service): assets store service support #762

Merged
merged 15 commits into from
Apr 29, 2023
2 changes: 2 additions & 0 deletions apps/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ConfigPublicModule } from './modules/configs/configs.module';
import { REDIS_TRANSPORTER } from '~/shared/constants/transporter.constants';
import { ConsoleModule } from './modules/console/console.module';
import { ThemesModule } from './modules/themes/themes.module';
import { StoreModule } from './modules/store/store.module';

@Module({
imports: [
Expand All @@ -41,6 +42,7 @@ import { ThemesModule } from './modules/themes/themes.module';
ConfigPublicModule,
ConsoleModule,
ThemesModule,
StoreModule,
ClientsModule.register([
{
name: ServicesEnum.notification,
Expand Down
2 changes: 0 additions & 2 deletions apps/core/src/common/adapt/fastify.adapt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ const app: FastifyAdapter = new FastifyAdapter({
});
export { app as fastifyApp };

// @ts-ignore
// HACK: There is nothing wrong during runtime, but the type is wrong. I don't know why.
app.register(FastifyMultipart, {
limits: {
fields: 10, // Max number of non-file fields
Expand Down
127 changes: 127 additions & 0 deletions apps/core/src/modules/store/store.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
Body,
Controller,
Get,
Inject,
Param,
Post,
Req,
Res,
} from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { ApiOperation } from '@nestjs/swagger';
import { FastifyReply, FastifyRequest } from 'fastify';
import { Auth } from '~/shared/common/decorator/auth.decorator';
import { HTTPDecorators } from '~/shared/common/decorator/http.decorator';
import { ApiName } from '~/shared/common/decorator/openapi.decorator';
import { StoreEvents } from '~/shared/constants/event.constant';
import { ServicesEnum } from '~/shared/constants/services.constant';
import { BadRequestRpcExcption } from '~/shared/exceptions/bad-request-rpc-exception';
import { transportReqToMicroservice } from '~/shared/microservice.transporter';

@Controller('store')
@ApiName
export class StoreController {
constructor(
@Inject(ServicesEnum.store) private readonly store: ClientProxy,
) {}

@Get('/ping')
@ApiOperation({ summary: '检测服务是否在线' })
ping() {
return transportReqToMicroservice(this.store, StoreEvents.Ping, {});
}

@Get(['/list/*', '/list'])
@Auth()
@ApiOperation({ summary: '获取文件列表' })
list(@Param('*') path?: string) {
return transportReqToMicroservice(
this.store,
StoreEvents.StoreFileGetList,
path || '',
);
}

@Get('/raw/*')
@ApiOperation({ summary: '获取文件' })
async raw(@Param('*') path: string, @Res() res: FastifyReply) {
const data = await transportReqToMicroservice<{
file: Buffer;
name: string;
ext: string;
mimetype: string;
}>(this.store, StoreEvents.StoreFileGet, path);
if (data.mimetype) {
res.type(data.mimetype);
res.header('cache-control', 'public, max-age=31536000');
res.header(
'expires',
new Date(Date.now() + 31536000 * 1000).toUTCString(),
);
}
const buffer = Buffer.from(data.file);
res.send(buffer);
}

@Post('/download')
@ApiOperation({ summary: '从远端下载文件' })
@Auth()
download(@Body('url') url: string, @Body('path') path?: string) {
return transportReqToMicroservice(
this.store,
StoreEvents.StoreFileDownloadFromRemote,
{ url, path },
);
}

@Post('/upload')
@ApiOperation({ summary: '上传文件' })
// @Auth()
@HTTPDecorators.FileUpload({ description: 'upload file' })
async upload(@Req() req: FastifyRequest, @Body('path') _path?: string) {
const data = await req.file();

if (!data) {
throw new BadRequestRpcExcption('仅能上传文件!');
}
if (data.fieldname != 'file') {
throw new BadRequestRpcExcption('字段必须为 file');
}

const filename = data.filename;
return transportReqToMicroservice(
this.store,
StoreEvents.StoreFileUploadByMaster,
{
file: {
filename,
file: await data.toBuffer(),
},
path: _path,
},
);
}

@Post('/delete')
@ApiOperation({ summary: '删除文件' })
@Auth()
delete(@Body('path') path: string) {
return transportReqToMicroservice(
this.store,
StoreEvents.StoreFileDeleteByMaster,
{ path },
);
}

@Post('/mkdir')
@ApiOperation({ summary: '创建文件夹' })
@Auth()
mkdir(@Body('path') path: string) {
return transportReqToMicroservice(
this.store,
StoreEvents.StoreFileMkdirByMaster,
{ path },
);
}
}
18 changes: 18 additions & 0 deletions apps/core/src/modules/store/store.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { ClientsModule } from '@nestjs/microservices';
import { ServicesEnum } from '~/shared/constants/services.constant';
import { REDIS_TRANSPORTER } from '~/shared/constants/transporter.constants';
import { StoreController } from './store.controller';

@Module({
imports: [
ClientsModule.register([
{
name: ServicesEnum.store,
...REDIS_TRANSPORTER,
},
]),
],
controllers: [StoreController],
})
export class StoreModule {}
35 changes: 35 additions & 0 deletions apps/store-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { REDIS } from '~/apps/core/src/app.config';
import { BasicCommer } from '~/shared/commander';
import { registerStdLogger } from '~/shared/global/consola.global';
import { mkStoreDir, registerGlobal } from '~/shared/global/index.global';
import { readEnv } from '~/shared/utils/rag-env';
import { StoreServiceModule } from './store-service.module';

async function bootstrap() {
registerGlobal();
registerStdLogger("store");
mkStoreDir();

const argv = BasicCommer.parse().opts();
readEnv(argv, argv.config);

const app = await NestFactory.createMicroservice<MicroserviceOptions>(
StoreServiceModule,
{
transport: Transport.REDIS,
options: {
port: REDIS.port,
host: REDIS.host,
password: REDIS.password,
username: REDIS.user,
},
},
);
app.listen().then(() => {
Logger.log(`>> StoreService 正在工作... <<`)
})
}
bootstrap();
52 changes: 52 additions & 0 deletions apps/store-service/src/store-service.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { StoreEvents } from '~/shared/constants/event.constant';
import { StoreServiceService } from './store-service.service';

@Controller()
export class StoreServiceController {
constructor(private readonly storeServiceService: StoreServiceService) {}

@MessagePattern({ cmd: StoreEvents.Ping })
ping() {
return true;
}

@MessagePattern({ cmd: StoreEvents.StoreFileUploadByMaster })
async storeFileUpload(data: {
file: {
filename: string;
file: Buffer;
};
path?: string;
}) {
data.file.file = Buffer.from(data.file.file);
return await this.storeServiceService.storeFile(data.file, data.path);
}

@MessagePattern({ cmd: StoreEvents.StoreFileDownloadFromRemote })
async storeFileDownloadFromRemote(data: { url: string; path?: string }) {
return await this.storeServiceService.downloadFile(data.url, data.path);
}

@MessagePattern({ cmd: StoreEvents.StoreFileDeleteByMaster })
async storeFileDelete(path: string) {
return await this.storeServiceService.deleteFile(path);
}

@MessagePattern({ cmd: StoreEvents.StoreFileGet })
async storeFileGet(path: string) {
return await this.storeServiceService.getFile(path);
}

@MessagePattern({ cmd: StoreEvents.StoreFileGetList })
async storeFileList(path?: string) {
return await this.storeServiceService.getFileList(path);
}

@MessagePattern({ cmd: StoreEvents.StoreFileMkdirByMaster })
async storeFileMkdir(path: string) {
return await this.storeServiceService.mkdir(path);
}

}
11 changes: 11 additions & 0 deletions apps/store-service/src/store-service.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { HelperModule } from '~/libs/helper/src';
import { StoreServiceController } from './store-service.controller';
import { StoreServiceService } from './store-service.service';

@Module({
imports: [HelperModule],
controllers: [StoreServiceController],
providers: [StoreServiceService],
})
export class StoreServiceModule {}
66 changes: 66 additions & 0 deletions apps/store-service/src/store-service.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import { join } from 'path';
import { AssetsService } from '~/libs/helper/src/helper.assets.service';
import { STORE_DIR } from '~/shared/constants/path.constant';
import { InternalServerErrorRpcExcption } from '~/shared/exceptions/internal-server-error-rpc-exception';

@Injectable()
export class StoreServiceService {
constructor(private readonly assetHelper: AssetsService) {}

async storeFile(
data: {
filename: string;
file: Buffer;
},
path?: string,
) {
const _path = join(STORE_DIR, path || '');
const name = data.filename;
if (this.assetHelper.exists(join(_path, name))) {
throw new InternalServerErrorRpcExcption('文件夹已存在');
}
return await this.assetHelper
.writeFile(data.file, _path, name)
.catch((e) => {
console.log(e);
throw new InternalServerErrorRpcExcption(e);
});
}

async downloadFile(url: string, path?: string) {
const _path = join(STORE_DIR, path || '');
await this.assetHelper.downloadFile(url, _path).catch((e) => {
throw new InternalServerErrorRpcExcption(e);
});
return true;
}

async deleteFile(path: string) {
await this.assetHelper.deleteFile(path).catch((e) => {
throw new InternalServerErrorRpcExcption(e);
});
return true;
}

async getFile(path: string) {
const _path = join(STORE_DIR, path || '');
return await this.assetHelper.getFile(_path).catch((e) => {
throw new InternalServerErrorRpcExcption(e);
});
}

async getFileList(path?: string) {
const _path = join(STORE_DIR, path || '');
return await this.assetHelper.getFileList(_path).catch((e) => {
throw new InternalServerErrorRpcExcption(e);
});
}

async mkdir(path: string) {
const _path = join(STORE_DIR, path || '');
return await this.assetHelper.mkdir(_path).catch((e) => {
throw new InternalServerErrorRpcExcption(e);
});
}
}
9 changes: 9 additions & 0 deletions apps/store-service/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/store-service"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
2 changes: 1 addition & 1 deletion apps/themes-service/src/themes-service.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ export class ThemesServiceService {
}
fs.renameSync(join(_path, `${_theme}.bak`), join(_path, _theme));
consola.info(`主题 ${chalk.green(id)} 更新失败, 正在回滚`);
throw e;
throw new InternalServerErrorException(e);
});
this.refreshThemes();
this.reloadConfig(id);
Expand Down
Loading