feat: backup module

This commit is contained in:
Innei
2021-09-07 14:33:41 +08:00
parent 7c65bf637d
commit bad54a2129
26 changed files with 993 additions and 69 deletions

3
global.d.ts vendored
View File

@@ -1,10 +1,13 @@
import { ModelType } from '@typegoose/typegoose/lib/types' import { ModelType } from '@typegoose/typegoose/lib/types'
import { Document, PaginateModel } from 'mongoose' import { Document, PaginateModel } from 'mongoose'
import 'zx/globals'
declare global { declare global {
export type KV<T = any> = Record<string, T> export type KV<T = any> = Record<string, T>
// @ts-ignore // @ts-ignore
export type MongooseModel<T> = ModelType<T> & PaginateModel<T & Document> export type MongooseModel<T> = ModelType<T> & PaginateModel<T & Document>
export const isDev: boolean
} }
export {} export {}

View File

@@ -60,13 +60,16 @@
"chalk": "*", "chalk": "*",
"class-transformer": "^0.4.0", "class-transformer": "^0.4.0",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"cos-nodejs-sdk-v5": "2.10.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"dotenv": "*", "dotenv": "*",
"ejs": "^3.1.6", "ejs": "^3.1.6",
"fastify-multipart": "^4.0.7",
"fastify-swagger": "^4.9.0", "fastify-swagger": "^4.9.0",
"image-size": "^1.0.0", "image-size": "^1.0.0",
"inquirer": "*", "inquirer": "*",
"lodash": "*", "lodash": "*",
"mkdirp": "*",
"mongoose": "*", "mongoose": "*",
"mongoose-lean-id": "^0.2.0", "mongoose-lean-id": "^0.2.0",
"mongoose-lean-virtuals": "^0.8.0", "mongoose-lean-virtuals": "^0.8.0",
@@ -82,9 +85,11 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.3.0", "rxjs": "^7.3.0",
"snakecase-keys": "^4.0.2", "snakecase-keys": "^4.0.2",
"ua-parser-js": "^0.7.28" "ua-parser-js": "^0.7.28",
"zx": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^3.0.2",
"@innei-util/eslint-config-ts": "^0.2.3", "@innei-util/eslint-config-ts": "^0.2.3",
"@innei-util/prettier": "^0.1.3", "@innei-util/prettier": "^0.1.3",
"@nestjs/cli": "^8.1.1", "@nestjs/cli": "^8.1.1",
@@ -111,7 +116,6 @@
"jest": "27.1.0", "jest": "27.1.0",
"lint-staged": "^11.1.2", "lint-staged": "^11.1.2",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"rimraf": "^3.0.2",
"run-script-webpack-plugin": "^0.0.11", "run-script-webpack-plugin": "^0.0.11",
"socket.io": "*", "socket.io": "*",
"supertest": "^6.1.6", "supertest": "^6.1.6",

BIN
paw.paw

Binary file not shown.

522
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ import { SecurityMiddleware } from './common/middlewares/security.middleware'
import { AnalyzeModule } from './modules/analyze/analyze.module' import { AnalyzeModule } from './modules/analyze/analyze.module'
import { AuthModule } from './modules/auth/auth.module' import { AuthModule } from './modules/auth/auth.module'
import { RolesGuard } from './modules/auth/roles.guard' import { RolesGuard } from './modules/auth/roles.guard'
import { BackupModule } from './modules/backup/backup.module'
import { CategoryModule } from './modules/category/category.module' import { CategoryModule } from './modules/category/category.module'
import { CommentModule } from './modules/comment/comment.module' import { CommentModule } from './modules/comment/comment.module'
import { ConfigsModule } from './modules/configs/configs.module' import { ConfigsModule } from './modules/configs/configs.module'
@@ -55,6 +56,7 @@ import { HelperModule } from './processors/helper/helper.module'
AnalyzeModule, AnalyzeModule,
AuthModule, AuthModule,
BackupModule,
CategoryModule, CategoryModule,
CommentModule, CommentModule,
ConfigsModule, ConfigsModule,

View File

@@ -1,9 +1,17 @@
import { FastifyAdapter } from '@nestjs/platform-fastify' import { FastifyAdapter } from '@nestjs/platform-fastify'
import FastifyMultipart from 'fastify-multipart'
export const fastifyApp: FastifyAdapter = new FastifyAdapter({ export const fastifyApp: FastifyAdapter = new FastifyAdapter({
trustProxy: true, trustProxy: true,
}) })
fastifyApp.register(FastifyMultipart, {
limits: {
fields: 10, // Max number of non-file fields
fileSize: 1024 * 1024 * 6, // limit size 6M
files: 5, // Max number of file fields
},
})
fastifyApp.getInstance().addHook('onRequest', (request, reply, done) => { fastifyApp.getInstance().addHook('onRequest', (request, reply, done) => {
const origin = request.headers.origin const origin = request.headers.origin
if (!origin) { if (!origin) {

View File

@@ -1,6 +1,8 @@
import { SetMetadata } from '@nestjs/common' import { applyDecorators, SetMetadata } from '@nestjs/common'
import { ApiBody, ApiConsumes } from '@nestjs/swagger'
import { HTTP_RES_TRANSFORM_PAGINATE } from '~/constants/meta.constant' import { HTTP_RES_TRANSFORM_PAGINATE } from '~/constants/meta.constant'
import * as SYSTEM from '~/constants/system.constant'
import { FileUploadDto } from '~/shared/dto/file.dto'
export const Paginator: MethodDecorator = ( export const Paginator: MethodDecorator = (
target, target,
key, key,
@@ -8,3 +10,34 @@ export const Paginator: MethodDecorator = (
) => { ) => {
SetMetadata(HTTP_RES_TRANSFORM_PAGINATE, true)(descriptor.value) SetMetadata(HTTP_RES_TRANSFORM_PAGINATE, true)(descriptor.value)
} }
/**
* @description 跳过响应体处理
*/
export const Bypass: MethodDecorator = (
target,
key,
descriptor: PropertyDescriptor,
) => {
SetMetadata(SYSTEM.RESPONSE_PASSTHROUGH_METADATA, true)(descriptor.value)
}
export declare interface FileDecoratorProps {
description: string
}
export function FileUpload({ description }: FileDecoratorProps) {
return applyDecorators(
ApiConsumes('multipart/form-data'),
ApiBody({
description,
type: FileUploadDto,
}),
)
}
export const HTTPDecorators = {
Paginator,
Bypass,
FileUpload,
}

View File

@@ -1,12 +1,8 @@
/* /**
* @Author: Innei * @module common/guard/spider.guard
* @Date: 2020-04-30 19:09:37 * @description 禁止爬虫的守卫
* @LastEditTime: 2020-07-08 21:35:06 * @author Innei <https://innei.ren>
* @LastEditors: Innei
* @FilePath: /mx-server/src/core/guards/spider.guard.ts
* @Coding with Love
*/ */
import { import {
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,

View File

@@ -3,6 +3,7 @@
* @file 缓存拦截器 * @file 缓存拦截器
* @module interceptor/cache * @module interceptor/cache
* @author Surmon <https://github.com/surmon-china> * @author Surmon <https://github.com/surmon-china>
* @author Innei <https://innei.ren>
*/ */
import { import {

View File

@@ -16,8 +16,8 @@ import { Observable } from 'rxjs'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
import snakecaseKeys from 'snakecase-keys' import snakecaseKeys from 'snakecase-keys'
import { HTTP_RES_TRANSFORM_PAGINATE } from '~/constants/meta.constant' import { HTTP_RES_TRANSFORM_PAGINATE } from '~/constants/meta.constant'
import * as SYSTEM from '~/constants/system.constant'
import { Paginator } from '~/shared/model/base.model' import { Paginator } from '~/shared/model/base.model'
export interface Response<T> { export interface Response<T> {
data: T data: T
} }
@@ -29,6 +29,17 @@ export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
context: ExecutionContext, context: ExecutionContext,
next: CallHandler, next: CallHandler,
): Observable<Response<T>> { ): Observable<Response<T>> {
const handler = context.getHandler()
// 跳过 bypass 装饰的请求
const bypass = this.reflector.get<boolean>(
SYSTEM.RESPONSE_PASSTHROUGH_METADATA,
handler,
)
if (bypass) {
return next.handle()
}
const reorganize = (data) => { const reorganize = (data) => {
if (!data) { if (!data) {
throw new UnprocessableEntityException('数据丢失了(。 ́︿ ̀。)') throw new UnprocessableEntityException('数据丢失了(。 ́︿ ̀。)')
@@ -37,13 +48,12 @@ export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
? { ...data } ? { ...data }
: { data } : { data }
} }
const handler = context.getHandler()
return next.handle().pipe( return next.handle().pipe(
map((data) => { map((data) => {
if (typeof data === 'undefined') { if (typeof data === 'undefined') {
context.switchToHttp().getResponse().status(204) context.switchToHttp().getResponse().status(204)
return return data
} }
// 分页转换 // 分页转换
if (this.reflector.get(HTTP_RES_TRANSFORM_PAGINATE, handler)) { if (this.reflector.get(HTTP_RES_TRANSFORM_PAGINATE, handler)) {

View File

@@ -5,7 +5,7 @@ import { IncomingMessage, ServerResponse } from 'http'
import { InjectModel } from 'nestjs-typegoose' import { InjectModel } from 'nestjs-typegoose'
import { UAParser } from 'ua-parser-js' import { UAParser } from 'ua-parser-js'
import { RedisKeys } from '~/constants/cache.constant' import { RedisKeys } from '~/constants/cache.constant'
import { localBotListDataFilePath } from '~/constants/path.constant' import { LOCAL_BOT_LIST_DATA_FILE_PATH } from '~/constants/path.constant'
import { AnalyzeModel } from '~/modules/analyze/analyze.model' import { AnalyzeModel } from '~/modules/analyze/analyze.model'
import { OptionModel } from '~/modules/configs/configs.model' import { OptionModel } from '~/modules/configs/configs.model'
import { CacheService } from '~/processors/cache/cache.service' import { CacheService } from '~/processors/cache/cache.service'
@@ -40,7 +40,7 @@ export class AnalyzeMiddleware implements NestMiddleware {
try { try {
return this.pickPattern2Regexp( return this.pickPattern2Regexp(
JSON.parse( JSON.parse(
readFileSync(localBotListDataFilePath, { readFileSync(LOCAL_BOT_LIST_DATA_FILE_PATH, {
encoding: 'utf-8', encoding: 'utf-8',
}), }),
), ),

View File

@@ -12,4 +12,8 @@ export const DATA_DIR = isDev
export const LOGGER_DIR = join(DATA_DIR, 'log') export const LOGGER_DIR = join(DATA_DIR, 'log')
export const localBotListDataFilePath = join(DATA_DIR, 'bot_list.json') export const LOCAL_BOT_LIST_DATA_FILE_PATH = join(DATA_DIR, 'bot_list.json')
export const BACKUP_DIR = !isDev
? join(DATA_DIR, 'backup')
: join(TEMP_DIR, 'backup')

View File

@@ -1,2 +1,4 @@
export const HTTP_ADAPTER_HOST = 'HttpAdapterHost' export const HTTP_ADAPTER_HOST = 'HttpAdapterHost'
export const REFLECTOR = 'Reflector' export const REFLECTOR = 'Reflector'
export const RESPONSE_PASSTHROUGH_METADATA = '__responsePassthrough__'

View File

@@ -7,11 +7,13 @@ import { AppModule } from './app.module'
import { fastifyApp } from './common/adapt/fastify' import { fastifyApp } from './common/adapt/fastify'
import { SpiderGuard } from './common/guard/spider.guard' import { SpiderGuard } from './common/guard/spider.guard'
import { LoggingInterceptor } from './common/interceptors/logging.interceptor' import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
import { isDev } from './utils/index.util' import './utils/global.util'
// const PORT = parseInt(process.env.PORT) || 2333 import './zx.global-fix'
const PORT = 2333 const PORT = 2333
const APIVersion = 1 const APIVersion = 1
const Origin = CROSS_DOMAIN.allowedOrigins const Origin = CROSS_DOMAIN.allowedOrigins
declare const module: any declare const module: any
async function bootstrap() { async function bootstrap() {

View File

@@ -0,0 +1,106 @@
import {
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
Req,
Res,
Scope,
UnprocessableEntityException,
} from '@nestjs/common'
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'
import { FastifyReply, FastifyRequest } from 'fastify'
import { Readable } from 'stream'
import { Auth } from '~/common/decorator/auth.decorator'
import { HTTPDecorators } from '~/common/decorator/http.decorator'
import { ApiName } from '~/common/decorator/openapi.decorator'
import { CronService } from '~/processors/helper/helper.cron.service'
import { UploadService } from '~/processors/helper/helper.upload.service'
import { BackupService } from './backup.service'
@Controller({ path: 'backups', scope: Scope.REQUEST })
@ApiName
@Auth()
export class BackupController {
constructor(
private readonly backupService: BackupService,
private readonly uploadService: UploadService,
private readonly cronService: CronService,
) {}
@Get('/new')
@ApiResponseProperty({ type: 'string', format: 'binary' })
@HTTPDecorators.Bypass
async createNewBackup(@Res() res: FastifyReply) {
const buffer = await this.cronService.backupDB({ uploadCOS: false })
const stream = new Readable()
stream.push(buffer)
stream.push(null)
res
.header(
'Content-Disposition',
`attachment; filename="backup-${new Date().toISOString()}.zip"`,
)
.type('application/zip')
.send(stream)
}
@Get('/')
async get() {
return this.backupService.list()
}
@HTTPDecorators.Bypass
@Get('/:dirname')
async download(@Param('dirname') dirname: string, @Res() res: FastifyReply) {
res.send(this.backupService.getFileStream(dirname))
}
@Post(['/rollback/', '/'])
@ApiProperty({ description: '上传备份恢复' })
@HTTPDecorators.FileUpload({ description: 'Upload backup and restore' })
async uploadAndRestore(@Req() req: FastifyRequest) {
const data = await this.uploadService.validMultipartField(req)
const { mimetype } = data
if (mimetype !== 'application/zip') {
throw new UnprocessableEntityException('备份格式必须为 application/zip')
}
await this.backupService.saveTempBackupByUpload(await data.toBuffer())
return
}
@Patch(['/rollback/:dirname', '/:dirname'])
async rollback(@Param('dirname') dirname: string) {
if (!dirname) {
throw new UnprocessableEntityException('参数有误')
}
return await this.backupService.rollbackTo(dirname)
}
@Delete('/')
async deleteBackups(@Query('files') files: string) {
if (!files) {
return
}
const _files = files.split(',')
for await (const f of _files) {
await this.backupService.deleteBackup(f)
}
return
}
@Delete('/:filename')
async delete(@Param('filename') filename: string) {
if (!filename) {
return
}
await this.backupService.deleteBackup(filename)
return
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common'
import { BackupController } from './backup.controller'
import { BackupService } from './backup.service'
@Module({
controllers: [BackupController],
providers: [BackupService],
exports: [BackupService],
})
export class BackupModule {}

View File

@@ -0,0 +1,151 @@
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common'
import {
existsSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from 'fs'
import mkdirp from 'mkdirp'
import { resolve } from 'path'
import { join } from 'path/posix'
import { Readable } from 'stream'
import { BACKUP_DIR } from '~/constants/path.constant'
import { AdminEventsGateway } from '~/processors/gateway/admin/events.gateway'
import { EventTypes } from '~/processors/gateway/events.types'
import { getFolderSize } from '~/utils/system.util'
@Injectable()
export class BackupService {
private logger: Logger
constructor(private readonly adminGateway: AdminEventsGateway) {
this.logger = new Logger(BackupService.name)
}
async list() {
const backupPath = BACKUP_DIR
if (!existsSync(backupPath)) {
return []
}
const backupFilenames = readdirSync(backupPath)
const backups = []
for (const filename of backupFilenames) {
const path = resolve(backupPath, filename)
if (!statSync(path).isDirectory()) {
continue
}
backups.push({
filename,
path,
})
}
return Promise.all(
backups.map(async (item) => {
const { path } = item
const { stdout } = await getFolderSize(path)
delete item.path
return { ...item, size: stdout }
}),
)
}
getFileStream(dirname: string) {
const path = this.checkBackupExist(dirname)
const stream = new Readable()
stream.push(readFileSync(path))
stream.push(null)
return stream
}
checkBackupExist(dirname: string) {
const path = join(BACKUP_DIR, dirname, 'backup-' + dirname + '.zip')
if (!existsSync(path)) {
throw new BadRequestException('文件不存在')
}
return path
}
async saveTempBackupByUpload(buffer: Buffer) {
const tempDirPath = '/tmp/mx-space/backup'
const tempBackupPath = join(tempDirPath, 'backup.zip')
mkdirp.sync(tempDirPath)
writeFileSync(tempBackupPath, buffer)
try {
cd(tempDirPath)
await $`unzip backup.zip`
await $`mongorestore -h ${
process.env.DB_URL || '127.0.0.1'
} -d mx-space ./mx-space --drop >/dev/null 2>&1`
this.logger.debug('恢复成功')
await this.adminGateway.broadcast(
EventTypes.CONTENT_REFRESH,
'restore_done',
)
} catch (e) {
const logDir = '/tmp/mx-space/log'
mkdirp.sync(logDir)
writeFileSync(logDir, e.message, { encoding: 'utf-8', flag: 'a+' })
throw new InternalServerErrorException(e.message)
} finally {
rmSync(tempDirPath, { recursive: true })
}
}
async rollbackTo(dirname: string) {
const bakFilePath = this.checkBackupExist(dirname) // zip file path
const dirPath = join(BACKUP_DIR, dirname)
try {
if (existsSync(join(join(dirPath, 'mx-space')))) {
rmSync(join(dirPath, 'mx-space'), { recursive: true })
}
cd(dirPath)
await $`unzip ${bakFilePath}`
} catch {
throw new InternalServerErrorException('服务端 unzip 命令未找到')
}
try {
if (!existsSync(join(dirPath, 'mx-space'))) {
throw new InternalServerErrorException('备份文件错误, 目录不存在')
}
cd(dirPath)
await $`mongorestore -h ${
process.env.DB_URL || '127.0.0.1'
} -d mx-space ./mx-space --drop >/dev/null 2>&1`
} catch (e) {
this.logger.error(e)
throw e
} finally {
try {
rmSync(join(dirPath, 'mx-space'), { recursive: true })
} catch {}
}
await this.adminGateway.broadcast(
EventTypes.CONTENT_REFRESH,
'restore_done',
)
}
async deleteBackup(filename) {
const path = join(BACKUP_DIR, filename)
if (!existsSync(path)) {
throw new BadRequestException('文件不存在')
}
rmSync(path, { recursive: true })
return true
}
}

View File

@@ -1,15 +1,31 @@
import { Injectable, Logger } from '@nestjs/common' import { Injectable, Logger } from '@nestjs/common'
import { Cron, CronExpression } from '@nestjs/schedule' import { Cron, CronExpression } from '@nestjs/schedule'
import { writeFileSync } from 'fs' import COS from 'cos-nodejs-sdk-v5'
import { localBotListDataFilePath } from '~/constants/path.constant' import dayjs from 'dayjs'
import { existsSync, readFileSync, writeFileSync } from 'fs'
import mkdirp from 'mkdirp'
import { join } from 'path'
import { $, cd } from 'zx'
import {
BACKUP_DIR,
LOCAL_BOT_LIST_DATA_FILE_PATH,
} from '~/constants/path.constant'
import { ConfigsService } from '~/modules/configs/configs.service'
import { isDev } from '~/utils/index.util'
import { HttpService } from './helper.http.service' import { HttpService } from './helper.http.service'
@Injectable() @Injectable()
export class CronService { export class CronService {
private logger: Logger private logger: Logger
constructor(private readonly http: HttpService) { constructor(
private readonly http: HttpService,
private readonly configs: ConfigsService,
) {
this.logger = new Logger(CronService.name) this.logger = new Logger(CronService.name)
} }
/**
*
* @description 每天凌晨更新 Bot 列表
*/
@Cron(CronExpression.EVERY_WEEK) @Cron(CronExpression.EVERY_WEEK)
async updateBotList() { async updateBotList() {
try { try {
@@ -17,7 +33,7 @@ export class CronService {
'https://cdn.jsdelivr.net/gh/atmire/COUNTER-Robots@master/COUNTER_Robots_list.json', 'https://cdn.jsdelivr.net/gh/atmire/COUNTER-Robots@master/COUNTER_Robots_list.json',
) )
writeFileSync(localBotListDataFilePath, JSON.stringify(json), { writeFileSync(LOCAL_BOT_LIST_DATA_FILE_PATH, JSON.stringify(json), {
encoding: 'utf-8', encoding: 'utf-8',
flag: 'w+', flag: 'w+',
}) })
@@ -27,4 +43,81 @@ export class CronService {
this.logger.warn('更新 Bot 列表错误') this.logger.warn('更新 Bot 列表错误')
} }
} }
@Cron(CronExpression.EVERY_DAY_AT_1AM, { name: 'backup' })
async backupDB({ uploadCOS = true }: { uploadCOS?: boolean } = {}) {
if (!this.configs.get('backupOptions').enable) {
return
}
this.logger.log('--> 备份数据库中')
const dateDir = this.nowStr
const backupDirPath = join(BACKUP_DIR, dateDir)
mkdirp.sync(backupDirPath)
try {
await $`mongodump -h 127.0.0.1 -d mx-space -o ${backupDirPath} >/dev/null 2>&1`
cd(backupDirPath)
await $`zip -r backup-${dateDir} mx-space/* && rm -r mx-space`
this.logger.log('--> 备份成功')
} catch (e) {
if (isDev) {
console.log(e)
}
this.logger.error(
'--> 备份失败, 请确保已安装 zip 或 mongo-tools, mongo-tools 的版本需要与 mongod 版本一致',
)
return
}
// 开始上传 COS
process.nextTick(() => {
if (!uploadCOS) {
return
}
const backupOptions = this.configs.get('backupOptions')
if (
!backupOptions.Bucket ||
!backupOptions.Region ||
!backupOptions.SecretId ||
!backupOptions.SecretKey
) {
return
}
const backupFilePath = join(backupDirPath, 'backup-' + dateDir + '.zip')
if (!existsSync(backupFilePath)) {
this.logger.warn('文件不存在, 无法上传到 COS')
return
}
this.logger.log('--> 开始上传到 COS')
const cos = new COS({
SecretId: backupOptions.SecretId,
SecretKey: backupOptions.SecretKey,
})
// 分片上传
cos.sliceUploadFile(
{
Bucket: backupOptions.Bucket,
Region: backupOptions.Region,
Key: `backup-${dateDir}.zip`,
FilePath: backupFilePath,
},
(err) => {
if (!err) {
this.logger.log('--> 上传成功')
} else {
this.logger.error('--> 上传失败了' + err)
}
},
)
})
return readFileSync(join(backupDirPath, 'backup-' + dateDir + '.zip'))
}
private get nowStr() {
return dayjs().format('YYYY-MM-DD-HH:mm:ss')
}
} }

View File

@@ -5,6 +5,7 @@ import { CronService } from './helper.cron.service'
import { EmailService } from './helper.email.service' import { EmailService } from './helper.email.service'
import { HttpService } from './helper.http.service' import { HttpService } from './helper.http.service'
import { ImageService } from './helper.image.service' import { ImageService } from './helper.image.service'
import { UploadService } from './helper.upload.service'
const providers: Provider<any>[] = [ const providers: Provider<any>[] = [
EmailService, EmailService,
@@ -12,6 +13,7 @@ const providers: Provider<any>[] = [
ImageService, ImageService,
CronService, CronService,
CountingService, CountingService,
UploadService,
] ]
@Module({ @Module({

View File

@@ -0,0 +1,20 @@
import { BadRequestException, Injectable } from '@nestjs/common'
import { FastifyRequest } from 'fastify'
import { MultipartFile } from 'fastify-multipart'
@Injectable()
export class UploadService {
public async validMultipartField(
req: FastifyRequest,
): Promise<MultipartFile> {
const data = await req.file()
if (!data) {
throw new BadRequestException('仅供上传文件!')
}
if (data.fieldname != 'file') {
throw new BadRequestException('字段必须为 file')
}
return data
}
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'
export class FileUploadDto {
@ApiProperty({ type: 'string', format: 'binary' })
file: any
}

5
src/utils/global.util.ts Normal file
View File

@@ -0,0 +1,5 @@
import { isDev } from './index.util'
Object.assign(globalThis, {
isDev: isDev,
})

View File

@@ -1,12 +1,7 @@
/* /**
* @Author: Innei * @module utils/ip
* @Date: 2020-05-10 15:31:44 * @description IP utility functions
* @LastEditTime: 2020-07-08 21:42:06
* @LastEditors: Innei
* @FilePath: /mx-server/src/utils/ip.ts
* @Coding with Love
*/ */
import type { FastifyRequest } from 'fastify' import type { FastifyRequest } from 'fastify'
import { IncomingMessage } from 'http' import { IncomingMessage } from 'http'
export const getIp = (request: FastifyRequest | IncomingMessage) => { export const getIp = (request: FastifyRequest | IncomingMessage) => {

3
src/utils/system.util.ts Normal file
View File

@@ -0,0 +1,3 @@
export function getFolderSize(folderPath: string) {
return $`du -shc ${folderPath} | head -n 1 | cut -f1`
}

12
src/zx.global-fix.ts Normal file
View File

@@ -0,0 +1,12 @@
// @ts-nocheck
import { registerGlobals } from 'zx'
import { isDev } from './utils/index.util'
// FIX: zx 4.1.1 import 'zx/globals' error
// ERROR: Package subpath './globals.mjs' is not defined by "exports" in /Users/xiaoxun/github/innei-repo/mx-space/server-next/node_modules/zx/package.json
// FIXME: registerGlobals manally
registerGlobals()
/// config for zx
$.verbose = isDev

View File

@@ -24,4 +24,8 @@
] ]
}, },
}, },
"exclude": [
"dist",
"tmp"
]
} }