add file model and service for file management functionality
This commit is contained in:
@@ -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
@@ -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!);
|
||||
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user