Dosya yöneticisi AppService ve FileSystem

This commit is contained in:
Sedat Öztürk 2025-10-26 11:10:02 +03:00
parent 753efd5f07
commit fc7e143e4e
11 changed files with 922 additions and 46 deletions

View file

@ -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<FileItemDto> Items { get; set; } = new();
}
public class FolderPathDto
{
public List<PathItemDto> Path { get; set; } = new();
}
public class PathItemDto
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}

View file

@ -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; }
}

View file

@ -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<GetFilesDto> GetItemsAsync();
Task<GetFilesDto> GetItemsByParentAsync(string parentId);
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<GetFilesDto> SearchItemsAsync(SearchFilesDto input);
Task<FolderPathDto> GetFolderPathAsync();
Task<FolderPathDto> GetFolderPathByIdAsync(string folderId);
}

View file

@ -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<List<FileMetadata>> GetFolderIndexAsync(string? parentId = null)
{
// Root seviyesinde default klasörleri göster
if (string.IsNullOrEmpty(parentId))
{
var defaultFolders = new List<FileMetadata>
{
// 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<FileMetadata>();
var indexJson = Encoding.UTF8.GetString(indexBytes);
return JsonSerializer.Deserialize<List<FileMetadata>>(indexJson) ?? new List<FileMetadata>();
}
catch
{
return new List<FileMetadata>();
}
}
private async Task<List<FileMetadata>> GetCustomFoldersAsync()
{
var indexPath = GetTenantPrefix() + IndexFileName;
try
{
var indexBytes = await _blobContainer.GetAllBytesOrNullAsync(indexPath);
if (indexBytes == null) return new List<FileMetadata>();
var indexJson = Encoding.UTF8.GetString(indexBytes);
return JsonSerializer.Deserialize<List<FileMetadata>>(indexJson) ?? new List<FileMetadata>();
}
catch
{
return new List<FileMetadata>();
}
}
private async Task SaveFolderIndexAsync(List<FileMetadata> 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<GetFilesDto> GetItemsAsync()
{
return await GetItemsInternalAsync(null);
}
public async Task<GetFilesDto> GetItemsByParentAsync(string parentId)
{
return await GetItemsInternalAsync(parentId);
}
private async Task<GetFilesDto> 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<FileItemDto> 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<byte>());
// Create folder index
await SaveFolderIndexAsync(new List<FileMetadata>(), 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<FileItemDto> 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<FileItemDto> 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<FileItemDto> 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<Stream> 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<Stream> GetFilePreviewAsync(string id)
{
return await DownloadFileAsync(id);
}
public async Task<GetFilesDto> 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<FolderPathDto> GetFolderPathAsync(string? folderId = null)
{
var path = new List<PathItemDto>();
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<FileMetadata?> 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<FileMetadata?> FindItemByPathAsync(string path)
{
var allItems = await GetAllItemsRecursivelyAsync();
return allItems.FirstOrDefault(x => x.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
}
private async Task<List<FileMetadata>> GetAllItemsRecursivelyAsync(string? parentId = null, List<FileMetadata>? result = null)
{
result ??= new List<FileMetadata>();
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<FolderPathDto> GetFolderPathAsync()
{
return await GetFolderPathInternalAsync(null);
}
public async Task<FolderPathDto> GetFolderPathByIdAsync(string folderId)
{
return await GetFolderPathInternalAsync(folderId);
}
private async Task<FolderPathDto> GetFolderPathInternalAsync(string? folderId)
{
var pathItems = new List<PathItemDto>();
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
}

View file

@ -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; }
}

View file

@ -1818,8 +1818,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Files", "key": "App.Files",
"en": "Files", "en": "File Manager",
"tr": "Dosyalar" "tr": "Dosya Yöneticisi"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",

View file

@ -1,3 +1,5 @@
#nullable enable
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -39,4 +41,76 @@ public class BlobManager : DomainService
var container = GetContainer(containerName); var container = GetContainer(containerName);
await container.DeleteAsync(blobName); 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<byte[]?> GetAllBytesOrNullAsync(string containerName, string blobName)
{
var container = GetContainer(containerName);
return await container.GetAllBytesOrNullAsync(blobName);
}
public async Task<byte[]> GetAllBytesAsync(string containerName, string blobName)
{
var container = GetContainer(containerName);
return await container.GetAllBytesAsync(blobName);
}
public async Task<bool> 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<Stream> GetAsync(string blobName)
{
var container = GetDefaultContainer();
return await container.GetAsync(blobName);
}
public async Task<byte[]?> GetAllBytesOrNullAsync(string blobName)
{
var container = GetDefaultContainer();
return await container.GetAllBytesOrNullAsync(blobName);
}
public async Task<byte[]> 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<bool> ExistsAsync(string blobName)
{
var container = GetDefaultContainer();
return await container.ExistsAsync(blobName);
}
} }

View file

@ -1,5 +1,5 @@
{ {
"commit": "c78845f", "commit": "753efd5",
"releases": [ "releases": [
{ {
"version": "1.0.32", "version": "1.0.32",

View file

@ -10,20 +10,21 @@ import type {
class FileManagementService { class FileManagementService {
// Get files and folders for a specific directory // Get files and folders for a specific directory
async getItems(folderId?: string): Promise<{ data: { items: FileItem[] } }> { async getItems(parentId?: string): Promise<{ data: { items: FileItem[] } }> {
const params = folderId ? { parentId: folderId } : {} const url = parentId
? `/api/app/file-management/items-by-parent/${parentId}`
: `/api/app/file-management/items`
return ApiService.fetchData<{ items: FileItem[] }>({ return ApiService.fetchData<{ items: FileItem[] }>({
url: `/api/files`, url,
method: 'GET', method: 'GET',
params,
}) })
} }
// Create a new folder // Create a new folder
async createFolder(request: CreateFolderRequest): Promise<{ data: FileItem }> { async createFolder(request: CreateFolderRequest): Promise<{ data: FileItem }> {
return ApiService.fetchData<FileItem>({ return ApiService.fetchData<FileItem>({
url: `/api/files/folders`, url: `/api/app/file-management/folder`,
method: 'POST', method: 'POST',
data: request as any, data: request as any,
}) })
@ -32,13 +33,16 @@ class FileManagementService {
// Upload a file // Upload a file
async uploadFile(request: UploadFileRequest): Promise<{ data: FileItem }> { async uploadFile(request: UploadFileRequest): Promise<{ data: FileItem }> {
const formData = new FormData() const formData = new FormData()
formData.append('file', request.file) formData.append('fileName', request.file.name)
if (request.parentId) { if (request.parentId) {
formData.append('parentId', request.parentId) formData.append('parentId', request.parentId)
} }
// Send the actual file for FileContent property
formData.append('fileContent', request.file)
return ApiService.fetchData<FileItem>({ return ApiService.fetchData<FileItem>({
url: `/api/files/upload`, url: `/api/app/file-management/upload-file`,
method: 'POST', method: 'POST',
data: formData as any, data: formData as any,
headers: { headers: {
@ -50,8 +54,8 @@ class FileManagementService {
// Rename a file or folder // Rename a file or folder
async renameItem(request: RenameItemRequest): Promise<{ data: FileItem }> { async renameItem(request: RenameItemRequest): Promise<{ data: FileItem }> {
return ApiService.fetchData<FileItem>({ return ApiService.fetchData<FileItem>({
url: `/api/files/${request.id}/rename`, url: `/api/app/file-management/${request.id}/rename-item`,
method: 'PUT', method: 'POST',
data: { name: request.newName }, data: { name: request.newName },
}) })
} }
@ -59,8 +63,8 @@ class FileManagementService {
// Move a file or folder // Move a file or folder
async moveItem(request: MoveItemRequest): Promise<{ data: FileItem }> { async moveItem(request: MoveItemRequest): Promise<{ data: FileItem }> {
return ApiService.fetchData<FileItem>({ return ApiService.fetchData<FileItem>({
url: `/api/files/${request.itemId}/move`, url: `/api/app/file-management/${request.itemId}/move-item`,
method: 'PUT', method: 'POST',
data: { targetFolderId: request.targetFolderId }, data: { targetFolderId: request.targetFolderId },
}) })
} }
@ -68,7 +72,7 @@ class FileManagementService {
// Delete a file or folder // Delete a file or folder
async deleteItem(request: DeleteItemRequest): Promise<void> { async deleteItem(request: DeleteItemRequest): Promise<void> {
await ApiService.fetchData<void>({ await ApiService.fetchData<void>({
url: `/api/files/${request.id}`, url: `/api/app/file-management/${request.id}/item`,
method: 'DELETE', method: 'DELETE',
}) })
} }
@ -76,8 +80,8 @@ class FileManagementService {
// Download a file // Download a file
async downloadFile(fileId: string): Promise<Blob> { async downloadFile(fileId: string): Promise<Blob> {
const response = await ApiService.fetchData<Blob>({ const response = await ApiService.fetchData<Blob>({
url: `/api/files/${fileId}/download`, url: `/api/app/file-management/${fileId}/download-file`,
method: 'GET', method: 'POST',
responseType: 'blob', responseType: 'blob',
}) })
return response.data return response.data
@ -86,7 +90,7 @@ class FileManagementService {
// Get file preview/thumbnail // Get file preview/thumbnail
async getFilePreview(fileId: string): Promise<string> { async getFilePreview(fileId: string): Promise<string> {
const response = await ApiService.fetchData<Blob>({ const response = await ApiService.fetchData<Blob>({
url: `/api/files/${fileId}/preview`, url: `/api/app/file-management/${fileId}/file-preview`,
method: 'GET', method: 'GET',
responseType: 'blob', responseType: 'blob',
}) })
@ -95,22 +99,26 @@ 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): Promise<{ data: { items: FileItem[] } }> {
const params = { const data = {
q: query, query: query,
...(folderId && { parentId: folderId }), ...(folderId && { parentId: folderId }),
} }
return ApiService.fetchData<{ items: FileItem[] }>({ return ApiService.fetchData<{ items: FileItem[] }>({
url: `/api/files/search`, url: `/api/app/file-management/search-items`,
method: 'GET', method: 'POST',
params, data,
}) })
} }
// Get folder breadcrumb path // Get folder breadcrumb path
async getFolderPath(folderId?: string): Promise<{ data: { path: Array<{ id: string; name: string }> } }> { 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 }> }>({ return ApiService.fetchData<{ path: Array<{ id: string; name: string }> }>({
url: `/api/files/folders/${folderId || 'root'}/path`, url,
method: 'GET', method: 'GET',
}) })
} }

View file

@ -1,15 +1,15 @@
export interface FileItem { export interface FileItem {
id: string id: string
name: string name: string
type: 'file' | 'folder' type: string // "file" or "folder"
size?: number size?: number
mimeType?: string mimeType?: string
createdAt: Date createdAt: Date
modifiedAt: Date modifiedAt: Date
parentId?: string parentId: string
path: string path: string
isDirectory: boolean extension: string
extension?: string isReadOnly: boolean
} }
export interface FolderItem extends Omit<FileItem, 'type' | 'size' | 'mimeType' | 'extension'> { export interface FolderItem extends Omit<FileItem, 'type' | 'size' | 'mimeType' | 'extension'> {

View file

@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui' import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui'
import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp } from 'react-icons/fa' import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp } from 'react-icons/fa'
import AdaptableCard from '@/components/shared/AdaptableCard'
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'
@ -60,7 +59,8 @@ const FileManager = () => {
try { try {
setLoading(true) setLoading(true)
const response = await fileManagementService.getItems(folderId) const response = await fileManagementService.getItems(folderId)
setItems(response.data.items) // Backend returns GetFilesDto which has Items property
setItems(response.data.items || [])
} catch (error) { } catch (error) {
console.error('Failed to fetch items:', error) console.error('Failed to fetch items:', error)
toast.push(<Notification type="danger">Failed to load files and folders</Notification>) toast.push(<Notification type="danger">Failed to load files and folders</Notification>)
@ -282,23 +282,15 @@ const FileManager = () => {
} }
return ( return (
<> <Container>
<Helmet> <Helmet
<title>File Manager</title> titleTemplate="%s | Sözsoft Kurs Platform"
</Helmet> title={translate('::' + 'App.Files')}
defaultTitle="Sözsoft Kurs Platform"
{/* Header */} ></Helmet>
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-2xl font-bold">Files</h3>
<p className="text-gray-600 dark:text-gray-400">
Upload, organize, and manage files in your application
</p>
</div>
</div>
{/* Toolbar */} {/* Toolbar */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-6"> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-4 mt-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="solid" variant="solid"
@ -488,7 +480,7 @@ const FileManager = () => {
items={itemsToDelete} items={itemsToDelete}
loading={deleting} loading={deleting}
/> />
</> </Container>
) )
} }