Skip to content

Commit

Permalink
feat: support import/export application db
Browse files Browse the repository at this point in the history
  • Loading branch information
0fatal committed Aug 21, 2023
1 parent f6d8aed commit f0e4d57
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 4 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@
"withs",
"xmlparser",
"zcube",
"zustand"
"zustand",
"Streamable",
],
"i18n-ally.localesPaths": "web/public/locales",
"i18n-ally.enabledParsers": ["json"],
Expand Down
6 changes: 6 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ ARG TARGETARCH
ENV MINIO_CLIENT_PATH /usr/local/bin/mc
RUN wget https://dl.min.io/client/mc/release/linux-$TARGETARCH/mc -O $MINIO_CLIENT_PATH && chmod +x $MINIO_CLIENT_PATH

ENV MONGO_TOOLS mongodb-database-tools-ubuntu1804-$TARGETARCH-100.8.0.tgz
RUN wget https://fastdl.mongodb.org/tools/db/${MONGO_TOOLS}.tgz
RUN tar -zxvf ${MONGO_TOOLS}.tgz
RUN mv $MONGO_TOOLS/bin/* /usr/local/bin/
RUN rm -rf ${MONGO_TOOLS}.tgz $MONGO_TOOLS

WORKDIR /app

EXPOSE 3000
Expand Down
111 changes: 108 additions & 3 deletions server/src/database/database.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import { Controller, Logger, Param, Post, Req, UseGuards } from '@nestjs/common'
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'
import {
Controller,
Get,
Logger,
Param,
Post,
Req,
Res,
UseGuards,
Put,
StreamableFile,
UploadedFile,
UseInterceptors,
Body,
} from '@nestjs/common'
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiOperation,
ApiTags,
} from '@nestjs/swagger'
import { Policy, Proxy } from 'database-proxy/dist'
import { ApplicationAuthGuard } from 'src/authentication/application.auth.guard'
import { JwtAuthGuard } from 'src/authentication/jwt.auth.guard'
import { IRequest } from 'src/utils/interface'
import { IRequest, IResponse } from 'src/utils/interface'
import { DatabaseService } from './database.service'
import * as path from 'path'
import { createReadStream, existsSync, mkdirSync } from 'fs'
import { FileInterceptor } from '@nestjs/platform-express'
import { unlink, writeFile } from 'node:fs/promises'
import * as os from 'os'
import { ResponseUtil } from 'src/utils/response'
import { ImportDatabaseDto } from './dto/import-database.dto'

@ApiTags('Database')
@ApiBearerAuth('Authorization')
Expand Down Expand Up @@ -57,4 +84,82 @@ export class DatabaseController {
}
}
}

@ApiOperation({ summary: 'Export database of an application' })
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
@Get('export')
async exportDatabase(
@Param('appid') appid: string,
@Res({ passthrough: true }) res: IResponse,
) {
const tempFilePath = path.join(
os.tmpdir(),
'mongodb-data',
'export',
`${appid}-db.gz`,
)

// check if dir exists
if (!existsSync(path.dirname(tempFilePath))) {
mkdirSync(path.dirname(tempFilePath), { recursive: true })
}

await this.dbService.exportDatabase(appid, tempFilePath)
const filename = path.basename(tempFilePath)

res.set({
'Content-Disposition': `attachment; filename="${filename}"`,
})
const file = createReadStream(tempFilePath)
return new StreamableFile(file)
}

@ApiOperation({ summary: 'Import database of an application' })
@ApiConsumes('multipart/form-data')
@ApiBody({
type: ImportDatabaseDto,
})
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
@Put('import')
@UseInterceptors(
FileInterceptor('file', {
limits: {
fileSize: 256 * 1024 * 1024 * 1024, // 256 GB
},
}),
)
async importDatabase(
@UploadedFile() file: Express.Multer.File,
@Body('sourceAppid') sourceAppid: string,
@Param('appid') appid: string,
) {
// check if db is valid
if (!/^[a-z0-9]{6}$/.test(sourceAppid)) {
return ResponseUtil.error('Invalid source appid')
}
// check if file is .gz
if (file.mimetype !== 'application/gzip') {
return ResponseUtil.error('Invalid db file')
}

const tempFilePath = path.join(
os.tmpdir(),
'mongodb-data',
'import',
`${appid}-${sourceAppid}.gz`,
)

// check if dir exists
if (!existsSync(path.dirname(tempFilePath))) {
mkdirSync(path.dirname(tempFilePath), { recursive: true })
}

try {
await writeFile(tempFilePath, file.buffer)
await this.dbService.importDatabase(appid, sourceAppid, tempFilePath)
return ResponseUtil.ok({})
} finally {
if (existsSync(tempFilePath)) await unlink(tempFilePath)
}
}
}
44 changes: 44 additions & 0 deletions server/src/database/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
DatabasePhase,
DatabaseState,
} from './entities/database'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'

const p_exec = promisify(exec)

@Injectable()
export class DatabaseService {
Expand Down Expand Up @@ -206,4 +210,44 @@ export class DatabaseService {
throw error
}
}

async exportDatabase(appid: string, filePath: string) {
const region = await this.regionService.findByAppId(appid)
const database = await this.findOne(appid)
assert(database, 'Database not found')

const connectionUri = this.getControlConnectionUri(region, database)
assert(connectionUri, 'Database connection uri not found')

try {
await p_exec(
`mongodump --uri='${connectionUri}' --gzip --archive=${filePath}`,
)
} catch (error) {
this.logger.error(`failed to export db ${appid}`, error)
throw error
}
}

async importDatabase(
appid: string,
dbName: string,
filePath: string,
): Promise<void> {
const region = await this.regionService.findByAppId(appid)
const database = await this.findOne(appid)
assert(database, 'Database not found')

const connectionUri = this.getControlConnectionUri(region, database)
assert(connectionUri, 'Database connection uri not found')

try {
await p_exec(
`mongorestore --uri='${connectionUri}' --gzip --archive='${filePath}' --nsFrom="${dbName}.*" --nsTo="${appid}.*" -v --nsInclude="${dbName}.*"`,
)
} catch (error) {
console.error(`failed to import db to ${appid}:`, error)
throw error
}
}
}
9 changes: 9 additions & 0 deletions server/src/database/dto/import-database.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'

export class ImportDatabaseDto {
@ApiProperty({ type: 'binary', format: 'binary' })
file: any

@ApiProperty({ type: 'string', description: 'source appid' })
sourceAppid: string
}

0 comments on commit f0e4d57

Please sign in to comment.