feat: add dedicated thumbnail upload endpoint for courses with old file cleanup and presigned URL generation.

This commit is contained in:
JakkrapartXD 2026-01-28 16:46:54 +07:00
parent b0383b78e9
commit b303c50865
2 changed files with 78 additions and 15 deletions

View file

@ -111,6 +111,29 @@ export class CoursesInstructorController {
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)

View file

@ -182,7 +182,7 @@ export class CoursesInstructorService {
static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise<createCourseResponse> {
try {
const courseInstructorId = await this.validateCourseInstructor(token, courseId);
await this.validateCourseInstructor(token, courseId);
const course = await prisma.course.update({
where: {
@ -191,23 +191,10 @@ export class CoursesInstructorService {
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 {
code: 200,
message: 'Course updated successfully',
data: {
...course,
thumbnail_url: thumbnail_presigned_url,
}
data: course
};
} catch (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> {
try {
const courseInstructorId = await this.validateCourseInstructor(token, courseId);