using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Drive.Index; public class FileIndexService(AppDatabase db, FolderService folderService) { /// /// Normalizes a path to ensure consistent formatting /// /// The path to normalize /// The normalized path public static string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) return "/"; // Ensure path starts with / if (!path.StartsWith('/')) path = "/" + path; // Remove trailing slash unless it's the root if (path.Length > 1 && path.EndsWith('/')) path = path.TrimEnd('/'); // Normalize double slashes while (path.Contains("//")) path = path.Replace("//", "/"); return path; } /// /// Gets or creates a folder hierarchy based on a file path /// /// The file path (e.g., "/folder/sub/file.txt") /// The account ID /// The folder where the file should be placed private async Task GetOrCreateFolderByPathAsync(string filePath, Guid accountId) { // Extract folder path from file path (remove filename) var lastSlashIndex = filePath.LastIndexOf('/'); var folderPath = lastSlashIndex == 0 ? "/" : filePath[..(lastSlashIndex + 1)]; // Ensure root folder exists var rootFolder = await folderService.EnsureRootFolderAsync(accountId); // If it's the root folder, return it if (folderPath == "/") return rootFolder; // Split the folder path into segments var pathSegments = folderPath.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries); var currentParent = rootFolder; var currentPath = "/"; // Create folder hierarchy foreach (var segment in pathSegments) { currentPath += segment + "/"; // Check if folder already exists var existingFolder = await db.Folders .FirstOrDefaultAsync(f => f.AccountId == accountId && f.Path == currentPath); if (existingFolder != null) { currentParent = existingFolder; continue; } // Create new folder var newFolder = await folderService.CreateAsync(segment, accountId, currentParent.Id); currentParent = newFolder; } return currentParent; } /// /// Creates a new file index entry at a specific path (creates folder hierarchy if needed) /// /// The path where the file should be indexed /// The file ID /// The account ID /// The created file index public async Task CreateAsync(string path, string fileId, Guid accountId) { var normalizedPath = NormalizePath(path); // Get the file to extract the file name var file = await db.Files .FirstOrDefaultAsync(f => f.Id == fileId) ?? throw new InvalidOperationException($"File with ID '{fileId}' not found"); // Get or create the folder hierarchy based on the path var folder = await GetOrCreateFolderByPathAsync(normalizedPath, accountId); // Check if a file with the same name already exists in the same folder for this account var existingFileIndex = await db.FileIndexes .FirstOrDefaultAsync(fi => fi.AccountId == accountId && fi.FolderId == folder.Id && fi.File.Name == file.Name); if (existingFileIndex != null) { throw new InvalidOperationException( $"A file with name '{file.Name}' already exists in folder '{folder.Name}' for account '{accountId}'"); } var fileIndex = SnCloudFileIndex.Create(folder, file, accountId); db.FileIndexes.Add(fileIndex); await db.SaveChangesAsync(); return fileIndex; } /// /// Creates a new file index entry in a specific folder /// /// The folder ID where the file should be placed /// The file ID /// The account ID /// The created file index public async Task CreateInFolderAsync(Guid folderId, string fileId, Guid accountId) { // Verify the folder exists and belongs to the account var folder = await db.Folders .FirstOrDefaultAsync(f => f.Id == folderId && f.AccountId == accountId); if (folder == null) { throw new InvalidOperationException($"Folder with ID '{folderId}' not found or access denied"); } // Get the file to extract the file name var file = await db.Files .FirstOrDefaultAsync(f => f.Id == fileId); if (file == null) { throw new InvalidOperationException($"File with ID '{fileId}' not found"); } // Check if a file with the same name already exists in the same folder for this account var existingFileIndex = await db.FileIndexes .FirstOrDefaultAsync(fi => fi.AccountId == accountId && fi.FolderId == folderId && fi.File.Name == file.Name); if (existingFileIndex != null) { throw new InvalidOperationException( $"A file with name '{file.Name}' already exists in folder '{folder.Name}' for account '{accountId}'"); } var fileIndex = SnCloudFileIndex.Create(folder, file, accountId); db.FileIndexes.Add(fileIndex); await db.SaveChangesAsync(); return fileIndex; } /// /// Moves a file to a different folder /// /// The file index ID /// The new folder ID /// The account ID /// The updated file index public async Task MoveAsync(Guid fileIndexId, Guid newFolderId, Guid accountId) { var fileIndex = await db.FileIndexes .Include(fi => fi.File) .FirstOrDefaultAsync(fi => fi.Id == fileIndexId && fi.AccountId == accountId); if (fileIndex == null) return null; // Verify the new folder exists and belongs to the account var newFolder = await db.Folders .FirstOrDefaultAsync(f => f.Id == newFolderId && f.AccountId == accountId); if (newFolder == null) { throw new InvalidOperationException($"Target folder with ID '{newFolderId}' not found or access denied"); } // Check if a file with the same name already exists in the target folder var existingFileIndex = await db.FileIndexes .FirstOrDefaultAsync(fi => fi.AccountId == accountId && fi.FolderId == newFolderId && fi.File.Name == fileIndex.File.Name && fi.Id != fileIndexId); if (existingFileIndex != null) { throw new InvalidOperationException( $"A file with name '{fileIndex.File.Name}' already exists in folder '{newFolder.Name}'"); } // Since properties are init-only, we need to remove the old index and create a new one db.FileIndexes.Remove(fileIndex); var newFileIndex = SnCloudFileIndex.Create(newFolder, fileIndex.File, accountId); newFileIndex.Id = fileIndexId; // Keep the same ID db.FileIndexes.Add(newFileIndex); await db.SaveChangesAsync(); return newFileIndex; } /// /// Removes a file index entry by ID /// /// The file index ID /// True if the index was found and removed, false otherwise public async Task RemoveAsync(Guid id) { var fileIndex = await db.FileIndexes.FindAsync(id); if (fileIndex == null) return false; db.FileIndexes.Remove(fileIndex); await db.SaveChangesAsync(); return true; } /// /// Removes file index entries by file ID /// /// The file ID /// The number of indexes removed public async Task RemoveByFileIdAsync(string fileId) { var indexes = await db.FileIndexes .Where(fi => fi.FileId == fileId) .ToListAsync(); if (indexes.Count == 0) return 0; db.FileIndexes.RemoveRange(indexes); await db.SaveChangesAsync(); return indexes.Count; } /// /// Removes file index entries by account ID and folder /// /// The account ID /// The folder ID /// The number of indexes removed public async Task RemoveByFolderAsync(Guid accountId, Guid folderId) { var indexes = await db.FileIndexes .Where(fi => fi.AccountId == accountId && fi.FolderId == folderId) .ToListAsync(); if (!indexes.Any()) return 0; db.FileIndexes.RemoveRange(indexes); await db.SaveChangesAsync(); return indexes.Count; } /// /// Gets file indexes by account ID and folder /// /// The account ID /// The folder ID /// List of file indexes public async Task> GetByFolderAsync(Guid accountId, Guid folderId) { return await db.FileIndexes .Where(fi => fi.AccountId == accountId && fi.FolderId == folderId) .Include(fi => fi.File) .ToListAsync(); } /// /// Gets file indexes by file ID with folder information /// /// The file ID /// List of file indexes public async Task> GetByFileIdAsync(string fileId) { return await db.FileIndexes .Where(fi => fi.FileId == fileId) .Include(fi => fi.File) .Include(fi => fi.Folder) .ToListAsync(); } /// /// Gets all file indexes for an account /// /// The account ID /// List of file indexes public async Task> GetByAccountIdAsync(Guid accountId) { return await db.FileIndexes .Where(fi => fi.AccountId == accountId) .Include(fi => fi.File) .Include(fi => fi.Folder) .ToListAsync(); } /// /// Gets file indexes by path for an account (finds folder by path and gets files in that folder) /// /// The account ID /// The path to search for /// List of file indexes at the specified path public async Task> GetByPathAsync(Guid accountId, string path) { var normalizedPath = NormalizePath(path); // Find the folder that corresponds to this path var folder = await db.Folders .FirstOrDefaultAsync(f => f.AccountId == accountId && f.Path == normalizedPath + (normalizedPath == "/" ? "" : "/")); if (folder == null) return new List(); return await db.FileIndexes .Where(fi => fi.AccountId == accountId && fi.FolderId == folder.Id) .Include(fi => fi.File) .Include(fi => fi.Folder) .ToListAsync(); } /// /// Updates the path of a file index /// /// The file index ID /// The new path /// The updated file index, or null if not found public async Task UpdateAsync(Guid fileIndexId, string newPath) { var fileIndex = await db.FileIndexes .Include(fi => fi.File) .Include(fi => fi.Folder) .FirstOrDefaultAsync(fi => fi.Id == fileIndexId); if (fileIndex == null) return null; var normalizedPath = NormalizePath(newPath); // Get or create the folder hierarchy based on the new path var newFolder = await GetOrCreateFolderByPathAsync(normalizedPath, fileIndex.AccountId); // Check if a file with the same name already exists in the new folder var existingFileIndex = await db.FileIndexes .FirstOrDefaultAsync(fi => fi.AccountId == fileIndex.AccountId && fi.FolderId == newFolder.Id && fi.File.Name == fileIndex.File.Name && fi.Id != fileIndexId); if (existingFileIndex != null) { throw new InvalidOperationException( $"A file with name '{fileIndex.File.Name}' already exists in folder '{newFolder.Name}'"); } // Since properties are init-only, we need to remove the old index and create a new one db.FileIndexes.Remove(fileIndex); var updatedFileIndex = SnCloudFileIndex.Create(newFolder, fileIndex.File, fileIndex.AccountId); updatedFileIndex.Id = fileIndexId; // Keep the same ID db.FileIndexes.Add(updatedFileIndex); await db.SaveChangesAsync(); return updatedFileIndex; } /// /// Removes all file index entries at a specific path for an account (finds folder by path and removes files from that folder) /// /// The account ID /// The path to clear /// The number of indexes removed public async Task RemoveByPathAsync(Guid accountId, string path) { var normalizedPath = NormalizePath(path); // Find the folder that corresponds to this path var folder = await db.Folders .FirstOrDefaultAsync(f => f.AccountId == accountId && f.Path == normalizedPath + (normalizedPath == "/" ? "" : "/")); if (folder == null) return 0; var indexes = await db.FileIndexes .Where(fi => fi.AccountId == accountId && fi.FolderId == folder.Id) .ToListAsync(); if (!indexes.Any()) return 0; db.FileIndexes.RemoveRange(indexes); await db.SaveChangesAsync(); return indexes.Count; } }