File size: 4,328 Bytes
51c6e04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
  S3Client,
  PutObjectCommand,
  DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
@Injectable()
export class RustfsStorageService {
  private readonly logger = new Logger(RustfsStorageService.name);
  private s3Client: S3Client;
  private bucket: string;
  private endpoint: string;

  constructor(private configService: ConfigService) {
    this.endpoint =
      this.configService.get<string>('rustfsStorage.endpoint') || '';
    const accessKeyId =
      this.configService.get<string>('rustfsStorage.accessKey') || '';
    const secretAccessKey =
      this.configService.get<string>('rustfsStorage.secretKey') || '';
    this.bucket =
      this.configService.get<string>('rustfsStorage.bucket') || 'streamflix';
    const region =
      this.configService.get<string>('rustfsStorage.region') || 'us-east-1';

    this.s3Client = new S3Client({
      region,
      endpoint: this.endpoint,
      credentials: {
        accessKeyId,
        secretAccessKey,
      },
      forcePathStyle: true,
    });
  }

  /**
   * Get folder path based on file type
   */
  private getFolderPath(fileType: string): string {
    const folderMap: Record<string, string> = {
      IMAGE: 'images',
      DOCUMENT: 'documents',
      VIDEO: 'videos',
    };
    return folderMap[fileType.toUpperCase()] || 'others';
  }

  /**
   * Generate presigned upload URL using S3
   */
  async getPresignedUploadUrl(fileName: string, fileType?: string) {
    try {
      const folder = fileType ? this.getFolderPath(fileType) : 'others';
      const timestamp = Date.now();
      const filePath = `${folder}/${timestamp}-${fileName}`;

      const command = new PutObjectCommand({
        Bucket: this.bucket,
        Key: filePath,
      });

      const uploadUrl = await getSignedUrl(this.s3Client, command, {
        expiresIn: 3600,
      });

      const fileUrl = `${this.endpoint}/${this.bucket}/${filePath}`;

      this.logger.log(
        `Generated presigned upload URL for: ${filePath}${fileType ? ` (type: ${fileType})` : ''}`,
      );

      return {
        uploadUrl,
        fileUrl,
        filePath,
        expiresIn: 3600,
        ...(fileType && { fileType }),
      };
    } catch (error) {
      this.logger.error('Failed to generate presigned upload URL', error);
      throw error;
    }
  }

  /**
   * Delete a file from RustFS Storage
   */
  async deleteFile(filePath: string): Promise<any> {
    try {
      const command = new DeleteObjectCommand({
        Bucket: this.bucket,
        Key: filePath,
      });

      const result = await this.s3Client.send(command);

      this.logger.log(`Successfully deleted file: ${filePath}`);
      return {
        success: true,
        deletedPath: filePath,
        result,
      };
    } catch (error) {
      this.logger.error(`Failed to delete file: ${filePath}`, error);
      throw new NotFoundException('File not found or already deleted');
    }
  }

  /**
   * Delete a file by URL
   */
  async deleteFileByUrl(url: string): Promise<any> {
    try {
      const filePath = this.extractFilePathFromUrl(url);

      if (!filePath) {
        throw new NotFoundException('Could not extract file path from URL');
      }

      const result = await this.deleteFile(filePath);

      this.logger.log(`Successfully deleted file from URL: ${url}`);

      return {
        ...result,
        deletedUrl: url,
      };
    } catch (error) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      this.logger.error(`Failed to delete file by URL: ${url}`, error);
      throw new NotFoundException('File not found or already deleted');
    }
  }

  /**
   * Extract file path from RustFS Storage URL
   */
  private extractFilePathFromUrl(url: string): string | null {
    try {
      const urlObj = new URL(url);
      const pathParts = urlObj.pathname.split('/');
      const bucketIndex = pathParts.indexOf(this.bucket);

      if (bucketIndex !== -1 && bucketIndex < pathParts.length - 1) {
        return pathParts.slice(bucketIndex + 1).join('/');
      }

      return pathParts.slice(1).join('/');
    } catch {
      return null;
    }
  }
}