feat: add dedicated thumbnail upload endpoint for courses with old file cleanup and presigned URL generation.
This commit is contained in:
parent
b0383b78e9
commit
b303c50865
2 changed files with 78 additions and 15 deletions
|
|
@ -111,6 +111,29 @@ export class CoursesInstructorController {
|
||||||
return await CoursesInstructorService.createCourse(value, decoded.id, thumbnail);
|
return await CoursesInstructorService.createCourse(value, decoded.id, thumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* อัปโหลดรูป thumbnail ของคอร์ส
|
||||||
|
* Upload course thumbnail image
|
||||||
|
* @param courseId - รหัสคอร์ส / Course ID
|
||||||
|
* @param file - ไฟล์รูปภาพ / Image file
|
||||||
|
*/
|
||||||
|
@Post('{courseId}/thumbnail')
|
||||||
|
@Security('jwt', ['instructor'])
|
||||||
|
@SuccessResponse('200', 'Thumbnail uploaded successfully')
|
||||||
|
@Response('401', 'Invalid or expired token')
|
||||||
|
@Response('400', 'Validation error')
|
||||||
|
public async uploadThumbnail(
|
||||||
|
@Request() request: any,
|
||||||
|
@Path() courseId: number,
|
||||||
|
@UploadedFile() file: Express.Multer.File
|
||||||
|
): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) throw new ValidationError('No token provided');
|
||||||
|
if (!file.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed');
|
||||||
|
|
||||||
|
return await CoursesInstructorService.uploadThumbnail(token, courseId, file);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบคอร์ส (เฉพาะผู้สอนหลักเท่านั้น)
|
* ลบคอร์ส (เฉพาะผู้สอนหลักเท่านั้น)
|
||||||
* Delete a course (only primary instructor can delete)
|
* Delete a course (only primary instructor can delete)
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ export class CoursesInstructorService {
|
||||||
|
|
||||||
static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise<createCourseResponse> {
|
static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise<createCourseResponse> {
|
||||||
try {
|
try {
|
||||||
const courseInstructorId = await this.validateCourseInstructor(token, courseId);
|
await this.validateCourseInstructor(token, courseId);
|
||||||
|
|
||||||
const course = await prisma.course.update({
|
const course = await prisma.course.update({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -191,23 +191,10 @@ export class CoursesInstructorService {
|
||||||
data: courseData
|
data: courseData
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate presigned URL for thumbnail
|
|
||||||
let thumbnail_presigned_url: string | null = null;
|
|
||||||
if (course.thumbnail_url) {
|
|
||||||
try {
|
|
||||||
thumbnail_presigned_url = await getPresignedUrl(course.thumbnail_url, 3600);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'Course updated successfully',
|
message: 'Course updated successfully',
|
||||||
data: {
|
data: course
|
||||||
...course,
|
|
||||||
thumbnail_url: thumbnail_presigned_url,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update course', { error });
|
logger.error('Failed to update course', { error });
|
||||||
|
|
@ -215,6 +202,59 @@ export class CoursesInstructorService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async uploadThumbnail(token: string, courseId: number, file: Express.Multer.File): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> {
|
||||||
|
try {
|
||||||
|
await this.validateCourseInstructor(token, courseId);
|
||||||
|
|
||||||
|
// Get current course to check for existing thumbnail
|
||||||
|
const currentCourse = await prisma.course.findUnique({
|
||||||
|
where: { id: courseId }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete old thumbnail if exists
|
||||||
|
if (currentCourse?.thumbnail_url) {
|
||||||
|
try {
|
||||||
|
await deleteFile(currentCourse.thumbnail_url);
|
||||||
|
logger.info(`Deleted old thumbnail: ${currentCourse.thumbnail_url}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to delete old thumbnail: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const uniqueId = Math.random().toString(36).substring(2, 15);
|
||||||
|
const extension = file.originalname.split('.').pop() || 'jpg';
|
||||||
|
const safeFilename = `${timestamp}-${uniqueId}.${extension}`;
|
||||||
|
const filePath = `courses/${courseId}/thumbnail/${safeFilename}`;
|
||||||
|
|
||||||
|
// Upload to MinIO
|
||||||
|
await uploadFile(filePath, file.buffer, file.mimetype || 'image/jpeg');
|
||||||
|
logger.info(`Uploaded thumbnail: ${filePath}`);
|
||||||
|
|
||||||
|
// Update course with new thumbnail path
|
||||||
|
await prisma.course.update({
|
||||||
|
where: { id: courseId },
|
||||||
|
data: { thumbnail_url: filePath }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate presigned URL for response
|
||||||
|
const presignedUrl = await getPresignedUrl(filePath, 3600);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Thumbnail uploaded successfully',
|
||||||
|
data: {
|
||||||
|
course_id: courseId,
|
||||||
|
thumbnail_url: presignedUrl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to upload thumbnail', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async deleteCourse(token: string, courseId: number): Promise<createCourseResponse> {
|
static async deleteCourse(token: string, courseId: number): Promise<createCourseResponse> {
|
||||||
try {
|
try {
|
||||||
const courseInstructorId = await this.validateCourseInstructor(token, courseId);
|
const courseInstructorId = await this.validateCourseInstructor(token, courseId);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue