diff --git a/api/src/Kurs.Platform.Application/FileManagement/FileManagementAppService.cs b/api/src/Kurs.Platform.Application/FileManagement/FileManagementAppService.cs index 63730cb8..ef803843 100644 --- a/api/src/Kurs.Platform.Application/FileManagement/FileManagementAppService.cs +++ b/api/src/Kurs.Platform.Application/FileManagement/FileManagementAppService.cs @@ -8,7 +8,6 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; using Kurs.Platform.BlobStoring; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Volo.Abp; @@ -200,42 +199,50 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe { ValidateFileName(input.Name); - var items = await GetFolderIndexAsync(input.ParentId); - - if (items.Any(x => x.Name.Equals(input.Name, StringComparison.OrdinalIgnoreCase))) + var cdnBasePath = _configuration["App:CdnPath"]; + if (string.IsNullOrEmpty(cdnBasePath)) { - throw new UserFriendlyException("A folder or file with this name already exists"); + throw new UserFriendlyException("CDN path is not configured"); } - var folderId = GenerateFileId(); - var folderPath = string.IsNullOrEmpty(input.ParentId) - ? input.Name - : $"{input.ParentId}/{input.Name}"; + var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var parentPath = Path.Combine(cdnBasePath, tenantId); + + if (!string.IsNullOrEmpty(input.ParentId)) + { + parentPath = Path.Combine(parentPath, input.ParentId); + } + + var folderPath = Path.Combine(parentPath, input.Name); + + // Klasör zaten var mı kontrol et + if (Directory.Exists(folderPath)) + { + throw new UserFriendlyException("A folder with this name already exists"); + } + + // Dosya ile aynı isimde bir şey var mı kontrol et + if (File.Exists(folderPath)) + { + throw new UserFriendlyException("A file with this name already exists"); + } + + // Klasörü oluştur + Directory.CreateDirectory(folderPath); var metadata = new FileMetadata { - Id = folderId, + Id = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}", Name = input.Name, Type = "folder", CreatedAt = DateTime.UtcNow, ModifiedAt = DateTime.UtcNow, - Path = folderPath, + Path = string.IsNullOrEmpty(input.ParentId) ? input.Name : $"{input.ParentId}/{input.Name}", ParentId = input.ParentId ?? string.Empty, IsReadOnly = false, TenantId = _currentTenant.Id?.ToString() }; - // Create folder marker blob - var folderMarkerPath = GetTenantPrefix() + folderPath + FolderMarkerSuffix; - await _blobContainer.SaveAsync(folderMarkerPath, Array.Empty()); - - // Create folder index - await SaveFolderIndexAsync(new List(), folderId); - - // Update parent index - items.Add(metadata); - await SaveFolderIndexAsync(items, input.ParentId); - return new FileItemDto { Id = metadata.Id, @@ -267,18 +274,37 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe var blobPath = GetTenantPrefix() + filePath; - // Save file content + // Save file content to CDN path + var cdnBasePath = _configuration["App:CdnPath"]; + if (string.IsNullOrEmpty(cdnBasePath)) + { + throw new UserFriendlyException("CDN path is not configured"); + } + + var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var fullCdnPath = Path.Combine(cdnBasePath, tenantId); + + if (!string.IsNullOrEmpty(input.ParentId)) + { + fullCdnPath = Path.Combine(fullCdnPath, input.ParentId); + } + + // Dizini oluştur + Directory.CreateDirectory(fullCdnPath); + + var fullFilePath = Path.Combine(fullCdnPath, input.FileName); + long fileSize; if (input.FileStream != null) { input.FileStream.Position = 0; - await _blobContainer.SaveAsync(blobPath, input.FileStream); + using var fileStream = File.Create(fullFilePath); + await input.FileStream.CopyToAsync(fileStream); fileSize = input.FileStream.Length; } else if (input.FileContent != null) { - using var stream = new MemoryStream(input.FileContent); - await _blobContainer.SaveAsync(blobPath, stream); + await File.WriteAllBytesAsync(fullFilePath, input.FileContent); fileSize = input.FileContent.Length; } else @@ -452,42 +478,48 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe public async Task DeleteItemAsync(string id) { - var metadata = await FindItemMetadataAsync(id); - if (metadata == null) + var cdnBasePath = _configuration["App:CdnPath"]; + if (string.IsNullOrEmpty(cdnBasePath)) { - throw new UserFriendlyException("Item not found"); + throw new UserFriendlyException("CDN path is not configured"); } - var blobPath = GetTenantPrefix() + metadata.Path; + var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var fullPath = Path.Combine(cdnBasePath, tenantId, id); - if (metadata.Type == "folder") + if (Directory.Exists(fullPath)) { - // Delete folder marker and index - await _blobContainer.DeleteAsync(blobPath + FolderMarkerSuffix); - await _blobContainer.DeleteAsync(GetTenantPrefix() + $"{id}/{IndexFileName}"); + // Klasör sil (içindeki tüm dosyalar ile birlikte) + Directory.Delete(fullPath, recursive: true); + } + else if (File.Exists(fullPath)) + { + // Dosya sil + File.Delete(fullPath); } else { - // Delete file - await _blobContainer.DeleteAsync(blobPath); + throw new UserFriendlyException("Item not found"); } - - // Remove from parent index - var parentItems = await GetFolderIndexAsync(metadata.ParentId == string.Empty ? null : metadata.ParentId); - parentItems.RemoveAll(x => x.Id == id); - await SaveFolderIndexAsync(parentItems, metadata.ParentId == string.Empty ? null : metadata.ParentId); } public async Task DownloadFileAsync(string id) { - var metadata = await FindItemMetadataAsync(id); - if (metadata == null || metadata.Type != "file") + var cdnBasePath = _configuration["App:CdnPath"]; + if (string.IsNullOrEmpty(cdnBasePath)) + { + throw new UserFriendlyException("CDN path is not configured"); + } + + var tenantId = _currentTenant.Id?.ToString() ?? "host"; + var fullFilePath = Path.Combine(cdnBasePath, tenantId, id); + + if (!File.Exists(fullFilePath)) { throw new UserFriendlyException("File not found"); } - var blobPath = GetTenantPrefix() + metadata.Path; - return await _blobContainer.GetAsync(blobPath); + return File.OpenRead(fullFilePath); } public async Task GetFilePreviewAsync(string id) diff --git a/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs b/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs index ce72af1a..9a857791 100644 --- a/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs +++ b/api/src/Kurs.Platform.Domain/BlobStoring/BlobManager.cs @@ -69,7 +69,7 @@ public class BlobManager : DomainService // Default container methods (for FileManagement and other general purposes) private IBlobContainer GetDefaultContainer() { - return _blobContainerFactory.Create(""); + return _blobContainerFactory.Create("default"); } public async Task SaveAsync(string blobName, Stream bytes, bool overrideExisting = true) diff --git a/ui/src/views/admin/files/FileManager.tsx b/ui/src/views/admin/files/FileManager.tsx index 97c806cc..e0f22bf4 100644 --- a/ui/src/views/admin/files/FileManager.tsx +++ b/ui/src/views/admin/files/FileManager.tsx @@ -390,19 +390,32 @@ const FileManager = () => { - {/* Content */} + {/* Files Grid/List */} {loading ? (
- +
) : ( -
+ {/* List View Header */} + {viewMode === 'list' && ( +
+
{/* Icon column */} +
İsim
+
Tür
+
Boyut
+
Değiştirilme
+
{/* Actions column */} +
)} - > + +
{filteredItems.length === 0 ? (
@@ -415,17 +428,37 @@ const FileManager = () => { { + // Klasör içinde yeni klasör oluşturmak için parent klasörü set et + setCurrentFolderId(parentItem.id) + setCreateFolderModalOpen(true) + }} onRename={openRenameModal} + onMove={(item) => { + // Move işlevi henüz implement edilmedi + toast.push(Move özelliği yakında eklenecek) + }} onDelete={(item) => openDeleteModal([item])} onDownload={item.type === 'file' ? handleDownload : undefined} - className={viewMode === 'list' ? 'flex items-center p-3 space-x-4' : undefined} + onPreview={item.type === 'file' ? (item) => { + // Preview işlevi - resimler için modal açabiliriz + if (item.mimeType?.startsWith('image/')) { + // Resim preview modal'ı açılabilir + toast.push(Resim önizleme özelliği yakında eklenecek) + } else { + // Diğer dosya tipleri için download + handleDownload(item) + } + } : undefined} /> )) )} -
+
+ )} {/* Modals */} diff --git a/ui/src/views/admin/files/components/FileItem.tsx b/ui/src/views/admin/files/components/FileItem.tsx index ef449bf3..5dd7c250 100644 --- a/ui/src/views/admin/files/components/FileItem.tsx +++ b/ui/src/views/admin/files/components/FileItem.tsx @@ -1,7 +1,8 @@ -import { forwardRef, useState } from 'react' +import { forwardRef, useState, useEffect } from 'react' import classNames from 'classnames' import { HiFolder, + HiFolderPlus, HiDocument, HiPhoto, HiFilm, @@ -14,14 +15,16 @@ import { HiArrowDownTray, HiEye, } from 'react-icons/hi2' -import { Dropdown } from '@/components/ui' +// import { Dropdown } from '@/components/ui' // Artık kullanmıyoruz import type { FileItem as FileItemType, FileActionMenuItem } from '@/types/fileManagement' export interface FileItemProps { item: FileItemType selected?: boolean + viewMode?: 'grid' | 'list' onSelect?: (item: FileItemType) => void onDoubleClick?: (item: FileItemType) => void + onCreateFolder?: (parentItem: FileItemType) => void onRename?: (item: FileItemType) => void onMove?: (item: FileItemType) => void onDelete?: (item: FileItemType) => void @@ -30,31 +33,33 @@ export interface FileItemProps { className?: string } -const getFileIcon = (item: FileItemType) => { +const getFileIcon = (item: FileItemType, large: boolean = false) => { + const iconSize = large ? "h-12 w-12" : "h-8 w-8" + if (item.type === 'folder') { - return + return } const extension = item.extension?.toLowerCase() const mimeType = item.mimeType?.toLowerCase() if (mimeType?.startsWith('image/')) { - return + return } if (mimeType?.startsWith('video/')) { - return + return } if (mimeType?.startsWith('audio/')) { - return + return } if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) { - return + return } - return + return } const formatFileSize = (bytes?: number): string => { @@ -67,12 +72,51 @@ const formatFileSize = (bytes?: number): string => { return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + ' ' + sizes[i] } +const formatDate = (date?: string | Date): string => { + if (!date) return '' + + try { + const dateObj = typeof date === 'string' ? new Date(date) : date + return dateObj.toLocaleDateString('tr-TR', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } catch { + return '' + } +} + +const getFileTypeLabel = (item: FileItemType): string => { + if (item.type === 'folder') return 'Klasör' + + const extension = item.extension?.toLowerCase() + const mimeType = item.mimeType?.toLowerCase() + + if (mimeType?.startsWith('image/')) return 'Resim' + if (mimeType?.startsWith('video/')) return 'Video' + if (mimeType?.startsWith('audio/')) return 'Ses' + if (mimeType?.includes('pdf')) return 'PDF' + if (mimeType?.includes('word')) return 'Word' + if (mimeType?.includes('excel') || mimeType?.includes('spreadsheet')) return 'Excel' + if (mimeType?.includes('powerpoint') || mimeType?.includes('presentation')) return 'PowerPoint' + if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) return 'Arşiv' + if (['txt', 'md'].includes(extension || '')) return 'Metin' + if (['json', 'xml', 'css', 'js', 'ts', 'html'].includes(extension || '')) return 'Kod' + + return extension?.toUpperCase() || 'Dosya' +} + const FileItem = forwardRef((props, ref) => { const { item, selected = false, + viewMode = 'grid', onSelect, onDoubleClick, + onCreateFolder, onRename, onMove, onDelete, @@ -92,119 +136,290 @@ const FileItem = forwardRef((props, ref) => { } const actionMenuItems: FileActionMenuItem[] = [ + // Preview - sadece dosyalar için ...(item.type === 'file' && onPreview ? [ { key: 'preview', - label: 'Preview', + label: 'Önizle', icon: 'HiEye', onClick: () => onPreview(item), }, ] : []), + // Download - sadece dosyalar için ...(item.type === 'file' && onDownload ? [ { key: 'download', - label: 'Download', + label: 'İndir', icon: 'HiArrowDownTray', onClick: () => onDownload(item), }, ] : []), - { - key: 'rename', - label: 'Rename', - icon: 'HiPencil', - onClick: () => onRename?.(item), - }, - { - key: 'move', - label: 'Move', - icon: 'HiArrowRightOnRectangle', - onClick: () => onMove?.(item), - }, - { - key: 'delete', - label: 'Delete', - icon: 'HiTrash', - dangerous: true, - onClick: () => onDelete?.(item), - }, + // Create Folder - sadece klasörler için + ...(item.type === 'folder' && onCreateFolder + ? [ + { + key: 'createFolder', + label: 'Yeni Klasör', + icon: 'HiFolderPlus', + onClick: () => onCreateFolder(item), + }, + ] + : []), + // Rename - her şey için + ...(onRename + ? [ + { + key: 'rename', + label: 'Yeniden Adlandır', + icon: 'HiPencil', + onClick: () => onRename(item), + }, + ] + : []), + // Move - her şey için + ...(onMove + ? [ + { + key: 'move', + label: 'Taşı', + icon: 'HiArrowRightOnRectangle', + onClick: () => onMove(item), + }, + ] + : []), + // Delete - her şey için + ...(onDelete + ? [ + { + key: 'delete', + label: 'Sil', + icon: 'HiTrash', + dangerous: true, + onClick: () => onDelete(item), + }, + ] + : []), ] + // Debug için + useEffect(() => { + console.log('Action menu items for', item.name, ':', actionMenuItems) + console.log('Props:', { onCreateFolder: !!onCreateFolder, onRename: !!onRename, onMove: !!onMove, onDelete: !!onDelete }) + }, [actionMenuItems, item.name]) + const dropdownList = ( -
+
{actionMenuItems.map((menuItem) => (
{ menuItem.onClick() setDropdownOpen(false) }} > - {menuItem.icon === 'HiEye' && } - {menuItem.icon === 'HiArrowDownTray' && } - {menuItem.icon === 'HiPencil' && } + {menuItem.icon === 'HiFolderPlus' && } + {menuItem.icon === 'HiEye' && } + {menuItem.icon === 'HiArrowDownTray' && } + {menuItem.icon === 'HiPencil' && } {menuItem.icon === 'HiArrowRightOnRectangle' && ( - + )} - {menuItem.icon === 'HiTrash' && } - {menuItem.label} + {menuItem.icon === 'HiTrash' && } + {menuItem.label}
))}
) + // Resim preview komponenti + const ImagePreview = ({ src, alt }: { src: string; alt: string }) => { + const [imageError, setImageError] = useState(false) + + return ( +
+ {!imageError ? ( + {alt} setImageError(true)} + /> + ) : ( +
+ {getFileIcon(item, false)} +
+ )} +
+ ) + } + + if (viewMode === 'list') { + return ( +
+ {/* File Icon or Preview */} +
+
+ {item.type === 'file' && item.mimeType?.startsWith('image/') ? ( + + ) : ( +
+ {getFileIcon(item, false)} +
+ )} +
+
+ + {/* File Name */} +
+

+ {item.name} +

+
+ + {/* File Type */} +
+ + {getFileTypeLabel(item)} + +
+ + {/* File Size */} +
+ {item.type === 'file' && item.size ? ( + + {formatFileSize(item.size)} + + ) : ( + - + )} +
+ + {/* Modified Date */} +
+ + {formatDate(item.modifiedAt)} + +
+ + {/* Action Menu */} +
+ + + {dropdownOpen && actionMenuItems.length > 0 && ( + <> +
setDropdownOpen(false)} + /> +
+ {dropdownList} +
+ + )} +
+
+ ) + } + + // Grid view (varsayılan) return (
{/* Action Menu */} -
- setDropdownOpen(open || false)} - renderTitle={ - - } +
+ + + {dropdownOpen && actionMenuItems.length > 0 && ( + <> +
setDropdownOpen(false)} + /> +
+ {dropdownList} +
+ + )}
- {/* File/Folder Icon */} -
{getFileIcon(item)}
+ {/* File/Folder Icon or Preview */} +
+ {item.type === 'file' && item.mimeType?.startsWith('image/') ? ( +
+ +
+ ) : ( +
+ {getFileIcon(item, true)} +
+ )} +
- {/* File/Folder Name */} + {/* File/Folder Name and Details */}

{item.name}

- {/* File Size */} - {item.type === 'file' && item.size && ( -

{formatFileSize(item.size)}

+ {/* File Size and Type */} + {item.type === 'file' && ( +
+

+ {getFileTypeLabel(item)} +

+ {item.size && ( +

+ {formatFileSize(item.size)} +

+ )} +
)} {/* Folder Child Count */} @@ -212,7 +427,7 @@ const FileItem = forwardRef((props, ref) => { 'childCount' in item && typeof (item as any).childCount === 'number' && (

- {(item as any).childCount} items + {(item as any).childCount} öğe

)}