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 Name { get; set; } = string.Empty;
public string? ParentId { get; set; } public string? ParentId { get; set; }
public string? TenantId { get; set; }
} }
public class RenameItemDto public class RenameItemDto
@ -20,11 +22,15 @@ public class RenameItemDto
[Required] [Required]
[StringLength(255)] [StringLength(255)]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string? TenantId { get; set; }
} }
public class MoveItemDto public class MoveItemDto
{ {
public string? TargetFolderId { get; set; } public string? TargetFolderId { get; set; }
public string? TenantId { get; set; }
} }
public class UploadFileDto public class UploadFileDto
@ -34,6 +40,8 @@ public class UploadFileDto
public string? ParentId { get; set; } public string? ParentId { get; set; }
public string? TenantId { get; set; }
// NoteModal pattern - Files array // NoteModal pattern - Files array
public IRemoteStreamContent[]? Files { get; set; } public IRemoteStreamContent[]? Files { get; set; }
} }
@ -44,12 +52,16 @@ public class SearchFilesDto
public string Query { get; set; } = string.Empty; public string Query { get; set; } = string.Empty;
public string? ParentId { get; set; } public string? ParentId { get; set; }
public string? TenantId { get; set; }
} }
public class BulkDeleteDto public class BulkDeleteDto
{ {
[Required] [Required]
public List<string> ItemIds { get; set; } = new(); public List<string> ItemIds { get; set; } = new();
public string? TenantId { get; set; }
} }
public class CopyItemsDto public class CopyItemsDto
@ -58,6 +70,8 @@ public class CopyItemsDto
public List<string> ItemIds { get; set; } = new(); public List<string> ItemIds { get; set; } = new();
public string? TargetFolderId { get; set; } public string? TargetFolderId { get; set; }
public string? TenantId { get; set; }
} }
public class MoveItemsDto public class MoveItemsDto
@ -66,4 +80,6 @@ public class MoveItemsDto
public List<string> ItemIds { get; set; } = new(); public List<string> ItemIds { get; set; } = new();
public string? TargetFolderId { get; set; } public string? TargetFolderId { get; set; }
public string? TenantId { get; set; }
} }

View file

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

View file

@ -14,9 +14,11 @@ using Microsoft.Extensions.Logging;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Microsoft.AspNetCore.Authorization;
namespace Sozsoft.Platform.FileManagement; namespace Sozsoft.Platform.FileManagement;
[Authorize]
public class FileManagementAppService : ApplicationService, IFileManagementAppService public class FileManagementAppService : ApplicationService, IFileManagementAppService
{ {
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
@ -47,11 +49,10 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
_configuration = configuration; _configuration = configuration;
} }
private string GetTenantPrefix() private string GetTenantPrefix(string tenantId) => $"tenants/{tenantId}/";
{
var tenantId = _currentTenant.Id?.ToString() ?? "host"; private string GetEffectiveTenantId(string? inputTenantId) =>
return $"tenants/{tenantId}/"; !string.IsNullOrEmpty(inputTenantId) ? inputTenantId : (_currentTenant.Id?.ToString() ?? "host");
}
private string EncodePathAsId(string path) private string EncodePathAsId(string path)
{ {
@ -95,12 +96,12 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Logger.LogInformation($"Folder {decodedPath} is not protected, allowing {operation}"); 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 items = new List<FileMetadata>();
var cdnBasePath = _configuration["App:CdnPath"]; var cdnBasePath = _configuration["App:CdnPath"];
@ -111,7 +112,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return items; return items;
} }
var tenantId = _currentTenant.Id?.ToString() ?? "host";
var fullPath = Path.Combine(cdnBasePath, tenantId); var fullPath = Path.Combine(cdnBasePath, tenantId);
if (!string.IsNullOrEmpty(folderPath)) if (!string.IsNullOrEmpty(folderPath))
@ -158,7 +158,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = relativePath, Path = relativePath,
ParentId = folderPath ?? "", ParentId = folderPath ?? "",
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString(), TenantId = tenantId == "host" ? null : tenantId,
ChildCount = childCount ChildCount = childCount
}); });
} }
@ -181,7 +181,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = relativePath, Path = relativePath,
ParentId = folderPath ?? "", ParentId = folderPath ?? "",
IsReadOnly = false, 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 var indexJson = JsonSerializer.Serialize(items, new JsonSerializerOptions
{ {
@ -207,20 +207,23 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
await _blobContainer.SaveAsync(indexPath, indexBytes); 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); 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 => var result = items.Select(metadata =>
{ {
@ -249,6 +252,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return new GetFilesDto { Items = result }; return new GetFilesDto { Items = result };
} }
[HttpPost("api/app/file-management/folder")]
public async Task<FileItemDto> CreateFolderAsync(CreateFolderDto input) public async Task<FileItemDto> CreateFolderAsync(CreateFolderDto input)
{ {
ValidateFileName(input.Name); ValidateFileName(input.Name);
@ -259,7 +263,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured"); 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); var parentPath = Path.Combine(cdnBasePath, tenantId);
string? decodedParentId = null; string? decodedParentId = null;
@ -298,7 +302,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = newFolderPath, Path = newFolderPath,
ParentId = decodedParentId ?? string.Empty, ParentId = decodedParentId ?? string.Empty,
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString() TenantId = tenantId == "host" ? null : tenantId
}; };
return new FileItemDto return new FileItemDto
@ -311,14 +315,17 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = metadata.Path, Path = metadata.Path,
ParentId = input.ParentId ?? string.Empty, ParentId = input.ParentId ?? string.Empty,
IsReadOnly = metadata.IsReadOnly, 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) public async Task<FileItemDto> UploadFileAsync([FromForm] UploadFileDto input)
{ {
ValidateFileName(input.FileName); ValidateFileName(input.FileName);
var tenantId = GetEffectiveTenantId(input.TenantId);
// Decode parent ID if provided // Decode parent ID if provided
string? decodedParentId = null; string? decodedParentId = null;
if (!string.IsNullOrEmpty(input.ParentId)) if (!string.IsNullOrEmpty(input.ParentId))
@ -326,7 +333,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
decodedParentId = DecodeIdAsPath(input.ParentId); decodedParentId = DecodeIdAsPath(input.ParentId);
} }
var items = await GetFolderIndexAsync(decodedParentId); var items = await GetFolderIndexAsync(decodedParentId, tenantId);
// Generate unique filename if file already exists // Generate unique filename if file already exists
var uniqueFileName = GetUniqueFileName(items, input.FileName); var uniqueFileName = GetUniqueFileName(items, input.FileName);
@ -344,7 +351,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured"); throw new UserFriendlyException("CDN path is not configured");
} }
var tenantId = _currentTenant.Id?.ToString() ?? "host";
var fullCdnPath = Path.Combine(cdnBasePath, tenantId); var fullCdnPath = Path.Combine(cdnBasePath, tenantId);
if (!string.IsNullOrEmpty(decodedParentId)) if (!string.IsNullOrEmpty(decodedParentId))
@ -387,7 +393,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = filePath, Path = filePath,
ParentId = decodedParentId ?? string.Empty, ParentId = decodedParentId ?? string.Empty,
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString() TenantId = tenantId == "host" ? null : tenantId
}; };
// File system'e kaydedildi, index güncellemeye gerek yok // File system'e kaydedildi, index güncellemeye gerek yok
@ -404,10 +410,11 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = metadata.Path, Path = metadata.Path,
ParentId = input.ParentId ?? string.Empty, ParentId = input.ParentId ?? string.Empty,
IsReadOnly = metadata.IsReadOnly, 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) public async Task<FileItemDto> RenameItemAsync(string id, RenameItemDto input)
{ {
// Check if this is a protected system folder // Check if this is a protected system folder
@ -415,24 +422,26 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
ValidateFileName(input.Name); ValidateFileName(input.Name);
var metadata = await FindItemMetadataAsync(id); var tenantId = GetEffectiveTenantId(input.TenantId);
var metadata = await FindItemMetadataAsync(id, tenantId);
if (metadata == null) if (metadata == null)
{ {
throw new UserFriendlyException("Item not found"); 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))) if (parentItems.Any(x => x.Id != id && x.Name.Equals(input.Name, StringComparison.OrdinalIgnoreCase)))
{ {
throw new UserFriendlyException("An item with this name already exists"); 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) var newPath = string.IsNullOrEmpty(metadata.ParentId)
? input.Name ? input.Name
: $"{metadata.ParentId}/{input.Name}"; : $"{metadata.ParentId}/{input.Name}";
var newBlobPath = GetTenantPrefix() + newPath; var newBlobPath = GetTenantPrefix(tenantId) + newPath;
if (metadata.Type == "folder") if (metadata.Type == "folder")
{ {
@ -457,7 +466,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
itemToUpdate.Path = newPath; itemToUpdate.Path = newPath;
itemToUpdate.ModifiedAt = DateTime.UtcNow; 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 return new FileItemDto
{ {
@ -472,19 +481,22 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = metadata.Path, Path = metadata.Path,
ParentId = metadata.ParentId, ParentId = metadata.ParentId,
IsReadOnly = metadata.IsReadOnly, 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) 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) if (metadata == null)
{ {
throw new UserFriendlyException("Item not found"); 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))) if (targetItems.Any(x => x.Name.Equals(metadata.Name, StringComparison.OrdinalIgnoreCase)))
{ {
@ -492,16 +504,16 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
} }
// Remove from source // 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); 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 // Move blob
var oldBlobPath = GetTenantPrefix() + metadata.Path; var oldBlobPath = GetTenantPrefix(tenantId) + metadata.Path;
var newPath = string.IsNullOrEmpty(input.TargetFolderId) var newPath = string.IsNullOrEmpty(input.TargetFolderId)
? metadata.Name ? metadata.Name
: $"{input.TargetFolderId}/{metadata.Name}"; : $"{input.TargetFolderId}/{metadata.Name}";
var newBlobPath = GetTenantPrefix() + newPath; var newBlobPath = GetTenantPrefix(tenantId) + newPath;
if (metadata.Type == "folder") if (metadata.Type == "folder")
{ {
@ -520,7 +532,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
// Add to target // Add to target
targetItems.Add(metadata); targetItems.Add(metadata);
await SaveFolderIndexAsync(targetItems, input.TargetFolderId); await SaveFolderIndexAsync(targetItems, tenantId, input.TargetFolderId);
return new FileItemDto return new FileItemDto
{ {
@ -535,11 +547,12 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = metadata.Path, Path = metadata.Path,
ParentId = metadata.ParentId, ParentId = metadata.ParentId,
IsReadOnly = metadata.IsReadOnly, 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 // Check if this is a protected system folder
ValidateNotProtectedFolder(id, "delete"); ValidateNotProtectedFolder(id, "delete");
@ -550,9 +563,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured"); throw new UserFriendlyException("CDN path is not configured");
} }
var tenantId = _currentTenant.Id?.ToString() ?? "host"; var effectiveTenantId = GetEffectiveTenantId(tenantId);
var actualPath = DecodeIdAsPath(id); var actualPath = DecodeIdAsPath(id);
var fullPath = Path.Combine(cdnBasePath, tenantId, actualPath); var fullPath = Path.Combine(cdnBasePath, effectiveTenantId, actualPath);
if (Directory.Exists(fullPath)) 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) public async Task BulkDeleteItemsAsync(BulkDeleteDto input)
{ {
if (input.ItemIds == null || !input.ItemIds.Any()) if (input.ItemIds == null || !input.ItemIds.Any())
@ -583,7 +597,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured"); throw new UserFriendlyException("CDN path is not configured");
} }
var tenantId = _currentTenant.Id?.ToString() ?? "host"; var tenantId = GetEffectiveTenantId(input.TenantId);
var errors = new List<string>(); var errors = new List<string>();
foreach (var itemId in input.ItemIds) 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) public async Task<List<FileItemDto>> CopyItemsAsync(CopyItemsDto input)
{ {
if (input.ItemIds == null || !input.ItemIds.Any()) if (input.ItemIds == null || !input.ItemIds.Any())
@ -636,7 +651,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured"); 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); var basePath = Path.Combine(cdnBasePath, tenantId);
string? targetPath = null; string? targetPath = null;
@ -681,7 +696,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = finalTargetPath, Path = finalTargetPath,
ParentId = input.TargetFolderId ?? string.Empty, ParentId = input.TargetFolderId ?? string.Empty,
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString() TenantId = tenantId == "host" ? null : tenantId
}); });
} }
else if (File.Exists(sourceFullPath)) else if (File.Exists(sourceFullPath))
@ -711,7 +726,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = finalTargetPath, Path = finalTargetPath,
ParentId = input.TargetFolderId ?? string.Empty, ParentId = input.TargetFolderId ?? string.Empty,
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString() TenantId = tenantId == "host" ? null : tenantId
}); });
} }
else else
@ -733,6 +748,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return copiedItems; return copiedItems;
} }
[HttpPost("api/app/file-management/move-items")]
public async Task<List<FileItemDto>> MoveItemsAsync(MoveItemsDto input) public async Task<List<FileItemDto>> MoveItemsAsync(MoveItemsDto input)
{ {
if (input.ItemIds == null || !input.ItemIds.Any()) if (input.ItemIds == null || !input.ItemIds.Any())
@ -746,7 +762,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured"); 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); var basePath = Path.Combine(cdnBasePath, tenantId);
string? targetPath = null; string? targetPath = null;
@ -808,7 +824,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = finalTargetPath, Path = finalTargetPath,
ParentId = input.TargetFolderId ?? string.Empty, ParentId = input.TargetFolderId ?? string.Empty,
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString() TenantId = tenantId == "host" ? null : tenantId
}); });
} }
else if (File.Exists(sourceFullPath)) else if (File.Exists(sourceFullPath))
@ -838,7 +854,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = finalTargetPath, Path = finalTargetPath,
ParentId = input.TargetFolderId ?? string.Empty, ParentId = input.TargetFolderId ?? string.Empty,
IsReadOnly = false, IsReadOnly = false,
TenantId = _currentTenant.Id?.ToString() TenantId = tenantId == "host" ? null : tenantId
}); });
} }
else else
@ -860,7 +876,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return movedItems; 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"]; var cdnBasePath = _configuration["App:CdnPath"];
if (string.IsNullOrEmpty(cdnBasePath)) if (string.IsNullOrEmpty(cdnBasePath))
@ -868,9 +885,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured"); throw new UserFriendlyException("CDN path is not configured");
} }
var tenantId = _currentTenant.Id?.ToString() ?? "host"; var effectiveTenantId = GetEffectiveTenantId(tenantId);
var actualPath = DecodeIdAsPath(id); var actualPath = DecodeIdAsPath(id);
var fullFilePath = Path.Combine(cdnBasePath, tenantId, actualPath); var fullFilePath = Path.Combine(cdnBasePath, effectiveTenantId, actualPath);
if (!File.Exists(fullFilePath)) if (!File.Exists(fullFilePath))
{ {
@ -880,14 +897,17 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return File.OpenRead(fullFilePath); 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) 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 query = input.Query.ToLowerInvariant();
var filteredItems = allItems var filteredItems = allItems
@ -905,7 +925,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = metadata.Path, Path = metadata.Path,
ParentId = metadata.ParentId, ParentId = metadata.ParentId,
IsReadOnly = metadata.IsReadOnly, IsReadOnly = metadata.IsReadOnly,
TenantId = _currentTenant.Id?.ToString() TenantId = tenantId == "host" ? null : tenantId
}) })
.OrderBy(x => x.Type == "folder" ? 0 : 1) .OrderBy(x => x.Type == "folder" ? 0 : 1)
.ThenBy(x => x.Name) .ThenBy(x => x.Name)
@ -914,9 +934,10 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return new GetFilesDto { Items = filteredItems }; 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 #region Private Helper Methods
@ -944,30 +965,28 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return uniqueName; 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 var allItems = await GetAllItemsRecursivelyAsync(tenantId);
// In a real scenario, you might want to use a database index or search service
var allItems = await GetAllItemsRecursivelyAsync();
return allItems.FirstOrDefault(x => x.Id == id); 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)); 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 ??= []; result ??= [];
var items = await GetFolderIndexAsync(parentId); var items = await GetFolderIndexAsync(parentId, tenantId);
result.AddRange(items); result.AddRange(items);
foreach (var folder in items.Where(x => x.Type == "folder")) foreach (var folder in items.Where(x => x.Type == "folder"))
{ {
await GetAllItemsRecursivelyAsync(folder.Id, result); await GetAllItemsRecursivelyAsync(tenantId, folder.Id, result);
} }
return result; return result;
@ -1029,12 +1048,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}; };
} }
public async Task<FolderPathDto> GetFolderPathAsync() [HttpGet("api/app/file-management/folder-path-by-id")]
{ public async Task<FolderPathDto> GetFolderPathByIdAsync(string folderId, string? tenantId = null)
return await GetFolderPathInternalAsync(null);
}
public async Task<FolderPathDto> GetFolderPathByIdAsync(string folderId)
{ {
return await GetFolderPathInternalAsync(folderId); return await GetFolderPathInternalAsync(folderId);
} }

View file

@ -10,33 +10,42 @@ import type {
class FileManagementService { class FileManagementService {
// Get files and folders for a specific directory // Get files and folders for a specific directory
async getItems(parentId?: string): Promise<{ data: { items: FileItem[] } }> { async getItems(parentId?: string, tenantId?: string): Promise<{ data: { items: FileItem[] } }> {
const effectiveTenantId = tenantId || 'host'
const url = parentId const url = parentId
? `/api/app/file-management/items-by-parent/${parentId}` ? `/api/app/file-management/items-by-parent`
: `/api/app/file-management/items` : `/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[] }>({ return ApiService.fetchData<{ items: FileItem[] }>({
url, url,
method: 'GET', method: 'GET',
params: Object.keys(params).length ? params : undefined,
}) })
} }
// Create a new folder // Create a new folder
async createFolder(request: CreateFolderRequest): Promise<{ data: FileItem }> { async createFolder(request: CreateFolderRequest, tenantId?: string): Promise<{ data: FileItem }> {
return ApiService.fetchData<FileItem>({ return ApiService.fetchData<FileItem>({
url: `/api/app/file-management/folder`, url: `/api/app/file-management/folder`,
method: 'POST', method: 'POST',
data: request as any, data: { ...request, tenantId } as any,
}) })
} }
// Upload a file (DTO pattern) // 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() const formData = new FormData()
formData.append('fileName', request.fileName) formData.append('fileName', request.fileName)
if (request.parentId) { if (request.parentId) {
formData.append('parentId', request.parentId) formData.append('parentId', request.parentId)
} }
if (tenantId) {
formData.append('tenantId', tenantId)
}
// NoteModal pattern - Files array // NoteModal pattern - Files array
request.files.forEach(file => { request.files.forEach(file => {
@ -47,16 +56,16 @@ class FileManagementService {
url: `/api/app/file-management/upload-file`, url: `/api/app/file-management/upload-file`,
method: 'POST', method: 'POST',
data: formData as any, data: formData as any,
// Browser otomatik olarak Content-Type'ı multipart/form-data boundary ile set eder
}) })
} }
// Upload a file directly with FormData (NoteModal pattern) // 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 { try {
console.log('Uploading file directly with FormData') console.log('Uploading file directly with FormData')
if (tenantId) {
// ABP convention-based routing: UploadFileAsync -> upload-file (Async suffix kaldırılır) formData.append('tenantId', tenantId)
}
return await ApiService.fetchData<FileItem>({ return await ApiService.fetchData<FileItem>({
url: 'api/app/file-management/upload-file', url: 'api/app/file-management/upload-file',
method: 'POST', method: 'POST',
@ -69,35 +78,37 @@ class FileManagementService {
} }
// Rename a file or folder // 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>({ return ApiService.fetchData<FileItem>({
url: `/api/app/file-management/${request.id}/rename-item`, url: `/api/app/file-management/${request.id}/rename-item`,
method: 'POST', method: 'POST',
data: { name: request.newName }, data: { name: request.newName, tenantId },
}) })
} }
// Move a file or folder // 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>({ return ApiService.fetchData<FileItem>({
url: `/api/app/file-management/${request.itemId}/move-item`, url: `/api/app/file-management/${request.itemId}/move-item`,
method: 'POST', method: 'POST',
data: { targetFolderId: request.targetFolderId }, data: { targetFolderId: request.targetFolderId, tenantId },
}) })
} }
// Delete a file or folder // 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>({ await ApiService.fetchData<void>({
url: `/api/app/file-management/${request.id}/item`, url: `/api/app/file-management/${request.id}/item/${effectiveTenantId}`,
method: 'DELETE', method: 'DELETE',
}) })
} }
// Download a file // 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>({ 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', method: 'POST',
responseType: 'blob', responseType: 'blob',
}) })
@ -105,9 +116,10 @@ class FileManagementService {
} }
// Get file preview/thumbnail // 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>({ 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', method: 'GET',
responseType: 'blob', responseType: 'blob',
}) })
@ -115,10 +127,11 @@ class FileManagementService {
} }
// Search files and folders // 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 = { const data = {
query: query, query,
...(folderId && { parentId: folderId }), ...(folderId && { parentId: folderId }),
...(tenantId && { tenantId }),
} }
return ApiService.fetchData<{ items: FileItem[] }>({ return ApiService.fetchData<{ items: FileItem[] }>({
@ -129,41 +142,47 @@ class FileManagementService {
} }
// Get folder breadcrumb path // Get folder breadcrumb path
async getFolderPath(folderId?: string): Promise<{ data: { path: Array<{ id: string; name: string }> } }> { async getFolderPath(folderId?: string, tenantId?: string): Promise<{ data: { path: Array<{ id: string; name: string }> } }> {
const effectiveTenantId = tenantId || 'host'
const url = folderId const url = folderId
? `/api/app/file-management/folder-path-by-id/${folderId}` ? `/api/app/file-management/folder-path-by-id`
: `/api/app/file-management/folder-path` : `/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 }> }>({ return ApiService.fetchData<{ path: Array<{ id: string; name: string }> }>({
url, url,
method: 'GET', method: 'GET',
params: Object.keys(params).length ? params : undefined,
}) })
} }
// Bulk delete items // Bulk delete items
async bulkDeleteItems(itemIds: string[]): Promise<{ data: any }> { async bulkDeleteItems(itemIds: string[], tenantId?: string): Promise<{ data: any }> {
return ApiService.fetchData<any>({ return ApiService.fetchData<any>({
url: `/api/app/file-management/bulk-delete-items`, url: `/api/app/file-management/bulk-delete-items`,
method: 'POST', method: 'POST',
data: { itemIds }, data: { itemIds, tenantId },
}) })
} }
// Copy items to target folder // 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[]>({ return ApiService.fetchData<FileItem[]>({
url: `/api/app/file-management/copy-items`, url: `/api/app/file-management/copy-items`,
method: 'POST', method: 'POST',
data: { itemIds, targetFolderId }, data: { itemIds, targetFolderId, tenantId },
}) })
} }
// Move items to target folder // 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[]>({ return ApiService.fetchData<FileItem[]>({
url: `/api/app/file-management/move-items`, url: `/api/app/file-management/move-items`,
method: 'POST', method: 'POST',
data: { itemIds, targetFolderId }, data: { itemIds, targetFolderId, tenantId },
}) })
} }
} }

View file

@ -16,10 +16,13 @@ import {
FaEdit, FaEdit,
FaDownload, FaDownload,
FaPaste, FaPaste,
FaBuilding,
} from 'react-icons/fa' } from 'react-icons/fa'
import Container from '@/components/shared/Container' import Container from '@/components/shared/Container'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import fileManagementService from '@/services/fileManagement.service' 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 Breadcrumb from './components/Breadcrumb'
import FileItem from './components/FileItem' import FileItem from './components/FileItem'
import FileUploadModal from './components/FileUploadModal' import FileUploadModal from './components/FileUploadModal'
@ -64,17 +67,46 @@ const FileManager = () => {
const [itemToRename, setItemToRename] = useState<FileItemType | undefined>() const [itemToRename, setItemToRename] = useState<FileItemType | undefined>()
const [itemsToDelete, setItemsToDelete] = useState<FileItemType[]>([]) 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 // Loading states
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [creating, setCreating] = useState(false) const [creating, setCreating] = useState(false)
const [renaming, setRenaming] = useState(false) const [renaming, setRenaming] = useState(false)
const [deleting, setDeleting] = 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 // Fetch items from API
const fetchItems = useCallback(async (folderId?: string) => { const fetchItems = useCallback(async (folderId?: string) => {
try { try {
setLoading(true) setLoading(true)
const response = await fileManagementService.getItems(folderId) const response = await fileManagementService.getItems(folderId, selectedTenant?.id)
// Backend returns GetFilesDto which has Items property // Backend returns GetFilesDto which has Items property
const items = response.data.items || [] const items = response.data.items || []
// Manual protection for system folders // Manual protection for system folders
@ -107,7 +139,7 @@ const FileManager = () => {
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, []) }, [selectedTenant])
// Fetch breadcrumb path // Fetch breadcrumb path
const fetchBreadcrumb = useCallback(async (folderId?: string) => { const fetchBreadcrumb = useCallback(async (folderId?: string) => {
@ -117,7 +149,7 @@ const FileManager = () => {
return return
} }
const response = await fileManagementService.getFolderPath(folderId) const response = await fileManagementService.getFolderPath(folderId, selectedTenant?.id)
// console.log('Breadcrumb response for folderId:', folderId, response) // console.log('Breadcrumb response for folderId:', folderId, response)
const pathItems: BreadcrumbItem[] = [ const pathItems: BreadcrumbItem[] = [
{ name: 'Files', path: '', id: undefined }, { name: 'Files', path: '', id: undefined },
@ -131,7 +163,7 @@ const FileManager = () => {
} catch (error) { } catch (error) {
console.error('Failed to fetch breadcrumb:', error) console.error('Failed to fetch breadcrumb:', error)
} }
}, []) }, [selectedTenant])
// Initial load // Initial load
useEffect(() => { useEffect(() => {
@ -237,7 +269,7 @@ const FileManager = () => {
formData.append('parentId', currentFolderId) formData.append('parentId', currentFolderId)
} }
await fileManagementService.uploadFileDirectly(formData) await fileManagementService.uploadFileDirectly(formData, selectedTenant?.id)
} }
await fetchItems(currentFolderId) await fetchItems(currentFolderId)
toast.push(<Notification type="success">Files uploaded successfully</Notification>) toast.push(<Notification type="success">Files uploaded successfully</Notification>)
@ -256,7 +288,7 @@ const FileManager = () => {
await fileManagementService.createFolder({ await fileManagementService.createFolder({
name, name,
parentId: currentFolderId, parentId: currentFolderId,
}) }, selectedTenant?.id)
await fetchItems(currentFolderId) await fetchItems(currentFolderId)
toast.push(<Notification type="success">Folder created successfully</Notification>) toast.push(<Notification type="success">Folder created successfully</Notification>)
} catch (error) { } catch (error) {
@ -276,7 +308,7 @@ const FileManager = () => {
await fileManagementService.renameItem({ await fileManagementService.renameItem({
id: itemToRename.id, id: itemToRename.id,
newName, newName,
}) }, selectedTenant?.id)
await fetchItems(currentFolderId) await fetchItems(currentFolderId)
toast.push(<Notification type="success">Item renamed successfully</Notification>) toast.push(<Notification type="success">Item renamed successfully</Notification>)
} catch (error) { } catch (error) {
@ -294,11 +326,11 @@ const FileManager = () => {
if (itemsToDelete.length === 1) { if (itemsToDelete.length === 1) {
// Single item delete - use existing API // Single item delete - use existing API
await fileManagementService.deleteItem({ id: itemsToDelete[0].id }) await fileManagementService.deleteItem({ id: itemsToDelete[0].id }, selectedTenant?.id)
} else { } else {
// Multiple items - use bulk delete API // Multiple items - use bulk delete API
const itemIds = itemsToDelete.map((item) => item.id) const itemIds = itemsToDelete.map((item) => item.id)
await fileManagementService.bulkDeleteItems(itemIds) await fileManagementService.bulkDeleteItems(itemIds, selectedTenant?.id)
} }
await fetchItems(currentFolderId) await fetchItems(currentFolderId)
@ -315,7 +347,7 @@ const FileManager = () => {
const handleDownload = async (item: FileItemType) => { const handleDownload = async (item: FileItemType) => {
try { try {
const blob = await fileManagementService.downloadFile(item.id) const blob = await fileManagementService.downloadFile(item.id, selectedTenant?.id)
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url
@ -456,7 +488,7 @@ const FileManager = () => {
// Check if a folder contains files (for security purposes) // Check if a folder contains files (for security purposes)
const checkFolderHasFiles = async (folderId: string): Promise<boolean> => { const checkFolderHasFiles = async (folderId: string): Promise<boolean> => {
try { try {
const response = await fileManagementService.getItems(folderId) const response = await fileManagementService.getItems(folderId, selectedTenant?.id)
const items = response.data.items || [] const items = response.data.items || []
// Check if folder contains any files (not just other folders) // Check if folder contains any files (not just other folders)
return items.some(item => item.type === 'file') return items.some(item => item.type === 'file')
@ -608,7 +640,7 @@ const FileManager = () => {
if (clipboard.operation === 'copy') { if (clipboard.operation === 'copy') {
setLoading(true) setLoading(true)
try { try {
await fileManagementService.copyItems(itemIds, currentFolderId) await fileManagementService.copyItems(itemIds, currentFolderId, selectedTenant?.id)
await fetchItems(currentFolderId) await fetchItems(currentFolderId)
toast.push( toast.push(
<Notification title="Success" type="success"> <Notification title="Success" type="success">
@ -638,7 +670,7 @@ const FileManager = () => {
setLoading(true) setLoading(true)
try { try {
await fileManagementService.moveItems(itemIds, currentFolderId) await fileManagementService.moveItems(itemIds, currentFolderId, selectedTenant?.id)
await fetchItems(currentFolderId) await fetchItems(currentFolderId)
// Clipboard'ı temizle // Clipboard'ı temizle
localStorage.removeItem('fileManager_clipboard') localStorage.removeItem('fileManager_clipboard')
@ -669,7 +701,7 @@ const FileManager = () => {
} }
return ( return (
<Container className="px-3 sm:px-4 md:px-6"> <Container className="px-3">
<Helmet <Helmet
titleTemplate={`%s | ${APP_NAME}`} titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.Files')} title={translate('::' + 'App.Files')}
@ -678,10 +710,40 @@ const FileManager = () => {
{/* Enhanced Unified Toolbar */} {/* 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"> <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 */} {/* Main Toolbar Row */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4"> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4">
{/* Left Section - Primary Actions */} {/* Left Section - Primary Actions */}
<div className="flex items-center gap-2 flex-wrap min-w-0"> <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 */} {/* File Operations */}
{/* Navigation */} {/* Navigation */}
<Button <Button
@ -764,7 +826,6 @@ const FileManager = () => {
className="text-gray-600 hover:text-blue-600 flex-shrink-0" className="text-gray-600 hover:text-blue-600 flex-shrink-0"
title="Rename selected item" title="Rename selected item"
> >
Rename
</Button> </Button>
<Button <Button
@ -795,7 +856,6 @@ const FileManager = () => {
className="text-gray-600 hover:text-green-600 flex-shrink-0" className="text-gray-600 hover:text-green-600 flex-shrink-0"
title="Download selected file" title="Download selected file"
> >
Download
</Button> </Button>
<Button <Button