251124:1700 Ready to Phase 7

This commit is contained in:
admin
2025-11-24 17:01:58 +07:00
parent 3d9b6e4d05
commit cb6faacba6
47 changed files with 2047 additions and 433 deletions

View File

@@ -1,3 +1,4 @@
// File: src/common/file-storage/file-storage.service.ts
import {
Injectable,
NotFoundException,
@@ -12,6 +13,7 @@ import * as path from 'path';
import * as crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid';
import { Attachment } from './entities/attachment.entity.js';
import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่ม
@Injectable()
export class FileStorageService {
@@ -29,7 +31,7 @@ export class FileStorageService {
? '/share/dms-data'
: path.join(process.cwd(), 'uploads');
// สร้างโฟลเดอร์รอไว้เลยถ้ายังไม่มี
// สร้างโฟลเดอร์ temp รอไว้เลยถ้ายังไม่มี
fs.ensureDirSync(path.join(this.uploadRoot, 'temp'));
}
@@ -75,11 +77,20 @@ export class FileStorageService {
* เมธอดนี้จะถูกเรียกโดย Service อื่น (เช่น CorrespondenceService) เมื่อกด Save
*/
async commit(tempIds: string[]): Promise<Attachment[]> {
if (!tempIds || tempIds.length === 0) {
return [];
}
const attachments = await this.attachmentRepository.find({
where: { tempId: In(tempIds), isTemporary: true },
});
if (attachments.length !== tempIds.length) {
// แจ้งเตือนแต่อาจจะไม่ throw ถ้าต้องการให้ process ต่อไปได้บางส่วน (ขึ้นอยู่กับ business logic)
// แต่เพื่อความปลอดภัยควรแจ้งว่าไฟล์ไม่ครบ
this.logger.warn(
`Expected ${tempIds.length} files to commit, but found ${attachments.length}`,
);
throw new NotFoundException('Some files not found or already committed');
}
@@ -98,21 +109,27 @@ export class FileStorageService {
try {
// ย้ายไฟล์
await fs.move(oldPath, newPath, { overwrite: true });
if (await fs.pathExists(oldPath)) {
await fs.move(oldPath, newPath, { overwrite: true });
// อัปเดตข้อมูลใน DB
att.filePath = newPath;
att.isTemporary = false;
att.tempId = undefined; // เคลียร์ tempId
att.expiresAt = undefined; // เคลียร์วันหมดอายุ
// อัปเดตข้อมูลใน DB
att.filePath = newPath;
att.isTemporary = false;
att.tempId = null as any; // เคลียร์ tempId (TypeORM อาจต้องการ null แทน undefined สำหรับ nullable)
att.expiresAt = null as any; // เคลียร์วันหมดอายุ
committedAttachments.push(await this.attachmentRepository.save(att));
committedAttachments.push(await this.attachmentRepository.save(att));
} else {
this.logger.error(`File missing during commit: ${oldPath}`);
throw new NotFoundException(
`File not found on disk: ${att.originalFilename}`,
);
}
} catch (error) {
this.logger.error(
`Failed to move file from ${oldPath} to ${newPath}`,
error,
);
// ถ้า error ตัวนึง ควรจะ rollback หรือ throw error (ในที่นี้ throw เพื่อให้ Transaction ของผู้เรียกจัดการ)
throw new BadRequestException(
`Failed to commit file: ${att.originalFilename}`,
);
@@ -122,7 +139,83 @@ export class FileStorageService {
return committedAttachments;
}
/**
* Download File
* ดึงไฟล์มาเป็น Stream เพื่อส่งกลับไปให้ Controller
*/
async download(
id: number,
): Promise<{ stream: fs.ReadStream; attachment: Attachment }> {
// 1. ค้นหาข้อมูลไฟล์จาก DB
const attachment = await this.attachmentRepository.findOne({
where: { id },
});
if (!attachment) {
throw new NotFoundException(`Attachment #${id} not found`);
}
// 2. ตรวจสอบว่าไฟล์มีอยู่จริงบน Disk หรือไม่
const filePath = attachment.filePath;
if (!fs.existsSync(filePath)) {
this.logger.error(`File missing on disk: ${filePath}`);
throw new NotFoundException('File not found on server storage');
}
// 3. สร้าง Read Stream (มีประสิทธิภาพกว่าการโหลดทั้งไฟล์เข้า Memory)
const stream = fs.createReadStream(filePath);
return { stream, attachment };
}
private calculateChecksum(buffer: Buffer): string {
return crypto.createHash('sha256').update(buffer).digest('hex');
}
/**
* ✅ NEW: Delete File
* ลบไฟล์ออกจาก Disk และ Database
*/
async delete(id: number, userId: number): Promise<void> {
// 1. ค้นหาไฟล์
const attachment = await this.attachmentRepository.findOne({
where: { id },
});
if (!attachment) {
throw new NotFoundException(`Attachment #${id} not found`);
}
// 2. ตรวจสอบความเป็นเจ้าของ (Security Check)
// อนุญาตให้ลบถ้าเป็นคนอัปโหลดเอง
// (ในอนาคตอาจเพิ่มเงื่อนไข OR User เป็น Admin/Document Control)
if (attachment.uploadedByUserId !== userId) {
this.logger.warn(
`User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`,
);
throw new ForbiddenException('You are not allowed to delete this file');
}
// 3. ลบไฟล์ออกจาก Disk
try {
if (await fs.pathExists(attachment.filePath)) {
await fs.remove(attachment.filePath);
} else {
this.logger.warn(
`File not found on disk during deletion: ${attachment.filePath}`,
);
}
} catch (error) {
this.logger.error(
`Failed to delete file from disk: ${attachment.filePath}`,
error,
);
throw new BadRequestException('Failed to delete file from storage');
}
// 4. ลบ Record ออกจาก Database
await this.attachmentRepository.remove(attachment);
this.logger.log(`File deleted: ${id} by user ${userId}`);
}
}