diff --git a/DysonNetwork.Drive/AppDatabase.cs b/DysonNetwork.Drive/AppDatabase.cs index 93c19f7..da5176d 100644 --- a/DysonNetwork.Drive/AppDatabase.cs +++ b/DysonNetwork.Drive/AppDatabase.cs @@ -26,7 +26,6 @@ public class AppDatabase( public DbSet Files { get; set; } = null!; public DbSet FileReferences { get; set; } = null!; public DbSet FileIndexes { get; set; } - public DbSet Folders { get; set; } = null!; public DbSet Tasks { get; set; } = null!; public DbSet UploadTasks { get; set; } = null!; // Backward compatibility diff --git a/DysonNetwork.Drive/Index/FileIndexController.cs b/DysonNetwork.Drive/Index/FileIndexController.cs index 0e6c5ee..74febab 100644 --- a/DysonNetwork.Drive/Index/FileIndexController.cs +++ b/DysonNetwork.Drive/Index/FileIndexController.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations; +using DysonNetwork.Drive.Storage; +using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; @@ -13,7 +15,6 @@ namespace DysonNetwork.Drive.Index; [Authorize] public class FileIndexController( FileIndexService fileIndexService, - FolderService folderService, AppDatabase db, ILogger logger ) : ControllerBase @@ -34,10 +35,13 @@ public class FileIndexController( try { var fileIndexes = await fileIndexService.GetByPathAsync(accountId, path); - - // Get child folders using the folder system - var childFolders = await GetChildFoldersAsync(accountId, path); - + + // Get all file indexes for this account to extract child folders + var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId); + + // Extract unique child folder paths + var childFolders = ExtractChildFolders(allFileIndexes, path); + return Ok(new { Path = path, @@ -59,108 +63,38 @@ public class FileIndexController( } /// - /// Gets child folders for a given parent path using the folder system + /// Extracts unique child folder paths from all file indexes for a given parent path /// - /// The account ID + /// All file indexes for the account /// The parent path to find children for - /// List of child folder objects - private async Task> GetChildFoldersAsync(Guid accountId, string parentPath) + /// List of unique child folder names + private List ExtractChildFolders(List allFileIndexes, string parentPath) { var normalizedParentPath = FileIndexService.NormalizePath(parentPath); + var childFolders = new HashSet(); - // Try to find a folder that corresponds to this path - var parentFolder = await FindFolderByPathAsync(accountId, normalizedParentPath); - - if (parentFolder != null) + foreach (var index in allFileIndexes) { - // Use folder-based approach - return await folderService.GetChildFoldersAsync(parentFolder.Id, accountId); - } - else - { - // Fall back to path-based approach - find folders that start with this path - var allFolders = await folderService.GetByAccountIdAsync(accountId); - var childFolders = new List(); - - foreach (var folder in allFolders) + var normalizedIndexPath = FileIndexService.NormalizePath(index.Path); + + // Check if this path is a direct child of the parent path + if (normalizedIndexPath.StartsWith(normalizedParentPath) && + normalizedIndexPath != normalizedParentPath) { - // For path-based folders, we need to check if they belong under this path - // This is a simplified approach - in a full implementation, folders would have path information - if (folder.ParentFolderId == null && normalizedParentPath == "/") + // Remove the parent path prefix to get the relative path + var relativePath = normalizedIndexPath.Substring(normalizedParentPath.Length); + + // Extract the first folder name (direct child) + var firstSlashIndex = relativePath.IndexOf('/'); + if (firstSlashIndex > 0) { - // Root level folders - childFolders.Add(folder); + var folderName = relativePath.Substring(0, firstSlashIndex); + childFolders.Add(folderName); } - // For nested folders, we'd need path information in the folder model } - - return childFolders.OrderBy(f => f.Name).ToList(); - } - } - - /// - /// Attempts to find a folder by its path - /// - /// The account ID - /// The path to search for - /// The folder if found, null otherwise - private async Task FindFolderByPathAsync(Guid accountId, string path) - { - // This is a simplified implementation - // In a full implementation, folders would have path information stored - var allFolders = await folderService.GetByAccountIdAsync(accountId); - - // For now, just return null to use path-based approach - // TODO: Implement proper path-to-folder mapping - return null; - } - - /// - /// 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; + return childFolders.OrderBy(f => f).ToList(); } /// @@ -182,7 +116,7 @@ public class FileIndexController( return Ok(new { Files = fileIndexes, - TotalCount = fileIndexes.Count + TotalCount = fileIndexes.Count() }); } catch (Exception ex) @@ -425,11 +359,9 @@ public class FileIndexController( if (file.AccountId != accountId) return new ObjectResult(ApiError.Unauthorized(forbidden: true)) { StatusCode = 403 }; - // Check if index already exists for this file and path - since Path is now optional, we need to check by folder - // For now, we'll check if any index exists for this file in the same folder that would result from the path - var targetFolder = await GetOrCreateFolderByPathAsync(FileIndexService.NormalizePath(request.Path), accountId); + // Check if index already exists for this file and path var existingIndex = await db.FileIndexes - .FirstOrDefaultAsync(fi => fi.FileId == request.FileId && fi.FolderId == targetFolder.Id && fi.AccountId == accountId); + .FirstOrDefaultAsync(fi => fi.FileId == request.FileId && fi.Path == request.Path && fi.AccountId == accountId); if (existingIndex != null) return new ObjectResult(ApiError.Validation(new Dictionary @@ -443,7 +375,7 @@ public class FileIndexController( { IndexId = fileIndex.Id, fileIndex.FileId, - Path = fileIndex.Path, + fileIndex.Path, Message = "File index created successfully" }); } @@ -460,56 +392,6 @@ public class FileIndexController( } } - /// - /// Gets unindexed files for the current user (files that exist but don't have file indexes) - /// - /// Pagination offset - /// Number of files to take - /// List of unindexed files - [HttpGet("unindexed")] - public async Task GetUnindexedFiles([FromQuery] int offset = 0, [FromQuery] int take = 20) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - try - { - // Get files that belong to the user but don't have any file indexes - var unindexedFiles = await db.Files - .Where(f => f.AccountId == accountId) - .Where(f => !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId)) - .OrderByDescending(f => f.CreatedAt) - .Skip(offset) - .Take(take) - .ToListAsync(); - - var totalCount = await db.Files - .Where(f => f.AccountId == accountId) - .Where(f => !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId)) - .CountAsync(); - - return Ok(new - { - Files = unindexedFiles, - TotalCount = totalCount, - Offset = offset, - Take = take - }); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to get unindexed files for account {AccountId}", accountId); - return new ObjectResult(new ApiError - { - Code = "GET_UNINDEXED_FAILED", - Message = "Failed to get unindexed files", - Status = 500 - }) { StatusCode = 500 }; - } - } - /// /// Searches for files by name or metadata /// @@ -528,29 +410,14 @@ public class FileIndexController( { // Build the query with all conditions at once var searchTerm = query.ToLower(); - var baseQuery = db.FileIndexes + var fileIndexes = await db.FileIndexes .Where(fi => fi.AccountId == accountId) - .Include(fi => fi.File); - - IQueryable queryable; - - // If a path is specified, find the folder and filter by folder ID - if (!string.IsNullOrEmpty(path)) - { - var normalizedPath = FileIndexService.NormalizePath(path); - var targetFolder = await GetOrCreateFolderByPathAsync(normalizedPath, accountId); - queryable = baseQuery.Where(fi => fi.FolderId == targetFolder.Id); - } - else - { - queryable = baseQuery; - } - - var fileIndexes = await queryable - .Where(fi => - fi.File.Name.ToLower().Contains(searchTerm) || - (fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) || - (fi.File.MimeType != null && fi.File.MimeType.ToLower().Contains(searchTerm))) + .Include(fi => fi.File) + .Where(fi => + (string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) && + (fi.File.Name.ToLower().Contains(searchTerm) || + (fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) || + (fi.File.MimeType != null && fi.File.MimeType.ToLower().Contains(searchTerm)))) .ToListAsync(); return Ok(new @@ -572,249 +439,6 @@ public class FileIndexController( }) { StatusCode = 500 }; } } - - /// - /// Creates a new folder - /// - /// The folder creation request - /// The created folder - [HttpPost("folders")] - public async Task CreateFolder([FromBody] CreateFolderRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - try - { - var folder = await folderService.CreateAsync(request.Name, accountId, request.ParentFolderId); - return Ok(folder); - } - catch (ArgumentException ex) - { - return BadRequest(ex.Message); - } - catch (InvalidOperationException ex) - { - return Conflict(ex.Message); - } - } - - /// - /// Gets a folder by ID with its contents - /// - /// The folder ID - /// The folder with child folders and files - [HttpGet("folders/{folderId:guid}")] - public async Task GetFolderById(Guid folderId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - var folder = await folderService.GetByIdAsync(folderId, accountId); - - if (folder == null) - { - return NotFound($"Folder with ID '{folderId}' not found"); - } - - return Ok(folder); - } - - /// - /// Gets all folders for the current account - /// - /// List of folders - [HttpGet("folders")] - public async Task GetAllFolders() - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - var folders = await folderService.GetByAccountIdAsync(accountId); - return Ok(folders); - } - - /// - /// Gets child folders of a parent folder - /// - /// The parent folder ID - /// List of child folders - [HttpGet("folders/children/{parentFolderId:guid}")] - public async Task GetChildFolders(Guid parentFolderId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - var folders = await folderService.GetChildFoldersAsync(parentFolderId, accountId); - return Ok(folders); - } - - /// - /// Updates a folder's name - /// - /// The folder ID - /// The update request - /// The updated folder - [HttpPut("folders/{folderId:guid}")] - public async Task UpdateFolder(Guid folderId, [FromBody] UpdateFolderRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - try - { - var folder = await folderService.UpdateAsync(folderId, request.Name, accountId); - - if (folder == null) - { - return NotFound($"Folder with ID '{folderId}' not found"); - } - - return Ok(folder); - } - catch (ArgumentException ex) - { - return BadRequest(ex.Message); - } - catch (InvalidOperationException ex) - { - return Conflict(ex.Message); - } - } - - /// - /// Deletes a folder and all its contents - /// - /// The folder ID - /// Success status - [HttpDelete("folders/{folderId:guid}")] - public async Task DeleteFolder(Guid folderId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - var deleted = await folderService.DeleteAsync(folderId, accountId); - - if (!deleted) - { - return NotFound($"Folder with ID '{folderId}' not found"); - } - - return NoContent(); - } - - /// - /// Moves a folder to a new parent folder - /// - /// The folder ID - /// The move request - /// The moved folder - [HttpPost("folders/{folderId:guid}/move")] - public async Task MoveFolder(Guid folderId, [FromBody] MoveFolderRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - try - { - var folder = await folderService.MoveAsync(folderId, request.NewParentFolderId, accountId); - - if (folder == null) - { - return NotFound($"Folder with ID '{folderId}' not found"); - } - - return Ok(folder); - } - catch (InvalidOperationException ex) - { - return Conflict(ex.Message); - } - } - - /// - /// Searches for folders by name - /// - /// The search term - /// List of matching folders - [HttpGet("folders/search")] - public async Task SearchFolders([FromQuery] string searchTerm) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - if (string.IsNullOrWhiteSpace(searchTerm)) - { - return BadRequest("Search term cannot be empty"); - } - - var folders = await folderService.SearchAsync(accountId, searchTerm); - return Ok(folders); - } - - /// - /// Gets files in a specific folder - /// - /// The folder ID - /// List of files in the folder - [HttpGet("folders/{folderId:guid}/files")] - public async Task GetFilesInFolder(Guid folderId) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - var files = await fileIndexService.GetByFolderAsync(accountId, folderId); - return Ok(files); - } - - /// - /// Moves a file to a different folder - /// - /// The file index ID - /// The move request - /// The updated file index - [HttpPost("files/{fileIndexId:guid}/move-to-folder")] - public async Task MoveFileToFolder(Guid fileIndexId, [FromBody] MoveFileToFolderRequest request) - { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) - return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 }; - - var accountId = Guid.Parse(currentUser.Id); - - try - { - var fileIndex = await fileIndexService.MoveAsync(fileIndexId, request.NewFolderId, accountId); - - if (fileIndex == null) - { - return NotFound($"File index with ID '{fileIndexId}' not found"); - } - - return Ok(fileIndex); - } - catch (InvalidOperationException ex) - { - return Conflict(ex.Message); - } - } } public class MoveFileRequest @@ -827,52 +451,3 @@ public class CreateFileIndexRequest [MaxLength(32)] public string FileId { get; set; } = null!; public string Path { get; set; } = null!; } - -/// -/// Request model for creating a folder -/// -public class CreateFolderRequest -{ - /// - /// The name of the folder - /// - public string Name { get; set; } = null!; - - /// - /// Optional parent folder ID (null for root folder) - /// - public Guid? ParentFolderId { get; set; } -} - -/// -/// Request model for updating a folder -/// -public class UpdateFolderRequest -{ - /// - /// The new name for the folder - /// - public string Name { get; set; } = null!; -} - -/// -/// Request model for moving a folder -/// -public class MoveFolderRequest -{ - /// - /// The new parent folder ID (null for root) - /// - public Guid? NewParentFolderId { get; set; } -} - -/// -/// Request model for moving a file to a folder -/// -public class MoveFileToFolderRequest -{ - /// - /// The new folder ID - /// - public Guid NewFolderId { get; set; } -} diff --git a/DysonNetwork.Drive/Index/FileIndexService.cs b/DysonNetwork.Drive/Index/FileIndexService.cs index 97ca17d..174d82d 100644 --- a/DysonNetwork.Drive/Index/FileIndexService.cs +++ b/DysonNetwork.Drive/Index/FileIndexService.cs @@ -3,111 +3,37 @@ using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Drive.Index; -public class FileIndexService(AppDatabase db, FolderService folderService) +public class FileIndexService(AppDatabase db) { /// - /// Normalizes a path to ensure consistent formatting + /// Creates a new file index entry /// - /// 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 parent folder path with a trailing slash /// The file ID /// The account ID /// The created file index public async Task CreateAsync(string path, string fileId, Guid accountId) { + // Ensure a path has a trailing slash and is query-safe 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 + // Check if a file with the same name already exists in the same path for this account var existingFileIndex = await db.FileIndexes - .FirstOrDefaultAsync(fi => fi.AccountId == accountId && - fi.FolderId == folder.Id && - fi.File.Name == file.Name); + .FirstOrDefaultAsync(fi => fi.AccountId == accountId && fi.Path == normalizedPath && fi.FileId == fileId); if (existingFileIndex != null) { throw new InvalidOperationException( - $"A file with name '{file.Name}' already exists in folder '{folder.Name}' for account '{accountId}'"); + $"A file with ID '{fileId}' already exists in path '{normalizedPath}' for account '{accountId}'"); } - var fileIndex = SnCloudFileIndex.Create(folder, file, accountId); + var fileIndex = new SnCloudFileIndex + { + Path = normalizedPath, + FileId = fileId, + AccountId = accountId + }; + db.FileIndexes.Add(fileIndex); await db.SaveChangesAsync(); @@ -115,94 +41,26 @@ public class FileIndexService(AppDatabase db, FolderService folderService) } /// - /// Creates a new file index entry in a specific folder + /// Updates an existing file index entry by removing the old one and creating a new one /// - /// 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 file index ID + /// The new parent folder path with trailing slash /// The updated file index - public async Task MoveAsync(Guid fileIndexId, Guid newFolderId, Guid accountId) + public async Task UpdateAsync(Guid id, string newPath) { - var fileIndex = await db.FileIndexes - .Include(fi => fi.File) - .FirstOrDefaultAsync(fi => fi.Id == fileIndexId && fi.AccountId == accountId); - + var fileIndex = await db.FileIndexes.FindAsync(id); 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 + var newFileIndex = new SnCloudFileIndex + { + Path = NormalizePath(newPath), + FileId = fileIndex.FileId, + AccountId = fileIndex.AccountId + }; db.FileIndexes.Add(newFileIndex); await db.SaveChangesAsync(); @@ -248,15 +106,17 @@ public class FileIndexService(AppDatabase db, FolderService folderService) } /// - /// Removes file index entries by account ID and folder + /// Removes file index entries by account ID and path /// /// The account ID - /// The folder ID + /// The parent folder path /// The number of indexes removed - public async Task RemoveByFolderAsync(Guid accountId, Guid folderId) + public async Task RemoveByPathAsync(Guid accountId, string path) { + var normalizedPath = NormalizePath(path); + var indexes = await db.FileIndexes - .Where(fi => fi.AccountId == accountId && fi.FolderId == folderId) + .Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath) .ToListAsync(); if (!indexes.Any()) @@ -269,21 +129,23 @@ public class FileIndexService(AppDatabase db, FolderService folderService) } /// - /// Gets file indexes by account ID and folder + /// Gets file indexes by account ID and path /// /// The account ID - /// The folder ID + /// The parent folder path /// List of file indexes - public async Task> GetByFolderAsync(Guid accountId, Guid folderId) + public async Task> GetByPathAsync(Guid accountId, string path) { + var normalizedPath = NormalizePath(path); + return await db.FileIndexes - .Where(fi => fi.AccountId == accountId && fi.FolderId == folderId) + .Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath) .Include(fi => fi.File) .ToListAsync(); } /// - /// Gets file indexes by file ID with folder information + /// Gets file indexes by file ID /// /// The file ID /// List of file indexes @@ -292,7 +154,6 @@ public class FileIndexService(AppDatabase db, FolderService folderService) return await db.FileIndexes .Where(fi => fi.FileId == fileId) .Include(fi => fi.File) - .Include(fi => fi.Folder) .ToListAsync(); } @@ -306,107 +167,31 @@ public class FileIndexService(AppDatabase db, FolderService folderService) 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) + /// Normalizes the path to ensure it has a trailing slash and is query-safe /// - /// The account ID - /// The path to search for - /// List of file indexes at the specified path - public async Task> GetByPathAsync(Guid accountId, string path) + /// The original path + /// The normalized path + public static string NormalizePath(string path) { - var normalizedPath = NormalizePath(path); + if (string.IsNullOrEmpty(path)) + return "/"; - // Find the folder that corresponds to this path - var folder = await db.Folders - .FirstOrDefaultAsync(f => f.AccountId == accountId && f.Path == normalizedPath + (normalizedPath == "/" ? "" : "/")); + // Ensure the path starts with a slash + if (!path.StartsWith('/')) + path = "/" + path; - if (folder == null) - return new List(); + // Ensure the path ends with a slash (unless it's just the root) + if (path != "/" && !path.EndsWith('/')) + path += "/"; - return await db.FileIndexes - .Where(fi => fi.AccountId == accountId && fi.FolderId == folder.Id) - .Include(fi => fi.File) - .Include(fi => fi.Folder) - .ToListAsync(); + // Make path query-safe by removing problematic characters + // This is a basic implementation - you might want to add more robust validation + path = path.Replace("%", "").Replace("'", "").Replace("\"", ""); + + return path; } - - /// - /// 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; - } -} +} \ No newline at end of file diff --git a/DysonNetwork.Drive/Index/FolderService.cs b/DysonNetwork.Drive/Index/FolderService.cs deleted file mode 100644 index 77b67fd..0000000 --- a/DysonNetwork.Drive/Index/FolderService.cs +++ /dev/null @@ -1,313 +0,0 @@ -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(); - } -} diff --git a/DysonNetwork.Drive/Index/README.md b/DysonNetwork.Drive/Index/README.md index d739c80..2a72155 100644 --- a/DysonNetwork.Drive/Index/README.md +++ b/DysonNetwork.Drive/Index/README.md @@ -12,11 +12,9 @@ And all the arguments will be transformed into snake case via the gateway. ### Core Components 1. **SnCloudFileIndex Model** - Represents the file-to-path mapping -2. **SnCloudFolder Model** - Represents hierarchical folder structure -3. **FileIndexService** - Business logic for file index operations -4. **FolderService** - Business logic for folder operations -5. **FileIndexController** - REST API endpoints for file and folder management -6. **FileUploadController Integration** - Automatic index creation during upload +2. **FileIndexService** - Business logic for file index operations +3. **FileIndexController** - REST API endpoints for file management +4. **FileUploadController Integration** - Automatic index creation during upload ### Database Schema @@ -202,138 +200,6 @@ Search for files by name or metadata. } ``` -### Folder Management - -The system provides comprehensive folder management capabilities alongside file indexing. - -#### Create Folder -**POST** `/api/index/folders` - -Create a new folder. - -**Request Body:** -```json -{ - "name": "Documents", - "parentFolderId": null // null for root folder -} -``` - -**Response:** -```json -{ - "id": "guid", - "name": "Documents", - "parentFolderId": null, - "accountId": "guid", - "createdAt": "2024-01-01T00:00:00Z", - "updatedAt": "2024-01-01T00:00:00Z" -} -``` - -#### Get Folder by ID -**GET** `/api/index/folders/{folderId}` - -Get a folder with its contents. - -**Path Parameters:** -- `folderId` - The folder ID - -**Response:** -```json -{ - "id": "guid", - "name": "Documents", - "parentFolderId": null, - "accountId": "guid", - "childFolders": [ - { - "id": "guid", - "name": "Reports", - "parentFolderId": "guid", - "accountId": "guid" - } - ], - "files": [ - // File index objects - ], - "createdAt": "2024-01-01T00:00:00Z", - "updatedAt": "2024-01-01T00:00:00Z" -} -``` - -#### Get All Folders -**GET** `/api/index/folders` - -Get all folders for the current user. - -**Response:** -```json -[ - { - "id": "guid", - "name": "Documents", - "parentFolderId": null, - "accountId": "guid", - "createdAt": "2024-01-01T00:00:00Z", - "updatedAt": "2024-01-01T00:00:00Z" - } -] -``` - -#### Update Folder -**PUT** `/api/index/folders/{folderId}` - -Update a folder's name. - -**Path Parameters:** -- `folderId` - The folder ID - -**Request Body:** -```json -{ - "name": "Updated Documents" -} -``` - -#### Delete Folder -**DELETE** `/api/index/folders/{folderId}` - -Delete a folder and all its contents. - -**Path Parameters:** -- `folderId` - The folder ID - -#### Move File to Folder -**POST** `/api/index/files/{fileIndexId}/move-to-folder` - -Move a file to a different folder. - -**Path Parameters:** -- `fileIndexId` - The file index ID - -**Request Body:** -```json -{ - "newFolderId": "guid" -} -``` - -#### Get Files in Folder -**GET** `/api/index/folders/{folderId}/files` - -Get all files in a specific folder. - -**Path Parameters:** -- `folderId` - The folder ID - -**Response:** -```json -[ - // File index objects -] -``` - ## Path Normalization The system automatically normalizes paths to ensure consistency: @@ -371,75 +237,32 @@ The system will automatically create a file index when the upload completes succ ```csharp public class FileIndexService { - // Create a new file index at path - Task CreateAsync(string path, string fileId, Guid accountId); - - // Create a new file index in folder - Task CreateInFolderAsync(Guid folderId, string fileId, Guid accountId); - + // Create a new file index + Task CreateAsync(string path, Guid fileId, Guid accountId); + // Get files by path Task> GetByPathAsync(Guid accountId, string path); - - // Get files by folder - Task> GetByFolderAsync(Guid accountId, Guid folderId); - + // Get all files for account Task> GetByAccountIdAsync(Guid accountId); - + // Get indexes for specific file - Task> GetByFileIdAsync(string fileId); - + Task> GetByFileIdAsync(Guid fileId); + // Move file to new path Task UpdateAsync(Guid indexId, string newPath); - - // Move file to different folder - Task MoveAsync(Guid fileIndexId, Guid newFolderId, Guid accountId); - + // Remove file index Task RemoveAsync(Guid indexId); - + // Remove all indexes in path Task RemoveByPathAsync(Guid accountId, string path); - - // Remove all indexes in folder - Task RemoveByFolderAsync(Guid accountId, Guid folderId); - + // Normalize path format public static string NormalizePath(string path); } ``` -### FolderService - -```csharp -public class FolderService -{ - // Create a new folder - Task CreateAsync(string name, Guid accountId, Guid? parentFolderId = null); - - // Get folder by ID with contents - Task GetByIdAsync(Guid folderId, Guid accountId); - - // Get all folders for account - Task> GetByAccountIdAsync(Guid accountId); - - // Get child folders - Task> GetChildFoldersAsync(Guid parentFolderId, Guid accountId); - - // Update folder name - Task UpdateAsync(Guid folderId, string name, Guid accountId); - - // Move folder to new parent - Task MoveAsync(Guid folderId, Guid? newParentFolderId, Guid accountId); - - // Delete folder and contents - Task DeleteAsync(Guid folderId, Guid accountId); - - // Search folders by name - Task> SearchAsync(Guid accountId, string searchTerm); -} -``` - ## Error Handling The API returns appropriate HTTP status codes and error messages: diff --git a/DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.Designer.cs b/DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.Designer.cs deleted file mode 100644 index 1df5c42..0000000 --- a/DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.Designer.cs +++ /dev/null @@ -1,722 +0,0 @@ -// -using System; -using System.Collections.Generic; -using DysonNetwork.Drive; -using DysonNetwork.Shared.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace DysonNetwork.Drive.Migrations -{ - [DbContext(typeof(AppDatabase))] - [Migration("20251113165508_AddFileFolders")] - partial class AddFileFolders - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("Description") - .IsRequired() - .HasColumnType("text") - .HasColumnName("description"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expired_at"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Quota") - .HasColumnType("bigint") - .HasColumnName("quota"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_quota_records"); - - b.ToTable("quota_records", (string)null); - }); - - modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("CompletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("completed_at"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("Description") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("description"); - - b.Property("Discriminator") - .IsRequired() - .HasMaxLength(21) - .HasColumnType("character varying(21)") - .HasColumnName("discriminator"); - - b.Property("ErrorMessage") - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("error_message"); - - b.Property("EstimatedDurationSeconds") - .HasColumnType("bigint") - .HasColumnName("estimated_duration_seconds"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expired_at"); - - b.Property("LastActivity") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_activity"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("name"); - - b.Property>("Parameters") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("parameters"); - - b.Property("Priority") - .HasColumnType("integer") - .HasColumnName("priority"); - - b.Property("Progress") - .HasColumnType("double precision") - .HasColumnName("progress"); - - b.Property>("Results") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("results"); - - b.Property("StartedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("started_at"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("TaskId") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasColumnName("task_id"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_tasks"); - - b.ToTable("tasks", (string)null); - - b.HasDiscriminator().HasValue("PersistentTask"); - - b.UseTphMappingStrategy(); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expired_at"); - - b.Property("FileId") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("file_id"); - - b.Property("ResourceId") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("resource_id"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("Usage") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("usage"); - - b.HasKey("Id") - .HasName("pk_file_references"); - - b.HasIndex("FileId") - .HasDatabaseName("ix_file_references_file_id"); - - b.ToTable("file_references", (string)null); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("BillingConfig") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("billing_config"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("Description") - .IsRequired() - .HasMaxLength(8192) - .HasColumnType("character varying(8192)") - .HasColumnName("description"); - - b.Property("IsHidden") - .HasColumnType("boolean") - .HasColumnName("is_hidden"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("name"); - - b.Property("PolicyConfig") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("policy_config"); - - b.Property("StorageConfig") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("storage_config"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_pools"); - - b.ToTable("pools", (string)null); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b => - { - b.Property("Id") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("BundleId") - .HasColumnType("uuid") - .HasColumnName("bundle_id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("Description") - .HasMaxLength(4096) - .HasColumnType("character varying(4096)") - .HasColumnName("description"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expired_at"); - - b.Property>("FileMeta") - .HasColumnType("jsonb") - .HasColumnName("file_meta"); - - b.Property("HasCompression") - .HasColumnType("boolean") - .HasColumnName("has_compression"); - - b.Property("HasThumbnail") - .HasColumnType("boolean") - .HasColumnName("has_thumbnail"); - - b.Property("Hash") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("hash"); - - b.Property("IsEncrypted") - .HasColumnType("boolean") - .HasColumnName("is_encrypted"); - - b.Property("IsMarkedRecycle") - .HasColumnType("boolean") - .HasColumnName("is_marked_recycle"); - - b.Property("MimeType") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("mime_type"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("name"); - - b.Property("PoolId") - .HasColumnType("uuid") - .HasColumnName("pool_id"); - - b.Property>("SensitiveMarks") - .HasColumnType("jsonb") - .HasColumnName("sensitive_marks"); - - b.Property("Size") - .HasColumnType("bigint") - .HasColumnName("size"); - - b.Property("StorageId") - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("storage_id"); - - b.Property("StorageUrl") - .HasMaxLength(4096) - .HasColumnType("character varying(4096)") - .HasColumnName("storage_url"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.Property("UploadedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("uploaded_at"); - - b.Property>("UserMeta") - .HasColumnType("jsonb") - .HasColumnName("user_meta"); - - b.HasKey("Id") - .HasName("pk_files"); - - b.HasIndex("BundleId") - .HasDatabaseName("ix_files_bundle_id"); - - b.HasIndex("PoolId") - .HasDatabaseName("ix_files_pool_id"); - - b.ToTable("files", (string)null); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("FileId") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)") - .HasColumnName("file_id"); - - b.Property("FolderId") - .HasColumnType("uuid") - .HasColumnName("folder_id"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)") - .HasColumnName("path"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_file_indexes"); - - b.HasIndex("FileId") - .HasDatabaseName("ix_file_indexes_file_id"); - - b.HasIndex("FolderId", "AccountId") - .HasDatabaseName("ix_file_indexes_folder_id_account_id"); - - b.ToTable("file_indexes", (string)null); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("Description") - .HasMaxLength(4096) - .HasColumnType("character varying(4096)") - .HasColumnName("description"); - - b.Property>("FolderMeta") - .HasColumnType("jsonb") - .HasColumnName("folder_meta"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("name"); - - b.Property("ParentFolderId") - .HasColumnType("uuid") - .HasColumnName("parent_folder_id"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(8192) - .HasColumnType("character varying(8192)") - .HasColumnName("path"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_folders"); - - b.HasIndex("ParentFolderId") - .HasDatabaseName("ix_folders_parent_folder_id"); - - b.HasIndex("Path", "AccountId") - .HasDatabaseName("ix_folders_path_account_id"); - - b.ToTable("folders", (string)null); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("Description") - .HasMaxLength(8192) - .HasColumnType("character varying(8192)") - .HasColumnName("description"); - - b.Property("ExpiredAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expired_at"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("name"); - - b.Property("Passcode") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("passcode"); - - b.Property("Slug") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)") - .HasColumnName("slug"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_bundles"); - - b.HasIndex("Slug") - .IsUnique() - .HasDatabaseName("ix_bundles_slug"); - - b.ToTable("bundles", (string)null); - }); - - modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b => - { - b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask"); - - b.Property("BundleId") - .HasColumnType("uuid") - .HasColumnName("bundle_id"); - - b.Property("ChunkSize") - .HasColumnType("bigint") - .HasColumnName("chunk_size"); - - b.Property("ChunksCount") - .HasColumnType("integer") - .HasColumnName("chunks_count"); - - b.Property("ChunksUploaded") - .HasColumnType("integer") - .HasColumnName("chunks_uploaded"); - - b.Property("ContentType") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("character varying(128)") - .HasColumnName("content_type"); - - b.Property("EncryptPassword") - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("encrypt_password"); - - b.Property("FileName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("file_name"); - - b.Property("FileSize") - .HasColumnType("bigint") - .HasColumnName("file_size"); - - b.Property("Hash") - .IsRequired() - .HasColumnType("text") - .HasColumnName("hash"); - - b.Property("Path") - .HasColumnType("text") - .HasColumnName("path"); - - b.Property("PoolId") - .HasColumnType("uuid") - .HasColumnName("pool_id"); - - b.PrimitiveCollection>("UploadedChunks") - .IsRequired() - .HasColumnType("integer[]") - .HasColumnName("uploaded_chunks"); - - b.HasDiscriminator().HasValue("PersistentUploadTask"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b => - { - b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File") - .WithMany("References") - .HasForeignKey("FileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_file_references_files_file_id"); - - b.Navigation("File"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b => - { - b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle") - .WithMany("Files") - .HasForeignKey("BundleId") - .HasConstraintName("fk_files_bundles_bundle_id"); - - b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool") - .WithMany() - .HasForeignKey("PoolId") - .HasConstraintName("fk_files_pools_pool_id"); - - b.Navigation("Bundle"); - - b.Navigation("Pool"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b => - { - b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File") - .WithMany("FileIndexes") - .HasForeignKey("FileId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_file_indexes_files_file_id"); - - b.HasOne("DysonNetwork.Shared.Models.SnCloudFolder", "Folder") - .WithMany("Files") - .HasForeignKey("FolderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_file_indexes_folders_folder_id"); - - b.Navigation("File"); - - b.Navigation("Folder"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b => - { - b.HasOne("DysonNetwork.Shared.Models.SnCloudFolder", "ParentFolder") - .WithMany("ChildFolders") - .HasForeignKey("ParentFolderId") - .HasConstraintName("fk_folders_folders_parent_folder_id"); - - b.Navigation("ParentFolder"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b => - { - b.Navigation("FileIndexes"); - - b.Navigation("References"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b => - { - b.Navigation("ChildFolders"); - - b.Navigation("Files"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b => - { - b.Navigation("Files"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.cs b/DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.cs deleted file mode 100644 index 30c3b78..0000000 --- a/DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace DysonNetwork.Drive.Migrations -{ - /// - public partial class AddFileFolders : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "ix_file_indexes_path_account_id", - table: "file_indexes"); - - migrationBuilder.AlterColumn( - name: "path", - table: "file_indexes", - type: "character varying(2048)", - maxLength: 2048, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(8192)", - oldMaxLength: 8192); - - migrationBuilder.AddColumn( - name: "folder_id", - table: "file_indexes", - type: "uuid", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - - migrationBuilder.CreateTable( - name: "folders", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - path = table.Column(type: "character varying(8192)", maxLength: 8192, nullable: false), - parent_folder_id = table.Column(type: "uuid", nullable: true), - account_id = table.Column(type: "uuid", nullable: false), - description = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: true), - folder_meta = table.Column>(type: "jsonb", nullable: true), - created_at = table.Column(type: "timestamp with time zone", nullable: false), - updated_at = table.Column(type: "timestamp with time zone", nullable: false), - deleted_at = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_folders", x => x.id); - table.ForeignKey( - name: "fk_folders_folders_parent_folder_id", - column: x => x.parent_folder_id, - principalTable: "folders", - principalColumn: "id"); - }); - - migrationBuilder.CreateIndex( - name: "ix_file_indexes_folder_id_account_id", - table: "file_indexes", - columns: new[] { "folder_id", "account_id" }); - - migrationBuilder.CreateIndex( - name: "ix_folders_parent_folder_id", - table: "folders", - column: "parent_folder_id"); - - migrationBuilder.CreateIndex( - name: "ix_folders_path_account_id", - table: "folders", - columns: new[] { "path", "account_id" }); - - migrationBuilder.AddForeignKey( - name: "fk_file_indexes_folders_folder_id", - table: "file_indexes", - column: "folder_id", - principalTable: "folders", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "fk_file_indexes_folders_folder_id", - table: "file_indexes"); - - migrationBuilder.DropTable( - name: "folders"); - - migrationBuilder.DropIndex( - name: "ix_file_indexes_folder_id_account_id", - table: "file_indexes"); - - migrationBuilder.DropColumn( - name: "folder_id", - table: "file_indexes"); - - migrationBuilder.AlterColumn( - name: "path", - table: "file_indexes", - type: "character varying(8192)", - maxLength: 8192, - nullable: false, - oldClrType: typeof(string), - oldType: "character varying(2048)", - oldMaxLength: 2048); - - migrationBuilder.CreateIndex( - name: "ix_file_indexes_path_account_id", - table: "file_indexes", - columns: new[] { "path", "account_id" }); - } - } -} diff --git a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs index d68945a..824af79 100644 --- a/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Drive/Migrations/AppDatabaseModelSnapshot.cs @@ -428,14 +428,10 @@ namespace DysonNetwork.Drive.Migrations .HasColumnType("character varying(32)") .HasColumnName("file_id"); - b.Property("FolderId") - .HasColumnType("uuid") - .HasColumnName("folder_id"); - b.Property("Path") .IsRequired() - .HasMaxLength(2048) - .HasColumnType("character varying(2048)") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") .HasColumnName("path"); b.Property("UpdatedAt") @@ -448,72 +444,12 @@ namespace DysonNetwork.Drive.Migrations b.HasIndex("FileId") .HasDatabaseName("ix_file_indexes_file_id"); - b.HasIndex("FolderId", "AccountId") - .HasDatabaseName("ix_file_indexes_folder_id_account_id"); + b.HasIndex("Path", "AccountId") + .HasDatabaseName("ix_file_indexes_path_account_id"); b.ToTable("file_indexes", (string)null); }); - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AccountId") - .HasColumnType("uuid") - .HasColumnName("account_id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("Description") - .HasMaxLength(4096) - .HasColumnType("character varying(4096)") - .HasColumnName("description"); - - b.Property>("FolderMeta") - .HasColumnType("jsonb") - .HasColumnName("folder_meta"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)") - .HasColumnName("name"); - - b.Property("ParentFolderId") - .HasColumnType("uuid") - .HasColumnName("parent_folder_id"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(8192) - .HasColumnType("character varying(8192)") - .HasColumnName("path"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_folders"); - - b.HasIndex("ParentFolderId") - .HasDatabaseName("ix_folders_parent_folder_id"); - - b.HasIndex("Path", "AccountId") - .HasDatabaseName("ix_folders_path_account_id"); - - b.ToTable("folders", (string)null); - }); - modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b => { b.Property("Id") @@ -673,26 +609,7 @@ namespace DysonNetwork.Drive.Migrations .IsRequired() .HasConstraintName("fk_file_indexes_files_file_id"); - b.HasOne("DysonNetwork.Shared.Models.SnCloudFolder", "Folder") - .WithMany("Files") - .HasForeignKey("FolderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_file_indexes_folders_folder_id"); - b.Navigation("File"); - - b.Navigation("Folder"); - }); - - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b => - { - b.HasOne("DysonNetwork.Shared.Models.SnCloudFolder", "ParentFolder") - .WithMany("ChildFolders") - .HasForeignKey("ParentFolderId") - .HasConstraintName("fk_folders_folders_parent_folder_id"); - - b.Navigation("ParentFolder"); }); modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b => @@ -702,13 +619,6 @@ namespace DysonNetwork.Drive.Migrations b.Navigation("References"); }); - modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b => - { - b.Navigation("ChildFolders"); - - b.Navigation("Files"); - }); - modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b => { b.Navigation("Files"); diff --git a/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs index cc1026b..3fbd280 100644 --- a/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Drive/Startup/ServiceCollectionExtensions.cs @@ -57,7 +57,6 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/DysonNetwork.Shared/Models/CloudFileIndex.cs b/DysonNetwork.Shared/Models/CloudFileIndex.cs index e882c2e..9e64e09 100644 --- a/DysonNetwork.Shared/Models/CloudFileIndex.cs +++ b/DysonNetwork.Shared/Models/CloudFileIndex.cs @@ -1,70 +1,30 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Shared.Models; -[Index(nameof(FolderId), nameof(AccountId))] -[Index(nameof(FileId))] +[Index(nameof(Path), nameof(AccountId))] public class SnCloudFileIndex : ModelBase { - public Guid Id { get; set; } = Guid.NewGuid(); + public Guid Id { get; init; } = Guid.NewGuid(); /// - /// Reference to the folder containing this file + /// The path of the file, + /// only store the parent folder. + /// With the trailing slash. + /// + /// Like /hello/here/ not /hello/here/text.txt or /hello/here + /// Or the user home folder files, store as / + /// + /// Besides, the folder name should be all query-safe, not contains % or + /// other special characters that will mess up the pgsql query /// - public Guid FolderId { get; init; } - - /// - /// Navigation property to the folder - /// - [JsonIgnore] - public SnCloudFolder Folder { get; init; } = null!; + [MaxLength(8192)] + public string Path { get; init; } = null!; [MaxLength(32)] public string FileId { get; init; } = null!; public SnCloudFile File { get; init; } = null!; public Guid AccountId { get; init; } [NotMapped] public SnAccount? Account { get; init; } - - /// - /// Cached full path of the file (stored in database for performance) - /// - [MaxLength(2048)] - public string Path { get; set; } = null!; - - /// - /// Creates a new file index with the specified folder and file - /// - public static SnCloudFileIndex Create(SnCloudFolder folder, SnCloudFile file, Guid accountId) - { - // Build the full path by traversing the folder hierarchy - var pathSegments = new List(); - var currentFolder = folder; - - while (currentFolder != null) - { - if (!string.IsNullOrEmpty(currentFolder.Name)) - { - pathSegments.Insert(0, currentFolder.Name); - } - currentFolder = currentFolder.ParentFolder; - } - - // Add the file name - if (!string.IsNullOrEmpty(file.Name)) - { - pathSegments.Add(file.Name); - } - - var fullPath = "/" + string.Join("/", pathSegments); - - return new SnCloudFileIndex - { - FolderId = folder.Id, - FileId = file.Id, - AccountId = accountId, - Path = fullPath - }; - } } diff --git a/DysonNetwork.Shared/Models/CloudFolder.cs b/DysonNetwork.Shared/Models/CloudFolder.cs deleted file mode 100644 index 609f0f2..0000000 --- a/DysonNetwork.Shared/Models/CloudFolder.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; - -namespace DysonNetwork.Shared.Models; - -[Index(nameof(Path), nameof(AccountId))] -[Index(nameof(ParentFolderId))] -public class SnCloudFolder : ModelBase -{ - public Guid Id { get; set; } = Guid.NewGuid(); - - /// - /// The name of the folder - /// - [MaxLength(256)] - public string Name { get; init; } = null!; - - /// - /// The full path of the folder (for querying purposes) - /// With trailing slash, e.g., /documents/work/ - /// - [MaxLength(8192)] - public string Path { get; set; } = null!; - - /// - /// Reference to the parent folder (null for root folders) - /// - public Guid? ParentFolderId { get; set; } - - /// - /// Navigation property to the parent folder - /// - [ForeignKey(nameof(ParentFolderId))] - public SnCloudFolder? ParentFolder { get; init; } - - /// - /// Navigation property to child folders - /// - [InverseProperty(nameof(ParentFolder))] - public List ChildFolders { get; init; } = []; - - /// - /// Navigation property to files in this folder - /// - [InverseProperty(nameof(SnCloudFileIndex.Folder))] - public List Files { get; init; } = []; - - /// - /// The account that owns this folder - /// - public Guid AccountId { get; init; } - - /// - /// Navigation property to the account - /// - [NotMapped] - public SnAccount? Account { get; init; } - - /// - /// Optional description for the folder - /// - [MaxLength(4096)] - public string? Description { get; init; } - - /// - /// Custom metadata for the folder - /// - [Column(TypeName = "jsonb")] - public Dictionary? FolderMeta { get; init; } - - /// - /// Creates a new folder with proper path normalization - /// - public static SnCloudFolder Create(string name, Guid accountId, SnCloudFolder? parentFolder = null) - { - var normalizedPath = parentFolder != null - ? $"{parentFolder.Path.TrimEnd('/')}/{name}/" - : $"/{name}/"; - - return new SnCloudFolder - { - Name = name, - Path = normalizedPath, - ParentFolderId = parentFolder?.Id, - AccountId = accountId - }; - } - - /// - /// Creates the root folder for an account - /// - public static SnCloudFolder CreateRoot(Guid accountId) - { - return new SnCloudFolder - { - Name = "Root", - Path = "/", - ParentFolderId = null, - AccountId = accountId - }; - } -}