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",
|
"resourceName": "Platform",
|
||||||
"key": "App.Files",
|
"key": "App.Files",
|
||||||
"en": "Files",
|
"en": "File Manager",
|
||||||
"tr": "Dosyalar"
|
"tr": "Dosya Yöneticisi"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"commit": "c78845f",
|
"commit": "753efd5",
|
||||||
"releases": [
|
"releases": [
|
||||||
{
|
{
|
||||||
"version": "1.0.32",
|
"version": "1.0.32",
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'> {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue