add file model and service for file management functionality

This commit is contained in:
StarAppeal
2025-09-26 04:59:17 +02:00
parent 7f683fa6bc
commit cc66b80589
4 changed files with 141 additions and 49 deletions
+48
View File
@@ -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<File>(
{
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>("File", fileSchema);
+3 -1
View File
@@ -8,6 +8,7 @@ import { SpotifyApiService } from "./services/spotifyApiService";
import { SpotifyPollingService } from "./services/spotifyPollingService"; import { SpotifyPollingService } from "./services/spotifyPollingService";
import { WeatherPollingService } from "./services/weatherPollingService"; import { WeatherPollingService } from "./services/weatherPollingService";
import { JwtAuthenticator } from "./utils/jwtAuthenticator"; import { JwtAuthenticator } from "./utils/jwtAuthenticator";
import { FileService } from "./services/db/fileService";
async function bootstrap() { async function bootstrap() {
const { const {
@@ -70,7 +71,8 @@ async function bootstrap() {
await connectToDatabase(dbConfig.dbName, dbConfig.dbConnString); 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 userService = await UserService.create();
const spotifyTokenService = new SpotifyTokenService(SPOTIFY_CLIENT_ID!, SPOTIFY_CLIENT_SECRET!); const spotifyTokenService = new SpotifyTokenService(SPOTIFY_CLIENT_ID!, SPOTIFY_CLIENT_SECRET!);
+62
View File
@@ -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<File> {
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<File[]> {
return FileModel.find({ userId: new mongoose.Types.ObjectId(userId) })
.sort({ uploadedAt: -1 })
.exec();
}
async getFileByObjectKey(objectKey: string): Promise<File | null> {
return FileModel.findOne({ objectKey }).exec();
}
async deleteFileRecord(objectKey: string): Promise<boolean> {
const result = await FileModel.deleteOne({ objectKey });
return result.deletedCount > 0;
}
async isFileDuplicate(originalName: string, userId: string): Promise<boolean> {
const count = await FileModel.countDocuments({
userId: new mongoose.Types.ObjectId(userId),
originalName: originalName,
});
return count > 0;
}
async updateObjectKey(fileId: string, objectKey: string): Promise<File | null> {
return FileModel.findByIdAndUpdate(fileId, { objectKey }, { new: true }).exec();
}
}
+28 -48
View File
@@ -3,10 +3,10 @@ import {
CreateBucketCommand, CreateBucketCommand,
PutObjectCommand, PutObjectCommand,
GetObjectCommand, GetObjectCommand,
ListObjectsV2Command,
DeleteObjectCommand, DeleteObjectCommand,
} from "@aws-sdk/client-s3"; } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { FileService } from "./db/fileService";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
export interface S3ClientConfig { export interface S3ClientConfig {
@@ -25,8 +25,9 @@ export class S3Service {
private readonly client: S3Client; private readonly client: S3Client;
private readonly bucketName: string; private readonly bucketName: string;
private readonly publicUrl: string; private readonly publicUrl: string;
private readonly fileService: FileService;
private constructor(clientConfig: S3ClientConfig) { private constructor(clientConfig: S3ClientConfig, fileService: FileService) {
this.client = new S3Client({ this.client = new S3Client({
endpoint: `${clientConfig.endpoint}:${clientConfig.port}`, endpoint: `${clientConfig.endpoint}:${clientConfig.port}`,
forcePathStyle: true, forcePathStyle: true,
@@ -39,14 +40,15 @@ export class S3Service {
this.bucketName = clientConfig.bucket; this.bucketName = clientConfig.bucket;
this.publicUrl = clientConfig.publicUrl; 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 (!this.instance) {
if (!config) { if (!config || !fileService) {
throw new Error("S3Service must be initialized with a config on first use."); 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; return this.instance;
} }
@@ -65,64 +67,39 @@ export class S3Service {
} }
async uploadFile(file: Express.Multer.File, userId: string): Promise<string> { async uploadFile(file: Express.Multer.File, userId: string): Promise<string> {
const objectKey = `user-${userId}/${randomUUID()}_${file.originalname}`; const uuid = randomUUID();
const objectKey = `user-${userId}/${uuid}_${file.originalname}`;
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: this.bucketName, Bucket: this.bucketName,
Key: objectKey, Key: objectKey,
Body: file.buffer, Body: file.buffer,
ContentType: file.mimetype, ContentType: file.mimetype,
Metadata: {
originalname: encodeURIComponent(file.originalname),
},
}); });
await this.client.send(command); await this.client.send(command);
await this.fileService.createFileRecord(userId, objectKey, file.originalname, file.mimetype, file.size);
return objectKey; return objectKey;
} }
async listFilesForUser(userId: string): Promise<{ key: string; lastModified: Date; originalName?: string }[]> { async listFilesForUser(
const command = new ListObjectsV2Command({ userId: string
Bucket: this.bucketName, ): Promise<{ key: string; lastModified: Date; originalName: string; mimeType: string; size: number }[]> {
Prefix: `user-${userId}/`, const files = await this.fileService.getFilesByUserId(userId);
});
const response = await this.client.send(command); return files.map((file) => ({
key: file.objectKey,
return ( lastModified: file.uploadedAt,
response.Contents?.map((item) => ({ originalName: file.originalName,
key: item.Key!, mimeType: file.mimeType,
lastModified: item.LastModified!, size: file.size,
originalName: this.extractOriginalNameFromKey(item.Key!), }));
})) || []
);
} }
async isFileDuplicate(file: Express.Multer.File, userId: string): Promise<boolean> { async isFileDuplicate(file: Express.Multer.File, userId: string): Promise<boolean> {
const existingFiles = await this.listFilesForUser(userId); return await this.fileService.isFileDuplicate(file.originalname, 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;
} }
async deleteFile(objectKey: string): Promise<void> { async deleteFile(objectKey: string): Promise<void> {
@@ -132,6 +109,9 @@ export class S3Service {
}); });
await this.client.send(command); await this.client.send(command);
await this.fileService.deleteFileRecord(objectKey);
console.log(`File deleted: ${objectKey}`); console.log(`File deleted: ${objectKey}`);
} }