feat: 新增file模块 (#101)
This commit is contained in:
parent
a23fc28f5f
commit
d3f64707a0
@ -9,4 +9,4 @@ XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
|
||||
|
||||
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log
|
||||
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log
|
||||
|
@ -26,18 +26,22 @@
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/typeorm": "^10.0.1",
|
||||
"ali-oss": "^6.20.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"log4js": "^6.9.1",
|
||||
"minio": "^7.1.3",
|
||||
"moment": "^2.30.1",
|
||||
"mongodb": "^5.9.2",
|
||||
"nanoid": "^3.3.7",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"qiniu": "^7.11.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"svg-captcha": "^1.4.0",
|
||||
@ -49,6 +53,7 @@
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/supertest": "^2.0.12",
|
||||
|
@ -11,6 +11,8 @@ import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { SurveyModule } from './modules/survey/survey.module';
|
||||
import { SurveyResponseModule } from './modules/surveyResponse/surveyResponse.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { MessageModule } from './modules/message/message.module';
|
||||
import { FileModule } from './modules/file/file.module';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
@ -39,7 +41,6 @@ import { Logger } from './logger';
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({}),
|
||||
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
@ -80,9 +81,17 @@ import { Logger } from './logger';
|
||||
AuthModule,
|
||||
SurveyModule,
|
||||
SurveyResponseModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', 'public'),
|
||||
ServeStaticModule.forRootAsync({
|
||||
useFactory: async () => {
|
||||
return [
|
||||
{
|
||||
rootPath: join(__dirname, '..', 'public'),
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
MessageModule,
|
||||
FileModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
@ -1,5 +1,5 @@
|
||||
export enum EXCEPTION_CODE {
|
||||
AUTHTIFICATION_FAILED = 1001, // 没有权限
|
||||
AUTHENTICATION_FAILED = 1001, // 没有权限
|
||||
PARAMETER_ERROR = 1002, // 参数有误
|
||||
USER_EXISTS = 2001, // 用户已存在
|
||||
USER_NOT_EXISTS = 2002, // 用户不存在
|
||||
@ -15,4 +15,6 @@ export enum EXCEPTION_CODE {
|
||||
RESPONSE_OVER_LIMIT = 9003, // 超出限制
|
||||
RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除
|
||||
RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除
|
||||
|
||||
UPLOAD_FILE_ERROR = 5001, // 上传文件错误
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HttpException } from './httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
export class AuthtificationException extends HttpException {
|
||||
export class AuthenticationException extends HttpException {
|
||||
constructor(public readonly message: string) {
|
||||
super(message, EXCEPTION_CODE.AUTHTIFICATION_FAILED);
|
||||
super(message, EXCEPTION_CODE.AUTHENTICATION_FAILED);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Authtication } from './authtication';
|
||||
import { UserService } from '../modules/auth/services/user.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthtificationException } from '../exceptions/authException';
|
||||
import { AuthenticationException } from '../exceptions/authException';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
@ -47,7 +47,7 @@ describe('Authtication', () => {
|
||||
};
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||
AuthtificationException,
|
||||
AuthenticationException,
|
||||
);
|
||||
});
|
||||
|
||||
@ -69,7 +69,7 @@ describe('Authtication', () => {
|
||||
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||
AuthtificationException,
|
||||
AuthenticationException,
|
||||
);
|
||||
});
|
||||
|
||||
@ -94,7 +94,7 @@ describe('Authtication', () => {
|
||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(null);
|
||||
|
||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||
AuthtificationException,
|
||||
AuthenticationException,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { UserService } from '../modules/auth/services/user.service';
|
||||
import { verify } from 'jsonwebtoken';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthtificationException } from '../exceptions/authException';
|
||||
import { AuthenticationException } from '../exceptions/authException';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class Authtication implements CanActivate {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
@ -16,25 +15,15 @@ export class Authtication implements CanActivate {
|
||||
const token = request.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
throw new AuthtificationException('请登录');
|
||||
throw new AuthenticationException('请登录');
|
||||
}
|
||||
|
||||
let decoded;
|
||||
try {
|
||||
decoded = verify(
|
||||
token,
|
||||
this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
|
||||
);
|
||||
} catch (err) {
|
||||
throw new AuthtificationException('用户凭证错误');
|
||||
const user = await this.authService.verifyToken(token);
|
||||
request.user = user;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new AuthenticationException(error?.message || '用户凭证错误');
|
||||
}
|
||||
const user = await this.userService.getUserByUsername(decoded.username); // 从数据库中查找用户
|
||||
|
||||
if (!user) {
|
||||
throw new AuthtificationException('用户不存在');
|
||||
}
|
||||
|
||||
request.user = user; // 将用户信息存储在请求中
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,6 @@ import { ConfigModule } from '@nestjs/config';
|
||||
imports: [TypeOrmModule.forFeature([User, Captcha]), ConfigModule],
|
||||
controllers: [AuthController],
|
||||
providers: [UserService, AuthService, CaptchaService],
|
||||
exports: [UserService],
|
||||
exports: [UserService, AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { sign } from 'jsonwebtoken';
|
||||
import { sign, verify } from 'jsonwebtoken';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
async generateToken(
|
||||
{ _id, username }: { _id: string; username: string },
|
||||
{ secret, expiresIn }: { secret: string; expiresIn: string },
|
||||
@ -12,4 +18,21 @@ export class AuthService {
|
||||
expiresIn,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyToken(token: string) {
|
||||
let decoded;
|
||||
try {
|
||||
decoded = verify(
|
||||
token,
|
||||
this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
|
||||
);
|
||||
} catch (err) {
|
||||
throw new Error('用户凭证错误');
|
||||
}
|
||||
const user = await this.userService.getUserByUsername(decoded.username);
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
62
server/src/modules/file/config/index.ts
Normal file
62
server/src/modules/file/config/index.ts
Normal file
@ -0,0 +1,62 @@
|
||||
const SERVER_LOCAL_CONFIG = {
|
||||
LOCAL_STATIC_RENDER_TYPE: 'server', // nginx
|
||||
IS_PRIVATE_READ: false,
|
||||
FILE_KEY_PREFIX: 'userUpload', // 存储路径
|
||||
NEED_AUTH: true,
|
||||
};
|
||||
|
||||
const QINIU_CONFIG = {
|
||||
FILE_STORAGE_PROVIDER: 'qiniu',
|
||||
IS_PRIVATE_READ: false,
|
||||
FILE_KEY_PREFIX: 'userUpload/{surveyPath}', // 文件key的前缀,会根据此处配置校验body的参数
|
||||
NEED_AUTH: true, // 是否需要登录
|
||||
LINK_EXPIRY_TIME: '2h',
|
||||
|
||||
// minio、oss或者七牛云配置
|
||||
ACCESS_KEY: '', // your_access_key
|
||||
SECRET_KEY: '', // your_secret_key
|
||||
BUCKET: '', // your_bucket
|
||||
ENDPOINT: '', // endpoint
|
||||
USE_SSL: false, // useSSL
|
||||
};
|
||||
|
||||
const ALI_OSS_CONFIG = {
|
||||
FILE_STORAGE_PROVIDER: 'ali-oss',
|
||||
IS_PRIVATE_READ: false,
|
||||
FILE_KEY_PREFIX: 'userUpload/{surveyPath}', // 文件key的前缀,会根据此处配置校验body的参数
|
||||
NEED_AUTH: true, // 是否需要登录
|
||||
LINK_EXPIRY_TIME: '2h',
|
||||
|
||||
ACCESS_KEY: '', // your_access_key
|
||||
SECRET_KEY: '', // your_secret_key
|
||||
BUCKET: '', // your_bucket
|
||||
REGION: '',
|
||||
ENDPOINT: '', // endpoint
|
||||
USE_SSL: false, // useSSL
|
||||
};
|
||||
|
||||
export const MINIO_CONFIG = {
|
||||
FILE_STORAGE_PROVIDER: 'minio',
|
||||
IS_PRIVATE_READ: false,
|
||||
FILE_KEY_PREFIX: 'userUpload/{surveyPath}', // 文件key的前缀,会根据此处配置校验body的参数
|
||||
NEED_AUTH: true, // 是否需要登录
|
||||
LINK_EXPIRY_TIME: '2h',
|
||||
|
||||
ACCESS_KEY: '', // your_access_key
|
||||
SECRET_KEY: '', // your_secret_key
|
||||
BUCKET: '', // your_bucket
|
||||
REGION: '',
|
||||
ENDPOINT: '', // endpoint
|
||||
USE_SSL: true, // useSSL
|
||||
};
|
||||
|
||||
export const channels = {
|
||||
upload: 'SERVER_LOCAL_CONFIG',
|
||||
};
|
||||
|
||||
export const uploadConfig = {
|
||||
SERVER_LOCAL_CONFIG,
|
||||
QINIU_CONFIG,
|
||||
ALI_OSS_CONFIG,
|
||||
MINIO_CONFIG,
|
||||
};
|
111
server/src/modules/file/controllers/file.controller.ts
Normal file
111
server/src/modules/file/controllers/file.controller.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
HttpCode,
|
||||
Request,
|
||||
Body,
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
|
||||
import { FileService } from '../services/file.service';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { AuthenticationException } from 'src/exceptions/authException';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Controller('/api/file')
|
||||
export class FileController {
|
||||
constructor(
|
||||
private readonly fileService: FileService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
private getPathPrefix(fileKeyPrefix: string, data: Record<string, string>) {
|
||||
const regex = /\{([^}]*)\}/g;
|
||||
let matches;
|
||||
const keys = [];
|
||||
while ((matches = regex.exec(fileKeyPrefix)) !== null) {
|
||||
keys.push(matches[1]);
|
||||
}
|
||||
let result = fileKeyPrefix;
|
||||
for (const key of keys) {
|
||||
if (!data[key]) {
|
||||
throw new HttpException(
|
||||
`参数有误:${data[key]}`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
result = result.replace(new RegExp(`{${key}}`), data[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post('upload')
|
||||
@HttpCode(200)
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async upload(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Request() req,
|
||||
@Body() reqBody,
|
||||
) {
|
||||
const { channel } = reqBody;
|
||||
|
||||
if (!channel || !this.configService.get<string>(channel)) {
|
||||
throw new HttpException(
|
||||
`参数有误:${channel}`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
const configKey = this.configService.get<string>(channel);
|
||||
const needAuth = this.configService.get<boolean>(configKey);
|
||||
if (needAuth) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationException('请登录');
|
||||
}
|
||||
await this.authService.verifyToken(token);
|
||||
}
|
||||
const fileKeyPrefix = this.configService.get<string>(
|
||||
`${configKey}.FILE_KEY_PREFIX`,
|
||||
);
|
||||
|
||||
const { key, url } = await this.fileService.upload({
|
||||
configKey,
|
||||
file,
|
||||
pathPrefix: fileKeyPrefix,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
url,
|
||||
key,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('getUrl')
|
||||
@HttpCode(200)
|
||||
async generateGetUrl(@Body() reqBody) {
|
||||
const { channel, key } = reqBody;
|
||||
if (!channel || !key || !this.configService.get<string>(channel)) {
|
||||
throw new HttpException(
|
||||
'参数有误,请检查channel、key',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
const configKey = this.configService.get<string>(channel);
|
||||
const url = this.fileService.getUrl({ configKey, key });
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
key,
|
||||
url,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
26
server/src/modules/file/file.module.ts
Normal file
26
server/src/modules/file/file.module.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
import { FileService } from './services/file.service';
|
||||
|
||||
import { FileController } from './controllers/file.controller';
|
||||
|
||||
import { uploadConfig, channels } from './config/index';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 管理端和渲染端分开配置,因为管理端上传的内容一般需要公开,渲染端上传的内容一般需要限制访问,防止被当作图床
|
||||
ConfigModule.forFeature(() => {
|
||||
return {
|
||||
...channels,
|
||||
...uploadConfig,
|
||||
};
|
||||
}),
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [FileController],
|
||||
providers: [FileService],
|
||||
})
|
||||
export class FileModule {}
|
102
server/src/modules/file/services/file.service.ts
Normal file
102
server/src/modules/file/services/file.service.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { LocalHandler } from './uploadHandlers/local.handler';
|
||||
// import { QiniuHandler } from './uploadHandlers/qiniu.handler';
|
||||
// import { AliOssHandler } from './uploadHandlers/alioss.handler';
|
||||
// import { MinIOHandler } from './uploadHandlers/minio.handler';
|
||||
|
||||
@Injectable()
|
||||
export class FileService {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
private getConfig<T>(channel, key) {
|
||||
return this.configService.get<T>(channel + '_' + key);
|
||||
}
|
||||
|
||||
async upload({
|
||||
configKey,
|
||||
file,
|
||||
pathPrefix,
|
||||
}: {
|
||||
configKey: string;
|
||||
file: Express.Multer.File;
|
||||
pathPrefix: string;
|
||||
}) {
|
||||
const handler = this.getHandler(configKey);
|
||||
const { key } = await handler.upload(file, { pathPrefix });
|
||||
const url = await handler.getUrl(key);
|
||||
return {
|
||||
key,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
getUrl({ configKey, key }) {
|
||||
const handler = this.getHandler(configKey);
|
||||
return handler.getUrl(key);
|
||||
}
|
||||
|
||||
private getHandler(configKey: string) {
|
||||
const staticType = this.configService.get<string>(
|
||||
`${configKey}.LOCAL_STATIC_RENDER_TYPE`,
|
||||
);
|
||||
let physicalRootPath;
|
||||
if (staticType === 'nginx') {
|
||||
physicalRootPath = this.configService.get<string>(
|
||||
`${configKey}.NGINX_STATIC_PATH`,
|
||||
);
|
||||
}
|
||||
if (!physicalRootPath) {
|
||||
physicalRootPath = 'public';
|
||||
}
|
||||
return new LocalHandler({ physicalRootPath });
|
||||
|
||||
// qiniu
|
||||
// return new QiniuHandler({
|
||||
// accessKey: this.configService.get<string>(`${configKey}.ACCESS_KEY`),
|
||||
// secretKey: this.configService.get<string>(`${configKey}.SECRET_KEY`),
|
||||
// bucket: this.configService.get<string>(`${configKey}.BUCKET`),
|
||||
// endPoint: this.configService.get<string>(`${configKey}.ENDPOINT`),
|
||||
// useSSL: this.configService.get<boolean>(`${configKey}.USE_SSL`),
|
||||
// isPrivateRead: this.configService.get<boolean>(
|
||||
// `${configKey}.IS_PRIVATE_READ`,
|
||||
// ),
|
||||
// expiryTime: this.configService.get<string>(
|
||||
// `${configKey}.LINK_EXPIRY_TIME`,
|
||||
// ),
|
||||
// });
|
||||
|
||||
// ali-oss
|
||||
// return new AliOssHandler({
|
||||
// accessKey: this.configService.get<string>(`${configKey}.ACCESS_KEY`),
|
||||
// secretKey: this.configService.get<string>(`${configKey}.SECRET_KEY`),
|
||||
// bucket: this.configService.get<string>(`${configKey}.BUCKET`),
|
||||
// endPoint: this.configService.get<string>(`${configKey}.ENDPOINT`),
|
||||
// useSSL: this.configService.get<boolean>(`${configKey}.USE_SSL`),
|
||||
// isPrivateRead: this.configService.get<boolean>(
|
||||
// `${configKey}.IS_PRIVATE_READ`,
|
||||
// ),
|
||||
// expiryTime: this.configService.get<string>(
|
||||
// `${configKey}.LINK_EXPIRY_TIME`,
|
||||
// ),
|
||||
// region: this.configService.get<string>(`${configKey}.REGION`),
|
||||
// });
|
||||
|
||||
// minio
|
||||
// return new MinIOHandler({
|
||||
// accessKey: this.configService.get<string>(`${configKey}.ACCESS_KEY`),
|
||||
// secretKey: this.configService.get<string>(`${configKey}.SECRET_KEY`),
|
||||
// bucket: this.configService.get<string>(`${configKey}.BUCKET`),
|
||||
// endPoint: this.configService.get<string>(`${configKey}.ENDPOINT`),
|
||||
// useSSL: this.configService.get<boolean>(`${configKey}.USE_SSL`),
|
||||
// isPrivateRead: this.configService.get<boolean>(
|
||||
// `${configKey}.IS_PRIVATE_READ`,
|
||||
// ),
|
||||
// expiryTime: this.configService.get<string>(
|
||||
// `${configKey}.LINK_EXPIRY_TIME`,
|
||||
// ),
|
||||
// region: this.configService.get<string>(`${configKey}.REGION`),
|
||||
// });
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import OSS from 'ali-oss';
|
||||
import { generateUniqueFilename } from '../../utils/generateUniqueFilename';
|
||||
import { join } from 'path';
|
||||
import { parseExpiryTimeToSeconds } from '../../utils/parseExpiryTimeToSeconds';
|
||||
import { FileUploadHandler } from './uploadHandler.interface';
|
||||
|
||||
export class AliOssHandler implements FileUploadHandler {
|
||||
private client: OSS;
|
||||
endPoint: string;
|
||||
useSSL: boolean;
|
||||
isPrivateRead: boolean;
|
||||
expiryTime: string;
|
||||
constructor({
|
||||
accessKey,
|
||||
secretKey,
|
||||
bucket,
|
||||
region,
|
||||
endPoint,
|
||||
useSSL,
|
||||
isPrivateRead,
|
||||
expiryTime,
|
||||
}) {
|
||||
const client = new OSS({
|
||||
region,
|
||||
accessKeyId: accessKey,
|
||||
accessKeySecret: secretKey,
|
||||
bucket,
|
||||
});
|
||||
this.client = client;
|
||||
this.endPoint = endPoint;
|
||||
this.useSSL = useSSL;
|
||||
this.isPrivateRead = isPrivateRead;
|
||||
this.expiryTime = expiryTime;
|
||||
}
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: {
|
||||
pathPrefix?: string;
|
||||
},
|
||||
): Promise<{ key: string }> {
|
||||
const { pathPrefix } = options || {};
|
||||
const key = join(
|
||||
pathPrefix || '',
|
||||
await generateUniqueFilename(file.originalname),
|
||||
);
|
||||
|
||||
await this.client.put(key, file.buffer);
|
||||
|
||||
return { key };
|
||||
}
|
||||
|
||||
getUrl(key: string): string {
|
||||
const expireTimeSeconds = parseExpiryTimeToSeconds(this.expiryTime);
|
||||
if (this.isPrivateRead) {
|
||||
const url = this.client.signatureUrl(key, {
|
||||
expires: expireTimeSeconds,
|
||||
method: 'GET',
|
||||
});
|
||||
return url;
|
||||
} else {
|
||||
return `${this.useSSL ? 'https' : 'http'}://${this.endPoint}/${key}`;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { join, dirname } from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { FileUploadHandler } from './uploadHandler.interface';
|
||||
import { generateUniqueFilename } from '../../utils/generateUniqueFilename';
|
||||
|
||||
export class LocalHandler implements FileUploadHandler {
|
||||
private physicalRootPath: string;
|
||||
constructor({ physicalRootPath }: { physicalRootPath: string }) {
|
||||
this.physicalRootPath = physicalRootPath;
|
||||
}
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: { pathPrefix?: string },
|
||||
): Promise<{ key: string }> {
|
||||
const filename = await generateUniqueFilename(file.originalname);
|
||||
const filePath = join(
|
||||
options?.pathPrefix ? options?.pathPrefix : '',
|
||||
filename,
|
||||
);
|
||||
const physicalPath = join(this.physicalRootPath, filePath);
|
||||
await fse.mkdir(dirname(physicalPath), { recursive: true });
|
||||
const writeStream = createWriteStream(physicalPath);
|
||||
return new Promise((resolve, reject) => {
|
||||
writeStream.on('finish', () =>
|
||||
resolve({
|
||||
key: filePath,
|
||||
}),
|
||||
);
|
||||
writeStream.on('error', reject);
|
||||
writeStream.write(file.buffer);
|
||||
writeStream.end();
|
||||
});
|
||||
}
|
||||
|
||||
getUrl(key: string): string {
|
||||
return `/${key}`;
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import { Client } from 'minio';
|
||||
import { generateUniqueFilename } from '../../utils/generateUniqueFilename';
|
||||
import { join } from 'path';
|
||||
import { parseExpiryTimeToSeconds } from '../../utils/parseExpiryTimeToSeconds';
|
||||
import { FileUploadHandler } from './uploadHandler.interface';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
export class MinIOHandler implements FileUploadHandler {
|
||||
private client: Client;
|
||||
endPoint: string;
|
||||
useSSL: boolean;
|
||||
isPrivateRead: boolean;
|
||||
expiryTime: string;
|
||||
bucket: string;
|
||||
constructor({
|
||||
accessKey,
|
||||
secretKey,
|
||||
bucket,
|
||||
region,
|
||||
endPoint,
|
||||
useSSL,
|
||||
isPrivateRead,
|
||||
expiryTime,
|
||||
}) {
|
||||
const client = new Client({
|
||||
endPoint,
|
||||
accessKey,
|
||||
secretKey,
|
||||
region,
|
||||
useSSL,
|
||||
pathStyle: true,
|
||||
});
|
||||
this.client = client;
|
||||
this.endPoint = endPoint;
|
||||
this.useSSL = useSSL;
|
||||
this.isPrivateRead = isPrivateRead;
|
||||
this.expiryTime = expiryTime;
|
||||
this.bucket = bucket;
|
||||
}
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: {
|
||||
pathPrefix?: string;
|
||||
},
|
||||
): Promise<{ key: string }> {
|
||||
const { pathPrefix } = options || {};
|
||||
const key = join(
|
||||
pathPrefix || '',
|
||||
await generateUniqueFilename(file.originalname),
|
||||
);
|
||||
|
||||
try {
|
||||
await this.client.putObject(this.bucket, key, file.buffer);
|
||||
|
||||
return { key };
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || error.stack || '上传失败',
|
||||
EXCEPTION_CODE.UPLOAD_FILE_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getUrl(key: string): Promise<string> {
|
||||
const expireTimeSeconds = parseExpiryTimeToSeconds(this.expiryTime);
|
||||
|
||||
if (this.isPrivateRead) {
|
||||
const url = await this.client.presignedGetObject(
|
||||
this.bucket,
|
||||
key,
|
||||
expireTimeSeconds,
|
||||
);
|
||||
return url;
|
||||
}
|
||||
return `${this.useSSL ? 'https' : 'http'}://${this.endPoint}/${this.bucket}/${key}`;
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import qiniu from 'qiniu';
|
||||
import { join } from 'path';
|
||||
import { generateUniqueFilename } from '../../utils/generateUniqueFilename';
|
||||
import { parseExpiryTimeToSeconds } from '../../utils/parseExpiryTimeToSeconds';
|
||||
import { FileUploadHandler } from './uploadHandler.interface';
|
||||
|
||||
export class QiniuHandler implements FileUploadHandler {
|
||||
private bucket: string;
|
||||
private endPoint: string;
|
||||
private useSSL: boolean;
|
||||
private isPrivateRead: boolean;
|
||||
private expiryTime: string;
|
||||
private mac: qiniu.auth.digest.Mac;
|
||||
|
||||
constructor({
|
||||
accessKey,
|
||||
secretKey,
|
||||
bucket,
|
||||
endPoint,
|
||||
useSSL,
|
||||
isPrivateRead,
|
||||
expiryTime,
|
||||
}) {
|
||||
this.bucket = bucket;
|
||||
this.endPoint = endPoint;
|
||||
this.useSSL = useSSL;
|
||||
this.isPrivateRead = isPrivateRead;
|
||||
this.expiryTime = expiryTime;
|
||||
|
||||
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
||||
this.mac = mac;
|
||||
}
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: { pathPrefix?: string },
|
||||
): Promise<{ key: string }> {
|
||||
const config = new qiniu.conf.Config();
|
||||
const formUploader = new qiniu.form_up.FormUploader(config);
|
||||
const putExtra = new qiniu.form_up.PutExtra();
|
||||
const key = join(
|
||||
options?.pathPrefix ? options?.pathPrefix : '',
|
||||
await generateUniqueFilename(file.originalname),
|
||||
);
|
||||
|
||||
const putPolicy = new qiniu.rs.PutPolicy({
|
||||
scope: this.bucket + ':' + key,
|
||||
});
|
||||
|
||||
const uploadToken = putPolicy.uploadToken(this.mac);
|
||||
|
||||
return new Promise<{ key: string }>((resolve, reject) => {
|
||||
formUploader.put(
|
||||
uploadToken,
|
||||
key,
|
||||
file.buffer,
|
||||
putExtra,
|
||||
(respErr, respBody, respInfo) => {
|
||||
if (respErr) {
|
||||
reject(respErr);
|
||||
}
|
||||
if (respInfo.statusCode === 200) {
|
||||
resolve({ key });
|
||||
} else {
|
||||
reject(respBody);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getUrl(key: string): string {
|
||||
if (!this.isPrivateRead) {
|
||||
return `${this.useSSL ? 'https' : 'http'}://${this.endPoint}/${key}`;
|
||||
}
|
||||
|
||||
const config = new qiniu.conf.Config({
|
||||
useHttpsDomain: this.useSSL,
|
||||
});
|
||||
const bucketManager = new qiniu.rs.BucketManager(this.mac, config);
|
||||
let url;
|
||||
if (this.isPrivateRead) {
|
||||
const deadline =
|
||||
Math.floor(Date.now() / 1000) +
|
||||
parseExpiryTimeToSeconds(this.expiryTime);
|
||||
url = bucketManager.privateDownloadUrl(this.endPoint, key, deadline);
|
||||
} else {
|
||||
url = bucketManager.publicDownloadUrl(this.endPoint, key);
|
||||
}
|
||||
|
||||
return this.useSSL ? `https://${url}` : `http://${url}`;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export interface FileUploadHandler {
|
||||
upload(
|
||||
file: Express.Multer.File,
|
||||
options?: { pathPrefix?: string },
|
||||
): Promise<{ key: string }>;
|
||||
getUrl(key: string): string | Promise<string>;
|
||||
}
|
11
server/src/modules/file/utils/generateUniqueFilename.ts
Normal file
11
server/src/modules/file/utils/generateUniqueFilename.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { promisify } from 'util';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export const generateUniqueFilename = async (
|
||||
originalname: string,
|
||||
): Promise<string> => {
|
||||
const randomBytesPromise = promisify(randomBytes);
|
||||
const randomString = (await randomBytesPromise(16)).toString('hex');
|
||||
const ext = originalname.split('.').pop();
|
||||
return `${randomString}.${ext}`;
|
||||
};
|
13
server/src/modules/file/utils/parseExpiryTimeToSeconds.ts
Normal file
13
server/src/modules/file/utils/parseExpiryTimeToSeconds.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const parseExpiryTimeToSeconds = (expiryTime: string): number => {
|
||||
const units: { [key: string]: number } = {
|
||||
s: 1,
|
||||
m: 60,
|
||||
h: 3600,
|
||||
d: 86400,
|
||||
w: 604800,
|
||||
};
|
||||
|
||||
const unit = expiryTime.charAt(expiryTime.length - 1);
|
||||
const time = parseInt(expiryTime.slice(0, -1));
|
||||
return units[unit] * time;
|
||||
};
|
12
server/src/modules/file/utils/replaceFileKey.ts
Normal file
12
server/src/modules/file/utils/replaceFileKey.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const replaceFileKey = (
|
||||
originStr: string,
|
||||
arr: Array<{ key; value }>,
|
||||
) => {
|
||||
let retStr = originStr;
|
||||
for (const { key, value } of arr) {
|
||||
if (value) {
|
||||
retStr = retStr.replace(new RegExp(`{${key}}`), value);
|
||||
}
|
||||
}
|
||||
return retStr;
|
||||
};
|
@ -28,4 +28,4 @@ import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
],
|
||||
exports: [MessagePushingTaskService],
|
||||
})
|
||||
export class MessagePushingModule {}
|
||||
export class MessageModule {}
|
@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { MessagePushingModule } from '../message/messagePushing.module';
|
||||
import { MessageModule } from '../message/message.module';
|
||||
|
||||
import { ResponseSchemaService } from './services/responseScheme.service';
|
||||
import { SurveyResponseService } from './services/surveyResponse.service';
|
||||
@ -30,7 +30,7 @@ import { ConfigModule } from '@nestjs/config';
|
||||
ClientEncrypt,
|
||||
]),
|
||||
ConfigModule,
|
||||
MessagePushingModule,
|
||||
MessageModule,
|
||||
],
|
||||
controllers: [
|
||||
ClientEncryptController,
|
||||
|
Loading…
Reference in New Issue
Block a user