PrithviGuardian / client /src /lib /projectManager.ts
Varad Bakshi
Fresh upload
e7427b5
Raw
History Blame Contribute Delete
16.2 kB
// Project-wise File Management System with Local Storage
// Comprehensive project organization and file handling
interface ProjectFile {
id: string;
name: string;
type: 'calculation' | 'report' | 'data' | 'image' | 'document';
content: any;
size: number;
createdAt: Date;
modifiedAt: Date;
tags: string[];
metadata: Record<string, any>;
}
interface Project {
id: string;
name: string;
description: string;
createdAt: Date;
modifiedAt: Date;
files: ProjectFile[];
settings: ProjectSettings;
status: 'active' | 'completed' | 'archived';
}
interface ProjectSettings {
autoSave: boolean;
backupFrequency: number; // hours
compressionEnabled: boolean;
maxFileSize: number; // MB
allowedFileTypes: string[];
}
class ProjectManager {
private readonly STORAGE_KEY = 'prithvi_projects';
private readonly MAX_STORAGE_SIZE = 50 * 1024 * 1024; // 50MB limit
private projects: Map<string, Project> = new Map();
constructor() {
this.loadProjects();
this.setupAutoSave();
}
// Project Management
async createProject(name: string, description: string = ''): Promise<Project> {
try {
if (!name || name.trim().length === 0) {
throw new Error('Project name is required');
}
if (name.length > 100) {
throw new Error('Project name must be less than 100 characters');
}
// Check for duplicate names
const existingProject = Array.from(this.projects.values()).find(p => p.name === name);
if (existingProject) {
throw new Error('Project with this name already exists');
}
const project: Project = {
id: this.generateId(),
name: name.trim(),
description: description.trim(),
createdAt: new Date(),
modifiedAt: new Date(),
files: [],
settings: this.getDefaultSettings(),
status: 'active'
};
this.projects.set(project.id, project);
await this.saveProjects();
return project;
} catch (error) {
throw new Error(`Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async updateProject(projectId: string, updates: Partial<Project>): Promise<Project> {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
// Validate updates
if (updates.name !== undefined) {
if (!updates.name || updates.name.trim().length === 0) {
throw new Error('Project name cannot be empty');
}
if (updates.name.length > 100) {
throw new Error('Project name must be less than 100 characters');
}
}
const updatedProject = {
...project,
...updates,
id: project.id, // Prevent ID changes
modifiedAt: new Date()
};
this.projects.set(projectId, updatedProject);
await this.saveProjects();
return updatedProject;
} catch (error) {
throw new Error(`Failed to update project: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async deleteProject(projectId: string): Promise<boolean> {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
this.projects.delete(projectId);
await this.saveProjects();
return true;
} catch (error) {
throw new Error(`Failed to delete project: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
getProject(projectId: string): Project | undefined {
return this.projects.get(projectId);
}
getAllProjects(): Project[] {
return Array.from(this.projects.values()).sort((a, b) =>
b.modifiedAt.getTime() - a.modifiedAt.getTime()
);
}
getProjectsByStatus(status: Project['status']): Project[] {
return this.getAllProjects().filter(p => p.status === status);
}
searchProjects(query: string): Project[] {
if (!query || query.trim().length === 0) {
return this.getAllProjects();
}
const searchTerm = query.toLowerCase().trim();
return this.getAllProjects().filter(project =>
project.name.toLowerCase().includes(searchTerm) ||
project.description.toLowerCase().includes(searchTerm) ||
project.files.some(file =>
file.name.toLowerCase().includes(searchTerm) ||
file.tags.some(tag => tag.toLowerCase().includes(searchTerm))
)
);
}
// File Management
async addFile(projectId: string, file: Omit<ProjectFile, 'id' | 'createdAt' | 'modifiedAt'>): Promise<ProjectFile> {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
// Validate file
this.validateFile(file, project.settings);
// Check storage limits
await this.checkStorageLimit(file.size);
const newFile: ProjectFile = {
...file,
id: this.generateId(),
createdAt: new Date(),
modifiedAt: new Date()
};
project.files.push(newFile);
project.modifiedAt = new Date();
this.projects.set(projectId, project);
await this.saveProjects();
return newFile;
} catch (error) {
throw new Error(`Failed to add file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async updateFile(projectId: string, fileId: string, updates: Partial<ProjectFile>): Promise<ProjectFile> {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
const fileIndex = project.files.findIndex(f => f.id === fileId);
if (fileIndex === -1) {
throw new Error('File not found');
}
const updatedFile = {
...project.files[fileIndex],
...updates,
id: fileId, // Prevent ID changes
modifiedAt: new Date()
};
// Validate updated file
this.validateFile(updatedFile, project.settings);
project.files[fileIndex] = updatedFile;
project.modifiedAt = new Date();
this.projects.set(projectId, project);
await this.saveProjects();
return updatedFile;
} catch (error) {
throw new Error(`Failed to update file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async deleteFile(projectId: string, fileId: string): Promise<boolean> {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
const fileIndex = project.files.findIndex(f => f.id === fileId);
if (fileIndex === -1) {
throw new Error('File not found');
}
project.files.splice(fileIndex, 1);
project.modifiedAt = new Date();
this.projects.set(projectId, project);
await this.saveProjects();
return true;
} catch (error) {
throw new Error(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
getFile(projectId: string, fileId: string): ProjectFile | undefined {
const project = this.projects.get(projectId);
return project?.files.find(f => f.id === fileId);
}
getFilesByType(projectId: string, type: ProjectFile['type']): ProjectFile[] {
const project = this.projects.get(projectId);
return project?.files.filter(f => f.type === type) || [];
}
searchFiles(projectId: string, query: string): ProjectFile[] {
const project = this.projects.get(projectId);
if (!project || !query.trim()) {
return project?.files || [];
}
const searchTerm = query.toLowerCase().trim();
return project.files.filter(file =>
file.name.toLowerCase().includes(searchTerm) ||
file.tags.some(tag => tag.toLowerCase().includes(searchTerm)) ||
(typeof file.content === 'string' && file.content.toLowerCase().includes(searchTerm))
);
}
// Data Export/Import
async exportProject(projectId: string): Promise<string> {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
const exportData = {
project,
exportedAt: new Date().toISOString(),
version: '1.0',
metadata: {
totalFiles: project.files.length,
totalSize: this.calculateProjectSize(project)
}
};
return JSON.stringify(exportData, null, 2);
} catch (error) {
throw new Error(`Failed to export project: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async importProject(importData: string): Promise<Project> {
try {
const data = JSON.parse(importData);
if (!data.project) {
throw new Error('Invalid import data: missing project information');
}
const project = data.project;
// Validate project structure
if (!project.name || !project.id) {
throw new Error('Invalid project data: missing required fields');
}
// Generate new ID to avoid conflicts
project.id = this.generateId();
project.createdAt = new Date();
project.modifiedAt = new Date();
// Validate and regenerate file IDs
if (project.files) {
project.files.forEach((file: ProjectFile) => {
file.id = this.generateId();
file.createdAt = new Date(file.createdAt);
file.modifiedAt = new Date(file.modifiedAt);
});
}
// Check storage limits
const projectSize = this.calculateProjectSize(project);
await this.checkStorageLimit(projectSize);
this.projects.set(project.id, project);
await this.saveProjects();
return project;
} catch (error) {
throw new Error(`Failed to import project: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Storage Management
async getStorageInfo(): Promise<{
used: number;
available: number;
total: number;
projects: number;
files: number;
}> {
try {
const projects = this.getAllProjects();
const totalFiles = projects.reduce((sum, p) => sum + p.files.length, 0);
const usedSpace = this.calculateTotalSize();
return {
used: usedSpace,
available: this.MAX_STORAGE_SIZE - usedSpace,
total: this.MAX_STORAGE_SIZE,
projects: projects.length,
files: totalFiles
};
} catch (error) {
throw new Error(`Failed to get storage info: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async cleanupStorage(daysOld: number = 30): Promise<{
deletedProjects: number;
freedSpace: number;
}> {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
let deletedProjects = 0;
let freedSpace = 0;
for (const [projectId, project] of this.projects) {
if (project.status === 'archived' && project.modifiedAt < cutoffDate) {
freedSpace += this.calculateProjectSize(project);
this.projects.delete(projectId);
deletedProjects++;
}
}
if (deletedProjects > 0) {
await this.saveProjects();
}
return { deletedProjects, freedSpace };
} catch (error) {
throw new Error(`Failed to cleanup storage: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Backup and Recovery
async createBackup(): Promise<string> {
try {
const backup = {
projects: Array.from(this.projects.values()),
backupDate: new Date().toISOString(),
version: '1.0'
};
return JSON.stringify(backup, null, 2);
} catch (error) {
throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async restoreFromBackup(backupData: string): Promise<boolean> {
try {
const backup = JSON.parse(backupData);
if (!backup.projects || !Array.isArray(backup.projects)) {
throw new Error('Invalid backup data');
}
// Clear existing projects
this.projects.clear();
// Restore projects
backup.projects.forEach((project: Project) => {
// Regenerate dates
project.createdAt = new Date(project.createdAt);
project.modifiedAt = new Date(project.modifiedAt);
if (project.files) {
project.files.forEach((file: ProjectFile) => {
file.createdAt = new Date(file.createdAt);
file.modifiedAt = new Date(file.modifiedAt);
});
}
this.projects.set(project.id, project);
});
await this.saveProjects();
return true;
} catch (error) {
throw new Error(`Failed to restore from backup: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Private helper methods
private generateId(): string {
return `proj_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private getDefaultSettings(): ProjectSettings {
return {
autoSave: true,
backupFrequency: 24,
compressionEnabled: true,
maxFileSize: 10,
allowedFileTypes: [
'text/plain', 'application/json', 'text/csv',
'image/jpeg', 'image/png', 'image/gif',
'application/pdf', 'text/html'
]
};
}
private validateFile(file: Partial<ProjectFile>, settings: ProjectSettings): void {
if (!file.name || file.name.trim().length === 0) {
throw new Error('File name is required');
}
if (file.name.length > 255) {
throw new Error('File name must be less than 255 characters');
}
if (file.size !== undefined && file.size > settings.maxFileSize * 1024 * 1024) {
throw new Error(`File size exceeds limit of ${settings.maxFileSize}MB`);
}
if (file.tags && file.tags.some(tag => tag.length > 50)) {
throw new Error('Tag length must be less than 50 characters');
}
}
private calculateProjectSize(project: Project): number {
return project.files.reduce((total, file) => total + file.size, 0);
}
private calculateTotalSize(): number {
let total = 0;
for (const project of this.projects.values()) {
total += this.calculateProjectSize(project);
}
return total;
}
private async checkStorageLimit(additionalSize: number): Promise<void> {
const currentSize = this.calculateTotalSize();
if (currentSize + additionalSize > this.MAX_STORAGE_SIZE) {
throw new Error(`Storage limit exceeded. Available: ${((this.MAX_STORAGE_SIZE - currentSize) / 1024 / 1024).toFixed(2)}MB, Required: ${(additionalSize / 1024 / 1024).toFixed(2)}MB`);
}
}
private loadProjects(): void {
try {
const data = localStorage.getItem(this.STORAGE_KEY);
if (data) {
const projects = JSON.parse(data);
projects.forEach((project: Project) => {
// Convert date strings back to Date objects
project.createdAt = new Date(project.createdAt);
project.modifiedAt = new Date(project.modifiedAt);
if (project.files) {
project.files.forEach((file: ProjectFile) => {
file.createdAt = new Date(file.createdAt);
file.modifiedAt = new Date(file.modifiedAt);
});
}
this.projects.set(project.id, project);
});
}
} catch (error) {
console.error('Failed to load projects from storage:', error);
}
}
private async saveProjects(): Promise<void> {
try {
const projects = Array.from(this.projects.values());
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(projects));
} catch (error) {
throw new Error(`Failed to save projects: ${error instanceof Error ? error.message : 'Storage quota exceeded'}`);
}
}
private setupAutoSave(): void {
// Auto-save every 5 minutes
setInterval(() => {
this.saveProjects().catch(error => {
console.error('Auto-save failed:', error);
});
}, 5 * 60 * 1000);
}
}
export const projectManager = new ProjectManager();
export { Project, ProjectFile, ProjectSettings };