|
@@ -0,0 +1,171 @@
|
|
|
+import { Injectable } from '@nestjs/common';
|
|
|
+import { ConfigService } from '@nestjs/config';
|
|
|
+import { InjectRepository } from '@nestjs/typeorm';
|
|
|
+import { Repository } from 'typeorm';
|
|
|
+import * as fs from 'fs';
|
|
|
+import * as path from 'path';
|
|
|
+import * as crypto from 'crypto';
|
|
|
+import { CreateFileDto } from './dto/create-file.dto';
|
|
|
+import { UpdateFileDto } from './dto/update-file.dto';
|
|
|
+import { File } from './entities/file.entity';
|
|
|
+
|
|
|
+interface UploadedFile {
|
|
|
+ originalname?: string;
|
|
|
+ buffer: Buffer;
|
|
|
+ size?: number;
|
|
|
+ mimetype?: string;
|
|
|
+}
|
|
|
+
|
|
|
+@Injectable()
|
|
|
+export class FileService {
|
|
|
+ constructor(
|
|
|
+ private readonly configService: ConfigService,
|
|
|
+ @InjectRepository(File)
|
|
|
+ private readonly fileRepository: Repository<File>,
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ async create(createFileDto: CreateFileDto): Promise<File> {
|
|
|
+ const file = this.fileRepository.create(createFileDto);
|
|
|
+ return await this.fileRepository.save(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ async findAll(): Promise<File[]> {
|
|
|
+ return await this.fileRepository.find();
|
|
|
+ }
|
|
|
+
|
|
|
+ async findOne(id: number): Promise<File> {
|
|
|
+ const file = await this.fileRepository.findOne({ where: { id } });
|
|
|
+ if (!file) {
|
|
|
+ throw new Error(`文件ID ${id} 不存在`);
|
|
|
+ }
|
|
|
+ return file;
|
|
|
+ }
|
|
|
+
|
|
|
+ async findByFileId(fileId: string): Promise<File> {
|
|
|
+ const file = await this.fileRepository.findOne({ where: { fileId } });
|
|
|
+ if (!file) {
|
|
|
+ throw new Error(`文件fileId ${fileId} 不存在`);
|
|
|
+ }
|
|
|
+ return file;
|
|
|
+ }
|
|
|
+
|
|
|
+ async update(id: number, updateFileDto: UpdateFileDto): Promise<File> {
|
|
|
+ const file = await this.findOne(id);
|
|
|
+ Object.assign(file, updateFileDto);
|
|
|
+ return await this.fileRepository.save(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ async remove(id: number): Promise<void> {
|
|
|
+ const file = await this.findOne(id);
|
|
|
+ await this.fileRepository.remove(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据hash生成UUID
|
|
|
+ private generateFileId(hash: string): string {
|
|
|
+ // 使用hash的前16个字符作为种子生成UUID
|
|
|
+ const hashSeed = hash.substring(0, 16);
|
|
|
+ const uuid = crypto.randomUUID();
|
|
|
+ // 将hash种子与UUID结合,确保基于hash的一致性
|
|
|
+ return crypto
|
|
|
+ .createHash('md5')
|
|
|
+ .update(hashSeed + uuid)
|
|
|
+ .digest('hex')
|
|
|
+ .replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5');
|
|
|
+ }
|
|
|
+
|
|
|
+ async uploadFile(file: Express.Multer.File) {
|
|
|
+ if (!file) {
|
|
|
+ throw new Error('没有上传文件');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从环境变量获取文件存储路径
|
|
|
+ const filePath = this.configService.get<string>('FILE_PATH', './uploads');
|
|
|
+
|
|
|
+ // 确保存储目录存在
|
|
|
+ if (!fs.existsSync(filePath)) {
|
|
|
+ fs.mkdirSync(filePath, { recursive: true });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 类型安全的文件处理
|
|
|
+ const uploadedFile = file as UploadedFile;
|
|
|
+
|
|
|
+ // 生成文件hash
|
|
|
+ const fileHash = crypto
|
|
|
+ .createHash('sha256')
|
|
|
+ .update(uploadedFile.buffer)
|
|
|
+ .digest('hex');
|
|
|
+
|
|
|
+ // 检查文件是否已存在(基于hash)
|
|
|
+ const existingFile = await this.fileRepository.findOne({
|
|
|
+ where: { fileHash },
|
|
|
+ });
|
|
|
+ if (existingFile) {
|
|
|
+ // 从环境变量获取服务器地址
|
|
|
+ const serverUrl = this.configService.get<string>(
|
|
|
+ 'SERVER_URL',
|
|
|
+ 'http://localhost:3000',
|
|
|
+ );
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: '文件已存在',
|
|
|
+ fileId: existingFile.fileId,
|
|
|
+ url: `${serverUrl}/file/download/${existingFile.fileId}`,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成唯一文件名
|
|
|
+ const timestamp = Date.now();
|
|
|
+ const originalName = uploadedFile.originalname || 'unknown';
|
|
|
+ const extension = path.extname(originalName);
|
|
|
+ const fileName = `${timestamp}_${path.basename(originalName, extension)}${extension}`;
|
|
|
+
|
|
|
+ // 相对存储路径
|
|
|
+ const relativePath = path.join('uploads', fileName);
|
|
|
+
|
|
|
+ // 完整的文件存储路径
|
|
|
+ const fullFilePath = path.join(filePath, fileName);
|
|
|
+
|
|
|
+ // 写入文件
|
|
|
+ await fs.promises.writeFile(fullFilePath, uploadedFile.buffer);
|
|
|
+
|
|
|
+ // 生成fileId
|
|
|
+ const fileId = this.generateFileId(fileHash);
|
|
|
+
|
|
|
+ // 创建文件记录
|
|
|
+ const fileEntity = this.fileRepository.create({
|
|
|
+ fileId,
|
|
|
+ fileName: path.basename(originalName, extension),
|
|
|
+ fileType: extension.replace('.', ''),
|
|
|
+ fileHash,
|
|
|
+ fileSize: uploadedFile.size || 0,
|
|
|
+ mimeType: uploadedFile.mimetype || 'application/octet-stream',
|
|
|
+ storagePath: relativePath,
|
|
|
+ uploadSource: 'web_upload',
|
|
|
+ originalName,
|
|
|
+ });
|
|
|
+
|
|
|
+ const savedFile = await this.fileRepository.save(fileEntity);
|
|
|
+
|
|
|
+ // 从环境变量获取服务器地址
|
|
|
+ const serverUrl = this.configService.get<string>(
|
|
|
+ 'SERVER_URL',
|
|
|
+ 'http://localhost:3000',
|
|
|
+ );
|
|
|
+
|
|
|
+ // 返回文件信息
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: '文件上传成功',
|
|
|
+ fileId: savedFile.fileId,
|
|
|
+ url: `${serverUrl}/file/download/${savedFile.fileId}`,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async findByHash(fileHash: string): Promise<File | null> {
|
|
|
+ return await this.fileRepository.findOne({ where: { fileHash } });
|
|
|
+ }
|
|
|
+
|
|
|
+ async findByType(fileType: string): Promise<File[]> {
|
|
|
+ return await this.fileRepository.find({ where: { fileType } });
|
|
|
+ }
|
|
|
+}
|