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 { 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!);
+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,
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<string> {
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<boolean> {
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<void> {
@@ -132,6 +109,9 @@ export class S3Service {
});
await this.client.send(command);
await this.fileService.deleteFileRecord(objectKey);
console.log(`File deleted: ${objectKey}`);
}