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 { 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!);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user