Dosya yöneticisi AppService ve FileSystem
This commit is contained in:
parent
753efd5f07
commit
fc7e143e4e
11 changed files with 922 additions and 46 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -1818,8 +1818,8 @@
|
|||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Files",
|
||||
"en": "Files",
|
||||
"tr": "Dosyalar"
|
||||
"en": "File Manager",
|
||||
"tr": "Dosya Yöneticisi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"commit": "c78845f",
|
||||
"commit": "753efd5",
|
||||
"releases": [
|
||||
{
|
||||
"version": "1.0.32",
|
||||
|
|
|
|||
|
|
@ -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<FileItem>({
|
||||
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<FileItem>({
|
||||
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<FileItem>({
|
||||
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<FileItem>({
|
||||
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<void> {
|
||||
await ApiService.fetchData<void>({
|
||||
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<Blob> {
|
||||
const response = await ApiService.fetchData<Blob>({
|
||||
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<string> {
|
||||
const response = await ApiService.fetchData<Blob>({
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FileItem, 'type' | 'size' | 'mimeType' | 'extension'> {
|
||||
|
|
|
|||
|
|
@ -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(<Notification type="danger">Failed to load files and folders</Notification>)
|
||||
|
|
@ -282,23 +282,15 @@ const FileManager = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>File Manager</title>
|
||||
</Helmet>
|
||||
|
||||
{/* Header */}
|
||||
<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>
|
||||
<Container>
|
||||
<Helmet
|
||||
titleTemplate="%s | Sözsoft Kurs Platform"
|
||||
title={translate('::' + 'App.Files')}
|
||||
defaultTitle="Sözsoft Kurs Platform"
|
||||
></Helmet>
|
||||
|
||||
{/* 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">
|
||||
<Button
|
||||
variant="solid"
|
||||
|
|
@ -488,7 +480,7 @@ const FileManager = () => {
|
|||
items={itemsToDelete}
|
||||
loading={deleting}
|
||||
/>
|
||||
</>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue