690525:1320 ADR-028-228-migration #06
CI / CD Pipeline / build (push) Successful in 4m18s
CI / CD Pipeline / deploy (push) Successful in 7m41s

This commit is contained in:
2026-05-25 13:20:17 +07:00
parent dcd1a9855e
commit 001237ea35
18 changed files with 967 additions and 128 deletions
@@ -46,6 +46,13 @@ describe('FileStorageService', () => {
find: jest.fn(),
findOne: jest.fn(),
remove: jest.fn(),
createQueryBuilder: jest.fn(() => ({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(null),
})),
},
},
{
@@ -56,19 +56,36 @@ export class FileStorageService {
/**
* Phase 1: Upload (บันทึกไฟล์ลง Temp)
* Idempotent: ถ้าไฟล์ checksum เดิมยังอยู่ใน temp (ไม่หมดอายุ) จะคืน record เดิมแทน
* ป้องกัน orphan files จาก n8n retry
*/
async upload(file: Express.Multer.File, userId: number): Promise<Attachment> {
const tempId = uuidv4();
// Fix: แปลงชื่อไฟล์จาก Latin1 → UTF-8 (Multer/busboy decodes as Latin1 by default)
const originalFilename = this.fixMulterFilename(file.originalname);
const fileExt = path.extname(originalFilename);
const storedFilename = `${uuidv4()}${fileExt}`;
const tempPath = path.join(this.tempDir, storedFilename);
// 1. คำนวณ Checksum (SHA-256) เพื่อความปลอดภัยและความถูกต้องของไฟล์
const checksum = this.calculateChecksum(file.buffer);
// 2. บันทึกไฟล์ลง Disk (Temp Folder)
// 2. Checksum-based dedup — ถ้า temp attachment เดิมยังไม่หมดอายุ คืน record เดิมทันที
const existing = await this.attachmentRepository
.createQueryBuilder('a')
.where('a.checksum = :checksum', { checksum })
.andWhere('a.isTemporary = :isTemp', { isTemp: true })
.andWhere('a.uploadedByUserId = :userId', { userId })
.andWhere('a.expiresAt > :now', { now: new Date() })
.getOne();
if (existing) {
this.logger.log(
`Dedup upload: returning existing temp attachment publicId=${existing.publicId} checksum=${checksum}`
);
return existing;
}
const fileExt = path.extname(originalFilename);
const storedFilename = `${uuidv4()}${fileExt}`;
const tempPath = path.join(this.tempDir, storedFilename);
// 3. บันทึกไฟล์ลง Disk (Temp Folder)
try {
await fs.writeFile(tempPath, file.buffer);
} catch (error) {
@@ -76,7 +93,7 @@ export class FileStorageService {
throw new BadRequestException('File upload failed');
}
// 3. สร้าง Record ใน Database
// 4. สร้าง Record ใน Database
const attachment = this.attachmentRepository.create({
originalFilename,
storedFilename: storedFilename,
@@ -84,7 +101,7 @@ export class FileStorageService {
mimeType: file.mimetype,
fileSize: file.size,
isTemporary: true,
tempId: tempId,
tempId: uuidv4(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // หมดอายุใน 24 ชม.
checksum: checksum,
uploadedByUserId: userId,