Spaces:
Runtime error
Runtime error
Akshar2325
refactor(wasabi-storage): improve comments on file deletion process and update description for fileUrl query parameter
2c01ad8 | import { | |
| Controller, | |
| Get, | |
| Delete, | |
| Post, | |
| Query, | |
| HttpCode, | |
| HttpStatus, | |
| Res, | |
| Header, | |
| UseInterceptors, | |
| UploadedFile, | |
| BadRequestException, | |
| } from '@nestjs/common'; | |
| import type { Response } from 'express'; | |
| import { FileInterceptor } from '@nestjs/platform-express'; | |
| import { | |
| ApiTags, | |
| ApiOperation, | |
| ApiQuery, | |
| ApiConsumes, | |
| ApiBody, | |
| } from '@nestjs/swagger'; | |
| import { WasabiStorageService } from './wasabi-storage.service'; | |
| import { FileType } from './enums/file-type.enum'; | |
| import { LogToDiscord } from 'src/shared/decorators/log-to-discord.decorator'; | |
| import { SkipApiToken } from 'src/shared/decorators/skip-api-token.decorator'; | |
| import { diskStorage } from 'multer'; | |
| import { extname } from 'path'; | |
| import * as fs from 'fs/promises'; | |
| ('Upload: Wasabi Storage') | |
| ('wasabi') | |
| export class WasabiStorageController { | |
| constructor(private readonly wasabiStorageService: WasabiStorageService) {} | |
| ('presigned-upload') | |
| () | |
| ({ summary: 'Get presigned upload URL' }) | |
| ({ name: 'fileName', required: true }) | |
| ({ name: 'fileType', enum: FileType, required: false }) | |
| async getPresignedUploadUrl( | |
| ('fileName') fileName: string, | |
| ('fileType') fileType?: FileType, | |
| ) { | |
| return this.wasabiStorageService.getPresignedUploadUrl(fileName, fileType); | |
| } | |
| ('file') | |
| () | |
| ({ | |
| summary: 'Stream file from Wasabi (Proxy CDN endpoint)', | |
| description: | |
| 'Acts as a proxy to stream files from Wasabi. Works without public bucket access. Automatically rewrites HLS playlists.', | |
| }) | |
| ({ | |
| name: 'path', | |
| required: true, | |
| description: 'File path in bucket (e.g., videos/1234567890-file.mp4)', | |
| }) | |
| ('Accept-Ranges', 'bytes') | |
| async streamFile( | |
| ('path') path: string, | |
| ({ passthrough: false }) res: Response, | |
| ) { | |
| try { | |
| const result = await this.wasabiStorageService.streamFile(path); | |
| // Set response headers | |
| res.set({ | |
| 'Content-Type': result.contentType, | |
| 'Content-Length': result.contentLength.toString(), | |
| 'Content-Disposition': `inline; filename="${result.fileName}"`, | |
| 'Cache-Control': 'public, max-age=2592000', // Cache for 30 days | |
| 'Access-Control-Allow-Origin': '*', // Allow CORS for video players | |
| }); | |
| // If HLS playlist was rewritten, send the modified content as string | |
| if (result.isRewritten && result.rewrittenContent) { | |
| return res.send(result.rewrittenContent); | |
| } | |
| // Otherwise, pipe the stream directly to response | |
| result.stream.pipe(res); | |
| } catch (error) { | |
| // Check if it's a 404 error (file not found) | |
| const isNotFound = | |
| error.status === 404 || error.status === HttpStatus.NOT_FOUND; | |
| if (isNotFound) { | |
| // Send beautiful 404 error page | |
| res.status(404).send(this.wasabiStorageService.generate404Page(path)); | |
| } else { | |
| // Other errors - send JSON | |
| res.status(error.status || 500).json({ | |
| success: false, | |
| message: error.message || 'Failed to stream file', | |
| }); | |
| } | |
| } | |
| } | |
| ('by-url') | |
| () | |
| (HttpStatus.OK) | |
| ({ summary: 'Delete file by CDN URL' }) | |
| async deleteFileByUrl(('url') url: string) { | |
| const result = await this.wasabiStorageService.deleteFileByUrl(url); | |
| return { | |
| message: 'File deleted successfully', | |
| ...result, | |
| }; | |
| } | |
| ('upload-video-hls') | |
| () | |
| (HttpStatus.OK) | |
| ({ | |
| summary: 'Upload MP4 video, convert to HLS, and store in Wasabi (Max 50MB)', | |
| description: | |
| 'Upload a video file up to 50MB. It will be converted to HLS format (1080p, 720p, 480p) and stored in Wasabi under hls/{uploadId}/. For files larger than 50MB, use the direct upload method (upload-video-hls-from-url endpoint).', | |
| }) | |
| ('multipart/form-data') | |
| ({ | |
| schema: { | |
| type: 'object', | |
| properties: { | |
| video: { | |
| type: 'string', | |
| format: 'binary', | |
| description: 'Video file (MP4, AVI, MOV, etc.)', | |
| }, | |
| }, | |
| }, | |
| }) | |
| ( | |
| FileInterceptor('video', { | |
| storage: diskStorage({ | |
| destination: './resources/temp', | |
| filename: (req, file, callback) => { | |
| const uniqueSuffix = | |
| Date.now() + '-' + Math.round(Math.random() * 1e9); | |
| callback(null, `video-${uniqueSuffix}${extname(file.originalname)}`); | |
| }, | |
| }), | |
| fileFilter: (req, file, callback) => { | |
| if (!file.mimetype.startsWith('video/')) { | |
| return callback( | |
| new BadRequestException('Only video files are allowed'), | |
| false, | |
| ); | |
| } | |
| callback(null, true); | |
| }, | |
| limits: { | |
| fileSize: 500 * 1024 * 1024, // 500MB max | |
| }, | |
| }), | |
| ) | |
| async uploadVideoAndConvertToHLS(() file: Express.Multer.File) { | |
| if (!file) { | |
| throw new BadRequestException('No video file provided'); | |
| } | |
| try { | |
| const { uploadId } = await this.wasabiStorageService.startVideoProcessing( | |
| file.originalname, | |
| file.size, | |
| file.path, | |
| ); | |
| return { | |
| success: true, | |
| message: 'Upload received. Conversion started and processing.', | |
| data: { | |
| uploadId, | |
| fileName: file.originalname, | |
| originalSize: file.size, | |
| status: 'processing', | |
| }, | |
| }; | |
| } catch (error) { | |
| try { | |
| await fs.unlink(file.path); | |
| } catch {} | |
| throw error; | |
| } | |
| } | |
| ('hls') | |
| () | |
| ({ | |
| summary: 'Stream HLS master m3u8 playlist with rewritten URLs', | |
| description: | |
| 'Serves the master.m3u8 file with rewritten URLs pointing to this server (not direct Wasabi)', | |
| }) | |
| ({ | |
| name: 'uploadId', | |
| required: true, | |
| description: 'Upload ID from HLS conversion', | |
| }) | |
| async streamHLS(('uploadId') uploadId: string, () res: Response) { | |
| try { | |
| const { m3u8Content, contentType } = | |
| await this.wasabiStorageService.getRewrittenM3U8(uploadId); | |
| res.set({ | |
| 'Content-Type': contentType, | |
| 'Cache-Control': 'no-cache', | |
| 'Access-Control-Allow-Origin': '*', | |
| }); | |
| res.send(m3u8Content); | |
| } catch (error) { | |
| res.status(error.status || 500).json({ | |
| success: false, | |
| message: error.message || 'Failed to stream HLS', | |
| }); | |
| } | |
| } | |
| ('hls') | |
| () | |
| (HttpStatus.OK) | |
| ({ summary: 'Delete HLS video files from Wasabi' }) | |
| ({ name: 'uploadId', required: true }) | |
| async deleteHLSVideo(('uploadId') uploadId: string) { | |
| if (!uploadId) { | |
| throw new BadRequestException('uploadId is required'); | |
| } | |
| const result = | |
| await this.wasabiStorageService.deleteProcessedVideo(uploadId); | |
| return { | |
| success: true, | |
| message: 'HLS video deleted successfully', | |
| data: result, | |
| }; | |
| } | |
| ('hls/status') | |
| () | |
| ({ summary: 'Get HLS conversion status' }) | |
| ({ name: 'uploadId', required: true }) | |
| async getHLSStatus(('uploadId') uploadId: string) { | |
| if (!uploadId) { | |
| throw new BadRequestException('uploadId is required'); | |
| } | |
| const status = await this.wasabiStorageService.getUploadStatus(uploadId); | |
| return { | |
| success: true, | |
| data: status, | |
| }; | |
| } | |
| ('upload-video-hls-from-url') | |
| () | |
| (HttpStatus.OK) | |
| ({ | |
| summary: 'Process large video from direct Wasabi upload (For files >50MB)', | |
| description: | |
| 'For large files: 1) Get presigned URL from /wasabi/presigned-upload, 2) Upload directly to Wasabi from client, 3) Call this endpoint with EITHER the "filePath" OR "fileUrl" from the presigned response. Server will download, convert to HLS, delete original, and store HLS files.', | |
| }) | |
| ({ | |
| name: 'fileUrl', | |
| required: true, | |
| description: | |
| 'Can be either: 1) filePath (videos/1234567890-movie.mp4) OR 2) fileUrl (http://localhost:3000/wasabi/file?path=...) from presigned response', | |
| }) | |
| async uploadVideoFromDirectUrl(('fileUrl') fileUrl: string) { | |
| if (!fileUrl) { | |
| throw new BadRequestException('fileUrl is required'); | |
| } | |
| const result = | |
| await this.wasabiStorageService.processVideoFromWasabiUrl(fileUrl); | |
| return { | |
| success: true, | |
| message: | |
| 'Video downloaded from Wasabi. Conversion started and processing.', | |
| data: result, | |
| }; | |
| } | |
| } | |