diff --git a/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/FileManagementDtos.cs b/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/FileManagementDtos.cs index ff871a4..7bd5b14 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/FileManagementDtos.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/FileManagementDtos.cs @@ -13,6 +13,8 @@ public class CreateFolderDto public string Name { get; set; } = string.Empty; public string? ParentId { get; set; } + + public string? TenantId { get; set; } } public class RenameItemDto @@ -20,11 +22,15 @@ public class RenameItemDto [Required] [StringLength(255)] public string Name { get; set; } = string.Empty; + + public string? TenantId { get; set; } } public class MoveItemDto { public string? TargetFolderId { get; set; } + + public string? TenantId { get; set; } } public class UploadFileDto @@ -33,6 +39,8 @@ public class UploadFileDto public string FileName { get; set; } = string.Empty; public string? ParentId { get; set; } + + public string? TenantId { get; set; } // NoteModal pattern - Files array public IRemoteStreamContent[]? Files { get; set; } @@ -44,12 +52,16 @@ public class SearchFilesDto public string Query { get; set; } = string.Empty; public string? ParentId { get; set; } + + public string? TenantId { get; set; } } public class BulkDeleteDto { [Required] public List ItemIds { get; set; } = new(); + + public string? TenantId { get; set; } } public class CopyItemsDto @@ -58,6 +70,8 @@ public class CopyItemsDto public List ItemIds { get; set; } = new(); public string? TargetFolderId { get; set; } + + public string? TenantId { get; set; } } public class MoveItemsDto @@ -66,4 +80,6 @@ public class MoveItemsDto public List ItemIds { get; set; } = new(); public string? TargetFolderId { get; set; } + + public string? TenantId { get; set; } } diff --git a/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/IFileManagementAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/IFileManagementAppService.cs index d615a6c..1e98639 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/IFileManagementAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/FileManagement/IFileManagementAppService.cs @@ -9,33 +9,18 @@ namespace Sozsoft.Platform.FileManagement; public interface IFileManagementAppService : IApplicationService { - Task GetItemsAsync(); - - Task GetItemsByParentAsync(string parentId); - + Task GetItemsAsync(string? tenantId = null); + Task GetItemsByParentAsync(string parentId, string? tenantId = null); Task CreateFolderAsync(CreateFolderDto input); - Task UploadFileAsync(UploadFileDto input); - Task RenameItemAsync(string id, RenameItemDto input); - Task MoveItemAsync(string id, MoveItemDto input); - - Task DeleteItemAsync(string id); - - Task DownloadFileAsync(string id); - - Task GetFilePreviewAsync(string id); - + Task DownloadFileAsync(string id, string? tenantId = null); + Task GetFilePreviewAsync(string id, string? tenantId = null); Task SearchItemsAsync(SearchFilesDto input); - - Task GetFolderPathAsync(); - - Task GetFolderPathByIdAsync(string folderId); - + Task GetFolderPathAsync(string? tenantId = null); + Task GetFolderPathByIdAsync(string folderId, string? tenantId = null); Task BulkDeleteItemsAsync(BulkDeleteDto input); - Task> CopyItemsAsync(CopyItemsDto input); - Task> MoveItemsAsync(MoveItemsDto input); } diff --git a/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs b/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs index cce9582..85f86c1 100644 --- a/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs +++ b/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs @@ -14,9 +14,11 @@ using Microsoft.Extensions.Logging; using Volo.Abp; using Volo.Abp.Application.Services; using Volo.Abp.MultiTenancy; +using Microsoft.AspNetCore.Authorization; namespace Sozsoft.Platform.FileManagement; +[Authorize] public class FileManagementAppService : ApplicationService, IFileManagementAppService { private readonly ICurrentTenant _currentTenant; @@ -47,11 +49,10 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe _configuration = configuration; } - private string GetTenantPrefix() - { - var tenantId = _currentTenant.Id?.ToString() ?? "host"; - return $"tenants/{tenantId}/"; - } + private string GetTenantPrefix(string tenantId) => $"tenants/{tenantId}/"; + + private string GetEffectiveTenantId(string? inputTenantId) => + !string.IsNullOrEmpty(inputTenantId) ? inputTenantId : (_currentTenant.Id?.ToString() ?? "host"); private string EncodePathAsId(string path) { @@ -95,12 +96,12 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Logger.LogInformation($"Folder {decodedPath} is not protected, allowing {operation}"); } - private async Task> GetFolderIndexAsync(string? parentId = null) + private async Task> GetFolderIndexAsync(string? parentId, string tenantId) { - return await GetRealCdnContentsAsync(parentId); + return await GetRealCdnContentsAsync(parentId, tenantId); } - private async Task> GetRealCdnContentsAsync(string? folderPath) + private async Task> GetRealCdnContentsAsync(string? folderPath, string tenantId) { var items = new List(); var cdnBasePath = _configuration["App:CdnPath"]; @@ -111,7 +112,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe return items; } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; var fullPath = Path.Combine(cdnBasePath, tenantId); if (!string.IsNullOrEmpty(folderPath)) @@ -158,7 +158,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = relativePath, ParentId = folderPath ?? "", IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString(), + TenantId = tenantId == "host" ? null : tenantId, ChildCount = childCount }); } @@ -181,7 +181,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = relativePath, ParentId = folderPath ?? "", IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }); } @@ -194,9 +194,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe } } - private async Task SaveFolderIndexAsync(List items, string? parentId = null) + private async Task SaveFolderIndexAsync(List items, string tenantId, string? parentId = null) { - var indexPath = GetTenantPrefix() + (string.IsNullOrEmpty(parentId) ? IndexFileName : $"{parentId}/{IndexFileName}"); + var indexPath = GetTenantPrefix(tenantId) + (string.IsNullOrEmpty(parentId) ? IndexFileName : $"{parentId}/{IndexFileName}"); var indexJson = JsonSerializer.Serialize(items, new JsonSerializerOptions { @@ -207,20 +207,23 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe await _blobContainer.SaveAsync(indexPath, indexBytes); } - public async Task GetItemsAsync() + [HttpGet("api/app/file-management/items/{tenantId}")] + public async Task GetItemsAsync(string? tenantId = null) { - return await GetItemsInternalAsync(null); + return await GetItemsInternalAsync(null, GetEffectiveTenantId(tenantId)); } - public async Task GetItemsByParentAsync(string parentId) + [HttpGet("api/app/file-management/items-by-parent")] + public async Task GetItemsByParentAsync(string parentId, string? tenantId = null) { + var effectiveTenantId = GetEffectiveTenantId(tenantId); var decodedParentId = DecodeIdAsPath(parentId); - return await GetItemsInternalAsync(decodedParentId); + return await GetItemsInternalAsync(decodedParentId, effectiveTenantId); } - private async Task GetItemsInternalAsync(string? parentId) + private async Task GetItemsInternalAsync(string? parentId, string tenantId) { - var items = await GetFolderIndexAsync(parentId); + var items = await GetFolderIndexAsync(parentId, tenantId); var result = items.Select(metadata => { @@ -249,6 +252,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe return new GetFilesDto { Items = result }; } + [HttpPost("api/app/file-management/folder")] public async Task CreateFolderAsync(CreateFolderDto input) { ValidateFileName(input.Name); @@ -259,7 +263,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe throw new UserFriendlyException("CDN path is not configured"); } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var tenantId = GetEffectiveTenantId(input.TenantId); var parentPath = Path.Combine(cdnBasePath, tenantId); string? decodedParentId = null; @@ -298,7 +302,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = newFolderPath, ParentId = decodedParentId ?? string.Empty, IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }; return new FileItemDto @@ -311,14 +315,17 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = metadata.Path, ParentId = input.ParentId ?? string.Empty, IsReadOnly = metadata.IsReadOnly, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }; } + [HttpPost("api/app/file-management/upload-file")] public async Task UploadFileAsync([FromForm] UploadFileDto input) { ValidateFileName(input.FileName); + var tenantId = GetEffectiveTenantId(input.TenantId); + // Decode parent ID if provided string? decodedParentId = null; if (!string.IsNullOrEmpty(input.ParentId)) @@ -326,7 +333,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe decodedParentId = DecodeIdAsPath(input.ParentId); } - var items = await GetFolderIndexAsync(decodedParentId); + var items = await GetFolderIndexAsync(decodedParentId, tenantId); // Generate unique filename if file already exists var uniqueFileName = GetUniqueFileName(items, input.FileName); @@ -344,7 +351,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe throw new UserFriendlyException("CDN path is not configured"); } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; var fullCdnPath = Path.Combine(cdnBasePath, tenantId); if (!string.IsNullOrEmpty(decodedParentId)) @@ -387,7 +393,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = filePath, ParentId = decodedParentId ?? string.Empty, IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }; // File system'e kaydedildi, index güncellemeye gerek yok @@ -404,10 +410,11 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = metadata.Path, ParentId = input.ParentId ?? string.Empty, IsReadOnly = metadata.IsReadOnly, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }; } + [HttpPost("api/app/file-management/{id}/rename-item")] public async Task RenameItemAsync(string id, RenameItemDto input) { // Check if this is a protected system folder @@ -415,24 +422,26 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe ValidateFileName(input.Name); - var metadata = await FindItemMetadataAsync(id); + var tenantId = GetEffectiveTenantId(input.TenantId); + + var metadata = await FindItemMetadataAsync(id, tenantId); if (metadata == null) { throw new UserFriendlyException("Item not found"); } - var parentItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId); + var parentItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId, tenantId); if (parentItems.Any(x => x.Id != id && x.Name.Equals(input.Name, StringComparison.OrdinalIgnoreCase))) { throw new UserFriendlyException("An item with this name already exists"); } - var oldBlobPath = GetTenantPrefix() + metadata.Path; + var oldBlobPath = GetTenantPrefix(tenantId) + metadata.Path; var newPath = string.IsNullOrEmpty(metadata.ParentId) ? input.Name : $"{metadata.ParentId}/{input.Name}"; - var newBlobPath = GetTenantPrefix() + newPath; + var newBlobPath = GetTenantPrefix(tenantId) + newPath; if (metadata.Type == "folder") { @@ -457,7 +466,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe itemToUpdate.Path = newPath; itemToUpdate.ModifiedAt = DateTime.UtcNow; - await SaveFolderIndexAsync(parentItems, metadata.ParentId == string.Empty ? null : metadata.ParentId); + await SaveFolderIndexAsync(parentItems, tenantId, metadata.ParentId == string.Empty ? null : metadata.ParentId); return new FileItemDto { @@ -472,19 +481,22 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = metadata.Path, ParentId = metadata.ParentId, IsReadOnly = metadata.IsReadOnly, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }; } + [HttpPost("api/app/file-management/{id}/move-item")] public async Task MoveItemAsync(string id, MoveItemDto input) { - var metadata = await FindItemMetadataAsync(id); + var tenantId = GetEffectiveTenantId(input.TenantId); + + var metadata = await FindItemMetadataAsync(id, tenantId); if (metadata == null) { throw new UserFriendlyException("Item not found"); } - var targetItems = await GetFolderIndexAsync(input.TargetFolderId); + var targetItems = await GetFolderIndexAsync(input.TargetFolderId, tenantId); if (targetItems.Any(x => x.Name.Equals(metadata.Name, StringComparison.OrdinalIgnoreCase))) { @@ -492,16 +504,16 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe } // Remove from source - var sourceItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId); + var sourceItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId, tenantId); sourceItems.RemoveAll(x => x.Id == id); - await SaveFolderIndexAsync(sourceItems, metadata.ParentId == string.Empty ? null : metadata.ParentId); + await SaveFolderIndexAsync(sourceItems, tenantId, metadata.ParentId == string.Empty ? null : metadata.ParentId); // Move blob - var oldBlobPath = GetTenantPrefix() + metadata.Path; + var oldBlobPath = GetTenantPrefix(tenantId) + metadata.Path; var newPath = string.IsNullOrEmpty(input.TargetFolderId) ? metadata.Name : $"{input.TargetFolderId}/{metadata.Name}"; - var newBlobPath = GetTenantPrefix() + newPath; + var newBlobPath = GetTenantPrefix(tenantId) + newPath; if (metadata.Type == "folder") { @@ -520,7 +532,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe // Add to target targetItems.Add(metadata); - await SaveFolderIndexAsync(targetItems, input.TargetFolderId); + await SaveFolderIndexAsync(targetItems, tenantId, input.TargetFolderId); return new FileItemDto { @@ -535,11 +547,12 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = metadata.Path, ParentId = metadata.ParentId, IsReadOnly = metadata.IsReadOnly, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }; } - public async Task DeleteItemAsync(string id) + [HttpDelete("api/app/file-management/items/{id}/item/{tenantId}")] + public async Task DeleteItemAsync(string id, string? tenantId = null) { // Check if this is a protected system folder ValidateNotProtectedFolder(id, "delete"); @@ -550,9 +563,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe throw new UserFriendlyException("CDN path is not configured"); } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var effectiveTenantId = GetEffectiveTenantId(tenantId); var actualPath = DecodeIdAsPath(id); - var fullPath = Path.Combine(cdnBasePath, tenantId, actualPath); + var fullPath = Path.Combine(cdnBasePath, effectiveTenantId, actualPath); if (Directory.Exists(fullPath)) { @@ -570,6 +583,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe } } + [HttpPost("api/app/file-management/bulk-delete-items")] public async Task BulkDeleteItemsAsync(BulkDeleteDto input) { if (input.ItemIds == null || !input.ItemIds.Any()) @@ -583,7 +597,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe throw new UserFriendlyException("CDN path is not configured"); } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var tenantId = GetEffectiveTenantId(input.TenantId); var errors = new List(); foreach (var itemId in input.ItemIds) @@ -623,6 +637,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe } } + [HttpPost("api/app/file-management/copy-items")] public async Task> CopyItemsAsync(CopyItemsDto input) { if (input.ItemIds == null || !input.ItemIds.Any()) @@ -636,7 +651,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe throw new UserFriendlyException("CDN path is not configured"); } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var tenantId = GetEffectiveTenantId(input.TenantId); var basePath = Path.Combine(cdnBasePath, tenantId); string? targetPath = null; @@ -681,7 +696,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = finalTargetPath, ParentId = input.TargetFolderId ?? string.Empty, IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }); } else if (File.Exists(sourceFullPath)) @@ -711,7 +726,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = finalTargetPath, ParentId = input.TargetFolderId ?? string.Empty, IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }); } else @@ -733,6 +748,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe return copiedItems; } + [HttpPost("api/app/file-management/move-items")] public async Task> MoveItemsAsync(MoveItemsDto input) { if (input.ItemIds == null || !input.ItemIds.Any()) @@ -746,7 +762,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe throw new UserFriendlyException("CDN path is not configured"); } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var tenantId = GetEffectiveTenantId(input.TenantId); var basePath = Path.Combine(cdnBasePath, tenantId); string? targetPath = null; @@ -808,7 +824,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = finalTargetPath, ParentId = input.TargetFolderId ?? string.Empty, IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }); } else if (File.Exists(sourceFullPath)) @@ -838,7 +854,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = finalTargetPath, ParentId = input.TargetFolderId ?? string.Empty, IsReadOnly = false, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }); } else @@ -860,7 +876,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe return movedItems; } - public async Task DownloadFileAsync(string id) + [HttpPost("api/app/file-management/{id}/download-file/{tenantId}")] + public async Task DownloadFileAsync(string id, string? tenantId = null) { var cdnBasePath = _configuration["App:CdnPath"]; if (string.IsNullOrEmpty(cdnBasePath)) @@ -868,9 +885,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe throw new UserFriendlyException("CDN path is not configured"); } - var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var effectiveTenantId = GetEffectiveTenantId(tenantId); var actualPath = DecodeIdAsPath(id); - var fullFilePath = Path.Combine(cdnBasePath, tenantId, actualPath); + var fullFilePath = Path.Combine(cdnBasePath, effectiveTenantId, actualPath); if (!File.Exists(fullFilePath)) { @@ -880,14 +897,17 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe return File.OpenRead(fullFilePath); } - public async Task GetFilePreviewAsync(string id) + [HttpGet("api/app/file-management/{id}/file-preview/{tenantId}")] + public async Task GetFilePreviewAsync(string id, string? tenantId = null) { - return await DownloadFileAsync(id); + return await DownloadFileAsync(id, tenantId); } + [HttpPost("api/app/file-management/search-items")] public async Task SearchItemsAsync(SearchFilesDto input) { - var allItems = await GetAllItemsRecursivelyAsync(input.ParentId); + var tenantId = GetEffectiveTenantId(input.TenantId); + var allItems = await GetAllItemsRecursivelyAsync(tenantId, input.ParentId); var query = input.Query.ToLowerInvariant(); var filteredItems = allItems @@ -905,7 +925,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe Path = metadata.Path, ParentId = metadata.ParentId, IsReadOnly = metadata.IsReadOnly, - TenantId = _currentTenant.Id?.ToString() + TenantId = tenantId == "host" ? null : tenantId }) .OrderBy(x => x.Type == "folder" ? 0 : 1) .ThenBy(x => x.Name) @@ -914,9 +934,10 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe return new GetFilesDto { Items = filteredItems }; } - public async Task GetFolderPathAsync(string? folderId = null) + [HttpGet("api/app/file-management/folder-path/{tenantId}")] + public async Task GetFolderPathAsync(string? tenantId = null) { - return await GetFolderPathInternalAsync(folderId); + return await GetFolderPathInternalAsync(null); } #region Private Helper Methods @@ -944,30 +965,28 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe return uniqueName; } - private async Task FindItemMetadataAsync(string id) + private async Task FindItemMetadataAsync(string id, string tenantId) { - // This is not efficient, but IBlobContainer doesn't have built-in search - // In a real scenario, you might want to use a database index or search service - var allItems = await GetAllItemsRecursivelyAsync(); + var allItems = await GetAllItemsRecursivelyAsync(tenantId); return allItems.FirstOrDefault(x => x.Id == id); } - private async Task FindItemByPathAsync(string path) + private async Task FindItemByPathAsync(string path, string tenantId) { - var allItems = await GetAllItemsRecursivelyAsync(); + var allItems = await GetAllItemsRecursivelyAsync(tenantId); return allItems.FirstOrDefault(x => x.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); } - private async Task> GetAllItemsRecursivelyAsync(string? parentId = null, List? result = null) + private async Task> GetAllItemsRecursivelyAsync(string tenantId, string? parentId = null, List? result = null) { result ??= []; - var items = await GetFolderIndexAsync(parentId); + var items = await GetFolderIndexAsync(parentId, tenantId); result.AddRange(items); foreach (var folder in items.Where(x => x.Type == "folder")) { - await GetAllItemsRecursivelyAsync(folder.Id, result); + await GetAllItemsRecursivelyAsync(tenantId, folder.Id, result); } return result; @@ -1029,12 +1048,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe }; } - public async Task GetFolderPathAsync() - { - return await GetFolderPathInternalAsync(null); - } - - public async Task GetFolderPathByIdAsync(string folderId) + [HttpGet("api/app/file-management/folder-path-by-id")] + public async Task GetFolderPathByIdAsync(string folderId, string? tenantId = null) { return await GetFolderPathInternalAsync(folderId); } diff --git a/ui/src/services/fileManagement.service.ts b/ui/src/services/fileManagement.service.ts index f3f9305..f149a00 100644 --- a/ui/src/services/fileManagement.service.ts +++ b/ui/src/services/fileManagement.service.ts @@ -10,33 +10,42 @@ import type { class FileManagementService { // Get files and folders for a specific directory - async getItems(parentId?: string): Promise<{ data: { items: FileItem[] } }> { - const url = parentId - ? `/api/app/file-management/items-by-parent/${parentId}` - : `/api/app/file-management/items` - + async getItems(parentId?: string, tenantId?: string): Promise<{ data: { items: FileItem[] } }> { + const effectiveTenantId = tenantId || 'host' + const url = parentId + ? `/api/app/file-management/items-by-parent` + : `/api/app/file-management/items/${effectiveTenantId}` + + const params: Record = {} + if (parentId) params['parentId'] = parentId + if (parentId && tenantId) params['tenantId'] = tenantId + return ApiService.fetchData<{ items: FileItem[] }>({ url, method: 'GET', + params: Object.keys(params).length ? params : undefined, }) } // Create a new folder - async createFolder(request: CreateFolderRequest): Promise<{ data: FileItem }> { + async createFolder(request: CreateFolderRequest, tenantId?: string): Promise<{ data: FileItem }> { return ApiService.fetchData({ url: `/api/app/file-management/folder`, method: 'POST', - data: request as any, + data: { ...request, tenantId } as any, }) } // Upload a file (DTO pattern) - async uploadFile(request: UploadFileRequest): Promise<{ data: FileItem }> { + async uploadFile(request: UploadFileRequest, tenantId?: string): Promise<{ data: FileItem }> { const formData = new FormData() formData.append('fileName', request.fileName) if (request.parentId) { formData.append('parentId', request.parentId) } + if (tenantId) { + formData.append('tenantId', tenantId) + } // NoteModal pattern - Files array request.files.forEach(file => { @@ -47,16 +56,16 @@ class FileManagementService { url: `/api/app/file-management/upload-file`, method: 'POST', data: formData as any, - // Browser otomatik olarak Content-Type'ı multipart/form-data boundary ile set eder }) } // Upload a file directly with FormData (NoteModal pattern) - async uploadFileDirectly(formData: FormData): Promise<{ data: FileItem }> { + async uploadFileDirectly(formData: FormData, tenantId?: string): Promise<{ data: FileItem }> { try { console.log('Uploading file directly with FormData') - - // ABP convention-based routing: UploadFileAsync -> upload-file (Async suffix kaldırılır) + if (tenantId) { + formData.append('tenantId', tenantId) + } return await ApiService.fetchData({ url: 'api/app/file-management/upload-file', method: 'POST', @@ -69,35 +78,37 @@ class FileManagementService { } // Rename a file or folder - async renameItem(request: RenameItemRequest): Promise<{ data: FileItem }> { + async renameItem(request: RenameItemRequest, tenantId?: string): Promise<{ data: FileItem }> { return ApiService.fetchData({ url: `/api/app/file-management/${request.id}/rename-item`, method: 'POST', - data: { name: request.newName }, + data: { name: request.newName, tenantId }, }) } // Move a file or folder - async moveItem(request: MoveItemRequest): Promise<{ data: FileItem }> { + async moveItem(request: MoveItemRequest, tenantId?: string): Promise<{ data: FileItem }> { return ApiService.fetchData({ url: `/api/app/file-management/${request.itemId}/move-item`, method: 'POST', - data: { targetFolderId: request.targetFolderId }, + data: { targetFolderId: request.targetFolderId, tenantId }, }) } // Delete a file or folder - async deleteItem(request: DeleteItemRequest): Promise { + async deleteItem(request: DeleteItemRequest, tenantId?: string): Promise { + const effectiveTenantId = tenantId || 'host' await ApiService.fetchData({ - url: `/api/app/file-management/${request.id}/item`, + url: `/api/app/file-management/${request.id}/item/${effectiveTenantId}`, method: 'DELETE', }) } // Download a file - async downloadFile(fileId: string): Promise { + async downloadFile(fileId: string, tenantId?: string): Promise { + const effectiveTenantId = tenantId || 'host' const response = await ApiService.fetchData({ - url: `/api/app/file-management/${fileId}/download-file`, + url: `/api/app/file-management/${fileId}/download-file/${effectiveTenantId}`, method: 'POST', responseType: 'blob', }) @@ -105,9 +116,10 @@ class FileManagementService { } // Get file preview/thumbnail - async getFilePreview(fileId: string): Promise { + async getFilePreview(fileId: string, tenantId?: string): Promise { + const effectiveTenantId = tenantId || 'host' const response = await ApiService.fetchData({ - url: `/api/app/file-management/${fileId}/file-preview`, + url: `/api/app/file-management/${fileId}/file-preview/${effectiveTenantId}`, method: 'GET', responseType: 'blob', }) @@ -115,10 +127,11 @@ class FileManagementService { } // Search files and folders - async searchItems(query: string, folderId?: string): Promise<{ data: { items: FileItem[] } }> { + async searchItems(query: string, folderId?: string, tenantId?: string): Promise<{ data: { items: FileItem[] } }> { const data = { - query: query, + query, ...(folderId && { parentId: folderId }), + ...(tenantId && { tenantId }), } return ApiService.fetchData<{ items: FileItem[] }>({ @@ -129,41 +142,47 @@ class FileManagementService { } // Get folder breadcrumb path - async getFolderPath(folderId?: string): Promise<{ data: { path: Array<{ id: string; name: string }> } }> { - const url = folderId - ? `/api/app/file-management/folder-path-by-id/${folderId}` - : `/api/app/file-management/folder-path` - + async getFolderPath(folderId?: string, tenantId?: string): Promise<{ data: { path: Array<{ id: string; name: string }> } }> { + const effectiveTenantId = tenantId || 'host' + const url = folderId + ? `/api/app/file-management/folder-path-by-id` + : `/api/app/file-management/folder-path/${effectiveTenantId}` + + const params: Record = {} + if (folderId) params['folderId'] = folderId + if (folderId && tenantId) params['tenantId'] = tenantId + return ApiService.fetchData<{ path: Array<{ id: string; name: string }> }>({ url, method: 'GET', + params: Object.keys(params).length ? params : undefined, }) } // Bulk delete items - async bulkDeleteItems(itemIds: string[]): Promise<{ data: any }> { + async bulkDeleteItems(itemIds: string[], tenantId?: string): Promise<{ data: any }> { return ApiService.fetchData({ url: `/api/app/file-management/bulk-delete-items`, method: 'POST', - data: { itemIds }, + data: { itemIds, tenantId }, }) } // Copy items to target folder - async copyItems(itemIds: string[], targetFolderId?: string): Promise<{ data: FileItem[] }> { + async copyItems(itemIds: string[], targetFolderId?: string, tenantId?: string): Promise<{ data: FileItem[] }> { return ApiService.fetchData({ url: `/api/app/file-management/copy-items`, method: 'POST', - data: { itemIds, targetFolderId }, + data: { itemIds, targetFolderId, tenantId }, }) } // Move items to target folder - async moveItems(itemIds: string[], targetFolderId?: string): Promise<{ data: FileItem[] }> { + async moveItems(itemIds: string[], targetFolderId?: string, tenantId?: string): Promise<{ data: FileItem[] }> { return ApiService.fetchData({ url: `/api/app/file-management/move-items`, method: 'POST', - data: { itemIds, targetFolderId }, + data: { itemIds, targetFolderId, tenantId }, }) } } diff --git a/ui/src/views/admin/files/FileManager.tsx b/ui/src/views/admin/files/FileManager.tsx index 76a479a..5e2d4ef 100644 --- a/ui/src/views/admin/files/FileManager.tsx +++ b/ui/src/views/admin/files/FileManager.tsx @@ -16,10 +16,13 @@ import { FaEdit, FaDownload, FaPaste, + FaBuilding, } from 'react-icons/fa' import Container from '@/components/shared/Container' import { useLocalization } from '@/utils/hooks/useLocalization' import fileManagementService from '@/services/fileManagement.service' +import { getTenants } from '@/services/tenant.service' +import type { TenantDto } from '@/proxy/config/models' import Breadcrumb from './components/Breadcrumb' import FileItem from './components/FileItem' import FileUploadModal from './components/FileUploadModal' @@ -64,17 +67,46 @@ const FileManager = () => { const [itemToRename, setItemToRename] = useState() const [itemsToDelete, setItemsToDelete] = useState([]) + // Tenant state + const [tenants, setTenants] = useState([]) + const [tenantsLoading, setTenantsLoading] = useState(false) + const [selectedTenant, setSelectedTenant] = useState<{ id: string; name: string } | undefined>(undefined) + // Loading states const [uploading, setUploading] = useState(false) const [creating, setCreating] = useState(false) const [renaming, setRenaming] = useState(false) const [deleting, setDeleting] = useState(false) + // Fetch tenants + const fetchTenants = useCallback(async () => { + try { + setTenantsLoading(true) + const response = await getTenants(0, 1000) + setTenants(response.data.items || []) + } catch (error) { + console.error('Failed to fetch tenants:', error) + } finally { + setTenantsLoading(false) + } + }, []) + + useEffect(() => { + fetchTenants() + }, [fetchTenants]) + + // Reset navigation when tenant changes + useEffect(() => { + setCurrentFolderId(undefined) + setSelectedItems([]) + setBreadcrumbItems([{ name: 'Files', path: '', id: undefined }]) + }, [selectedTenant]) + // Fetch items from API const fetchItems = useCallback(async (folderId?: string) => { try { setLoading(true) - const response = await fileManagementService.getItems(folderId) + const response = await fileManagementService.getItems(folderId, selectedTenant?.id) // Backend returns GetFilesDto which has Items property const items = response.data.items || [] // Manual protection for system folders @@ -107,7 +139,7 @@ const FileManager = () => { } finally { setLoading(false) } - }, []) + }, [selectedTenant]) // Fetch breadcrumb path const fetchBreadcrumb = useCallback(async (folderId?: string) => { @@ -117,7 +149,7 @@ const FileManager = () => { return } - const response = await fileManagementService.getFolderPath(folderId) + const response = await fileManagementService.getFolderPath(folderId, selectedTenant?.id) // console.log('Breadcrumb response for folderId:', folderId, response) const pathItems: BreadcrumbItem[] = [ { name: 'Files', path: '', id: undefined }, @@ -131,7 +163,7 @@ const FileManager = () => { } catch (error) { console.error('Failed to fetch breadcrumb:', error) } - }, []) + }, [selectedTenant]) // Initial load useEffect(() => { @@ -237,7 +269,7 @@ const FileManager = () => { formData.append('parentId', currentFolderId) } - await fileManagementService.uploadFileDirectly(formData) + await fileManagementService.uploadFileDirectly(formData, selectedTenant?.id) } await fetchItems(currentFolderId) toast.push(Files uploaded successfully) @@ -256,7 +288,7 @@ const FileManager = () => { await fileManagementService.createFolder({ name, parentId: currentFolderId, - }) + }, selectedTenant?.id) await fetchItems(currentFolderId) toast.push(Folder created successfully) } catch (error) { @@ -276,7 +308,7 @@ const FileManager = () => { await fileManagementService.renameItem({ id: itemToRename.id, newName, - }) + }, selectedTenant?.id) await fetchItems(currentFolderId) toast.push(Item renamed successfully) } catch (error) { @@ -294,11 +326,11 @@ const FileManager = () => { if (itemsToDelete.length === 1) { // Single item delete - use existing API - await fileManagementService.deleteItem({ id: itemsToDelete[0].id }) + await fileManagementService.deleteItem({ id: itemsToDelete[0].id }, selectedTenant?.id) } else { // Multiple items - use bulk delete API const itemIds = itemsToDelete.map((item) => item.id) - await fileManagementService.bulkDeleteItems(itemIds) + await fileManagementService.bulkDeleteItems(itemIds, selectedTenant?.id) } await fetchItems(currentFolderId) @@ -315,7 +347,7 @@ const FileManager = () => { const handleDownload = async (item: FileItemType) => { try { - const blob = await fileManagementService.downloadFile(item.id) + const blob = await fileManagementService.downloadFile(item.id, selectedTenant?.id) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url @@ -456,7 +488,7 @@ const FileManager = () => { // Check if a folder contains files (for security purposes) const checkFolderHasFiles = async (folderId: string): Promise => { try { - const response = await fileManagementService.getItems(folderId) + const response = await fileManagementService.getItems(folderId, selectedTenant?.id) const items = response.data.items || [] // Check if folder contains any files (not just other folders) return items.some(item => item.type === 'file') @@ -608,7 +640,7 @@ const FileManager = () => { if (clipboard.operation === 'copy') { setLoading(true) try { - await fileManagementService.copyItems(itemIds, currentFolderId) + await fileManagementService.copyItems(itemIds, currentFolderId, selectedTenant?.id) await fetchItems(currentFolderId) toast.push( @@ -638,7 +670,7 @@ const FileManager = () => { setLoading(true) try { - await fileManagementService.moveItems(itemIds, currentFolderId) + await fileManagementService.moveItems(itemIds, currentFolderId, selectedTenant?.id) await fetchItems(currentFolderId) // Clipboard'ı temizle localStorage.removeItem('fileManager_clipboard') @@ -669,7 +701,7 @@ const FileManager = () => { } return ( - + { {/* Enhanced Unified Toolbar */}
+ + {/* Main Toolbar Row */}
{/* Left Section - Primary Actions */}
+ {/* Tenant Selector Row */} +
+ +