diff --git a/src/db/models/file.ts b/src/db/models/file.ts new file mode 100644 index 0000000..d7a186b --- /dev/null +++ b/src/db/models/file.ts @@ -0,0 +1,48 @@ +import mongoose from "mongoose"; + +export interface File { + _id: mongoose.Types.ObjectId; + userId: mongoose.Types.ObjectId; + objectKey: string; + originalName: string; + mimeType: string; + size: number; + uploadedAt: Date; +} + +const fileSchema = new mongoose.Schema( + { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, + objectKey: { + type: String, + required: true, + unique: true, + }, + originalName: { + type: String, + required: true, + }, + mimeType: { + type: String, + required: true, + }, + size: { + type: Number, + required: true, + }, + uploadedAt: { + type: Date, + default: Date.now, + }, + }, + { timestamps: true } +); + +fileSchema.index({ userId: 1 }); + +export const FileModel = mongoose.model("File", fileSchema); diff --git a/src/index.ts b/src/index.ts index b33c963..54c26c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { SpotifyApiService } from "./services/spotifyApiService"; import { SpotifyPollingService } from "./services/spotifyPollingService"; import { WeatherPollingService } from "./services/weatherPollingService"; import { JwtAuthenticator } from "./utils/jwtAuthenticator"; +import { FileService } from "./services/db/fileService"; async function bootstrap() { const { @@ -70,7 +71,8 @@ async function bootstrap() { await connectToDatabase(dbConfig.dbName, dbConfig.dbConnString); - const s3Service = S3Service.getInstance(s3ClientConfig); + const fileService = FileService.getInstance(); + const s3Service = S3Service.getInstance(s3ClientConfig, fileService); const userService = await UserService.create(); const spotifyTokenService = new SpotifyTokenService(SPOTIFY_CLIENT_ID!, SPOTIFY_CLIENT_SECRET!); diff --git a/src/services/db/fileService.ts b/src/services/db/fileService.ts new file mode 100644 index 0000000..bb03ab1 --- /dev/null +++ b/src/services/db/fileService.ts @@ -0,0 +1,62 @@ +import mongoose from "mongoose"; +import { FileModel, File } from "../../db/models/file"; + +export class FileService { + private static instance: FileService; + + private constructor() {} + + public static getInstance(): FileService { + if (!this.instance) { + this.instance = new FileService(); + } + return this.instance; + } + + async createFileRecord( + userId: string, + objectKey: string, + originalName: string, + mimeType: string, + size: number + ): Promise { + const fileRecord = new FileModel({ + userId: new mongoose.Types.ObjectId(userId), + objectKey, + originalName, + mimeType, + size, + uploadedAt: new Date(), + }); + + return await fileRecord.save(); + } + + async getFilesByUserId(userId: string): Promise { + return FileModel.find({ userId: new mongoose.Types.ObjectId(userId) }) + .sort({ uploadedAt: -1 }) + .exec(); + } + + async getFileByObjectKey(objectKey: string): Promise { + return FileModel.findOne({ objectKey }).exec(); + } + + async deleteFileRecord(objectKey: string): Promise { + const result = await FileModel.deleteOne({ objectKey }); + return result.deletedCount > 0; + } + + async isFileDuplicate(originalName: string, userId: string): Promise { + const count = await FileModel.countDocuments({ + userId: new mongoose.Types.ObjectId(userId), + originalName: originalName, + }); + + return count > 0; + } + + async updateObjectKey(fileId: string, objectKey: string): Promise { + return FileModel.findByIdAndUpdate(fileId, { objectKey }, { new: true }).exec(); + } +} diff --git a/src/services/s3Service.ts b/src/services/s3Service.ts index b5b1bce..f944669 100644 --- a/src/services/s3Service.ts +++ b/src/services/s3Service.ts @@ -3,10 +3,10 @@ import { CreateBucketCommand, PutObjectCommand, GetObjectCommand, - ListObjectsV2Command, DeleteObjectCommand, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { FileService } from "./db/fileService"; import { randomUUID } from "crypto"; export interface S3ClientConfig { @@ -25,8 +25,9 @@ export class S3Service { private readonly client: S3Client; private readonly bucketName: string; private readonly publicUrl: string; + private readonly fileService: FileService; - private constructor(clientConfig: S3ClientConfig) { + private constructor(clientConfig: S3ClientConfig, fileService: FileService) { this.client = new S3Client({ endpoint: `${clientConfig.endpoint}:${clientConfig.port}`, forcePathStyle: true, @@ -39,14 +40,15 @@ export class S3Service { this.bucketName = clientConfig.bucket; this.publicUrl = clientConfig.publicUrl; + this.fileService = fileService; } - public static getInstance(config?: S3ClientConfig): S3Service { + public static getInstance(config?: S3ClientConfig, fileService?: FileService): S3Service { if (!this.instance) { - if (!config) { - throw new Error("S3Service must be initialized with a config on first use."); + if (!config || !fileService) { + throw new Error("S3Service must be initialized with a config and fileService on first use."); } - this.instance = new S3Service(config); + this.instance = new S3Service(config, fileService); } return this.instance; } @@ -65,64 +67,39 @@ export class S3Service { } async uploadFile(file: Express.Multer.File, userId: string): Promise { - const objectKey = `user-${userId}/${randomUUID()}_${file.originalname}`; + const uuid = randomUUID(); + const objectKey = `user-${userId}/${uuid}_${file.originalname}`; const command = new PutObjectCommand({ Bucket: this.bucketName, Key: objectKey, Body: file.buffer, ContentType: file.mimetype, - Metadata: { - originalname: encodeURIComponent(file.originalname), - }, }); await this.client.send(command); + + await this.fileService.createFileRecord(userId, objectKey, file.originalname, file.mimetype, file.size); + return objectKey; } - async listFilesForUser(userId: string): Promise<{ key: string; lastModified: Date; originalName?: string }[]> { - const command = new ListObjectsV2Command({ - Bucket: this.bucketName, - Prefix: `user-${userId}/`, - }); + async listFilesForUser( + userId: string + ): Promise<{ key: string; lastModified: Date; originalName: string; mimeType: string; size: number }[]> { + const files = await this.fileService.getFilesByUserId(userId); - const response = await this.client.send(command); - - return ( - response.Contents?.map((item) => ({ - key: item.Key!, - lastModified: item.LastModified!, - originalName: this.extractOriginalNameFromKey(item.Key!), - })) || [] - ); + return files.map((file) => ({ + key: file.objectKey, + lastModified: file.uploadedAt, + originalName: file.originalName, + mimeType: file.mimeType, + size: file.size, + })); } async isFileDuplicate(file: Express.Multer.File, userId: string): Promise { - const existingFiles = await this.listFilesForUser(userId); - const fileName = file.originalname.toLowerCase(); - - // Prüfen, ob eine Datei mit demselben Namen bereits existiert - for (const existingFile of existingFiles) { - const existingFileName = this.extractOriginalNameFromKey(existingFile.key); - if (existingFileName && existingFileName.toLowerCase() === fileName) { - return true; - } - } - - return false; - } - - private extractOriginalNameFromKey(key: string): string | undefined { - // Extrahiere den Dateinamen aus dem Objektschlüssel - // Format: user-{userId}/{uuid}_{originalname} - const parts = key.split("/"); - if (parts.length >= 2) { - const filename = parts[parts.length - 1]; - const filenameMatch = filename.match(/[^_]+_(.+)$/); - return filenameMatch ? filenameMatch[1] : undefined; - } - return undefined; + return await this.fileService.isFileDuplicate(file.originalname, userId); } async deleteFile(objectKey: string): Promise { @@ -132,6 +109,9 @@ export class S3Service { }); await this.client.send(command); + + await this.fileService.deleteFileRecord(objectKey); + console.log(`File deleted: ${objectKey}`); }