Compare commits

...

2 Commits

Author SHA1 Message Date
绝弹 785ed04ccb fix: 修复上传文件名乱码的问题
自动部署 / build (push) Failing after 2s Details
2023-11-23 21:21:58 +08:00
luoer 2fd52a3d98 feat: 文件添加哈希计算 2023-11-03 17:31:42 +08:00
40 changed files with 186 additions and 26 deletions

View File

@ -38,6 +38,11 @@
- .dockerignore 配置哪些文件应该被忽略掉
- .gitea/workflows/depoy.yaml 流水线任务的配置文件,语法上与 Github Actions 一致
## 计划
- 双token无感刷新
- session/cookiejwtsso单点登陆
- 大文件上传,断点续传
## 笔记
- createUserDto与User分开

Binary file not shown.

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Leckerli One
- Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de hallo@gesine-todt.de), with Reserved Font Names "Leckerli"
- Font Source: http://fonts.gstatic.com/s/leckerlione/v20/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Binary file not shown.

View File

@ -11,12 +11,12 @@ export class LoginLogInterceptor implements NestInterceptor {
tap({
next: (data) => {
const status = true;
const description = '登录成功';
const description = '用户登录成功';
this.recordLoginLog(context, { status, description });
},
error: (err) => {
const status = false;
const description = err?.message || '登录失败';
const description = err?.message || '用户登录失败';
this.recordLoginLog(context, { status, description });
},
}),

View File

@ -1,6 +1,7 @@
import { BaseEntity } from '@/database';
import { FileCategory } from '@/storage/fileCategory';
import { ApiHideProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
@Entity({ orderBy: { id: 'DESC' } })
@ -54,6 +55,13 @@ export class File extends BaseEntity {
@Column({ comment: '文件后缀' })
extension: string;
/**
*
* @example 1
*/
@Column({ comment: '类型(1: 文本2: 图片3: 音频4: 视频5: 其他)', nullable: true })
type: number;
/**
*
*/
@ -66,6 +74,8 @@ export class File extends BaseEntity {
* ID
* @example 0
*/
@Exclude()
@ApiHideProperty()
@Column({ comment: '分类ID', nullable: true })
categoryId: number;
}

View File

@ -1,33 +1,35 @@
import { ConfigService } from '@/config';
import { dayjs } from '@/libraries';
import { Module, forwardRef } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { TypeOrmModule } from '@nestjs/typeorm';
import { existsSync, mkdirSync } from 'fs';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
import { FileCategoryModule } from '../fileCategory';
import { File } from './entities/file.entity';
import { FileController } from './file.controller';
import { FileService } from './file.service';
import { dayjs } from '@/libraries';
import { FileCategoryModule } from '../fileCategory';
const MulteredModule = MulterModule.registerAsync({
useFactory: (config: ConfigService) => {
return {
storage: diskStorage({
destination: (req, file, next) => {
const dest = join(config.uploadDir, dayjs().format(dayjs.DATE));
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
next(null, dest);
},
filename: (req, file, next) => {
next(null, Date.now() + extname(file.originalname));
},
}),
};
},
useFactory: (config: ConfigService) => ({
storage: diskStorage({
destination: (req, file, next) => {
const dest = join(config.uploadDir, dayjs().format(dayjs.DATE));
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
next(null, dest);
},
filename: (req, file, next) => {
next(null, Date.now() + extname(file.originalname));
},
}),
fileFilter(req, file, callback) {
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf-8');
callback(null, true);
},
}),
inject: [ConfigService],
});

View File

@ -7,6 +7,9 @@ import { UpdateFileDto } from './dto/update-file.dto';
import { File } from './entities/file.entity';
import { FindFileDto } from './dto/find-file.dto';
import { FileCategoryService } from '../fileCategory';
import { createReadStream } from 'fs';
import { createHash } from 'crypto';
import { getTypeByMimetype } from './util';
@Injectable()
export class FileService extends BaseService {
@ -23,13 +26,17 @@ export class FileService extends BaseService {
* @returns
*/
async create(uploadFile: Express.Multer.File) {
const { originalname: name, mimetype, size, path: hash } = uploadFile;
const relativePath = relative(this.config.uploadDir, uploadFile.path).split(sep).join('/');
const path = `${this.config.uploadPrefix}/${relativePath}`;
const { originalname: name, mimetype, size } = uploadFile;
const { uploadDir, uploadPrefix } = this.config;
const relativePath = relative(uploadDir, uploadFile.path).split(sep).join('/');
const path = `${uploadPrefix}/${relativePath}`;
const extension = extname(uploadFile.originalname);
const description = '';
const hash = await this.hashByFilePath(uploadFile.path);
const type = getTypeByMimetype(mimetype)
const file = this.repository.create({
name,
type,
mimetype,
size,
hash,
@ -41,7 +48,28 @@ export class FileService extends BaseService {
return file.id;
}
findMany(findFileDto: FindFileDto) {
/**
* MD5
* @param path
* @returns
*/
hashByFilePath(path: string): Promise<string> {
const hash = createHash('md5');
const stream = createReadStream(path);
return new Promise((res, rej) => {
stream.on('data', (chunk) => {
hash.update(chunk);
});
stream.on('end', () => {
res(hash.digest('hex'));
});
stream.on('error', () => {
rej('获取文件哈希值失败');
});
});
}
async findMany(findFileDto: FindFileDto) {
const { page, size, name, categoryId } = findFileDto;
const { skip, take } = this.formatPagination(page, size, true);
const where: FindOptionsWhere<File> = {};
@ -51,7 +79,20 @@ export class FileService extends BaseService {
if (categoryId) {
where.categoryId = categoryId;
}
return this.repository.findAndCount({ skip, take, where });
return this.repository.findAndCount({
skip,
take,
where,
relations: {
category: true,
},
select: {
category: {
id: true,
name: true,
},
},
});
}
findOne(id: number) {

30
src/storage/file/util.ts Normal file
View File

@ -0,0 +1,30 @@
/**
*
*/
enum FileType {
/**
*
*/
TEXT = 1,
/**
*
*/
IMAGE = 2,
/**
*
*/
AUDIO = 3,
/**
*
*/
VIDEO = 4,
/**
*
*/
OTHER = 5,
}
export function getTypeByMimetype(mimetype: string) {
const [type] = mimetype.split('/');
return FileType[type.toLowerCase()] ?? FileType.OTHER;
}

View File

@ -29,5 +29,5 @@ export class CreateRoleDto {
*/
@IsOptional()
@IsInt({ each: true })
menuIds: number[];
menuIds?: number[];
}

View File

@ -43,6 +43,7 @@ export class Role extends BaseEntity {
/**
* ID
* @example [1]
*/
@RelationId('menus')
menuIds: number[];