diff --git a/api/src/Kurs.Platform.Application.Contracts/FileManagement/FileItemDto.cs b/api/src/Kurs.Platform.Application.Contracts/FileManagement/FileItemDto.cs new file mode 100644 index 00000000..cd57a91d --- /dev/null +++ b/api/src/Kurs.Platform.Application.Contracts/FileManagement/FileItemDto.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace Kurs.Platform.FileManagement; + +public class FileItemDto +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // "file" or "folder" + public long? Size { get; set; } + public string Extension { get; set; } = string.Empty; + public string MimeType { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public DateTime ModifiedAt { get; set; } + public string Path { get; set; } = string.Empty; + public string ParentId { get; set; } = string.Empty; + public bool IsReadOnly { get; set; } +} + +public class GetFilesDto +{ + public List Items { get; set; } = new(); +} + +public class FolderPathDto +{ + public List Path { get; set; } = new(); +} + +public class PathItemDto +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application.Contracts/FileManagement/FileManagementDtos.cs b/api/src/Kurs.Platform.Application.Contracts/FileManagement/FileManagementDtos.cs new file mode 100644 index 00000000..c5b5eb2e --- /dev/null +++ b/api/src/Kurs.Platform.Application.Contracts/FileManagement/FileManagementDtos.cs @@ -0,0 +1,48 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.IO; + +namespace Kurs.Platform.FileManagement; + +public class CreateFolderDto +{ + [Required] + [StringLength(255)] + public string Name { get; set; } = string.Empty; + + public string? ParentId { get; set; } +} + +public class RenameItemDto +{ + [Required] + [StringLength(255)] + public string Name { get; set; } = string.Empty; +} + +public class MoveItemDto +{ + public string? TargetFolderId { get; set; } +} + +public class UploadFileDto +{ + [Required] + public string FileName { get; set; } = string.Empty; + + // Either FileStream or FileContent can be used + public Stream? FileStream { get; set; } + + public byte[]? FileContent { get; set; } + + public string? ParentId { get; set; } +} + +public class SearchFilesDto +{ + [Required] + public string Query { get; set; } = string.Empty; + + public string? ParentId { get; set; } +} \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application.Contracts/FileManagement/IFileManagementAppService.cs b/api/src/Kurs.Platform.Application.Contracts/FileManagement/IFileManagementAppService.cs new file mode 100644 index 00000000..74bfc42a --- /dev/null +++ b/api/src/Kurs.Platform.Application.Contracts/FileManagement/IFileManagementAppService.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System.IO; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Kurs.Platform.FileManagement; + +public interface IFileManagementAppService : IApplicationService +{ + Task GetItemsAsync(); + + Task GetItemsByParentAsync(string parentId); + + 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 SearchItemsAsync(SearchFilesDto input); + + Task GetFolderPathAsync(); + + Task GetFolderPathByIdAsync(string folderId); +} \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application/FileManagement/FileManagementAppService.cs b/api/src/Kurs.Platform.Application/FileManagement/FileManagementAppService.cs new file mode 100644 index 00000000..4ccd3307 --- /dev/null +++ b/api/src/Kurs.Platform.Application/FileManagement/FileManagementAppService.cs @@ -0,0 +1,666 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Kurs.Platform.BlobStoring; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.MultiTenancy; + +namespace Kurs.Platform.FileManagement; + +public class FileManagementAppService : ApplicationService, IFileManagementAppService +{ + private readonly ICurrentTenant _currentTenant; + private readonly BlobManager _blobContainer; + + private const string FileMetadataSuffix = ".metadata.json"; + private const string FolderMarkerSuffix = ".folder"; + private const string IndexFileName = "index.json"; + + public FileManagementAppService( + ICurrentTenant currentTenant, + BlobManager blobContainer + ) + { + _currentTenant = currentTenant; + _blobContainer = blobContainer; + } + + private string GetTenantPrefix() + { + var tenantId = _currentTenant.Id?.ToString() ?? "host"; + return $"files/{tenantId}/"; + } + + private string GenerateFileId() + { + return Guid.NewGuid().ToString("N"); + } + + private async Task> GetFolderIndexAsync(string? parentId = null) + { + // Root seviyesinde default klasörleri göster + if (string.IsNullOrEmpty(parentId)) + { + var defaultFolders = new List + { + // Default system folders + new() { + Id = BlobContainerNames.Avatar, + Name = "Avatar", + Type = "folder", + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow, + Path = BlobContainerNames.Avatar, + ParentId = "", + IsReadOnly = false, + TenantId = _currentTenant.Id?.ToString() + }, + new() { + Id = BlobContainerNames.Import, + Name = "Import", + Type = "folder", + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow, + Path = BlobContainerNames.Import, + ParentId = "", + IsReadOnly = false, + TenantId = _currentTenant.Id?.ToString() + }, + new() { + Id = BlobContainerNames.Activity, + Name = "Activity", + Type = "folder", + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow, + Path = BlobContainerNames.Activity, + ParentId = "", + IsReadOnly = false, + TenantId = _currentTenant.Id?.ToString() + } + }; + + // Custom folders from index + var customFolders = await GetCustomFoldersAsync(); + defaultFolders.AddRange(customFolders); + + return defaultFolders; + } + + // Alt klasörlerin indexini oku + var indexPath = GetTenantPrefix() + $"{parentId}/{IndexFileName}"; + + try + { + var indexBytes = await _blobContainer.GetAllBytesOrNullAsync(indexPath); + if (indexBytes == null) return new List(); + + var indexJson = Encoding.UTF8.GetString(indexBytes); + return JsonSerializer.Deserialize>(indexJson) ?? new List(); + } + catch + { + return new List(); + } + } + + private async Task> GetCustomFoldersAsync() + { + var indexPath = GetTenantPrefix() + IndexFileName; + + try + { + var indexBytes = await _blobContainer.GetAllBytesOrNullAsync(indexPath); + if (indexBytes == null) return new List(); + + var indexJson = Encoding.UTF8.GetString(indexBytes); + return JsonSerializer.Deserialize>(indexJson) ?? new List(); + } + catch + { + return new List(); + } + } + + private async Task SaveFolderIndexAsync(List items, string? parentId = null) + { + var indexPath = GetTenantPrefix() + (string.IsNullOrEmpty(parentId) ? IndexFileName : $"{parentId}/{IndexFileName}"); + + var indexJson = JsonSerializer.Serialize(items, new JsonSerializerOptions + { + WriteIndented = true + }); + var indexBytes = Encoding.UTF8.GetBytes(indexJson); + + await _blobContainer.SaveAsync(indexPath, indexBytes); + } + + public async Task GetItemsAsync() + { + return await GetItemsInternalAsync(null); + } + + public async Task GetItemsByParentAsync(string parentId) + { + return await GetItemsInternalAsync(parentId); + } + + private async Task GetItemsInternalAsync(string? parentId) + { + var items = await GetFolderIndexAsync(parentId); + + var result = items.Select(metadata => new FileItemDto + { + Id = metadata.Id, + Name = metadata.Name, + Type = metadata.Type, + Size = metadata.Size, + Extension = metadata.Extension, + MimeType = metadata.MimeType, + CreatedAt = metadata.CreatedAt, + ModifiedAt = metadata.ModifiedAt, + Path = metadata.Path, + ParentId = parentId ?? string.Empty, + IsReadOnly = metadata.IsReadOnly + }).OrderBy(x => x.Type == "folder" ? 0 : 1).ThenBy(x => x.Name).ToList(); + + return new GetFilesDto { Items = result }; + } + + public async Task CreateFolderAsync(CreateFolderDto input) + { + ValidateFileName(input.Name); + + var items = await GetFolderIndexAsync(input.ParentId); + + if (items.Any(x => x.Name.Equals(input.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new UserFriendlyException("A folder or file with this name already exists"); + } + + var folderId = GenerateFileId(); + var folderPath = string.IsNullOrEmpty(input.ParentId) + ? input.Name + : $"{input.ParentId}/{input.Name}"; + + var metadata = new FileMetadata + { + Id = folderId, + Name = input.Name, + Type = "folder", + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow, + Path = folderPath, + ParentId = input.ParentId ?? string.Empty, + IsReadOnly = false, + TenantId = _currentTenant.Id?.ToString() + }; + + // Create folder marker blob + var folderMarkerPath = GetTenantPrefix() + folderPath + FolderMarkerSuffix; + await _blobContainer.SaveAsync(folderMarkerPath, Array.Empty()); + + // Create folder index + await SaveFolderIndexAsync(new List(), folderId); + + // Update parent index + items.Add(metadata); + await SaveFolderIndexAsync(items, input.ParentId); + + return new FileItemDto + { + Id = metadata.Id, + Name = metadata.Name, + Type = metadata.Type, + CreatedAt = metadata.CreatedAt, + ModifiedAt = metadata.ModifiedAt, + Path = metadata.Path, + ParentId = input.ParentId ?? string.Empty, + IsReadOnly = metadata.IsReadOnly + }; + } + + public async Task UploadFileAsync(UploadFileDto input) + { + ValidateFileName(input.FileName); + + var items = await GetFolderIndexAsync(input.ParentId); + + if (items.Any(x => x.Name.Equals(input.FileName, StringComparison.OrdinalIgnoreCase))) + { + throw new UserFriendlyException("A file with this name already exists"); + } + + var fileId = GenerateFileId(); + var filePath = string.IsNullOrEmpty(input.ParentId) + ? input.FileName + : $"{input.ParentId}/{input.FileName}"; + + var blobPath = GetTenantPrefix() + filePath; + + // Save file content + long fileSize; + if (input.FileStream != null) + { + input.FileStream.Position = 0; + await _blobContainer.SaveAsync(blobPath, input.FileStream); + fileSize = input.FileStream.Length; + } + else if (input.FileContent != null) + { + using var stream = new MemoryStream(input.FileContent); + await _blobContainer.SaveAsync(blobPath, stream); + fileSize = input.FileContent.Length; + } + else + { + throw new UserFriendlyException("Either FileStream or FileContent must be provided"); + } + + var fileInfo = new FileInfo(input.FileName); + + var metadata = new FileMetadata + { + Id = fileId, + Name = input.FileName, + Type = "file", + Size = fileSize, + Extension = fileInfo.Extension, + MimeType = GetMimeType(fileInfo.Extension), + CreatedAt = DateTime.UtcNow, + ModifiedAt = DateTime.UtcNow, + Path = filePath, + ParentId = input.ParentId ?? string.Empty, + IsReadOnly = false, + TenantId = _currentTenant.Id?.ToString() + }; + + // Update parent index + items.Add(metadata); + await SaveFolderIndexAsync(items, input.ParentId); + + return new FileItemDto + { + Id = metadata.Id, + Name = metadata.Name, + Type = metadata.Type, + Size = metadata.Size, + Extension = metadata.Extension, + MimeType = metadata.MimeType, + CreatedAt = metadata.CreatedAt, + ModifiedAt = metadata.ModifiedAt, + Path = metadata.Path, + ParentId = input.ParentId ?? string.Empty, + IsReadOnly = metadata.IsReadOnly + }; + } + + public async Task RenameItemAsync(string id, RenameItemDto input) + { + ValidateFileName(input.Name); + + var metadata = await FindItemMetadataAsync(id); + if (metadata == null) + { + throw new UserFriendlyException("Item not found"); + } + + var parentItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId); + + 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 newPath = string.IsNullOrEmpty(metadata.ParentId) + ? input.Name + : $"{metadata.ParentId}/{input.Name}"; + var newBlobPath = GetTenantPrefix() + newPath; + + if (metadata.Type == "folder") + { + // Rename folder marker + oldBlobPath += FolderMarkerSuffix; + newBlobPath += FolderMarkerSuffix; + } + + // Move blob + var blobContent = await _blobContainer.GetAllBytesAsync(oldBlobPath); + await _blobContainer.SaveAsync(newBlobPath, blobContent); + await _blobContainer.DeleteAsync(oldBlobPath); + + // Update metadata + metadata.Name = input.Name; + metadata.Path = newPath; + metadata.ModifiedAt = DateTime.UtcNow; + + // Update parent index + var itemToUpdate = parentItems.First(x => x.Id == id); + itemToUpdate.Name = input.Name; + itemToUpdate.Path = newPath; + itemToUpdate.ModifiedAt = DateTime.UtcNow; + + await SaveFolderIndexAsync(parentItems, metadata.ParentId == string.Empty ? null : metadata.ParentId); + + return new FileItemDto + { + Id = metadata.Id, + Name = metadata.Name, + Type = metadata.Type, + Size = metadata.Size, + Extension = metadata.Extension, + MimeType = metadata.MimeType, + CreatedAt = metadata.CreatedAt, + ModifiedAt = metadata.ModifiedAt, + Path = metadata.Path, + ParentId = metadata.ParentId, + IsReadOnly = metadata.IsReadOnly + }; + } + + public async Task MoveItemAsync(string id, MoveItemDto input) + { + var metadata = await FindItemMetadataAsync(id); + if (metadata == null) + { + throw new UserFriendlyException("Item not found"); + } + + var targetItems = await GetFolderIndexAsync(input.TargetFolderId); + + if (targetItems.Any(x => x.Name.Equals(metadata.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new UserFriendlyException("An item with this name already exists in the target folder"); + } + + // Remove from source + var sourceItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId); + sourceItems.RemoveAll(x => x.Id == id); + await SaveFolderIndexAsync(sourceItems, metadata.ParentId == string.Empty ? null : metadata.ParentId); + + // Move blob + var oldBlobPath = GetTenantPrefix() + metadata.Path; + var newPath = string.IsNullOrEmpty(input.TargetFolderId) + ? metadata.Name + : $"{input.TargetFolderId}/{metadata.Name}"; + var newBlobPath = GetTenantPrefix() + newPath; + + if (metadata.Type == "folder") + { + oldBlobPath += FolderMarkerSuffix; + newBlobPath += FolderMarkerSuffix; + } + + var blobContent = await _blobContainer.GetAllBytesAsync(oldBlobPath); + await _blobContainer.SaveAsync(newBlobPath, blobContent); + await _blobContainer.DeleteAsync(oldBlobPath); + + // Update metadata + metadata.Path = newPath; + metadata.ParentId = input.TargetFolderId ?? string.Empty; + metadata.ModifiedAt = DateTime.UtcNow; + + // Add to target + targetItems.Add(metadata); + await SaveFolderIndexAsync(targetItems, input.TargetFolderId); + + return new FileItemDto + { + Id = metadata.Id, + Name = metadata.Name, + Type = metadata.Type, + Size = metadata.Size, + Extension = metadata.Extension, + MimeType = metadata.MimeType, + CreatedAt = metadata.CreatedAt, + ModifiedAt = metadata.ModifiedAt, + Path = metadata.Path, + ParentId = metadata.ParentId, + IsReadOnly = metadata.IsReadOnly + }; + } + + public async Task DeleteItemAsync(string id) + { + var metadata = await FindItemMetadataAsync(id); + if (metadata == null) + { + throw new UserFriendlyException("Item not found"); + } + + var blobPath = GetTenantPrefix() + metadata.Path; + + if (metadata.Type == "folder") + { + // Delete folder marker and index + await _blobContainer.DeleteAsync(blobPath + FolderMarkerSuffix); + await _blobContainer.DeleteAsync(GetTenantPrefix() + $"{id}/{IndexFileName}"); + } + else + { + // Delete file + await _blobContainer.DeleteAsync(blobPath); + } + + // Remove from parent index + var parentItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId); + parentItems.RemoveAll(x => x.Id == id); + await SaveFolderIndexAsync(parentItems, metadata.ParentId == string.Empty ? null : metadata.ParentId); + } + + public async Task DownloadFileAsync(string id) + { + var metadata = await FindItemMetadataAsync(id); + if (metadata == null || metadata.Type != "file") + { + throw new UserFriendlyException("File not found"); + } + + var blobPath = GetTenantPrefix() + metadata.Path; + return await _blobContainer.GetAsync(blobPath); + } + + public async Task GetFilePreviewAsync(string id) + { + return await DownloadFileAsync(id); + } + + public async Task SearchItemsAsync(SearchFilesDto input) + { + var allItems = await GetAllItemsRecursivelyAsync(input.ParentId); + var query = input.Query.ToLowerInvariant(); + + var filteredItems = allItems + .Where(x => x.Name.ToLowerInvariant().Contains(query)) + .Select(metadata => new FileItemDto + { + Id = metadata.Id, + Name = metadata.Name, + Type = metadata.Type, + Size = metadata.Size, + Extension = metadata.Extension, + MimeType = metadata.MimeType, + CreatedAt = metadata.CreatedAt, + ModifiedAt = metadata.ModifiedAt, + Path = metadata.Path, + ParentId = metadata.ParentId, + IsReadOnly = metadata.IsReadOnly + }) + .OrderBy(x => x.Type == "folder" ? 0 : 1) + .ThenBy(x => x.Name) + .ToList(); + + return new GetFilesDto { Items = filteredItems }; + } + + public async Task GetFolderPathAsync(string? folderId = null) + { + var path = new List(); + + if (string.IsNullOrEmpty(folderId)) + { + return new FolderPathDto { Path = path }; + } + + var metadata = await FindItemMetadataAsync(folderId); + if (metadata == null || metadata.Type != "folder") + { + throw new UserFriendlyException("Folder not found"); + } + + var pathParts = metadata.Path.Split('/', StringSplitOptions.RemoveEmptyEntries); + var currentPath = ""; + + foreach (var part in pathParts) + { + currentPath = string.IsNullOrEmpty(currentPath) ? part : $"{currentPath}/{part}"; + var folderMetadata = await FindItemByPathAsync(currentPath); + + if (folderMetadata != null) + { + path.Add(new PathItemDto + { + Id = folderMetadata.Id, + Name = folderMetadata.Name + }); + } + } + + return new FolderPathDto { Path = path }; + } + + #region Private Helper Methods + + private async Task FindItemMetadataAsync(string id) + { + // 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(); + return allItems.FirstOrDefault(x => x.Id == id); + } + + private async Task FindItemByPathAsync(string path) + { + var allItems = await GetAllItemsRecursivelyAsync(); + return allItems.FirstOrDefault(x => x.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + } + + private async Task> GetAllItemsRecursivelyAsync(string? parentId = null, List? result = null) + { + result ??= new List(); + + var items = await GetFolderIndexAsync(parentId); + result.AddRange(items); + + foreach (var folder in items.Where(x => x.Type == "folder")) + { + await GetAllItemsRecursivelyAsync(folder.Id, result); + } + + return result; + } + + private void ValidateFileName(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new UserFriendlyException("File name cannot be empty"); + } + + var invalidChars = Path.GetInvalidFileNameChars(); + if (fileName.Any(c => invalidChars.Contains(c))) + { + throw new UserFriendlyException("File name contains invalid characters"); + } + + if (fileName.Length > 255) + { + throw new UserFriendlyException("File name is too long"); + } + } + + private string GetMimeType(string extension) + { + return extension.ToLowerInvariant() switch + { + ".txt" => "text/plain", + ".pdf" => "application/pdf", + ".doc" => "application/msword", + ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".xls" => "application/vnd.ms-excel", + ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".ppt" => "application/vnd.ms-powerpoint", + ".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".bmp" => "image/bmp", + ".svg" => "image/svg+xml", + ".mp4" => "video/mp4", + ".avi" => "video/x-msvideo", + ".mov" => "video/quicktime", + ".wmv" => "video/x-ms-wmv", + ".mp3" => "audio/mpeg", + ".wav" => "audio/wav", + ".zip" => "application/zip", + ".rar" => "application/vnd.rar", + ".7z" => "application/x-7z-compressed", + ".tar" => "application/x-tar", + ".gz" => "application/gzip", + ".html" => "text/html", + ".css" => "text/css", + ".js" => "application/javascript", + ".json" => "application/json", + ".xml" => "application/xml", + _ => "application/octet-stream" + }; + } + + public async Task GetFolderPathAsync() + { + return await GetFolderPathInternalAsync(null); + } + + public async Task GetFolderPathByIdAsync(string folderId) + { + return await GetFolderPathInternalAsync(folderId); + } + + private async Task GetFolderPathInternalAsync(string? folderId) + { + var pathItems = new List(); + + if (string.IsNullOrEmpty(folderId)) + { + // Root path + return new FolderPathDto { Path = pathItems }; + } + + // Reconstruct path from folderId + var currentPath = folderId; + var pathParts = currentPath.Split('/', StringSplitOptions.RemoveEmptyEntries); + + var reconstructedPath = ""; + foreach (var part in pathParts) + { + reconstructedPath = string.IsNullOrEmpty(reconstructedPath) ? part : $"{reconstructedPath}/{part}"; + pathItems.Add(new PathItemDto + { + Id = reconstructedPath, + Name = part + }); + } + + return new FolderPathDto { Path = pathItems }; + } + + #endregion +} \ No newline at end of file diff --git a/api/src/Kurs.Platform.Application/FileManagement/FileMetadata.cs b/api/src/Kurs.Platform.Application/FileManagement/FileMetadata.cs new file mode 100644 index 00000000..bd90e9f6 --- /dev/null +++ b/api/src/Kurs.Platform.Application/FileManagement/FileMetadata.cs @@ -0,0 +1,19 @@ +using System; + +namespace Kurs.Platform.FileManagement; + +public class FileMetadata +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // "file" or "folder" + public long? Size { get; set; } + public string Extension { get; set; } = string.Empty; + public string MimeType { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public DateTime ModifiedAt { get; set; } + public string Path { get; set; } = string.Empty; + public string ParentId { get; set; } = string.Empty; + public bool IsReadOnly { get; set; } + public string? TenantId { get; set; } +} \ No newline at end of file diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json index 874fdc3a..8e04e434 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json @@ -1818,8 +1818,8 @@ { "resourceName": "Platform", "key": "App.Files", - "en": "Files", - "tr": "Dosyalar" + "en": "File Manager", + "tr": "Dosya Yöneticisi" }, { "resourceName": "Platform", diff --git a/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs b/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs index 7044803c..ce72af1a 100644 --- a/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs +++ b/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs @@ -1,3 +1,5 @@ +#nullable enable + using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -39,4 +41,76 @@ public class BlobManager : DomainService var container = GetContainer(containerName); await container.DeleteAsync(blobName); } + + public async Task SaveAsync(string containerName, string blobName, byte[] bytes, bool overrideExisting = true) + { + var container = GetContainer(containerName); + await container.SaveAsync(blobName, bytes, overrideExisting); + } + + public async Task GetAllBytesOrNullAsync(string containerName, string blobName) + { + var container = GetContainer(containerName); + return await container.GetAllBytesOrNullAsync(blobName); + } + + public async Task GetAllBytesAsync(string containerName, string blobName) + { + var container = GetContainer(containerName); + return await container.GetAllBytesAsync(blobName); + } + + public async Task ExistsAsync(string containerName, string blobName) + { + var container = GetContainer(containerName); + return await container.ExistsAsync(blobName); + } + + // Default container methods (for FileManagement and other general purposes) + private IBlobContainer GetDefaultContainer() + { + return _blobContainerFactory.Create(""); + } + + public async Task SaveAsync(string blobName, Stream bytes, bool overrideExisting = true) + { + var container = GetDefaultContainer(); + await container.SaveAsync(blobName, bytes, overrideExisting); + } + + public async Task SaveAsync(string blobName, byte[] bytes, bool overrideExisting = true) + { + var container = GetDefaultContainer(); + await container.SaveAsync(blobName, bytes, overrideExisting); + } + + public async Task GetAsync(string blobName) + { + var container = GetDefaultContainer(); + return await container.GetAsync(blobName); + } + + public async Task GetAllBytesOrNullAsync(string blobName) + { + var container = GetDefaultContainer(); + return await container.GetAllBytesOrNullAsync(blobName); + } + + public async Task GetAllBytesAsync(string blobName) + { + var container = GetDefaultContainer(); + return await container.GetAllBytesAsync(blobName); + } + + public async Task DeleteAsync(string blobName) + { + var container = GetDefaultContainer(); + await container.DeleteAsync(blobName); + } + + public async Task ExistsAsync(string blobName) + { + var container = GetDefaultContainer(); + return await container.ExistsAsync(blobName); + } } diff --git a/ui/public/version.json b/ui/public/version.json index 1f2dbe35..61eb85cb 100644 --- a/ui/public/version.json +++ b/ui/public/version.json @@ -1,5 +1,5 @@ { - "commit": "c78845f", + "commit": "753efd5", "releases": [ { "version": "1.0.32", diff --git a/ui/src/services/fileManagement.service.ts b/ui/src/services/fileManagement.service.ts index ab9f1914..a67a4eae 100644 --- a/ui/src/services/fileManagement.service.ts +++ b/ui/src/services/fileManagement.service.ts @@ -10,20 +10,21 @@ import type { class FileManagementService { // Get files and folders for a specific directory - async getItems(folderId?: string): Promise<{ data: { items: FileItem[] } }> { - const params = folderId ? { parentId: folderId } : {} + async getItems(parentId?: string): Promise<{ data: { items: FileItem[] } }> { + const url = parentId + ? `/api/app/file-management/items-by-parent/${parentId}` + : `/api/app/file-management/items` return ApiService.fetchData<{ items: FileItem[] }>({ - url: `/api/files`, + url, method: 'GET', - params, }) } // Create a new folder async createFolder(request: CreateFolderRequest): Promise<{ data: FileItem }> { return ApiService.fetchData({ - url: `/api/files/folders`, + url: `/api/app/file-management/folder`, method: 'POST', data: request as any, }) @@ -32,13 +33,16 @@ class FileManagementService { // Upload a file async uploadFile(request: UploadFileRequest): Promise<{ data: FileItem }> { const formData = new FormData() - formData.append('file', request.file) + formData.append('fileName', request.file.name) if (request.parentId) { formData.append('parentId', request.parentId) } + + // Send the actual file for FileContent property + formData.append('fileContent', request.file) return ApiService.fetchData({ - url: `/api/files/upload`, + url: `/api/app/file-management/upload-file`, method: 'POST', data: formData as any, headers: { @@ -50,8 +54,8 @@ class FileManagementService { // Rename a file or folder async renameItem(request: RenameItemRequest): Promise<{ data: FileItem }> { return ApiService.fetchData({ - url: `/api/files/${request.id}/rename`, - method: 'PUT', + url: `/api/app/file-management/${request.id}/rename-item`, + method: 'POST', data: { name: request.newName }, }) } @@ -59,8 +63,8 @@ class FileManagementService { // Move a file or folder async moveItem(request: MoveItemRequest): Promise<{ data: FileItem }> { return ApiService.fetchData({ - url: `/api/files/${request.itemId}/move`, - method: 'PUT', + url: `/api/app/file-management/${request.itemId}/move-item`, + method: 'POST', data: { targetFolderId: request.targetFolderId }, }) } @@ -68,7 +72,7 @@ class FileManagementService { // Delete a file or folder async deleteItem(request: DeleteItemRequest): Promise { await ApiService.fetchData({ - url: `/api/files/${request.id}`, + url: `/api/app/file-management/${request.id}/item`, method: 'DELETE', }) } @@ -76,8 +80,8 @@ class FileManagementService { // Download a file async downloadFile(fileId: string): Promise { const response = await ApiService.fetchData({ - url: `/api/files/${fileId}/download`, - method: 'GET', + url: `/api/app/file-management/${fileId}/download-file`, + method: 'POST', responseType: 'blob', }) return response.data @@ -86,7 +90,7 @@ class FileManagementService { // Get file preview/thumbnail async getFilePreview(fileId: string): Promise { const response = await ApiService.fetchData({ - url: `/api/files/${fileId}/preview`, + url: `/api/app/file-management/${fileId}/file-preview`, method: 'GET', responseType: 'blob', }) @@ -95,22 +99,26 @@ class FileManagementService { // Search files and folders async searchItems(query: string, folderId?: string): Promise<{ data: { items: FileItem[] } }> { - const params = { - q: query, + const data = { + query: query, ...(folderId && { parentId: folderId }), } return ApiService.fetchData<{ items: FileItem[] }>({ - url: `/api/files/search`, - method: 'GET', - params, + url: `/api/app/file-management/search-items`, + method: 'POST', + data, }) } // 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` + return ApiService.fetchData<{ path: Array<{ id: string; name: string }> }>({ - url: `/api/files/folders/${folderId || 'root'}/path`, + url, method: 'GET', }) } diff --git a/ui/src/types/fileManagement.ts b/ui/src/types/fileManagement.ts index a96218d5..78fdfef9 100644 --- a/ui/src/types/fileManagement.ts +++ b/ui/src/types/fileManagement.ts @@ -1,15 +1,15 @@ export interface FileItem { id: string name: string - type: 'file' | 'folder' + type: string // "file" or "folder" size?: number mimeType?: string createdAt: Date modifiedAt: Date - parentId?: string + parentId: string path: string - isDirectory: boolean - extension?: string + extension: string + isReadOnly: boolean } export interface FolderItem extends Omit { diff --git a/ui/src/views/admin/files/FileManager.tsx b/ui/src/views/admin/files/FileManager.tsx index 7d8864f7..1e5b2d71 100644 --- a/ui/src/views/admin/files/FileManager.tsx +++ b/ui/src/views/admin/files/FileManager.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react' import { Helmet } from 'react-helmet' import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui' import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp } from 'react-icons/fa' -import AdaptableCard from '@/components/shared/AdaptableCard' import Container from '@/components/shared/Container' import { useLocalization } from '@/utils/hooks/useLocalization' import fileManagementService from '@/services/fileManagement.service' @@ -60,7 +59,8 @@ const FileManager = () => { try { setLoading(true) const response = await fileManagementService.getItems(folderId) - setItems(response.data.items) + // Backend returns GetFilesDto which has Items property + setItems(response.data.items || []) } catch (error) { console.error('Failed to fetch items:', error) toast.push(Failed to load files and folders) @@ -282,23 +282,15 @@ const FileManager = () => { } return ( - <> - - File Manager - - - {/* Header */} -
-
-

Files

-

- Upload, organize, and manage files in your application -

-
-
+ + {/* Toolbar */} -
+