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_SECRET=xiaojuSurveyJwtSecret
|
||||||
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
|
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/serve-static": "^4.0.0",
|
||||||
"@nestjs/swagger": "^7.3.0",
|
"@nestjs/swagger": "^7.3.0",
|
||||||
"@nestjs/typeorm": "^10.0.1",
|
"@nestjs/typeorm": "^10.0.1",
|
||||||
|
"ali-oss": "^6.20.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^16.3.2",
|
"dotenv": "^16.3.2",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
|
"minio": "^7.1.3",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongodb": "^5.9.2",
|
"mongodb": "^5.9.2",
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.7",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
|
"qiniu": "^7.11.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"svg-captcha": "^1.4.0",
|
"svg-captcha": "^1.4.0",
|
||||||
@ -49,6 +53,7 @@
|
|||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/multer": "^1.4.11",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/node-forge": "^1.3.11",
|
"@types/node-forge": "^1.3.11",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
|
@ -11,6 +11,8 @@ import { ServeStaticModule } from '@nestjs/serve-static';
|
|||||||
import { SurveyModule } from './modules/survey/survey.module';
|
import { SurveyModule } from './modules/survey/survey.module';
|
||||||
import { SurveyResponseModule } from './modules/surveyResponse/surveyResponse.module';
|
import { SurveyResponseModule } from './modules/surveyResponse/surveyResponse.module';
|
||||||
import { AuthModule } from './modules/auth/auth.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';
|
import { join } from 'path';
|
||||||
|
|
||||||
@ -39,7 +41,6 @@ import { Logger } from './logger';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({}),
|
ConfigModule.forRoot({}),
|
||||||
|
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
@ -80,9 +81,17 @@ import { Logger } from './logger';
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
SurveyModule,
|
SurveyModule,
|
||||||
SurveyResponseModule,
|
SurveyResponseModule,
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRootAsync({
|
||||||
rootPath: join(__dirname, '..', 'public'),
|
useFactory: async () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
rootPath: join(__dirname, '..', 'public'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
MessageModule,
|
||||||
|
FileModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export enum EXCEPTION_CODE {
|
export enum EXCEPTION_CODE {
|
||||||
AUTHTIFICATION_FAILED = 1001, // 没有权限
|
AUTHENTICATION_FAILED = 1001, // 没有权限
|
||||||
PARAMETER_ERROR = 1002, // 参数有误
|
PARAMETER_ERROR = 1002, // 参数有误
|
||||||
USER_EXISTS = 2001, // 用户已存在
|
USER_EXISTS = 2001, // 用户已存在
|
||||||
USER_NOT_EXISTS = 2002, // 用户不存在
|
USER_NOT_EXISTS = 2002, // 用户不存在
|
||||||
@ -15,4 +15,6 @@ export enum EXCEPTION_CODE {
|
|||||||
RESPONSE_OVER_LIMIT = 9003, // 超出限制
|
RESPONSE_OVER_LIMIT = 9003, // 超出限制
|
||||||
RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除
|
RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除
|
||||||
RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除
|
RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除
|
||||||
|
|
||||||
|
UPLOAD_FILE_ERROR = 5001, // 上传文件错误
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { HttpException } from './httpException';
|
import { HttpException } from './httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
export class AuthtificationException extends HttpException {
|
export class AuthenticationException extends HttpException {
|
||||||
constructor(public readonly message: string) {
|
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 { Authtication } from './authtication';
|
||||||
import { UserService } from '../modules/auth/services/user.service';
|
import { UserService } from '../modules/auth/services/user.service';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { AuthtificationException } from '../exceptions/authException';
|
import { AuthenticationException } from '../exceptions/authException';
|
||||||
import { User } from 'src/models/user.entity';
|
import { User } from 'src/models/user.entity';
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ describe('Authtication', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||||
AuthtificationException,
|
AuthenticationException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ describe('Authtication', () => {
|
|||||||
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
.mockReturnValue('XIAOJU_SURVEY_JWT_SECRET');
|
||||||
|
|
||||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||||
AuthtificationException,
|
AuthenticationException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ describe('Authtication', () => {
|
|||||||
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(null);
|
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
await expect(guard.canActivate(context as any)).rejects.toThrow(
|
||||||
AuthtificationException,
|
AuthenticationException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
import { UserService } from '../modules/auth/services/user.service';
|
import { UserService } from '../modules/auth/services/user.service';
|
||||||
import { verify } from 'jsonwebtoken';
|
import { AuthenticationException } from '../exceptions/authException';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
import { AuthtificationException } from '../exceptions/authException';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Authtication implements CanActivate {
|
export class Authtication implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly configService: ConfigService,
|
private readonly authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
@ -16,25 +15,15 @@ export class Authtication implements CanActivate {
|
|||||||
const token = request.headers.authorization?.split(' ')[1];
|
const token = request.headers.authorization?.split(' ')[1];
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new AuthtificationException('请登录');
|
throw new AuthenticationException('请登录');
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoded;
|
|
||||||
try {
|
try {
|
||||||
decoded = verify(
|
const user = await this.authService.verifyToken(token);
|
||||||
token,
|
request.user = user;
|
||||||
this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
|
return true;
|
||||||
);
|
} catch (error) {
|
||||||
} catch (err) {
|
throw new AuthenticationException(error?.message || '用户凭证错误');
|
||||||
throw new AuthtificationException('用户凭证错误');
|
|
||||||
}
|
}
|
||||||
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],
|
imports: [TypeOrmModule.forFeature([User, Captcha]), ConfigModule],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [UserService, AuthService, CaptchaService],
|
providers: [UserService, AuthService, CaptchaService],
|
||||||
exports: [UserService],
|
exports: [UserService, AuthService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
) {}
|
||||||
async generateToken(
|
async generateToken(
|
||||||
{ _id, username }: { _id: string; username: string },
|
{ _id, username }: { _id: string; username: string },
|
||||||
{ secret, expiresIn }: { secret: string; expiresIn: string },
|
{ secret, expiresIn }: { secret: string; expiresIn: string },
|
||||||
@ -12,4 +18,21 @@ export class AuthService {
|
|||||||
expiresIn,
|
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],
|
exports: [MessagePushingTaskService],
|
||||||
})
|
})
|
||||||
export class MessagePushingModule {}
|
export class MessageModule {}
|
@ -1,6 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { MessagePushingModule } from '../message/messagePushing.module';
|
import { MessageModule } from '../message/message.module';
|
||||||
|
|
||||||
import { ResponseSchemaService } from './services/responseScheme.service';
|
import { ResponseSchemaService } from './services/responseScheme.service';
|
||||||
import { SurveyResponseService } from './services/surveyResponse.service';
|
import { SurveyResponseService } from './services/surveyResponse.service';
|
||||||
@ -30,7 +30,7 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
ClientEncrypt,
|
ClientEncrypt,
|
||||||
]),
|
]),
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
MessagePushingModule,
|
MessageModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
ClientEncryptController,
|
ClientEncryptController,
|
||||||
|
Loading…
Reference in New Issue
Block a user