Skip to content

Commit

Permalink
feat: 新增 GridFS 文件存储方式;
Browse files Browse the repository at this point in the history
  • Loading branch information
maslow committed Aug 13, 2021
1 parent 3fb2b68 commit 0ddc915
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 113 deletions.
40 changes: 29 additions & 11 deletions packages/app-server/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import * as path from 'path'
import * as dotenv from 'dotenv'

/**
* parse environment vars from the `.env` file if existing
*/
dotenv.config()

/**
* 应用运行配置管理
* configuration management
*/
export default class Config {
/**
* 获取数据库连接配置
* mongodb connection configuration
*/
static get db() {
if (!process.env['DB']) {
Expand All @@ -21,12 +25,12 @@ export default class Config {
return {
database: process.env['DB'],
uri: process.env['DB_URI'],
poolSize: (process.env['DB_POOL_LIMIT'] ?? 100) as number,
maxPoolSize: (process.env['DB_POOL_LIMIT'] ?? 10) as number,
}
}

/**
* 指定服务端密钥,用于生成 token
* the server secret salt, mainly used for generating tokens
*/
static get SERVER_SECRET_SALT(): string {
const secret_salt = process.env['SERVER_SECRET_SALT'] ?? process.env['SERVER_SALT']
Expand All @@ -36,40 +40,54 @@ export default class Config {
return secret_salt
}

// 本地上传文件存储目录
/**
* the root path of local file system driver, only used while `FILE_SYSTEM_DRIVER` equals to 'local'
*/
static get LOCAL_STORAGE_ROOT_PATH(): string {
return process.env['LOCAL_STORAGE_ROOT_PATH'] ?? path.join(process.cwd(), "data")
}

// 临时文件目录
/**
* the file system driver: 'local', 'gridfs'
*/
static get FILE_SYSTEM_DRIVER(): string {
return process.env['FILE_SYSTEM_DRIVER'] ?? 'gridfs'
}

/**
* the `temp path`
*/
static get TMP_PATH(): string {
const tmp_path = process.env['TMP_PATH'] ?? path.join(process.cwd(), "tmp")
return tmp_path
}

/**
* 指定日志级别
* the logger level : 'fatal', 'error', 'warning', 'debug', 'info', 'trace'
*/
static get LOG_LEVEL(): string {
return process.env['LOG_LEVEL'] ?? (this.isProd ? 'info' : 'debug')
}

/**
* 指定服务监听端口号,缺省为 8000
* the serving port, default is 8000
*/
static get PORT(): number {
return (process.env.PORT ?? 8000) as number
}

/**
* 指定开启云函数日志: "always" | "debug" | "none"
* enable cloud function logging, default is `always`
* - `always` means that all cloud functions' execution will be logged
* - `debug` means that only logging for debug invokes
* - `never` no logging any case
*/
static get ENABLE_CLOUD_FUNCTION_LOG(): string {
return (process.env.ENABLE_CLOUD_FUNCTION_LOG ?? 'debug')
return (process.env.ENABLE_CLOUD_FUNCTION_LOG ?? 'always')
}

/**
* 是否生产环境
* in production deploy or not
*/
static get isProd(): boolean {
return process.env.NODE_ENV === 'production'
Expand Down
2 changes: 1 addition & 1 deletion packages/app-server/src/lib/globals/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class Globals {
* @returns
*/
private static _createAccessor() {
const accessor = new MongoAccessor(Config.db.database, Config.db.uri, { directConnection: true, maxPoolSize: Config.db.poolSize })
const accessor = new MongoAccessor(Config.db.database, Config.db.uri, { directConnection: true, maxPoolSize: Config.db.maxPoolSize })

accessor.setLogger(createLogger('db', 'warning'))
accessor.init()
Expand Down
8 changes: 4 additions & 4 deletions packages/app-server/src/lib/storage/local_file_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export class LocalFileStorage implements FileStorageInterface {
rootPath: string
namespace: string

constructor(rootPath: string, namespace: string = 'default') {
constructor(rootPath: string, bucket: string = 'public') {
this.rootPath = rootPath
this.namespace = namespace
this.namespace = bucket
}

/**
Expand Down Expand Up @@ -104,7 +104,7 @@ export class LocalFileStorage implements FileStorageInterface {
* @param name
* @returns
*/
checkSafeDirectoryName(name: string): boolean {
checkSafeDirectoryName(name: string): boolean {
assert(typeof name === 'string', 'name must be a string')

const reg = /^([0-9]|[A-z]|_|-){3,64}$/
Expand All @@ -118,7 +118,7 @@ export class LocalFileStorage implements FileStorageInterface {
assert(typeof name === 'string', 'name must be a string')
const reg = /^([0-9]|[A-z]|_|-|\.){3,64}$/
return reg.test(name)
}
}

/**
* 生成文件名
Expand Down
105 changes: 105 additions & 0 deletions packages/app-server/src/router/file/gridfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

import * as fs from 'fs'
import * as express from 'express'
import { GridFSBucket } from 'mongodb'
import { Globals } from '../../lib/globals'
import { checkFileOperationToken, FS_OPERATION } from './utils'


export const GridFSHandlers = {
handleUploadFile,
handleDownloadFile
}

/**
* upload file to the gridfs
* @param {string} bucket the bucket of file to store into, for example if `bucket = public`, the file path would be `/public/xxx.png`
*/
async function handleUploadFile(req: express.Request, res: express.Response) {
const auth = req['auth']
const bucket_name = req.params.bucket

// check file operation token if bucket is not 'public'
if (bucket_name !== 'public') {
const [code, message] = checkFileOperationToken(bucket_name, req.query?.token as string, FS_OPERATION.CREATE)
if (code) {
return res.status(code).send(message)
}
}

// check given file
const file = req.file
if (!file) {
return res.status(422).send('file cannot be empty')
}

// create a local file system driver
const bucket = new GridFSBucket(Globals.accessor.db, { bucketName: bucket_name })

const stream = bucket.openUploadStream(file.filename, {
metadata: { original_name: file.originalname, created_by: auth?.uid, bucket: bucket_name, created_at: Date.now() },
contentType: file.mimetype
})


// save to gridfs
fs.createReadStream(file.path)
.pipe(stream as any)
.on('finish', () => {
res.send({
code: 0,
data: {
id: stream.id,
filename: stream.filename,
bucket: bucket_name,
contentType: file.mimetype
}
})
})
.on('error', (err: Error) => {
Globals.logger.error('upload file to gridfs stream occurred error', err)
res.status(500).send('internal server error')
})
}


/**
* download file from gridfs by pointed bucket name and filename
* @param {string} bucket the bucket of file to store into, for example if `bucket = public`, the file path would be `/public/xxx.png`
*/
async function handleDownloadFile(req: express.Request, res: express.Response) {

const { bucket: bucket_name, filename } = req.params

// check file operation token if bucket is not 'public'
if (bucket_name !== 'public') {
const [code, message] = checkFileOperationToken(bucket_name, req.query?.token as string, FS_OPERATION.READ)
if (code) {
return res.status(code).send(message)
}
}


try {

const bucket = new GridFSBucket(Globals.accessor.db, { bucketName: bucket_name })
const stream = bucket.openDownloadStreamByName(filename)

const files = await bucket.find({ filename: filename }).toArray()
const file = files.shift()
if (!file) {
return res.status(404).send('Not Found')
}

if (file?.contentType) {
res.contentType(file.contentType)
}

res.set('x-bucket', bucket_name)
res.set('x-uri', `/${bucket_name}/${filename}`)
return stream.pipe(res)
} catch (error) {
Globals.logger.error('get file info failed', error)
return res.status(404).send('Not Found')
}
}
Loading

0 comments on commit 0ddc915

Please sign in to comment.