import mongoose from "mongoose"; import { FileType } from "@/types"; const FileSchema = new mongoose.Schema({ name: { type: String, required: true, trim: true, maxlength: 255, validate: { validator: function(v: string) { // التحقق من صحة اسم الملف const invalidChars = /[<>:"/\\|?*\x00-\x1f]/; return !invalidChars.test(v) && v.length > 0; }, message: 'Invalid file name. Contains forbidden characters.' } }, content: { type: String, required: true, default: "", maxlength: 10 * 1024 * 1024, // 10MB max file size }, type: { type: String, required: true, enum: [ 'html', 'css', 'js', 'ts', 'jsx', 'tsx', 'vue', 'json', 'md', 'txt', 'py', 'php', 'xml', 'svg', 'yaml', 'yml', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'webp', 'pdf' ] as FileType[], default: 'txt' }, path: { type: String, required: true, trim: true, validate: { validator: function(v: string) { // التحقق من صحة المسار return v.startsWith('/') && !v.includes('..') && v.length <= 1000; }, message: 'Invalid file path.' } }, projectId: { type: mongoose.Schema.Types.ObjectId, ref: 'Project', required: true, index: true }, parentFolderId: { type: mongoose.Schema.Types.ObjectId, ref: 'Folder', default: null, index: true }, size: { type: Number, required: true, min: 0, max: 10 * 1024 * 1024, // 10MB max default: 0 }, encoding: { type: String, required: true, enum: ['utf-8', 'base64', 'binary'], default: 'utf-8' }, isGenerated: { type: Boolean, required: true, default: false, index: true }, lastModifiedBy: { type: String, required: true, enum: ['user', 'ai'], default: 'user', index: true }, // Metadata for better organization metadata: { description: { type: String, maxlength: 500, default: "" }, tags: [{ type: String, maxlength: 50 }], isPublic: { type: Boolean, default: false }, language: { type: String, maxlength: 20, default: "" } }, // Performance tracking performance: { lastAccessTime: { type: Date, default: Date.now }, accessCount: { type: Number, default: 0, min: 0 }, editCount: { type: Number, default: 0, min: 0 } } }, { timestamps: true, // Indexes for better performance indexes: [ { projectId: 1, name: 1 }, // Unique file names per project { projectId: 1, type: 1 }, // Files by type { projectId: 1, parentFolderId: 1 }, // Files in folder { projectId: 1, isGenerated: 1 }, // AI generated files { projectId: 1, lastModifiedBy: 1 }, // Files by modifier { path: 1 }, // Path lookup { 'performance.lastAccessTime': 1 } // Recently accessed files ] }); // Pre-save middleware to calculate file size and validate FileSchema.pre('save', function(this: any, next) { // Calculate file size in bytes if (this.isModified('content')) { this.size = Buffer.byteLength(this.content, this.encoding as BufferEncoding); if (this.performance) this.performance.editCount = (this.performance.editCount || 0) + 1; } // Update access time if (this.performance) this.performance.lastAccessTime = new Date(); // Validate file size if (this.size > 10 * 1024 * 1024) { return next(new Error('File size exceeds 10MB limit')); } // Auto-detect file type if not set if (!this.type || this.type === 'txt') { this.type = this.detectFileType(); } // Generate path if not set if (!this.path) { this.path = this.generatePath(); } next(); }); // Instance methods FileSchema.methods.detectFileType = function(): FileType { const extension = this.name.split('.').pop()?.toLowerCase(); const validTypes: FileType[] = [ 'html', 'css', 'js', 'ts', 'jsx', 'tsx', 'vue', 'json', 'md', 'txt', 'py', 'php', 'xml', 'svg', 'yaml', 'yml', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'webp', 'pdf' ]; if (extension && validTypes.includes(extension as FileType)) { return extension as FileType; } // Content-based detection for files without extensions const content = this.content.toLowerCase(); if (content.includes(' = { html: '🌐', css: '🎨', js: '⚡', ts: '🔷', jsx: '⚛️', tsx: '⚛️', vue: '💚', json: '📋', md: '📝', txt: '📄', py: '🐍', php: '🐘', xml: '📰', svg: '🖼️', yaml: '⚙️', yml: '⚙️', png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', ico: '🖼️', webp: '🖼️', pdf: '📕' }; return icons[this.type as FileType] || '📄'; }; // Static methods FileSchema.statics.findByProject = function(projectId: string) { return this.find({ projectId }).sort({ name: 1 }); }; FileSchema.statics.findByType = function(projectId: string, type: FileType) { return this.find({ projectId, type }).sort({ name: 1 }); }; FileSchema.statics.findByFolder = function(projectId: string, folderId?: string) { return this.find({ projectId, parentFolderId: folderId || null }).sort({ name: 1 }); }; FileSchema.statics.searchFiles = function(projectId: string, query: string) { return this.find({ projectId, $or: [ { name: { $regex: query, $options: 'i' } }, { content: { $regex: query, $options: 'i' } }, { 'metadata.description': { $regex: query, $options: 'i' } }, { 'metadata.tags': { $in: [new RegExp(query, 'i')] } } ] }).sort({ 'performance.lastAccessTime': -1 }); }; // Compound unique index to prevent duplicate file names in same folder FileSchema.index( { projectId: 1, parentFolderId: 1, name: 1 }, { unique: true } ); export interface FileModel extends mongoose.Model { findByFolder(projectId: string, folderId?: string): Promise; } const FileModelTyped = (mongoose.models.File as FileModel) || (mongoose.model("File", FileSchema) as unknown as FileModel); export default FileModelTyped;