elearning/Backend/.agent/workflowss/file-upload.md
2026-01-08 04:20:49 +00:00

12 KiB

description
How to handle file uploads (videos, attachments)

File Upload Workflow

Follow these steps to implement file upload functionality for videos and attachments.


Prerequisites

  • MinIO/S3 configured and running
  • Multer installed: npm install multer
  • AWS SDK or MinIO client installed

Step 1: Configure S3/MinIO Client

Create src/config/s3.config.js:

const { S3Client } = require('@aws-sdk/client-s3');
const { Upload } = require('@aws-sdk/lib-storage');

const s3Client = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region: process.env.S3_REGION || 'us-east-1',
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY,
    secretAccessKey: process.env.S3_SECRET_KEY
  },
  forcePathStyle: true // Required for MinIO
});

module.exports = { s3Client };

Step 2: Create Upload Service

Create src/services/upload.service.js:

const { s3Client } = require('../config/s3.config');
const { Upload } = require('@aws-sdk/lib-storage');
const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { v4: uuidv4 } = require('uuid');
const path = require('path');

class UploadService {
  async uploadFile(file, folder) {
    const fileExt = path.extname(file.originalname);
    const fileName = `${Date.now()}-${uuidv4()}${fileExt}`;
    const key = `${folder}/${fileName}`;

    const upload = new Upload({
      client: s3Client,
      params: {
        Bucket: this.getBucket(folder),
        Key: key,
        Body: file.buffer,
        ContentType: file.mimetype
      }
    });

    await upload.done();

    return {
      key,
      url: `${process.env.S3_ENDPOINT}/${this.getBucket(folder)}/${key}`,
      fileName: file.originalname,
      fileSize: file.size,
      mimeType: file.mimetype
    };
  }

  async deleteFile(key, folder) {
    const command = new DeleteObjectCommand({
      Bucket: this.getBucket(folder),
      Key: key
    });

    await s3Client.send(command);
  }

  getBucket(folder) {
    const bucketMap = {
      videos: process.env.S3_BUCKET_VIDEOS,
      documents: process.env.S3_BUCKET_DOCUMENTS,
      images: process.env.S3_BUCKET_IMAGES,
      attachments: process.env.S3_BUCKET_ATTACHMENTS
    };

    return bucketMap[folder] || process.env.S3_BUCKET_COURSES;
  }

  validateFileType(file, allowedTypes) {
    return allowedTypes.includes(file.mimetype);
  }

  validateFileSize(file, maxSize) {
    return file.size <= maxSize;
  }
}

module.exports = new UploadService();

Step 3: Create Upload Middleware

Create src/middleware/upload.middleware.js:

const multer = require('multer');

// File type validators
const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'];
const ALLOWED_DOCUMENT_TYPES = [
  'application/pdf',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'application/vnd.ms-excel',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];

// File size limits
const MAX_VIDEO_SIZE = 500 * 1024 * 1024; // 500 MB
const MAX_ATTACHMENT_SIZE = 100 * 1024 * 1024; // 100 MB
const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10 MB

// Multer configuration
const storage = multer.memoryStorage();

const fileFilter = (allowedTypes) => (req, file, cb) => {
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type'), false);
  }
};

// Upload configurations
const uploadVideo = multer({
  storage,
  limits: { fileSize: MAX_VIDEO_SIZE },
  fileFilter: fileFilter(ALLOWED_VIDEO_TYPES)
}).single('video');

const uploadAttachment = multer({
  storage,
  limits: { fileSize: MAX_ATTACHMENT_SIZE },
  fileFilter: fileFilter([...ALLOWED_DOCUMENT_TYPES, ...ALLOWED_IMAGE_TYPES])
}).single('file');

const uploadAttachments = multer({
  storage,
  limits: { fileSize: MAX_ATTACHMENT_SIZE },
  fileFilter: fileFilter([...ALLOWED_DOCUMENT_TYPES, ...ALLOWED_IMAGE_TYPES])
}).array('attachments', 10); // Max 10 files

const uploadImage = multer({
  storage,
  limits: { fileSize: MAX_IMAGE_SIZE },
  fileFilter: fileFilter(ALLOWED_IMAGE_TYPES)
}).single('image');

module.exports = {
  uploadVideo,
  uploadAttachment,
  uploadAttachments,
  uploadImage,
  ALLOWED_VIDEO_TYPES,
  ALLOWED_DOCUMENT_TYPES,
  ALLOWED_IMAGE_TYPES
};

Step 4: Create Upload Controller

Create src/controllers/upload.controller.js:

const uploadService = require('../services/upload.service');

class UploadController {
  async uploadVideo(req, res) {
    try {
      if (!req.file) {
        return res.status(400).json({
          error: {
            code: 'NO_FILE',
            message: 'No video file provided'
          }
        });
      }

      const result = await uploadService.uploadFile(
        req.file,
        'videos'
      );

      return res.status(200).json({
        video_url: result.url,
        file_size: result.fileSize,
        duration: null // Will be processed later
      });
    } catch (error) {
      console.error('Upload video error:', error);
      return res.status(500).json({
        error: {
          code: 'UPLOAD_FAILED',
          message: 'Failed to upload video'
        }
      });
    }
  }

  async uploadAttachment(req, res) {
    try {
      if (!req.file) {
        return res.status(400).json({
          error: {
            code: 'NO_FILE',
            message: 'No file provided'
          }
        });
      }

      const result = await uploadService.uploadFile(
        req.file,
        'attachments'
      );

      return res.status(200).json({
        file_name: result.fileName,
        file_path: result.key,
        file_size: result.fileSize,
        mime_type: result.mimeType,
        download_url: result.url
      });
    } catch (error) {
      console.error('Upload attachment error:', error);
      return res.status(500).json({
        error: {
          code: 'UPLOAD_FAILED',
          message: 'Failed to upload file'
        }
      });
    }
  }
}

module.exports = new UploadController();

Step 5: Create Upload Routes

Create src/routes/upload.routes.js:

const express = require('express');
const router = express.Router();
const uploadController = require('../controllers/upload.controller');
const { authenticate, authorize } = require('../middleware/auth');
const { uploadVideo, uploadAttachment } = require('../middleware/upload.middleware');

// Video upload
router.post(
  '/video',
  authenticate,
  authorize(['INSTRUCTOR', 'ADMIN']),
  (req, res, next) => {
    uploadVideo(req, res, (err) => {
      if (err) {
        if (err.message === 'Invalid file type') {
          return res.status(422).json({
            error: {
              code: 'INVALID_FILE_TYPE',
              message: 'Only MP4, WebM, and QuickTime videos are allowed'
            }
          });
        }
        if (err.code === 'LIMIT_FILE_SIZE') {
          return res.status(422).json({
            error: {
              code: 'FILE_TOO_LARGE',
              message: 'Video file size exceeds 500 MB limit'
            }
          });
        }
        return res.status(500).json({
          error: {
            code: 'UPLOAD_ERROR',
            message: err.message
          }
        });
      }
      next();
    });
  },
  uploadController.uploadVideo
);

// Attachment upload
router.post(
  '/attachment',
  authenticate,
  authorize(['INSTRUCTOR', 'ADMIN']),
  (req, res, next) => {
    uploadAttachment(req, res, (err) => {
      if (err) {
        if (err.message === 'Invalid file type') {
          return res.status(422).json({
            error: {
              code: 'INVALID_FILE_TYPE',
              message: 'File type not allowed'
            }
          });
        }
        if (err.code === 'LIMIT_FILE_SIZE') {
          return res.status(422).json({
            error: {
              code: 'FILE_TOO_LARGE',
              message: 'File size exceeds 100 MB limit'
            }
          });
        }
        return res.status(500).json({
          error: {
            code: 'UPLOAD_ERROR',
            message: err.message
          }
        });
      }
      next();
    });
  },
  uploadController.uploadAttachment
);

module.exports = router;

Step 6: Integrate with Lesson Creation

Update lesson controller to handle file uploads:

async createLesson(req, res) {
  try {
    const { courseId, chapterId } = req.params;
    
    // Create lesson
    const lesson = await lessonService.create({
      ...req.body,
      chapter_id: parseInt(chapterId)
    });

    // Handle video upload if present
    if (req.files?.video) {
      const videoResult = await uploadService.uploadFile(
        req.files.video[0],
        `courses/${courseId}/lessons/${lesson.id}`
      );
      
      await lessonService.update(lesson.id, {
        video_url: videoResult.url
      });
    }

    // Handle attachments if present
    if (req.files?.attachments) {
      const attachments = await Promise.all(
        req.files.attachments.map(async (file, index) => {
          const result = await uploadService.uploadFile(
            file,
            `lessons/${lesson.id}/attachments`
          );
          
          return {
            lesson_id: lesson.id,
            file_name: result.fileName,
            file_path: result.key,
            file_size: result.fileSize,
            mime_type: result.mimeType,
            description: req.body.descriptions?.[index] || {},
            sort_order: index + 1
          };
        })
      );

      await prisma.attachment.createMany({
        data: attachments
      });
    }

    return res.status(201).json(lesson);
  } catch (error) {
    console.error('Create lesson error:', error);
    return res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'Failed to create lesson'
      }
    });
  }
}

Step 7: Test File Upload

# Upload video
curl -X POST http://localhost:4000/api/upload/video \
  -H "Authorization: Bearer <token>" \
  -F "video=@/path/to/video.mp4"

# Upload attachment
curl -X POST http://localhost:4000/api/upload/attachment \
  -H "Authorization: Bearer <token>" \
  -F "file=@/path/to/document.pdf"

Error Handling

Common Errors

  1. File Too Large
{
  "error": {
    "code": "FILE_TOO_LARGE",
    "message": "File size exceeds maximum limit"
  }
}
  1. Invalid File Type
{
  "error": {
    "code": "INVALID_FILE_TYPE",
    "message": "File type not allowed"
  }
}
  1. S3 Upload Failed
{
  "error": {
    "code": "UPLOAD_FAILED",
    "message": "Failed to upload file to storage"
  }
}

Checklist

  • S3/MinIO client configured
  • Upload service created
  • Upload middleware configured
  • File type validation implemented
  • File size validation implemented
  • Upload routes created
  • Error handling implemented
  • File deletion implemented
  • Tests written
  • Manual testing done

Best Practices

  1. Validate Before Upload: Check file type and size before uploading
  2. Unique Filenames: Use UUID to prevent filename conflicts
  3. Organized Storage: Use folder structure (courses/1/lessons/5/video.mp4)
  4. Error Handling: Provide clear error messages
  5. Cleanup: Delete files from S3 when deleting records
  6. Security: Validate file content, not just extension
  7. Progress: Consider upload progress for large files

Advanced: Video Processing

For video processing (transcoding, thumbnails):

const ffmpeg = require('fluent-ffmpeg');

async function processVideo(videoPath) {
  // Generate thumbnail
  await new Promise((resolve, reject) => {
    ffmpeg(videoPath)
      .screenshots({
        timestamps: ['00:00:01'],
        filename: 'thumbnail.jpg',
        folder: './thumbnails'
      })
      .on('end', resolve)
      .on('error', reject);
  });

  // Get video duration
  const metadata = await new Promise((resolve, reject) => {
    ffmpeg.ffprobe(videoPath, (err, metadata) => {
      if (err) reject(err);
      else resolve(metadata);
    });
  });

  return {
    duration: metadata.format.duration,
    thumbnail: './thumbnails/thumbnail.jpg'
  };
}