File Manamegement Tenant bazlı çalışması

This commit is contained in:
Sedat Öztürk 2026-03-11 02:07:07 +03:00
parent 6a06bf24c8
commit 96b61e7801
5 changed files with 241 additions and 146 deletions

View file

@ -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<string> ItemIds { get; set; } = new();
public string? TenantId { get; set; }
}
public class CopyItemsDto
@ -58,6 +70,8 @@ public class CopyItemsDto
public List<string> 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<string> ItemIds { get; set; } = new();
public string? TargetFolderId { get; set; }
public string? TenantId { get; set; }
}

View file

@ -9,33 +9,18 @@ namespace Sozsoft.Platform.FileManagement;
public interface IFileManagementAppService : IApplicationService
{
Task<GetFilesDto> GetItemsAsync();
Task<GetFilesDto> GetItemsByParentAsync(string parentId);
Task<GetFilesDto> GetItemsAsync(string? tenantId = null);
Task<GetFilesDto> GetItemsByParentAsync(string parentId, string? tenantId = null);
Task<FileItemDto> CreateFolderAsync(CreateFolderDto input);
Task<FileItemDto> UploadFileAsync(UploadFileDto input);
Task<FileItemDto> RenameItemAsync(string id, RenameItemDto input);
Task<FileItemDto> MoveItemAsync(string id, MoveItemDto input);
Task DeleteItemAsync(string id);
Task<Stream> DownloadFileAsync(string id);
Task<Stream> GetFilePreviewAsync(string id);
Task<Stream> DownloadFileAsync(string id, string? tenantId = null);
Task<Stream> GetFilePreviewAsync(string id, string? tenantId = null);
Task<GetFilesDto> SearchItemsAsync(SearchFilesDto input);
Task<FolderPathDto> GetFolderPathAsync();
Task<FolderPathDto> GetFolderPathByIdAsync(string folderId);
Task<FolderPathDto> GetFolderPathAsync(string? tenantId = null);
Task<FolderPathDto> GetFolderPathByIdAsync(string folderId, string? tenantId = null);
Task BulkDeleteItemsAsync(BulkDeleteDto input);
Task<List<FileItemDto>> CopyItemsAsync(CopyItemsDto input);
Task<List<FileItemDto>> MoveItemsAsync(MoveItemsDto input);
}

View file

@ -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<List<FileMetadata>> GetFolderIndexAsync(string? parentId = null)
private async Task<List<FileMetadata>> GetFolderIndexAsync(string? parentId, string tenantId)
{
return await GetRealCdnContentsAsync(parentId);
return await GetRealCdnContentsAsync(parentId, tenantId);
}
private async Task<List<FileMetadata>> GetRealCdnContentsAsync(string? folderPath)
private async Task<List<FileMetadata>> GetRealCdnContentsAsync(string? folderPath, string tenantId)
{
var items = new List<FileMetadata>();
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<FileMetadata> items, string? parentId = null)
private async Task SaveFolderIndexAsync(List<FileMetadata> 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<GetFilesDto> GetItemsAsync()
[HttpGet("api/app/file-management/items/{tenantId}")]
public async Task<GetFilesDto> GetItemsAsync(string? tenantId = null)
{
return await GetItemsInternalAsync(null);
return await GetItemsInternalAsync(null, GetEffectiveTenantId(tenantId));
}
public async Task<GetFilesDto> GetItemsByParentAsync(string parentId)
[HttpGet("api/app/file-management/items-by-parent")]
public async Task<GetFilesDto> 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<GetFilesDto> GetItemsInternalAsync(string? parentId)
private async Task<GetFilesDto> 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<FileItemDto> 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<FileItemDto> 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<FileItemDto> 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<FileItemDto> 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<string>();
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<List<FileItemDto>> 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<List<FileItemDto>> 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<Stream> DownloadFileAsync(string id)
[HttpPost("api/app/file-management/{id}/download-file/{tenantId}")]
public async Task<Stream> 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<Stream> GetFilePreviewAsync(string id)
[HttpGet("api/app/file-management/{id}/file-preview/{tenantId}")]
public async Task<Stream> 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<GetFilesDto> 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<FolderPathDto> GetFolderPathAsync(string? folderId = null)
[HttpGet("api/app/file-management/folder-path/{tenantId}")]
public async Task<FolderPathDto> 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<FileMetadata?> FindItemMetadataAsync(string id)
private async Task<FileMetadata?> 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<FileMetadata?> FindItemByPathAsync(string path)
private async Task<FileMetadata?> 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<List<FileMetadata>> GetAllItemsRecursivelyAsync(string? parentId = null, List<FileMetadata>? result = null)
private async Task<List<FileMetadata>> GetAllItemsRecursivelyAsync(string tenantId, string? parentId = null, List<FileMetadata>? 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<FolderPathDto> GetFolderPathAsync()
{
return await GetFolderPathInternalAsync(null);
}
public async Task<FolderPathDto> GetFolderPathByIdAsync(string folderId)
[HttpGet("api/app/file-management/folder-path-by-id")]
public async Task<FolderPathDto> GetFolderPathByIdAsync(string folderId, string? tenantId = null)
{
return await GetFolderPathInternalAsync(folderId);
}

View file

@ -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<string, string> = {}
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<FileItem>({
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<FileItem>({
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<FileItem>({
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<FileItem>({
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<void> {
async deleteItem(request: DeleteItemRequest, tenantId?: string): Promise<void> {
const effectiveTenantId = tenantId || 'host'
await ApiService.fetchData<void>({
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<Blob> {
async downloadFile(fileId: string, tenantId?: string): Promise<Blob> {
const effectiveTenantId = tenantId || 'host'
const response = await ApiService.fetchData<Blob>({
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<string> {
async getFilePreview(fileId: string, tenantId?: string): Promise<string> {
const effectiveTenantId = tenantId || 'host'
const response = await ApiService.fetchData<Blob>({
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<string, string> = {}
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<any>({
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<FileItem[]>({
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<FileItem[]>({
url: `/api/app/file-management/move-items`,
method: 'POST',
data: { itemIds, targetFolderId },
data: { itemIds, targetFolderId, tenantId },
})
}
}

View file

@ -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<FileItemType | undefined>()
const [itemsToDelete, setItemsToDelete] = useState<FileItemType[]>([])
// Tenant state
const [tenants, setTenants] = useState<TenantDto[]>([])
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(<Notification type="success">Files uploaded successfully</Notification>)
@ -256,7 +288,7 @@ const FileManager = () => {
await fileManagementService.createFolder({
name,
parentId: currentFolderId,
})
}, selectedTenant?.id)
await fetchItems(currentFolderId)
toast.push(<Notification type="success">Folder created successfully</Notification>)
} catch (error) {
@ -276,7 +308,7 @@ const FileManager = () => {
await fileManagementService.renameItem({
id: itemToRename.id,
newName,
})
}, selectedTenant?.id)
await fetchItems(currentFolderId)
toast.push(<Notification type="success">Item renamed successfully</Notification>)
} 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<boolean> => {
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(
<Notification title="Success" type="success">
@ -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 (
<Container className="px-3 sm:px-4 md:px-6">
<Container className="px-3">
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.Files')}
@ -678,10 +710,40 @@ const FileManager = () => {
{/* Enhanced Unified Toolbar */}
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-3 sm:p-4 mb-4 mt-2">
{/* Main Toolbar Row */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4">
{/* Left Section - Primary Actions */}
<div className="flex items-center gap-2 flex-wrap min-w-0">
{/* Tenant Selector Row */}
<div className="flex items-center gap-2">
<FaBuilding className="text-gray-500 flex-shrink-0" />
<Select
size="xs"
isLoading={tenantsLoading}
options={[
{ value: '', label: 'Host' },
...tenants.map((t) => ({ value: t.id ?? '', label: t.name ?? '' })),
]}
value={{
value: selectedTenant ? selectedTenant.id : '',
label: selectedTenant ? selectedTenant.name : 'Host',
}}
onChange={(option) => {
if (option && 'value' in option) {
const val = option.value as string
if (!val) {
setSelectedTenant(undefined)
} else {
const found = tenants.find((t) => t.id === val)
if (found) setSelectedTenant({ id: found.id!, name: found.name ?? '' })
}
}
}}
/>
</div>
{/* File Operations */}
{/* Navigation */}
<Button
@ -764,7 +826,6 @@ const FileManager = () => {
className="text-gray-600 hover:text-blue-600 flex-shrink-0"
title="Rename selected item"
>
Rename
</Button>
<Button
@ -795,7 +856,6 @@ const FileManager = () => {
className="text-gray-600 hover:text-green-600 flex-shrink-0"
title="Download selected file"
>
Download
</Button>
<Button