Akshar2325 commited on
Commit
498cc9f
·
1 Parent(s): ae1f03b

feat(storage): add backblaze b2 storage module

Browse files

- 【feat】introduce a complete nestjs module for backblaze b2 storage
- 【feat】implement service to generate presigned urls for secure uploads
- 【feat】create controller with endpoints for upload and deletion
- 【feat】organize files into folders based on type (e.g., images, videos)
- 【chore】add required environment variables to .env.example
- 【chore】integrate the new module into the main application module

.env.example CHANGED
@@ -4,6 +4,11 @@
4
  # Copy this file to .env and fill in your values
5
  # cp .env.example .env
6
 
 
 
 
 
 
7
  # Database (PostgreSQL)
8
  DATABASE_URL="postgresql://username:password@localhost:5432/database_name?schema=public"
9
 
@@ -48,3 +53,11 @@ SUPABASE_S3_URL="https://your-project.storage.supabase.co/storage/v1/s3"
48
  SUPABASE_S3_ACCESS_KEY="your-access-key"
49
  SUPABASE_S3_SECRET_KEY="your-secret-key"
50
  SUPABASE_BUCKET="Streamflix"
 
 
 
 
 
 
 
 
 
4
  # Copy this file to .env and fill in your values
5
  # cp .env.example .env
6
 
7
+ # Application
8
+ APP_URL="http://localhost:3000"
9
+ PORT=3000
10
+ NODE_ENV="development"
11
+
12
  # Database (PostgreSQL)
13
  DATABASE_URL="postgresql://username:password@localhost:5432/database_name?schema=public"
14
 
 
53
  SUPABASE_S3_ACCESS_KEY="your-access-key"
54
  SUPABASE_S3_SECRET_KEY="your-secret-key"
55
  SUPABASE_BUCKET="Streamflix"
56
+
57
+ # Backblaze B2 Storage Credentials
58
+ B2_BUCKET_NAME="StreamflixAkshar"
59
+ B2_REGION="us-west-004"
60
+ B2_S3_ENDPOINT="https://s3.us-west-004.backblazeb2.com"
61
+ B2_ACCESS_KEY_ID="your-access-key-id"
62
+ B2_SECRET_ACCESS_KEY="your-secret-access-key"
63
+ B2_CDN_URL="https://cdn.aksharbhesaniya.dev"
src/app.module.ts CHANGED
@@ -10,6 +10,7 @@ import { BaseQueryCoreModule } from './core/base-query-core/base-query-core.modu
10
  import { ImagekitioModule } from './shared/modules/imagekitio/imagekitio.module';
11
  import { CloudinaryModule } from './shared/modules/cloudinary/cloudinary.module';
12
  import { SupabaseStorageModule } from './shared/modules/supabase-storage/supabase-storage.module';
 
13
  import { AppController } from './app.controller';
14
 
15
  @Module({
@@ -30,6 +31,7 @@ import { AppController } from './app.controller';
30
  ImagekitioModule,
31
  CloudinaryModule,
32
  SupabaseStorageModule,
 
33
  UserModule,
34
  SuperAdminModule,
35
  ],
 
10
  import { ImagekitioModule } from './shared/modules/imagekitio/imagekitio.module';
11
  import { CloudinaryModule } from './shared/modules/cloudinary/cloudinary.module';
12
  import { SupabaseStorageModule } from './shared/modules/supabase-storage/supabase-storage.module';
13
+ import { BackblazeStorageModule } from './shared/modules/backblaze-storage/backblaze-storage.module';
14
  import { AppController } from './app.controller';
15
 
16
  @Module({
 
31
  ImagekitioModule,
32
  CloudinaryModule,
33
  SupabaseStorageModule,
34
+ BackblazeStorageModule,
35
  UserModule,
36
  SuperAdminModule,
37
  ],
src/shared/modules/backblaze-storage/backblaze-storage.config.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { registerAs } from '@nestjs/config';
2
+
3
+ export default registerAs('backblazeStorage', () => ({
4
+ endpoint: process.env.B2_S3_ENDPOINT,
5
+ region: process.env.B2_REGION || 'us-west-004',
6
+ accessKeyId: process.env.B2_ACCESS_KEY_ID,
7
+ secretAccessKey: process.env.B2_SECRET_ACCESS_KEY,
8
+ bucket: process.env.B2_BUCKET_NAME || 'StreamflixAkshar',
9
+ cdnUrl: process.env.B2_CDN_URL || 'https://cdn.aksharbhesaniya.dev',
10
+ }));
src/shared/modules/backblaze-storage/backblaze-storage.controller.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Delete,
5
+ Query,
6
+ HttpCode,
7
+ HttpStatus,
8
+ } from '@nestjs/common';
9
+ import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
10
+ import { BackblazeStorageService } from './backblaze-storage.service';
11
+ import { FileType } from './enums/file-type.enum';
12
+
13
+ @ApiTags('Backblaze B2 Storage')
14
+ @Controller('backblaze')
15
+ export class BackblazeStorageController {
16
+ constructor(
17
+ private readonly backblazeStorageService: BackblazeStorageService,
18
+ ) {}
19
+
20
+ @Get('presigned-upload')
21
+ @ApiOperation({ summary: 'Get presigned upload URL' })
22
+ @ApiQuery({ name: 'fileName', required: true })
23
+ @ApiQuery({ name: 'fileType', enum: FileType, required: false })
24
+ async getPresignedUploadUrl(
25
+ @Query('fileName') fileName: string,
26
+ @Query('fileType') fileType?: FileType,
27
+ ) {
28
+ return this.backblazeStorageService.getPresignedUploadUrl(
29
+ fileName,
30
+ fileType,
31
+ );
32
+ }
33
+
34
+ @Delete('by-url')
35
+ @HttpCode(HttpStatus.OK)
36
+ @ApiOperation({ summary: 'Delete file by CDN URL' })
37
+ async deleteFileByUrl(@Query('url') url: string) {
38
+ const result = await this.backblazeStorageService.deleteFileByUrl(url);
39
+ return {
40
+ message: 'File deleted successfully',
41
+ ...result,
42
+ };
43
+ }
44
+ }
src/shared/modules/backblaze-storage/backblaze-storage.module.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { ConfigModule } from '@nestjs/config';
3
+ import { BackblazeStorageController } from './backblaze-storage.controller';
4
+ import { BackblazeStorageService } from './backblaze-storage.service';
5
+ import backblazeStorageConfig from './backblaze-storage.config';
6
+
7
+ @Module({
8
+ imports: [ConfigModule.forFeature(backblazeStorageConfig)],
9
+ controllers: [BackblazeStorageController],
10
+ providers: [BackblazeStorageService],
11
+ exports: [BackblazeStorageService],
12
+ })
13
+ export class BackblazeStorageModule {}
src/shared/modules/backblaze-storage/backblaze-storage.service.ts ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger, NotFoundException } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import {
4
+ S3Client,
5
+ PutObjectCommand,
6
+ DeleteObjectCommand,
7
+ } from '@aws-sdk/client-s3';
8
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
9
+
10
+ @Injectable()
11
+ export class BackblazeStorageService {
12
+ private readonly logger = new Logger(BackblazeStorageService.name);
13
+ private s3Client: S3Client;
14
+ private bucket: string;
15
+ private region: string;
16
+ private endpoint: string;
17
+ private cdnUrl: string;
18
+
19
+ constructor(private configService: ConfigService) {
20
+ this.endpoint =
21
+ this.configService.get<string>('backblazeStorage.endpoint') || '';
22
+ this.region =
23
+ this.configService.get<string>('backblazeStorage.region') ||
24
+ 'us-west-004';
25
+ const accessKeyId =
26
+ this.configService.get<string>('backblazeStorage.accessKeyId') || '';
27
+ const secretAccessKey =
28
+ this.configService.get<string>('backblazeStorage.secretAccessKey') || '';
29
+ this.bucket =
30
+ this.configService.get<string>('backblazeStorage.bucket') ||
31
+ 'StreamflixAkshar';
32
+ this.cdnUrl =
33
+ this.configService.get<string>('backblazeStorage.cdnUrl') ||
34
+ 'https://cdn.aksharbhesaniya.dev';
35
+
36
+ this.s3Client = new S3Client({
37
+ region: this.region,
38
+ endpoint: this.endpoint,
39
+ credentials: {
40
+ accessKeyId,
41
+ secretAccessKey,
42
+ },
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Get folder path based on file type
48
+ * @param fileType - The file type (IMAGE, DOCUMENT, VIDEO)
49
+ * @returns Folder path for the file type
50
+ */
51
+ private getFolderPath(fileType: string): string {
52
+ const folderMap: Record<string, string> = {
53
+ IMAGE: 'images',
54
+ DOCUMENT: 'documents',
55
+ VIDEO: 'videos',
56
+ };
57
+ return folderMap[fileType.toUpperCase()] || 'others';
58
+ }
59
+
60
+ /**
61
+ * Generate presigned upload URL using S3
62
+ * @param fileName - The file name
63
+ * @param fileType - The file type (IMAGE, DOCUMENT, VIDEO)
64
+ * @returns Presigned upload URL and CDN file URL
65
+ */
66
+ async getPresignedUploadUrl(fileName: string, fileType?: string) {
67
+ try {
68
+ // Determine folder based on file type
69
+ const folder = fileType ? this.getFolderPath(fileType) : 'others';
70
+
71
+ // Create full file path: folder/timestamp-filename
72
+ const timestamp = Date.now();
73
+ const filePath = `${folder}/${timestamp}-${fileName}`;
74
+
75
+ // Create PutObject command
76
+ const command = new PutObjectCommand({
77
+ Bucket: this.bucket,
78
+ Key: filePath,
79
+ });
80
+
81
+ // Generate presigned URL (valid for 1 hour)
82
+ const uploadUrl = await getSignedUrl(this.s3Client, command, {
83
+ expiresIn: 3600,
84
+ });
85
+
86
+ // Construct the permanent public URL through Cloudflare CDN
87
+ const fileUrl = `${this.cdnUrl}/${filePath}`;
88
+
89
+ this.logger.log(
90
+ `Generated presigned upload URL for: ${filePath}${fileType ? ` (type: ${fileType})` : ''}`,
91
+ );
92
+
93
+ return {
94
+ uploadUrl,
95
+ fileUrl, // Permanent CDN URL (use this for access/delete)
96
+ filePath,
97
+ expiresIn: 3600, // 1 hour in seconds
98
+ ...(fileType && { fileType }),
99
+ };
100
+ } catch (error) {
101
+ this.logger.error('Failed to generate presigned upload URL', error);
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Delete a file from Backblaze B2 using S3
108
+ * @param filePath - The file path in the bucket
109
+ * @returns Deletion result
110
+ */
111
+ async deleteFile(filePath: string): Promise<any> {
112
+ try {
113
+ const command = new DeleteObjectCommand({
114
+ Bucket: this.bucket,
115
+ Key: filePath,
116
+ });
117
+
118
+ const result = await this.s3Client.send(command);
119
+
120
+ this.logger.log(`Successfully deleted file: ${filePath}`);
121
+ return {
122
+ success: true,
123
+ deletedPath: filePath,
124
+ result,
125
+ };
126
+ } catch (error) {
127
+ this.logger.error(`Failed to delete file: ${filePath}`, error);
128
+ throw new NotFoundException('File not found or already deleted');
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Delete a file by CDN URL
134
+ * @param url - The CDN URL (e.g., https://cdn.aksharbhesaniya.dev/images/1234567890-file.jpg)
135
+ * @returns Deletion result
136
+ */
137
+ async deleteFileByUrl(url: string): Promise<any> {
138
+ try {
139
+ // Extract file path from URL
140
+ const filePath = this.extractFilePathFromUrl(url);
141
+
142
+ if (!filePath) {
143
+ throw new NotFoundException('Could not extract file path from URL');
144
+ }
145
+
146
+ const result = await this.deleteFile(filePath);
147
+
148
+ this.logger.log(`Successfully deleted file from URL: ${url}`);
149
+
150
+ return {
151
+ ...result,
152
+ deletedUrl: url,
153
+ };
154
+ } catch (error) {
155
+ if (error instanceof NotFoundException) {
156
+ throw error;
157
+ }
158
+ this.logger.error(`Failed to delete file by URL: ${url}`, error);
159
+ throw new NotFoundException('File not found or already deleted');
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Extract file path from CDN URL
165
+ * @param url - The CDN URL (e.g., https://cdn.aksharbhesaniya.dev/images/1234567890-file.jpg)
166
+ * @returns File path (e.g., images/1234567890-file.jpg)
167
+ */
168
+ private extractFilePathFromUrl(url: string): string | null {
169
+ try {
170
+ // Match pattern: https://cdn.aksharbhesaniya.dev/{path}
171
+ // Extract everything after the domain
172
+ const match = url.match(/^https?:\/\/[^/]+\/(.+)$/);
173
+ return match ? match[1] : null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ }
src/shared/modules/backblaze-storage/enums/file-type.enum.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export enum FileType {
2
+ IMAGE = 'IMAGE',
3
+ DOCUMENT = 'DOCUMENT',
4
+ VIDEO = 'VIDEO',
5
+ }