migration to typescript
This commit is contained in:
parent
924000b084
commit
9fde77468a
41 changed files with 11952 additions and 10164 deletions
|
|
@ -4,54 +4,61 @@ description: How to handle file uploads (videos, attachments)
|
|||
|
||||
# File Upload Workflow
|
||||
|
||||
Follow these steps to implement file upload functionality for videos and attachments.
|
||||
Follow these steps to implement file upload functionality for videos and attachments using TypeScript and TSOA.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- MinIO/S3 configured and running
|
||||
- Multer installed: `npm install multer`
|
||||
- AWS SDK or MinIO client installed
|
||||
- Dependencies installed: `npm install multer @aws-sdk/client-s3 @aws-sdk/lib-storage`
|
||||
- TypeScript configured
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Configure S3/MinIO Client
|
||||
|
||||
Create `src/config/s3.config.js`:
|
||||
Create `src/config/s3.config.ts`:
|
||||
|
||||
```javascript
|
||||
const { S3Client } = require('@aws-sdk/client-s3');
|
||||
const { Upload } = require('@aws-sdk/lib-storage');
|
||||
```typescript
|
||||
import { S3Client } from '@aws-sdk/client-s3';
|
||||
|
||||
const s3Client = new S3Client({
|
||||
export 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
|
||||
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`:
|
||||
Create `src/services/upload.service.ts`:
|
||||
|
||||
```javascript
|
||||
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');
|
||||
```typescript
|
||||
import { s3Client } from '../config/s3.config';
|
||||
import { Upload } from '@aws-sdk/lib-storage';
|
||||
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import path from 'path';
|
||||
|
||||
export interface UploadResult {
|
||||
key: string;
|
||||
url: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
type FolderType = 'videos' | 'documents' | 'images' | 'attachments';
|
||||
|
||||
class UploadService {
|
||||
async uploadFile(file, folder) {
|
||||
async uploadFile(file: Express.Multer.File, folder: FolderType): Promise<UploadResult> {
|
||||
const fileExt = path.extname(file.originalname);
|
||||
const fileName = `${Date.now()}-${uuidv4()}${fileExt}`;
|
||||
const key = `${folder}/${fileName}`;
|
||||
|
|
@ -77,7 +84,7 @@ class UploadService {
|
|||
};
|
||||
}
|
||||
|
||||
async deleteFile(key, folder) {
|
||||
async deleteFile(key: string, folder: FolderType): Promise<void> {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: this.getBucket(folder),
|
||||
Key: key
|
||||
|
|
@ -86,179 +93,182 @@ class UploadService {
|
|||
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
|
||||
private getBucket(folder: FolderType): string {
|
||||
const bucketMap: Record<FolderType, string> = {
|
||||
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;
|
||||
return bucketMap[folder] || process.env.S3_BUCKET_COURSES!;
|
||||
}
|
||||
|
||||
validateFileType(file, allowedTypes) {
|
||||
validateFileType(file: Express.Multer.File, allowedTypes: string[]): boolean {
|
||||
return allowedTypes.includes(file.mimetype);
|
||||
}
|
||||
|
||||
validateFileSize(file, maxSize) {
|
||||
validateFileSize(file: Express.Multer.File, maxSize: number): boolean {
|
||||
return file.size <= maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new UploadService();
|
||||
export const uploadService = new UploadService();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create Upload Middleware
|
||||
|
||||
Create `src/middleware/upload.middleware.js`:
|
||||
Create `src/middleware/upload.middleware.ts`:
|
||||
|
||||
```javascript
|
||||
const multer = require('multer');
|
||||
```typescript
|
||||
import multer from 'multer';
|
||||
import { Request } from 'express';
|
||||
|
||||
// File type validators
|
||||
const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'];
|
||||
const ALLOWED_DOCUMENT_TYPES = [
|
||||
export const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'];
|
||||
export 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'];
|
||||
export 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
|
||||
export const MAX_VIDEO_SIZE = 500 * 1024 * 1024; // 500 MB
|
||||
export const MAX_ATTACHMENT_SIZE = 100 * 1024 * 1024; // 100 MB
|
||||
export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
// Multer configuration
|
||||
const storage = multer.memoryStorage();
|
||||
|
||||
const fileFilter = (allowedTypes) => (req, file, cb) => {
|
||||
const fileFilter = (allowedTypes: string[]) => (
|
||||
req: Request,
|
||||
file: Express.Multer.File,
|
||||
cb: multer.FileFilterCallback
|
||||
) => {
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type'), false);
|
||||
cb(new Error('Invalid file type'));
|
||||
}
|
||||
};
|
||||
|
||||
// Upload configurations
|
||||
const uploadVideo = multer({
|
||||
export const uploadVideo = multer({
|
||||
storage,
|
||||
limits: { fileSize: MAX_VIDEO_SIZE },
|
||||
fileFilter: fileFilter(ALLOWED_VIDEO_TYPES)
|
||||
}).single('video');
|
||||
|
||||
const uploadAttachment = multer({
|
||||
export const uploadAttachment = multer({
|
||||
storage,
|
||||
limits: { fileSize: MAX_ATTACHMENT_SIZE },
|
||||
fileFilter: fileFilter([...ALLOWED_DOCUMENT_TYPES, ...ALLOWED_IMAGE_TYPES])
|
||||
}).single('file');
|
||||
|
||||
const uploadAttachments = multer({
|
||||
export 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({
|
||||
export 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
|
||||
## Step 4: Create Upload Controller with TSOA
|
||||
|
||||
Create `src/controllers/upload.controller.js`:
|
||||
Create `src/controllers/upload.controller.ts`:
|
||||
|
||||
```javascript
|
||||
const uploadService = require('../services/upload.service');
|
||||
```typescript
|
||||
import { Request } from 'express';
|
||||
import { Controller, Post, Route, Tags, Security, UploadedFile, SuccessResponse } from 'tsoa';
|
||||
import { uploadService } from '../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'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
interface UploadResponse {
|
||||
video_url?: string;
|
||||
file_size: number;
|
||||
duration?: number | null;
|
||||
file_name?: string;
|
||||
file_path?: string;
|
||||
mime_type?: string;
|
||||
download_url?: string;
|
||||
}
|
||||
|
||||
module.exports = new UploadController();
|
||||
@Route('api/upload')
|
||||
@Tags('File Upload')
|
||||
export class UploadController extends Controller {
|
||||
|
||||
/**
|
||||
* Upload video file
|
||||
* @summary Upload video for lesson (Instructor/Admin only)
|
||||
*/
|
||||
@Post('video')
|
||||
@Security('jwt', ['INSTRUCTOR', 'ADMIN'])
|
||||
@SuccessResponse(200, 'Success')
|
||||
public async uploadVideo(
|
||||
@UploadedFile() file: Express.Multer.File
|
||||
): Promise<UploadResponse> {
|
||||
if (!file) {
|
||||
this.setStatus(400);
|
||||
throw new Error('No video file provided');
|
||||
}
|
||||
|
||||
const result = await uploadService.uploadFile(file, 'videos');
|
||||
|
||||
return {
|
||||
video_url: result.url,
|
||||
file_size: result.fileSize,
|
||||
duration: null // Will be processed later
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload attachment file
|
||||
* @summary Upload attachment for lesson (Instructor/Admin only)
|
||||
*/
|
||||
@Post('attachment')
|
||||
@Security('jwt', ['INSTRUCTOR', 'ADMIN'])
|
||||
@SuccessResponse(200, 'Success')
|
||||
public async uploadAttachment(
|
||||
@UploadedFile() file: Express.Multer.File
|
||||
): Promise<UploadResponse> {
|
||||
if (!file) {
|
||||
this.setStatus(400);
|
||||
throw new Error('No file provided');
|
||||
}
|
||||
|
||||
const result = await uploadService.uploadFile(file, 'attachments');
|
||||
|
||||
return {
|
||||
file_name: result.fileName,
|
||||
file_path: result.key,
|
||||
file_size: result.fileSize,
|
||||
mime_type: result.mimeType,
|
||||
download_url: result.url
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Generate TSOA Routes
|
||||
|
||||
// turbo
|
||||
After creating the upload controller, generate routes and API documentation:
|
||||
|
||||
```bash
|
||||
npm run tsoa:gen
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue