251124:1700 Ready to Phase 7
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user