Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 87 additions & 15 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,95 @@
"version": "2.0.0",
"tasks": [
{
"label": "API",
"label": "🌐 API Gateway",
"type": "shell",
"command": "npx nx serve api --no-tui",
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^$",
"file": 1,
"location": 2,
"message": 3
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": ".*Application is running on.*"
"command": "npx",
"args": ["nx", "serve", "api", "--no-tui"],
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": false
},
"options": {
"env": {
"NODE_ENV": "development"
}
}
},
"problemMatcher": []
},
{
"label": "🛑 Stop Nx Daemon",
"type": "shell",
"command": "npx",
"args": ["nx", "daemon", "--stop"],
"group": "build",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
},
"problemMatcher": []
},
{
"label": "🔄 Reset Nx Cache",
"type": "shell",
"command": "npx",
"args": ["nx", "reset"],
"group": "build",
"dependsOn": "🛑 Stop Nx Daemon",
"dependsOrder": "sequence",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
},
"problemMatcher": []
},
{
"label": "🚀 Start Nx Daemon",
"type": "shell",
"command": "npx",
"args": ["nx", "daemon", "--start", "--verbose"],
"group": "build",
"dependsOn": "🔄 Reset Nx Cache",
"dependsOrder": "sequence",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"problemMatcher": []
},
{
"label": "🔄 Restart Nx Daemon",
"type": "shell",
"command": "echo",
"args": ["Nx Daemon restarted successfully"],
"dependsOn": "🚀 Start Nx Daemon",
"dependsOrder": "sequence",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"problemMatcher": []
}
]
}
39 changes: 0 additions & 39 deletions apps/api/src/app/app.controller.spec.ts

This file was deleted.

157 changes: 146 additions & 11 deletions apps/api/src/app/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HttpCode,
HttpStatus,
Logger,
Param,
Post,
UploadedFile,
UseInterceptors,
Expand All @@ -15,12 +16,13 @@ import {
ApiCreatedResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
ApiTags,
} from '@nestjs/swagger';
import { v4 as uuidv4 } from 'uuid';
import { AppService } from './app.service';
import { StorageService } from './storage/storage.service';
import { UploadVideoDto } from './videos/dto/upload-video.dto';
import { Video, VideoStatus } from './videos/entities/video.entity';
import { Video } from './videos/entities/video.entity';
import { VideosService } from './videos/videos.service';

interface UploadedFileInterface {
Expand All @@ -40,6 +42,7 @@ export class AppController {
constructor(
private readonly appService: AppService,
private readonly videosService: VideosService,
private readonly storageService: StorageService,
) {
this.logger.log(`AppController constructor`);
}
Expand Down Expand Up @@ -102,19 +105,16 @@ export class AppController {
`Received file upload: filename=${file.originalname}, mimetype=${file.mimetype}, size=${file.size}`,
);

// Generate a unique S3 key (placeholder until MinIO is implemented)
const fileExtension = file.originalname.split('.').pop();
const s3Key = `raw-videos/${uuidv4()}.${fileExtension}`;

// Create video record
const video = await this.videosService.create(
// Pass buffer directly instead of converting to stream to avoid MinIO signature issues
// Videos service handles MinIO upload and database record creation
const video = await this.videosService.uploadAndCreate(
file.buffer,
file.originalname,
s3Key,
file.mimetype,
VideoStatus.UPLOADED,
file.size,
);

this.logger.log(`Video record created: id=${video.id}`);
this.logger.log(`Video uploaded and record created: id=${video.id}`);
return video;
}

Expand All @@ -130,4 +130,139 @@ export class AppController {
async testGetVideos(): Promise<Video[]> {
return await this.videosService.findAll();
}

@Post('test-storage/delete/:objectName')
@ApiOperation({
summary: 'Delete an object from MinIO storage',
description:
'Deletes an object from MinIO storage. Handles object not found gracefully (idempotent operation).',
})
@ApiParam({
name: 'objectName',
description: 'Full object key/path (e.g., raw-videos/video.mp4)',
type: 'string',
example: 'raw-videos/123e4567-e89b-12d3-a456-426614174000.mp4',
})
@ApiOkResponse({
description: 'Object deleted successfully or already deleted',
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
objectName: { type: 'string' },
},
},
})
@HttpCode(HttpStatus.OK)
async testDeleteObject(
@Param('objectName') objectName: string,
): Promise<{ success: boolean; message: string; objectName: string }> {
this.logger.log(`Test delete object request: objectName=${objectName}`);

try {
// Check if object exists before deletion
const exists = await this.storageService.objectExists(objectName);
this.logger.log(
`Object '${objectName}' exists before deletion: ${exists}`,
);

// Delete the object
await this.storageService.deleteObject(objectName);

// Verify deletion
const stillExists = await this.storageService.objectExists(objectName);
this.logger.log(
`Object '${objectName}' exists after deletion: ${stillExists}`,
);
return {
success: true,
message: exists
? 'Object deleted successfully'
: 'Object was already deleted (idempotent operation)',
objectName,
};
} catch (error) {
this.logger.error(
`Failed to delete object '${objectName}': ${error.message}`,
);
throw error;
}
}

@Post('test-storage/delete-video/:id')
@ApiOperation({
summary: 'Delete a video object by video ID',
description:
'Deletes a video file from MinIO storage using the video ID. Retrieves the s3Key from the database and deletes the object.',
})
@ApiParam({
name: 'id',
description: 'Video ID (UUID)',
type: 'string',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@ApiOkResponse({
description: 'Video object deleted successfully',
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
videoId: { type: 'string' },
s3Key: { type: 'string' },
},
},
})
@HttpCode(HttpStatus.OK)
async testDeleteVideoObject(
@Param('id') id: string,
): Promise<{
success: boolean;
message: string;
videoId: string;
s3Key: string;
}> {
this.logger.log(`Test delete video object request: videoId=${id}`);

// Get video record from database
const video = await this.videosService.findOne(id);
if (!video) {
throw new Error(`Video with ID ${id} not found`);
}

this.logger.log(
`Found video: id=${video.id}, s3Key=${video.s3Key}`,
);

try {
// Check if object exists
const exists = await this.storageService.objectExists(video.s3Key);
this.logger.log(
`Video object '${video.s3Key}' exists before deletion: ${exists}`,
);

// Delete the object
await this.storageService.deleteObject(video.s3Key);

// Verify deletion
const stillExists = await this.storageService.objectExists(video.s3Key);
this.logger.log(
`Video object '${video.s3Key}' exists after deletion: ${stillExists}`,
);
return {
success: true,
message: exists
? 'Video object deleted successfully from MinIO'
: 'Video object was already deleted (idempotent operation)',
videoId: video.id,
s3Key: video.s3Key,
};
} catch (error) {
this.logger.error(
`Failed to delete video object for video ${id}: ${error.message}`,
);
throw error;
}
}
}
2 changes: 2 additions & 0 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import configuration from '../config/configuration';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StorageModule } from './storage/storage.module';
import { VideosModule } from './videos/videos.module';

@Module({
Expand All @@ -29,6 +30,7 @@ import { VideosModule } from './videos/videos.module';
inject: [ConfigService],
}),
VideosModule,
StorageModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
Loading