♻️ Proper folder system to index
This commit is contained in:
@@ -26,6 +26,7 @@ public class AppDatabase(
|
|||||||
public DbSet<SnCloudFile> Files { get; set; } = null!;
|
public DbSet<SnCloudFile> Files { get; set; } = null!;
|
||||||
public DbSet<CloudFileReference> FileReferences { get; set; } = null!;
|
public DbSet<CloudFileReference> FileReferences { get; set; } = null!;
|
||||||
public DbSet<SnCloudFileIndex> FileIndexes { get; set; }
|
public DbSet<SnCloudFileIndex> FileIndexes { get; set; }
|
||||||
|
public DbSet<SnCloudFolder> Folders { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<PersistentTask> Tasks { get; set; } = null!;
|
public DbSet<PersistentTask> Tasks { get; set; } = null!;
|
||||||
public DbSet<PersistentUploadTask> UploadTasks { get; set; } = null!; // Backward compatibility
|
public DbSet<PersistentUploadTask> UploadTasks { get; set; } = null!; // Backward compatibility
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using DysonNetwork.Drive.Storage;
|
|
||||||
using DysonNetwork.Shared.Auth;
|
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
@@ -15,6 +13,7 @@ namespace DysonNetwork.Drive.Index;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class FileIndexController(
|
public class FileIndexController(
|
||||||
FileIndexService fileIndexService,
|
FileIndexService fileIndexService,
|
||||||
|
FolderService folderService,
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ILogger<FileIndexController> logger
|
ILogger<FileIndexController> logger
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
@@ -36,11 +35,8 @@ public class FileIndexController(
|
|||||||
{
|
{
|
||||||
var fileIndexes = await fileIndexService.GetByPathAsync(accountId, path);
|
var fileIndexes = await fileIndexService.GetByPathAsync(accountId, path);
|
||||||
|
|
||||||
// Get all file indexes for this account to extract child folders
|
// Get child folders using the folder system
|
||||||
var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
var childFolders = await GetChildFoldersAsync(accountId, path);
|
||||||
|
|
||||||
// Extract unique child folder paths
|
|
||||||
var childFolders = ExtractChildFolders(allFileIndexes, path);
|
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
@@ -63,38 +59,108 @@ public class FileIndexController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts unique child folder paths from all file indexes for a given parent path
|
/// Gets child folders for a given parent path using the folder system
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="allFileIndexes">All file indexes for the account</param>
|
/// <param name="accountId">The account ID</param>
|
||||||
/// <param name="parentPath">The parent path to find children for</param>
|
/// <param name="parentPath">The parent path to find children for</param>
|
||||||
/// <returns>List of unique child folder names</returns>
|
/// <returns>List of child folder objects</returns>
|
||||||
private List<string> ExtractChildFolders(List<SnCloudFileIndex> allFileIndexes, string parentPath)
|
private async Task<List<SnCloudFolder>> GetChildFoldersAsync(Guid accountId, string parentPath)
|
||||||
{
|
{
|
||||||
var normalizedParentPath = FileIndexService.NormalizePath(parentPath);
|
var normalizedParentPath = FileIndexService.NormalizePath(parentPath);
|
||||||
var childFolders = new HashSet<string>();
|
|
||||||
|
|
||||||
foreach (var index in allFileIndexes)
|
// Try to find a folder that corresponds to this path
|
||||||
{
|
var parentFolder = await FindFolderByPathAsync(accountId, normalizedParentPath);
|
||||||
var normalizedIndexPath = FileIndexService.NormalizePath(index.Path);
|
|
||||||
|
|
||||||
// Check if this path is a direct child of the parent path
|
if (parentFolder != null)
|
||||||
if (normalizedIndexPath.StartsWith(normalizedParentPath) &&
|
|
||||||
normalizedIndexPath != normalizedParentPath)
|
|
||||||
{
|
{
|
||||||
// Remove the parent path prefix to get the relative path
|
// Use folder-based approach
|
||||||
var relativePath = normalizedIndexPath.Substring(normalizedParentPath.Length);
|
return await folderService.GetChildFoldersAsync(parentFolder.Id, accountId);
|
||||||
|
|
||||||
// Extract the first folder name (direct child)
|
|
||||||
var firstSlashIndex = relativePath.IndexOf('/');
|
|
||||||
if (firstSlashIndex > 0)
|
|
||||||
{
|
|
||||||
var folderName = relativePath.Substring(0, firstSlashIndex);
|
|
||||||
childFolders.Add(folderName);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fall back to path-based approach - find folders that start with this path
|
||||||
|
var allFolders = await folderService.GetByAccountIdAsync(accountId);
|
||||||
|
var childFolders = new List<SnCloudFolder>();
|
||||||
|
|
||||||
|
foreach (var folder in allFolders)
|
||||||
|
{
|
||||||
|
// 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 == "/")
|
||||||
|
{
|
||||||
|
// Root level folders
|
||||||
|
childFolders.Add(folder);
|
||||||
}
|
}
|
||||||
|
// For nested folders, we'd need path information in the folder model
|
||||||
}
|
}
|
||||||
|
|
||||||
return childFolders.OrderBy(f => f).ToList();
|
return childFolders.OrderBy(f => f.Name).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to find a folder by its path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <param name="path">The path to search for</param>
|
||||||
|
/// <returns>The folder if found, null otherwise</returns>
|
||||||
|
private async Task<SnCloudFolder?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or creates a folder hierarchy based on a file path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">The file path (e.g., "/folder/sub/file.txt")</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>The folder where the file should be placed</returns>
|
||||||
|
private async Task<SnCloudFolder> 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -116,7 +182,7 @@ public class FileIndexController(
|
|||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Files = fileIndexes,
|
Files = fileIndexes,
|
||||||
TotalCount = fileIndexes.Count()
|
TotalCount = fileIndexes.Count
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -359,9 +425,11 @@ public class FileIndexController(
|
|||||||
if (file.AccountId != accountId)
|
if (file.AccountId != accountId)
|
||||||
return new ObjectResult(ApiError.Unauthorized(forbidden: true)) { StatusCode = 403 };
|
return new ObjectResult(ApiError.Unauthorized(forbidden: true)) { StatusCode = 403 };
|
||||||
|
|
||||||
// Check if index already exists for this file and path
|
// 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);
|
||||||
var existingIndex = await db.FileIndexes
|
var existingIndex = await db.FileIndexes
|
||||||
.FirstOrDefaultAsync(fi => fi.FileId == request.FileId && fi.Path == request.Path && fi.AccountId == accountId);
|
.FirstOrDefaultAsync(fi => fi.FileId == request.FileId && fi.FolderId == targetFolder.Id && fi.AccountId == accountId);
|
||||||
|
|
||||||
if (existingIndex != null)
|
if (existingIndex != null)
|
||||||
return new ObjectResult(ApiError.Validation(new Dictionary<string, string[]>
|
return new ObjectResult(ApiError.Validation(new Dictionary<string, string[]>
|
||||||
@@ -375,7 +443,7 @@ public class FileIndexController(
|
|||||||
{
|
{
|
||||||
IndexId = fileIndex.Id,
|
IndexId = fileIndex.Id,
|
||||||
fileIndex.FileId,
|
fileIndex.FileId,
|
||||||
fileIndex.Path,
|
Path = fileIndex.Path,
|
||||||
Message = "File index created successfully"
|
Message = "File index created successfully"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -392,6 +460,56 @@ public class FileIndexController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets unindexed files for the current user (files that exist but don't have file indexes)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="offset">Pagination offset</param>
|
||||||
|
/// <param name="take">Number of files to take</param>
|
||||||
|
/// <returns>List of unindexed files</returns>
|
||||||
|
[HttpGet("unindexed")]
|
||||||
|
public async Task<IActionResult> 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Searches for files by name or metadata
|
/// Searches for files by name or metadata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -410,14 +528,29 @@ public class FileIndexController(
|
|||||||
{
|
{
|
||||||
// Build the query with all conditions at once
|
// Build the query with all conditions at once
|
||||||
var searchTerm = query.ToLower();
|
var searchTerm = query.ToLower();
|
||||||
var fileIndexes = await db.FileIndexes
|
var baseQuery = db.FileIndexes
|
||||||
.Where(fi => fi.AccountId == accountId)
|
.Where(fi => fi.AccountId == accountId)
|
||||||
.Include(fi => fi.File)
|
.Include(fi => fi.File);
|
||||||
|
|
||||||
|
IQueryable<SnCloudFileIndex> 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 =>
|
.Where(fi =>
|
||||||
(string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) &&
|
fi.File.Name.ToLower().Contains(searchTerm) ||
|
||||||
(fi.File.Name.ToLower().Contains(searchTerm) ||
|
|
||||||
(fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) ||
|
(fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) ||
|
||||||
(fi.File.MimeType != null && fi.File.MimeType.ToLower().Contains(searchTerm))))
|
(fi.File.MimeType != null && fi.File.MimeType.ToLower().Contains(searchTerm)))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
@@ -439,6 +572,249 @@ public class FileIndexController(
|
|||||||
}) { StatusCode = 500 };
|
}) { StatusCode = 500 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The folder creation request</param>
|
||||||
|
/// <returns>The created folder</returns>
|
||||||
|
[HttpPost("folders")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a folder by ID with its contents
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <returns>The folder with child folders and files</returns>
|
||||||
|
[HttpGet("folders/{folderId:guid}")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all folders for the current account
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List of folders</returns>
|
||||||
|
[HttpGet("folders")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets child folders of a parent folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parentFolderId">The parent folder ID</param>
|
||||||
|
/// <returns>List of child folders</returns>
|
||||||
|
[HttpGet("folders/children/{parentFolderId:guid}")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a folder's name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <param name="request">The update request</param>
|
||||||
|
/// <returns>The updated folder</returns>
|
||||||
|
[HttpPut("folders/{folderId:guid}")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a folder and all its contents
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <returns>Success status</returns>
|
||||||
|
[HttpDelete("folders/{folderId:guid}")]
|
||||||
|
public async Task<IActionResult> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a folder to a new parent folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <param name="request">The move request</param>
|
||||||
|
/// <returns>The moved folder</returns>
|
||||||
|
[HttpPost("folders/{folderId:guid}/move")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches for folders by name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="searchTerm">The search term</param>
|
||||||
|
/// <returns>List of matching folders</returns>
|
||||||
|
[HttpGet("folders/search")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets files in a specific folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <returns>List of files in the folder</returns>
|
||||||
|
[HttpGet("folders/{folderId:guid}/files")]
|
||||||
|
public async Task<IActionResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a file to a different folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileIndexId">The file index ID</param>
|
||||||
|
/// <param name="request">The move request</param>
|
||||||
|
/// <returns>The updated file index</returns>
|
||||||
|
[HttpPost("files/{fileIndexId:guid}/move-to-folder")]
|
||||||
|
public async Task<IActionResult> 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
|
public class MoveFileRequest
|
||||||
@@ -451,3 +827,52 @@ public class CreateFileIndexRequest
|
|||||||
[MaxLength(32)] public string FileId { get; set; } = null!;
|
[MaxLength(32)] public string FileId { get; set; } = null!;
|
||||||
public string Path { get; set; } = null!;
|
public string Path { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for creating a folder
|
||||||
|
/// </summary>
|
||||||
|
public class CreateFolderRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the folder
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional parent folder ID (null for root folder)
|
||||||
|
/// </summary>
|
||||||
|
public Guid? ParentFolderId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for updating a folder
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateFolderRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The new name for the folder
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for moving a folder
|
||||||
|
/// </summary>
|
||||||
|
public class MoveFolderRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The new parent folder ID (null for root)
|
||||||
|
/// </summary>
|
||||||
|
public Guid? NewParentFolderId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for moving a file to a folder
|
||||||
|
/// </summary>
|
||||||
|
public class MoveFileToFolderRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The new folder ID
|
||||||
|
/// </summary>
|
||||||
|
public Guid NewFolderId { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,37 +3,111 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace DysonNetwork.Drive.Index;
|
namespace DysonNetwork.Drive.Index;
|
||||||
|
|
||||||
public class FileIndexService(AppDatabase db)
|
public class FileIndexService(AppDatabase db, FolderService folderService)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new file index entry
|
/// Normalizes a path to ensure consistent formatting
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The parent folder path with a trailing slash</param>
|
/// <param name="path">The path to normalize</param>
|
||||||
|
/// <returns>The normalized path</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or creates a folder hierarchy based on a file path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">The file path (e.g., "/folder/sub/file.txt")</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>The folder where the file should be placed</returns>
|
||||||
|
private async Task<SnCloudFolder> 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;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new file index entry at a specific path (creates folder hierarchy if needed)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path where the file should be indexed</param>
|
||||||
/// <param name="fileId">The file ID</param>
|
/// <param name="fileId">The file ID</param>
|
||||||
/// <param name="accountId">The account ID</param>
|
/// <param name="accountId">The account ID</param>
|
||||||
/// <returns>The created file index</returns>
|
/// <returns>The created file index</returns>
|
||||||
public async Task<SnCloudFileIndex> CreateAsync(string path, string fileId, Guid accountId)
|
public async Task<SnCloudFileIndex> CreateAsync(string path, string fileId, Guid accountId)
|
||||||
{
|
{
|
||||||
// Ensure a path has a trailing slash and is query-safe
|
|
||||||
var normalizedPath = NormalizePath(path);
|
var normalizedPath = NormalizePath(path);
|
||||||
|
|
||||||
// Check if a file with the same name already exists in the same path for this account
|
// 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
|
var existingFileIndex = await db.FileIndexes
|
||||||
.FirstOrDefaultAsync(fi => fi.AccountId == accountId && fi.Path == normalizedPath && fi.FileId == fileId);
|
.FirstOrDefaultAsync(fi => fi.AccountId == accountId &&
|
||||||
|
fi.FolderId == folder.Id &&
|
||||||
|
fi.File.Name == file.Name);
|
||||||
|
|
||||||
if (existingFileIndex != null)
|
if (existingFileIndex != null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"A file with ID '{fileId}' already exists in path '{normalizedPath}' for account '{accountId}'");
|
$"A file with name '{file.Name}' already exists in folder '{folder.Name}' for account '{accountId}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileIndex = new SnCloudFileIndex
|
var fileIndex = SnCloudFileIndex.Create(folder, file, accountId);
|
||||||
{
|
|
||||||
Path = normalizedPath,
|
|
||||||
FileId = fileId,
|
|
||||||
AccountId = accountId
|
|
||||||
};
|
|
||||||
|
|
||||||
db.FileIndexes.Add(fileIndex);
|
db.FileIndexes.Add(fileIndex);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
@@ -41,26 +115,94 @@ public class FileIndexService(AppDatabase db)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an existing file index entry by removing the old one and creating a new one
|
/// Creates a new file index entry in a specific folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The file index ID</param>
|
/// <param name="folderId">The folder ID where the file should be placed</param>
|
||||||
/// <param name="newPath">The new parent folder path with trailing slash</param>
|
/// <param name="fileId">The file ID</param>
|
||||||
/// <returns>The updated file index</returns>
|
/// <param name="accountId">The account ID</param>
|
||||||
public async Task<SnCloudFileIndex?> UpdateAsync(Guid id, string newPath)
|
/// <returns>The created file index</returns>
|
||||||
|
public async Task<SnCloudFileIndex> CreateInFolderAsync(Guid folderId, string fileId, Guid accountId)
|
||||||
{
|
{
|
||||||
var fileIndex = await db.FileIndexes.FindAsync(id);
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a file to a different folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileIndexId">The file index ID</param>
|
||||||
|
/// <param name="newFolderId">The new folder ID</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>The updated file index</returns>
|
||||||
|
public async Task<SnCloudFileIndex?> 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)
|
if (fileIndex == null)
|
||||||
return 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
|
// Since properties are init-only, we need to remove the old index and create a new one
|
||||||
db.FileIndexes.Remove(fileIndex);
|
db.FileIndexes.Remove(fileIndex);
|
||||||
|
|
||||||
var newFileIndex = new SnCloudFileIndex
|
var newFileIndex = SnCloudFileIndex.Create(newFolder, fileIndex.File, accountId);
|
||||||
{
|
newFileIndex.Id = fileIndexId; // Keep the same ID
|
||||||
Path = NormalizePath(newPath),
|
|
||||||
FileId = fileIndex.FileId,
|
|
||||||
AccountId = fileIndex.AccountId
|
|
||||||
};
|
|
||||||
|
|
||||||
db.FileIndexes.Add(newFileIndex);
|
db.FileIndexes.Add(newFileIndex);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@@ -106,17 +248,15 @@ public class FileIndexService(AppDatabase db)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes file index entries by account ID and path
|
/// Removes file index entries by account ID and folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="accountId">The account ID</param>
|
/// <param name="accountId">The account ID</param>
|
||||||
/// <param name="path">The parent folder path</param>
|
/// <param name="folderId">The folder ID</param>
|
||||||
/// <returns>The number of indexes removed</returns>
|
/// <returns>The number of indexes removed</returns>
|
||||||
public async Task<int> RemoveByPathAsync(Guid accountId, string path)
|
public async Task<int> RemoveByFolderAsync(Guid accountId, Guid folderId)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePath(path);
|
|
||||||
|
|
||||||
var indexes = await db.FileIndexes
|
var indexes = await db.FileIndexes
|
||||||
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
|
.Where(fi => fi.AccountId == accountId && fi.FolderId == folderId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
if (!indexes.Any())
|
if (!indexes.Any())
|
||||||
@@ -129,23 +269,21 @@ public class FileIndexService(AppDatabase db)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets file indexes by account ID and path
|
/// Gets file indexes by account ID and folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="accountId">The account ID</param>
|
/// <param name="accountId">The account ID</param>
|
||||||
/// <param name="path">The parent folder path</param>
|
/// <param name="folderId">The folder ID</param>
|
||||||
/// <returns>List of file indexes</returns>
|
/// <returns>List of file indexes</returns>
|
||||||
public async Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path)
|
public async Task<List<SnCloudFileIndex>> GetByFolderAsync(Guid accountId, Guid folderId)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePath(path);
|
|
||||||
|
|
||||||
return await db.FileIndexes
|
return await db.FileIndexes
|
||||||
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
|
.Where(fi => fi.AccountId == accountId && fi.FolderId == folderId)
|
||||||
.Include(fi => fi.File)
|
.Include(fi => fi.File)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets file indexes by file ID
|
/// Gets file indexes by file ID with folder information
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileId">The file ID</param>
|
/// <param name="fileId">The file ID</param>
|
||||||
/// <returns>List of file indexes</returns>
|
/// <returns>List of file indexes</returns>
|
||||||
@@ -154,6 +292,7 @@ public class FileIndexService(AppDatabase db)
|
|||||||
return await db.FileIndexes
|
return await db.FileIndexes
|
||||||
.Where(fi => fi.FileId == fileId)
|
.Where(fi => fi.FileId == fileId)
|
||||||
.Include(fi => fi.File)
|
.Include(fi => fi.File)
|
||||||
|
.Include(fi => fi.Folder)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,31 +306,107 @@ public class FileIndexService(AppDatabase db)
|
|||||||
return await db.FileIndexes
|
return await db.FileIndexes
|
||||||
.Where(fi => fi.AccountId == accountId)
|
.Where(fi => fi.AccountId == accountId)
|
||||||
.Include(fi => fi.File)
|
.Include(fi => fi.File)
|
||||||
|
.Include(fi => fi.Folder)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normalizes the path to ensure it has a trailing slash and is query-safe
|
/// Gets file indexes by path for an account (finds folder by path and gets files in that folder)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The original path</param>
|
/// <param name="accountId">The account ID</param>
|
||||||
/// <returns>The normalized path</returns>
|
/// <param name="path">The path to search for</param>
|
||||||
public static string NormalizePath(string path)
|
/// <returns>List of file indexes at the specified path</returns>
|
||||||
|
public async Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
var normalizedPath = NormalizePath(path);
|
||||||
return "/";
|
|
||||||
|
|
||||||
// Ensure the path starts with a slash
|
// Find the folder that corresponds to this path
|
||||||
if (!path.StartsWith('/'))
|
var folder = await db.Folders
|
||||||
path = "/" + path;
|
.FirstOrDefaultAsync(f => f.AccountId == accountId && f.Path == normalizedPath + (normalizedPath == "/" ? "" : "/"));
|
||||||
|
|
||||||
// Ensure the path ends with a slash (unless it's just the root)
|
if (folder == null)
|
||||||
if (path != "/" && !path.EndsWith('/'))
|
return new List<SnCloudFileIndex>();
|
||||||
path += "/";
|
|
||||||
|
|
||||||
// Make path query-safe by removing problematic characters
|
return await db.FileIndexes
|
||||||
// This is a basic implementation - you might want to add more robust validation
|
.Where(fi => fi.AccountId == accountId && fi.FolderId == folder.Id)
|
||||||
path = path.Replace("%", "").Replace("'", "").Replace("\"", "");
|
.Include(fi => fi.File)
|
||||||
|
.Include(fi => fi.Folder)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
return path;
|
/// <summary>
|
||||||
|
/// Updates the path of a file index
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileIndexId">The file index ID</param>
|
||||||
|
/// <param name="newPath">The new path</param>
|
||||||
|
/// <returns>The updated file index, or null if not found</returns>
|
||||||
|
public async Task<SnCloudFileIndex?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all file index entries at a specific path for an account (finds folder by path and removes files from that folder)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <param name="path">The path to clear</param>
|
||||||
|
/// <returns>The number of indexes removed</returns>
|
||||||
|
public async Task<int> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
313
DysonNetwork.Drive/Index/FolderService.cs
Normal file
313
DysonNetwork.Drive/Index/FolderService.cs
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Drive.Index;
|
||||||
|
|
||||||
|
public class FolderService(AppDatabase db)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The folder name</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <param name="parentFolderId">Optional parent folder ID</param>
|
||||||
|
/// <returns>The created folder</returns>
|
||||||
|
public async Task<SnCloudFolder> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the root folder for an account (if it doesn't exist)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>The root folder</returns>
|
||||||
|
public async Task<SnCloudFolder> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a folder by ID with its contents
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <param name="accountId">The account ID (for authorization)</param>
|
||||||
|
/// <returns>The folder with child folders and files</returns>
|
||||||
|
public async Task<SnCloudFolder?> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all folders for an account
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>List of folders</returns>
|
||||||
|
public async Task<List<SnCloudFolder>> GetByAccountIdAsync(Guid accountId)
|
||||||
|
{
|
||||||
|
return await db.Folders
|
||||||
|
.Where(f => f.AccountId == accountId)
|
||||||
|
.Include(f => f.ParentFolder)
|
||||||
|
.OrderBy(f => f.Path)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets child folders of a parent folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parentFolderId">The parent folder ID</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>List of child folders</returns>
|
||||||
|
public async Task<List<SnCloudFolder>> GetChildFoldersAsync(Guid parentFolderId, Guid accountId)
|
||||||
|
{
|
||||||
|
return await db.Folders
|
||||||
|
.Where(f => f.ParentFolderId == parentFolderId && f.AccountId == accountId)
|
||||||
|
.OrderBy(f => f.Name)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a folder's name and path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <param name="newName">The new folder name</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>The updated folder</returns>
|
||||||
|
public async Task<SnCloudFolder?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively updates child folder paths when a parent folder is renamed
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a folder and all its contents
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>True if the folder was deleted, false otherwise</returns>
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a folder to a new parent folder
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="folderId">The folder ID</param>
|
||||||
|
/// <param name="newParentFolderId">The new parent folder ID</param>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <returns>The moved folder</returns>
|
||||||
|
public async Task<SnCloudFolder?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if moving a folder would create a circular reference
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches for folders by name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountId">The account ID</param>
|
||||||
|
/// <param name="searchTerm">The search term</param>
|
||||||
|
/// <returns>List of matching folders</returns>
|
||||||
|
public async Task<List<SnCloudFolder>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,11 @@ And all the arguments will be transformed into snake case via the gateway.
|
|||||||
### Core Components
|
### Core Components
|
||||||
|
|
||||||
1. **SnCloudFileIndex Model** - Represents the file-to-path mapping
|
1. **SnCloudFileIndex Model** - Represents the file-to-path mapping
|
||||||
2. **FileIndexService** - Business logic for file index operations
|
2. **SnCloudFolder Model** - Represents hierarchical folder structure
|
||||||
3. **FileIndexController** - REST API endpoints for file management
|
3. **FileIndexService** - Business logic for file index operations
|
||||||
4. **FileUploadController Integration** - Automatic index creation during upload
|
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
|
||||||
|
|
||||||
### Database Schema
|
### Database Schema
|
||||||
|
|
||||||
@@ -200,6 +202,138 @@ 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
|
## Path Normalization
|
||||||
|
|
||||||
The system automatically normalizes paths to ensure consistency:
|
The system automatically normalizes paths to ensure consistency:
|
||||||
@@ -237,32 +371,75 @@ The system will automatically create a file index when the upload completes succ
|
|||||||
```csharp
|
```csharp
|
||||||
public class FileIndexService
|
public class FileIndexService
|
||||||
{
|
{
|
||||||
// Create a new file index
|
// Create a new file index at path
|
||||||
Task<SnCloudFileIndex> CreateAsync(string path, Guid fileId, Guid accountId);
|
Task<SnCloudFileIndex> CreateAsync(string path, string fileId, Guid accountId);
|
||||||
|
|
||||||
|
// Create a new file index in folder
|
||||||
|
Task<SnCloudFileIndex> CreateInFolderAsync(Guid folderId, string fileId, Guid accountId);
|
||||||
|
|
||||||
// Get files by path
|
// Get files by path
|
||||||
Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path);
|
Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path);
|
||||||
|
|
||||||
|
// Get files by folder
|
||||||
|
Task<List<SnCloudFileIndex>> GetByFolderAsync(Guid accountId, Guid folderId);
|
||||||
|
|
||||||
// Get all files for account
|
// Get all files for account
|
||||||
Task<List<SnCloudFileIndex>> GetByAccountIdAsync(Guid accountId);
|
Task<List<SnCloudFileIndex>> GetByAccountIdAsync(Guid accountId);
|
||||||
|
|
||||||
// Get indexes for specific file
|
// Get indexes for specific file
|
||||||
Task<List<SnCloudFileIndex>> GetByFileIdAsync(Guid fileId);
|
Task<List<SnCloudFileIndex>> GetByFileIdAsync(string fileId);
|
||||||
|
|
||||||
// Move file to new path
|
// Move file to new path
|
||||||
Task<SnCloudFileIndex?> UpdateAsync(Guid indexId, string newPath);
|
Task<SnCloudFileIndex?> UpdateAsync(Guid indexId, string newPath);
|
||||||
|
|
||||||
|
// Move file to different folder
|
||||||
|
Task<SnCloudFileIndex?> MoveAsync(Guid fileIndexId, Guid newFolderId, Guid accountId);
|
||||||
|
|
||||||
// Remove file index
|
// Remove file index
|
||||||
Task<bool> RemoveAsync(Guid indexId);
|
Task<bool> RemoveAsync(Guid indexId);
|
||||||
|
|
||||||
// Remove all indexes in path
|
// Remove all indexes in path
|
||||||
Task<int> RemoveByPathAsync(Guid accountId, string path);
|
Task<int> RemoveByPathAsync(Guid accountId, string path);
|
||||||
|
|
||||||
|
// Remove all indexes in folder
|
||||||
|
Task<int> RemoveByFolderAsync(Guid accountId, Guid folderId);
|
||||||
|
|
||||||
// Normalize path format
|
// Normalize path format
|
||||||
public static string NormalizePath(string path);
|
public static string NormalizePath(string path);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### FolderService
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class FolderService
|
||||||
|
{
|
||||||
|
// Create a new folder
|
||||||
|
Task<SnCloudFolder> CreateAsync(string name, Guid accountId, Guid? parentFolderId = null);
|
||||||
|
|
||||||
|
// Get folder by ID with contents
|
||||||
|
Task<SnCloudFolder?> GetByIdAsync(Guid folderId, Guid accountId);
|
||||||
|
|
||||||
|
// Get all folders for account
|
||||||
|
Task<List<SnCloudFolder>> GetByAccountIdAsync(Guid accountId);
|
||||||
|
|
||||||
|
// Get child folders
|
||||||
|
Task<List<SnCloudFolder>> GetChildFoldersAsync(Guid parentFolderId, Guid accountId);
|
||||||
|
|
||||||
|
// Update folder name
|
||||||
|
Task<SnCloudFolder?> UpdateAsync(Guid folderId, string name, Guid accountId);
|
||||||
|
|
||||||
|
// Move folder to new parent
|
||||||
|
Task<SnCloudFolder?> MoveAsync(Guid folderId, Guid? newParentFolderId, Guid accountId);
|
||||||
|
|
||||||
|
// Delete folder and contents
|
||||||
|
Task<bool> DeleteAsync(Guid folderId, Guid accountId);
|
||||||
|
|
||||||
|
// Search folders by name
|
||||||
|
Task<List<SnCloudFolder>> SearchAsync(Guid accountId, string searchTerm);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
The API returns appropriate HTTP status codes and error messages:
|
The API returns appropriate HTTP status codes and error messages:
|
||||||
|
|||||||
722
DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.Designer.cs
generated
Normal file
722
DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.Designer.cs
generated
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<long>("Quota")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("quota");
|
||||||
|
|
||||||
|
b.Property<Instant>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("completed_at");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<string>("Discriminator")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(21)
|
||||||
|
.HasColumnType("character varying(21)")
|
||||||
|
.HasColumnName("discriminator");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("error_message");
|
||||||
|
|
||||||
|
b.Property<long?>("EstimatedDurationSeconds")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("estimated_duration_seconds");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<Instant>("LastActivity")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_activity");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("Parameters")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("parameters");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("priority");
|
||||||
|
|
||||||
|
b.Property<double>("Progress")
|
||||||
|
.HasColumnType("double precision")
|
||||||
|
.HasColumnName("progress");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("Results")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("results");
|
||||||
|
|
||||||
|
b.Property<Instant?>("StartedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("started_at");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("status");
|
||||||
|
|
||||||
|
b.Property<string>("TaskId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("task_id");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.Property<Instant>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("file_id");
|
||||||
|
|
||||||
|
b.Property<string>("ResourceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("resource_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<string>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<BillingConfig>("BillingConfig")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("billing_config");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8192)
|
||||||
|
.HasColumnType("character varying(8192)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<bool>("IsHidden")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_hidden");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<PolicyConfig>("PolicyConfig")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("policy_config");
|
||||||
|
|
||||||
|
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("storage_config");
|
||||||
|
|
||||||
|
b.Property<Instant>("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<string>("Id")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Guid?>("BundleId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("bundle_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("FileMeta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("file_meta");
|
||||||
|
|
||||||
|
b.Property<bool>("HasCompression")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("has_compression");
|
||||||
|
|
||||||
|
b.Property<bool>("HasThumbnail")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("has_thumbnail");
|
||||||
|
|
||||||
|
b.Property<string>("Hash")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("hash");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEncrypted")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_encrypted");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMarkedRecycle")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_marked_recycle");
|
||||||
|
|
||||||
|
b.Property<string>("MimeType")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("mime_type");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PoolId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("pool_id");
|
||||||
|
|
||||||
|
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("sensitive_marks");
|
||||||
|
|
||||||
|
b.Property<long>("Size")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("size");
|
||||||
|
|
||||||
|
b.Property<string>("StorageId")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("storage_id");
|
||||||
|
|
||||||
|
b.Property<string>("StorageUrl")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("storage_url");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("UploadedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("uploaded_at");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("file_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("FolderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("folder_id");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<Instant>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("FolderMeta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("folder_meta");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ParentFolderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("parent_folder_id");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8192)
|
||||||
|
.HasColumnType("character varying(8192)")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<Instant>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(8192)
|
||||||
|
.HasColumnType("character varying(8192)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string>("Passcode")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("passcode");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("slug");
|
||||||
|
|
||||||
|
b.Property<Instant>("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<Guid?>("BundleId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("bundle_id");
|
||||||
|
|
||||||
|
b.Property<long>("ChunkSize")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("chunk_size");
|
||||||
|
|
||||||
|
b.Property<int>("ChunksCount")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("chunks_count");
|
||||||
|
|
||||||
|
b.Property<int>("ChunksUploaded")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("chunks_uploaded");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("content_type");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptPassword")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("encrypt_password");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("file_name");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("file_size");
|
||||||
|
|
||||||
|
b.Property<string>("Hash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("hash");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<Guid>("PoolId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("pool_id");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<List<int>>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.cs
Normal file
120
DysonNetwork.Drive/Migrations/20251113165508_AddFileFolders.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Drive.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFileFolders : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_file_indexes_path_account_id",
|
||||||
|
table: "file_indexes");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "path",
|
||||||
|
table: "file_indexes",
|
||||||
|
type: "character varying(2048)",
|
||||||
|
maxLength: 2048,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(8192)",
|
||||||
|
oldMaxLength: 8192);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
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<Guid>(type: "uuid", nullable: false),
|
||||||
|
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
path = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
|
||||||
|
parent_folder_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||||
|
folder_meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<string>(
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -428,10 +428,14 @@ namespace DysonNetwork.Drive.Migrations
|
|||||||
.HasColumnType("character varying(32)")
|
.HasColumnType("character varying(32)")
|
||||||
.HasColumnName("file_id");
|
.HasColumnName("file_id");
|
||||||
|
|
||||||
|
b.Property<Guid>("FolderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("folder_id");
|
||||||
|
|
||||||
b.Property<string>("Path")
|
b.Property<string>("Path")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(8192)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(8192)")
|
.HasColumnType("character varying(2048)")
|
||||||
.HasColumnName("path");
|
.HasColumnName("path");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
@@ -444,12 +448,72 @@ namespace DysonNetwork.Drive.Migrations
|
|||||||
b.HasIndex("FileId")
|
b.HasIndex("FileId")
|
||||||
.HasDatabaseName("ix_file_indexes_file_id");
|
.HasDatabaseName("ix_file_indexes_file_id");
|
||||||
|
|
||||||
b.HasIndex("Path", "AccountId")
|
b.HasIndex("FolderId", "AccountId")
|
||||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
.HasDatabaseName("ix_file_indexes_folder_id_account_id");
|
||||||
|
|
||||||
b.ToTable("file_indexes", (string)null);
|
b.ToTable("file_indexes", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("FolderMeta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("folder_meta");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ParentFolderId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("parent_folder_id");
|
||||||
|
|
||||||
|
b.Property<string>("Path")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8192)
|
||||||
|
.HasColumnType("character varying(8192)")
|
||||||
|
.HasColumnName("path");
|
||||||
|
|
||||||
|
b.Property<Instant>("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 =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -609,7 +673,26 @@ namespace DysonNetwork.Drive.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
.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("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 =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||||
@@ -619,6 +702,13 @@ namespace DysonNetwork.Drive.Migrations
|
|||||||
b.Navigation("References");
|
b.Navigation("References");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFolder", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ChildFolders");
|
||||||
|
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Files");
|
b.Navigation("Files");
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<Storage.FileService>();
|
services.AddScoped<Storage.FileService>();
|
||||||
services.AddScoped<Storage.FileReferenceService>();
|
services.AddScoped<Storage.FileReferenceService>();
|
||||||
services.AddScoped<Storage.PersistentTaskService>();
|
services.AddScoped<Storage.PersistentTaskService>();
|
||||||
|
services.AddScoped<FolderService>();
|
||||||
services.AddScoped<FileIndexService>();
|
services.AddScoped<FileIndexService>();
|
||||||
services.AddScoped<Billing.UsageService>();
|
services.AddScoped<Billing.UsageService>();
|
||||||
services.AddScoped<Billing.QuotaService>();
|
services.AddScoped<Billing.QuotaService>();
|
||||||
|
|||||||
@@ -1,30 +1,70 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
[Index(nameof(Path), nameof(AccountId))]
|
[Index(nameof(FolderId), nameof(AccountId))]
|
||||||
|
[Index(nameof(FileId))]
|
||||||
public class SnCloudFileIndex : ModelBase
|
public class SnCloudFileIndex : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The path of the file,
|
/// Reference to the folder containing this 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
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MaxLength(8192)]
|
public Guid FolderId { get; init; }
|
||||||
public string Path { get; init; } = null!;
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation property to the folder
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public SnCloudFolder Folder { get; init; } = null!;
|
||||||
|
|
||||||
[MaxLength(32)] public string FileId { get; init; } = null!;
|
[MaxLength(32)] public string FileId { get; init; } = null!;
|
||||||
public SnCloudFile File { get; init; } = null!;
|
public SnCloudFile File { get; init; } = null!;
|
||||||
public Guid AccountId { get; init; }
|
public Guid AccountId { get; init; }
|
||||||
[NotMapped] public SnAccount? Account { get; init; }
|
[NotMapped] public SnAccount? Account { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached full path of the file (stored in database for performance)
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(2048)]
|
||||||
|
public string Path { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new file index with the specified folder and file
|
||||||
|
/// </summary>
|
||||||
|
public static SnCloudFileIndex Create(SnCloudFolder folder, SnCloudFile file, Guid accountId)
|
||||||
|
{
|
||||||
|
// Build the full path by traversing the folder hierarchy
|
||||||
|
var pathSegments = new List<string>();
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
DysonNetwork.Shared/Models/CloudFolder.cs
Normal file
103
DysonNetwork.Shared/Models/CloudFolder.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the folder
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Name { get; init; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The full path of the folder (for querying purposes)
|
||||||
|
/// With trailing slash, e.g., /documents/work/
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(8192)]
|
||||||
|
public string Path { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to the parent folder (null for root folders)
|
||||||
|
/// </summary>
|
||||||
|
public Guid? ParentFolderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation property to the parent folder
|
||||||
|
/// </summary>
|
||||||
|
[ForeignKey(nameof(ParentFolderId))]
|
||||||
|
public SnCloudFolder? ParentFolder { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation property to child folders
|
||||||
|
/// </summary>
|
||||||
|
[InverseProperty(nameof(ParentFolder))]
|
||||||
|
public List<SnCloudFolder> ChildFolders { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation property to files in this folder
|
||||||
|
/// </summary>
|
||||||
|
[InverseProperty(nameof(SnCloudFileIndex.Folder))]
|
||||||
|
public List<SnCloudFileIndex> Files { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The account that owns this folder
|
||||||
|
/// </summary>
|
||||||
|
public Guid AccountId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigation property to the account
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public SnAccount? Account { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional description for the folder
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(4096)]
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom metadata for the folder
|
||||||
|
/// </summary>
|
||||||
|
[Column(TypeName = "jsonb")]
|
||||||
|
public Dictionary<string, object?>? FolderMeta { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new folder with proper path normalization
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the root folder for an account
|
||||||
|
/// </summary>
|
||||||
|
public static SnCloudFolder CreateRoot(Guid accountId)
|
||||||
|
{
|
||||||
|
return new SnCloudFolder
|
||||||
|
{
|
||||||
|
Name = "Root",
|
||||||
|
Path = "/",
|
||||||
|
ParentFolderId = null,
|
||||||
|
AccountId = accountId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user