feat: backup module
This commit is contained in:
3
global.d.ts
vendored
3
global.d.ts
vendored
@@ -1,10 +1,13 @@
|
||||
import { ModelType } from '@typegoose/typegoose/lib/types'
|
||||
import { Document, PaginateModel } from 'mongoose'
|
||||
import 'zx/globals'
|
||||
declare global {
|
||||
export type KV<T = any> = Record<string, T>
|
||||
|
||||
// @ts-ignore
|
||||
export type MongooseModel<T> = ModelType<T> & PaginateModel<T & Document>
|
||||
|
||||
export const isDev: boolean
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
@@ -60,13 +60,16 @@
|
||||
"chalk": "*",
|
||||
"class-transformer": "^0.4.0",
|
||||
"class-validator": "^0.13.1",
|
||||
"cos-nodejs-sdk-v5": "2.10.0",
|
||||
"dayjs": "^1.10.6",
|
||||
"dotenv": "*",
|
||||
"ejs": "^3.1.6",
|
||||
"fastify-multipart": "^4.0.7",
|
||||
"fastify-swagger": "^4.9.0",
|
||||
"image-size": "^1.0.0",
|
||||
"inquirer": "*",
|
||||
"lodash": "*",
|
||||
"mkdirp": "*",
|
||||
"mongoose": "*",
|
||||
"mongoose-lean-id": "^0.2.0",
|
||||
"mongoose-lean-virtuals": "^0.8.0",
|
||||
@@ -82,9 +85,11 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.3.0",
|
||||
"snakecase-keys": "^4.0.2",
|
||||
"ua-parser-js": "^0.7.28"
|
||||
"ua-parser-js": "^0.7.28",
|
||||
"zx": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^3.0.2",
|
||||
"@innei-util/eslint-config-ts": "^0.2.3",
|
||||
"@innei-util/prettier": "^0.1.3",
|
||||
"@nestjs/cli": "^8.1.1",
|
||||
@@ -111,7 +116,6 @@
|
||||
"jest": "27.1.0",
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"run-script-webpack-plugin": "^0.0.11",
|
||||
"socket.io": "*",
|
||||
"supertest": "^6.1.6",
|
||||
|
||||
522
pnpm-lock.yaml
generated
522
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ import { SecurityMiddleware } from './common/middlewares/security.middleware'
|
||||
import { AnalyzeModule } from './modules/analyze/analyze.module'
|
||||
import { AuthModule } from './modules/auth/auth.module'
|
||||
import { RolesGuard } from './modules/auth/roles.guard'
|
||||
import { BackupModule } from './modules/backup/backup.module'
|
||||
import { CategoryModule } from './modules/category/category.module'
|
||||
import { CommentModule } from './modules/comment/comment.module'
|
||||
import { ConfigsModule } from './modules/configs/configs.module'
|
||||
@@ -55,6 +56,7 @@ import { HelperModule } from './processors/helper/helper.module'
|
||||
|
||||
AnalyzeModule,
|
||||
AuthModule,
|
||||
BackupModule,
|
||||
CategoryModule,
|
||||
CommentModule,
|
||||
ConfigsModule,
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { FastifyAdapter } from '@nestjs/platform-fastify'
|
||||
|
||||
import FastifyMultipart from 'fastify-multipart'
|
||||
export const fastifyApp: FastifyAdapter = new FastifyAdapter({
|
||||
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) => {
|
||||
const origin = request.headers.origin
|
||||
if (!origin) {
|
||||
|
||||
@@ -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 * as SYSTEM from '~/constants/system.constant'
|
||||
import { FileUploadDto } from '~/shared/dto/file.dto'
|
||||
export const Paginator: MethodDecorator = (
|
||||
target,
|
||||
key,
|
||||
@@ -8,3 +10,34 @@ export const Paginator: MethodDecorator = (
|
||||
) => {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
/*
|
||||
* @Author: Innei
|
||||
* @Date: 2020-04-30 19:09:37
|
||||
* @LastEditTime: 2020-07-08 21:35:06
|
||||
* @LastEditors: Innei
|
||||
* @FilePath: /mx-server/src/core/guards/spider.guard.ts
|
||||
* @Coding with Love
|
||||
/**
|
||||
* @module common/guard/spider.guard
|
||||
* @description 禁止爬虫的守卫
|
||||
* @author Innei <https://innei.ren>
|
||||
*/
|
||||
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* @file 缓存拦截器
|
||||
* @module interceptor/cache
|
||||
* @author Surmon <https://github.com/surmon-china>
|
||||
* @author Innei <https://innei.ren>
|
||||
*/
|
||||
|
||||
import {
|
||||
|
||||
@@ -16,8 +16,8 @@ import { Observable } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import snakecaseKeys from 'snakecase-keys'
|
||||
import { HTTP_RES_TRANSFORM_PAGINATE } from '~/constants/meta.constant'
|
||||
import * as SYSTEM from '~/constants/system.constant'
|
||||
import { Paginator } from '~/shared/model/base.model'
|
||||
|
||||
export interface Response<T> {
|
||||
data: T
|
||||
}
|
||||
@@ -29,6 +29,17 @@ export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
): 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) => {
|
||||
if (!data) {
|
||||
throw new UnprocessableEntityException('数据丢失了(。 ́︿ ̀。)')
|
||||
@@ -37,13 +48,12 @@ export class ResponseInterceptor<T> implements NestInterceptor<T, Response<T>> {
|
||||
? { ...data }
|
||||
: { data }
|
||||
}
|
||||
const handler = context.getHandler()
|
||||
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
if (typeof data === 'undefined') {
|
||||
context.switchToHttp().getResponse().status(204)
|
||||
return
|
||||
return data
|
||||
}
|
||||
// 分页转换
|
||||
if (this.reflector.get(HTTP_RES_TRANSFORM_PAGINATE, handler)) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
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 { OptionModel } from '~/modules/configs/configs.model'
|
||||
import { CacheService } from '~/processors/cache/cache.service'
|
||||
@@ -40,7 +40,7 @@ export class AnalyzeMiddleware implements NestMiddleware {
|
||||
try {
|
||||
return this.pickPattern2Regexp(
|
||||
JSON.parse(
|
||||
readFileSync(localBotListDataFilePath, {
|
||||
readFileSync(LOCAL_BOT_LIST_DATA_FILE_PATH, {
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -12,4 +12,8 @@ export const DATA_DIR = isDev
|
||||
|
||||
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')
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export const HTTP_ADAPTER_HOST = 'HttpAdapterHost'
|
||||
export const REFLECTOR = 'Reflector'
|
||||
|
||||
export const RESPONSE_PASSTHROUGH_METADATA = '__responsePassthrough__'
|
||||
|
||||
@@ -7,11 +7,13 @@ import { AppModule } from './app.module'
|
||||
import { fastifyApp } from './common/adapt/fastify'
|
||||
import { SpiderGuard } from './common/guard/spider.guard'
|
||||
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
|
||||
import { isDev } from './utils/index.util'
|
||||
// const PORT = parseInt(process.env.PORT) || 2333
|
||||
import './utils/global.util'
|
||||
import './zx.global-fix'
|
||||
|
||||
const PORT = 2333
|
||||
const APIVersion = 1
|
||||
const Origin = CROSS_DOMAIN.allowedOrigins
|
||||
|
||||
declare const module: any
|
||||
|
||||
async function bootstrap() {
|
||||
|
||||
106
src/modules/backup/backup.controller.ts
Normal file
106
src/modules/backup/backup.controller.ts
Normal 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
|
||||
}
|
||||
}
|
||||
10
src/modules/backup/backup.module.ts
Normal file
10
src/modules/backup/backup.module.ts
Normal 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 {}
|
||||
151
src/modules/backup/backup.service.ts
Normal file
151
src/modules/backup/backup.service.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,31 @@
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { Cron, CronExpression } from '@nestjs/schedule'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { localBotListDataFilePath } from '~/constants/path.constant'
|
||||
import COS from 'cos-nodejs-sdk-v5'
|
||||
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'
|
||||
@Injectable()
|
||||
export class CronService {
|
||||
private logger: Logger
|
||||
constructor(private readonly http: HttpService) {
|
||||
constructor(
|
||||
private readonly http: HttpService,
|
||||
private readonly configs: ConfigsService,
|
||||
) {
|
||||
this.logger = new Logger(CronService.name)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @description 每天凌晨更新 Bot 列表
|
||||
*/
|
||||
@Cron(CronExpression.EVERY_WEEK)
|
||||
async updateBotList() {
|
||||
try {
|
||||
@@ -17,7 +33,7 @@ export class CronService {
|
||||
'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',
|
||||
flag: 'w+',
|
||||
})
|
||||
@@ -27,4 +43,81 @@ export class CronService {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CronService } from './helper.cron.service'
|
||||
import { EmailService } from './helper.email.service'
|
||||
import { HttpService } from './helper.http.service'
|
||||
import { ImageService } from './helper.image.service'
|
||||
import { UploadService } from './helper.upload.service'
|
||||
|
||||
const providers: Provider<any>[] = [
|
||||
EmailService,
|
||||
@@ -12,6 +13,7 @@ const providers: Provider<any>[] = [
|
||||
ImageService,
|
||||
CronService,
|
||||
CountingService,
|
||||
UploadService,
|
||||
]
|
||||
|
||||
@Module({
|
||||
|
||||
20
src/processors/helper/helper.upload.service.ts
Normal file
20
src/processors/helper/helper.upload.service.ts
Normal 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
|
||||
}
|
||||
}
|
||||
6
src/shared/dto/file.dto.ts
Normal file
6
src/shared/dto/file.dto.ts
Normal 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
5
src/utils/global.util.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { isDev } from './index.util'
|
||||
|
||||
Object.assign(globalThis, {
|
||||
isDev: isDev,
|
||||
})
|
||||
@@ -1,12 +1,7 @@
|
||||
/*
|
||||
* @Author: Innei
|
||||
* @Date: 2020-05-10 15:31:44
|
||||
* @LastEditTime: 2020-07-08 21:42:06
|
||||
* @LastEditors: Innei
|
||||
* @FilePath: /mx-server/src/utils/ip.ts
|
||||
* @Coding with Love
|
||||
/**
|
||||
* @module utils/ip
|
||||
* @description IP utility functions
|
||||
*/
|
||||
|
||||
import type { FastifyRequest } from 'fastify'
|
||||
import { IncomingMessage } from 'http'
|
||||
export const getIp = (request: FastifyRequest | IncomingMessage) => {
|
||||
|
||||
3
src/utils/system.util.ts
Normal file
3
src/utils/system.util.ts
Normal 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
12
src/zx.global-fix.ts
Normal 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
|
||||
@@ -24,4 +24,8 @@
|
||||
]
|
||||
},
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"tmp"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user