using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Drive.Index; public class FolderService(AppDatabase db) { /// /// Creates a new folder /// /// The folder name /// The account ID /// Optional parent folder ID /// The created folder public async Task CreateAsync(string name, Guid accountId, Guid? parentFolderId = null) { // Validate folder name if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Folder name cannot be empty", nameof(name)); // Check if parent folder exists and belongs to the same account SnCloudFolder? parentFolder = null; if (parentFolderId.HasValue) { parentFolder = await db.Folders .FirstOrDefaultAsync(f => f.Id == parentFolderId && f.AccountId == accountId); if (parentFolder == null) throw new InvalidOperationException($"Parent folder with ID '{parentFolderId}' not found or access denied"); } // Check if folder with same name already exists in the same location var existingFolder = await db.Folders .FirstOrDefaultAsync(f => f.AccountId == accountId && f.ParentFolderId == parentFolderId && f.Name == name); if (existingFolder != null) { throw new InvalidOperationException( $"A folder with name '{name}' already exists in the specified location"); } var folder = SnCloudFolder.Create(name, accountId, parentFolder); db.Folders.Add(folder); await db.SaveChangesAsync(); return folder; } /// /// Creates the root folder for an account (if it doesn't exist) /// /// The account ID /// The root folder public async Task EnsureRootFolderAsync(Guid accountId) { var rootFolder = await db.Folders .FirstOrDefaultAsync(f => f.AccountId == accountId && f.ParentFolderId == null); if (rootFolder == null) { rootFolder = SnCloudFolder.CreateRoot(accountId); db.Folders.Add(rootFolder); await db.SaveChangesAsync(); } return rootFolder; } /// /// Gets a folder by ID with its contents /// /// The folder ID /// The account ID (for authorization) /// The folder with child folders and files public async Task GetByIdAsync(Guid folderId, Guid accountId) { return await db.Folders .Include(f => f.ChildFolders.OrderBy(cf => cf.Name)) .Include(f => f.Files.OrderBy(fi => fi.File.Name)) .ThenInclude(fi => fi.File) .FirstOrDefaultAsync(f => f.Id == folderId && f.AccountId == accountId); } /// /// Gets all folders for an account /// /// The account ID /// List of folders public async Task> GetByAccountIdAsync(Guid accountId) { return await db.Folders .Where(f => f.AccountId == accountId) .Include(f => f.ParentFolder) .OrderBy(f => f.Path) .ToListAsync(); } /// /// Gets child folders of a parent folder /// /// The parent folder ID /// The account ID /// List of child folders public async Task> GetChildFoldersAsync(Guid parentFolderId, Guid accountId) { return await db.Folders .Where(f => f.ParentFolderId == parentFolderId && f.AccountId == accountId) .OrderBy(f => f.Name) .ToListAsync(); } /// /// Updates a folder's name and path /// /// The folder ID /// The new folder name /// The account ID /// The updated folder public async Task UpdateAsync(Guid folderId, string newName, Guid accountId) { var folder = await db.Folders .Include(f => f.ParentFolder) .FirstOrDefaultAsync(f => f.Id == folderId && f.AccountId == accountId); if (folder == null) return null; // Check if folder with same name already exists in the same location var existingFolder = await db.Folders .FirstOrDefaultAsync(f => f.AccountId == accountId && f.ParentFolderId == folder.ParentFolderId && f.Name == newName && f.Id != folderId); if (existingFolder != null) { throw new InvalidOperationException( $"A folder with name '{newName}' already exists in the specified location"); } // Update folder name and path var oldPath = folder.Path; folder = SnCloudFolder.Create(newName, accountId, folder.ParentFolder); folder.Id = folderId; // Keep the same ID // Update all child folders' paths recursively await UpdateChildFolderPathsAsync(folderId, oldPath, folder.Path); db.Folders.Update(folder); await db.SaveChangesAsync(); return folder; } /// /// Recursively updates child folder paths when a parent folder is renamed /// private async Task UpdateChildFolderPathsAsync(Guid parentFolderId, string oldParentPath, string newParentPath) { var childFolders = await db.Folders .Where(f => f.ParentFolderId == parentFolderId) .ToListAsync(); foreach (var childFolder in childFolders) { var newPath = childFolder.Path.Replace(oldParentPath, newParentPath); childFolder.Path = newPath; // Recursively update grandchildren await UpdateChildFolderPathsAsync(childFolder.Id, oldParentPath, newParentPath); } await db.SaveChangesAsync(); } /// /// Deletes a folder and all its contents /// /// The folder ID /// The account ID /// True if the folder was deleted, false otherwise public async Task DeleteAsync(Guid folderId, Guid accountId) { var folder = await db.Folders .Include(f => f.ChildFolders) .Include(f => f.Files) .FirstOrDefaultAsync(f => f.Id == folderId && f.AccountId == accountId); if (folder == null) return false; // Recursively delete child folders foreach (var childFolder in folder.ChildFolders.ToList()) { await DeleteAsync(childFolder.Id, accountId); } // Remove file indexes db.FileIndexes.RemoveRange(folder.Files); // Remove the folder itself db.Folders.Remove(folder); await db.SaveChangesAsync(); return true; } /// /// Moves a folder to a new parent folder /// /// The folder ID /// The new parent folder ID /// The account ID /// The moved folder public async Task MoveAsync(Guid folderId, Guid? newParentFolderId, Guid accountId) { var folder = await db.Folders .FirstOrDefaultAsync(f => f.Id == folderId && f.AccountId == accountId); if (folder == null) return null; // Check if new parent exists and belongs to the same account SnCloudFolder? newParentFolder = null; if (newParentFolderId.HasValue) { newParentFolder = await db.Folders .FirstOrDefaultAsync(f => f.Id == newParentFolderId && f.AccountId == accountId); if (newParentFolder == null) throw new InvalidOperationException($"Target folder with ID '{newParentFolderId}' not found or access denied"); } // Check for circular reference if (newParentFolderId.HasValue && await IsCircularReferenceAsync(folderId, newParentFolderId.Value)) { throw new InvalidOperationException("Cannot move folder to its own descendant"); } // Check if folder with same name already exists in the target location var existingFolder = await db.Folders .FirstOrDefaultAsync(f => f.AccountId == accountId && f.ParentFolderId == newParentFolderId && f.Name == folder.Name); if (existingFolder != null) { throw new InvalidOperationException( $"A folder with name '{folder.Name}' already exists in the target location"); } var oldPath = folder.Path; var newPath = newParentFolder != null ? $"{newParentFolder.Path.TrimEnd('/')}/{folder.Name}/" : $"/{folder.Name}/"; // Update folder parent and path folder.ParentFolderId = newParentFolderId; folder.Path = newPath; // Update all child folders' paths recursively await UpdateChildFolderPathsAsync(folderId, oldPath, newPath); db.Folders.Update(folder); await db.SaveChangesAsync(); return folder; } /// /// Checks if moving a folder would create a circular reference /// private async Task IsCircularReferenceAsync(Guid folderId, Guid potentialParentId) { if (folderId == potentialParentId) return true; var currentFolderId = potentialParentId; while (currentFolderId != Guid.Empty) { var currentFolder = await db.Folders .Where(f => f.Id == currentFolderId) .Select(f => new { f.Id, f.ParentFolderId }) .FirstOrDefaultAsync(); if (currentFolder == null) break; if (currentFolder.Id == folderId) return true; currentFolderId = currentFolder.ParentFolderId ?? Guid.Empty; } return false; } /// /// Searches for folders by name /// /// The account ID /// The search term /// List of matching folders public async Task> SearchAsync(Guid accountId, string searchTerm) { return await db.Folders .Where(f => f.AccountId == accountId && f.Name.Contains(searchTerm)) .Include(f => f.ParentFolder) .OrderBy(f => f.Name) .ToListAsync(); } }