260211:1703 First Deploy (Not Complete)
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2026-02-11 17:03:38 +07:00
parent d9df4e66b4
commit 912f379016
11 changed files with 98 additions and 91 deletions

View File

@@ -43,8 +43,8 @@ COPY backend/ ./backend/
# Build NestJS → backend/dist
RUN cd backend && pnpm run build
# Prune dev dependencies
RUN pnpm prune --prod --filter backend...
# Deploy with production deps only (pnpm workspace isolation)
RUN pnpm --filter backend deploy --prod --legacy /app/backend-prod
# =========================
# Stage 3: Production Runtime
@@ -65,8 +65,8 @@ RUN addgroup -g 1001 -S nestjs && \
# Copy production artifacts only
COPY --from=build --chown=nestjs:nestjs /app/backend/dist ./dist
COPY --from=build --chown=nestjs:nestjs /app/backend/node_modules ./node_modules
COPY --from=build --chown=nestjs:nestjs /app/backend/package.json ./
COPY --from=build --chown=nestjs:nestjs /app/backend-prod/node_modules ./node_modules
COPY --from=build --chown=nestjs:nestjs /app/backend-prod/package.json ./
# Create uploads directory (Two-Phase Storage)
RUN mkdir -p /app/uploads/temp /app/uploads/permanent && \

View File

@@ -56,6 +56,7 @@
"cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"express": "^5.1.0",
"fs-extra": "^11.3.2",
"helmet": "^8.1.0",
"ioredis": "^5.8.2",

View File

@@ -18,21 +18,26 @@ import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่
@Injectable()
export class FileStorageService {
private readonly logger = new Logger(FileStorageService.name);
private readonly uploadRoot: string;
private readonly tempDir: string;
private readonly permanentDir: string;
constructor(
@InjectRepository(Attachment)
private attachmentRepository: Repository<Attachment>,
private configService: ConfigService,
private configService: ConfigService
) {
// ใช้ Path จริงถ้าอยู่บน Server (Production) หรือใช้ ./uploads ถ้าอยู่ Local
this.uploadRoot =
this.configService.get('NODE_ENV') === 'production'
? '/share/dms-data'
: path.join(process.cwd(), 'uploads');
// ใช้ env vars จาก docker-compose สำหรับ Production
// ถ้าไม่ได้กำหนดจะ fallback เป็น ./uploads/temp และ ./uploads/permanent
this.tempDir =
this.configService.get<string>('UPLOAD_TEMP_DIR') ||
path.join(process.cwd(), 'uploads', 'temp');
this.permanentDir =
this.configService.get<string>('UPLOAD_PERMANENT_DIR') ||
path.join(process.cwd(), 'uploads', 'permanent');
// สร้างโฟลเดอร์ temp รอไว้เลยถ้ายังไม่มี
fs.ensureDirSync(path.join(this.uploadRoot, 'temp'));
// สร้างโฟลเดอร์ temp และ permanent รอไว้เลยถ้ายังไม่มี
fs.ensureDirSync(this.tempDir);
fs.ensureDirSync(this.permanentDir);
}
/**
@@ -42,7 +47,7 @@ export class FileStorageService {
const tempId = uuidv4();
const fileExt = path.extname(file.originalname);
const storedFilename = `${uuidv4()}${fileExt}`;
const tempPath = path.join(this.uploadRoot, 'temp', storedFilename);
const tempPath = path.join(this.tempDir, storedFilename);
// 1. คำนวณ Checksum (SHA-256) เพื่อความปลอดภัยและความถูกต้องของไฟล์
const checksum = this.calculateChecksum(file.buffer);
@@ -89,7 +94,7 @@ export class FileStorageService {
// แจ้งเตือนแต่อาจจะไม่ throw ถ้าต้องการให้ process ต่อไปได้บางส่วน (ขึ้นอยู่กับ business logic)
// แต่เพื่อความปลอดภัยควรแจ้งว่าไฟล์ไม่ครบ
this.logger.warn(
`Expected ${tempIds.length} files to commit, but found ${attachments.length}`,
`Expected ${tempIds.length} files to commit, but found ${attachments.length}`
);
throw new NotFoundException('Some files not found or already committed');
}
@@ -100,7 +105,7 @@ export class FileStorageService {
const month = (today.getMonth() + 1).toString().padStart(2, '0');
// โฟลเดอร์ถาวรแยกตาม ปี/เดือน
const permanentDir = path.join(this.uploadRoot, 'permanent', year, month);
const permanentDir = path.join(this.permanentDir, year, month);
await fs.ensureDir(permanentDir);
for (const att of attachments) {
@@ -122,16 +127,16 @@ export class FileStorageService {
} else {
this.logger.error(`File missing during commit: ${oldPath}`);
throw new NotFoundException(
`File not found on disk: ${att.originalFilename}`,
`File not found on disk: ${att.originalFilename}`
);
}
} catch (error) {
this.logger.error(
`Failed to move file from ${oldPath} to ${newPath}`,
error,
error
);
throw new BadRequestException(
`Failed to commit file: ${att.originalFilename}`,
`Failed to commit file: ${att.originalFilename}`
);
}
}
@@ -144,7 +149,7 @@ export class FileStorageService {
* ดึงไฟล์มาเป็น Stream เพื่อส่งกลับไปให้ Controller
*/
async download(
id: number,
id: number
): Promise<{ stream: fs.ReadStream; attachment: Attachment }> {
// 1. ค้นหาข้อมูลไฟล์จาก DB
const attachment = await this.attachmentRepository.findOne({
@@ -191,7 +196,7 @@ export class FileStorageService {
// (ในอนาคตอาจเพิ่มเงื่อนไข OR User เป็น Admin/Document Control)
if (attachment.uploadedByUserId !== userId) {
this.logger.warn(
`User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`,
`User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`
);
throw new ForbiddenException('You are not allowed to delete this file');
}
@@ -202,13 +207,13 @@ export class FileStorageService {
await fs.remove(attachment.filePath);
} else {
this.logger.warn(
`File not found on disk during deletion: ${attachment.filePath}`,
`File not found on disk during deletion: ${attachment.filePath}`
);
}
} catch (error) {
this.logger.error(
`Failed to delete file from disk: ${attachment.filePath}`,
error,
error
);
throw new BadRequestException('Failed to delete file from storage');
}

View File

@@ -1,7 +1,4 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from 'dotenv';
config();
export const databaseConfig: TypeOrmModuleOptions = {
type: 'mysql',

View File

@@ -48,8 +48,10 @@ async function bootstrap() {
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ extended: true, limit: '50mb' }));
// 🌐 4. Global Prefix
app.setGlobalPrefix('api');
// 🌐 4. Global Prefix (ยกเว้น /health, /metrics สำหรับ monitoring)
app.setGlobalPrefix('api', {
exclude: ['health', 'metrics'],
});
// ⚙️ 5. Global Pipes & Interceptors & Filters
app.useGlobalPipes(

View File

@@ -1,4 +1,11 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "documentation"]
"exclude": [
"node_modules",
"test",
"dist",
"scripts",
"**/*spec.ts",
"documentation"
]
}