Dosya Yöneticisi ana klasör tools düzeltildi

This commit is contained in:
Sedat Öztürk 2025-10-26 11:59:02 +03:00
parent c93007cc07
commit f839d1fec0
4 changed files with 401 additions and 121 deletions

View file

@ -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<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,
@ -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<Stream> 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<Stream> GetFilePreviewAsync(string id)

View file

@ -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)

View file

@ -390,17 +390,30 @@ const FileManager = () => {
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
</div>
{/* Content */}
{/* Files Grid/List */}
{loading ? (
<div className="flex justify-center items-center py-20">
<Spinner size={40} />
<Spinner size="lg" />
</div>
) : (
<>
{/* List View Header */}
{viewMode === 'list' && (
<div className="grid grid-cols-12 gap-4 px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 border-b dark:border-gray-700 mb-2">
<div className="col-span-1"></div> {/* Icon column */}
<div className="col-span-4">İsim</div>
<div className="col-span-2">Tür</div>
<div className="col-span-2">Boyut</div>
<div className="col-span-2">Değiştirilme</div>
<div className="col-span-1"></div> {/* Actions column */}
</div>
)}
<div
className={classNames(
viewMode === 'grid'
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4'
: 'space-y-2',
: 'space-y-1',
)}
>
{filteredItems.length === 0 ? (
@ -415,17 +428,37 @@ const FileManager = () => {
<FileItem
key={item.id}
item={item}
viewMode={viewMode}
selected={selectedItems.includes(item.id)}
onSelect={handleItemSelect}
onDoubleClick={handleItemDoubleClick}
onCreateFolder={(parentItem) => {
// 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(<Notification type="info">Move özelliği yakında eklenecek</Notification>)
}}
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'ıılabilir
toast.push(<Notification type="info">Resim önizleme özelliği yakında eklenecek</Notification>)
} else {
// Diğer dosya tipleri için download
handleDownload(item)
}
} : undefined}
/>
))
)}
</div>
</>
)}
{/* Modals */}

View file

@ -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 <HiFolder className="h-8 w-8 text-blue-500" />
return <HiFolder className={`${iconSize} text-blue-500`} />
}
const extension = item.extension?.toLowerCase()
const mimeType = item.mimeType?.toLowerCase()
if (mimeType?.startsWith('image/')) {
return <HiPhoto className="h-8 w-8 text-green-500" />
return <HiPhoto className={`${iconSize} text-green-500`} />
}
if (mimeType?.startsWith('video/')) {
return <HiFilm className="h-8 w-8 text-purple-500" />
return <HiFilm className={`${iconSize} text-purple-500`} />
}
if (mimeType?.startsWith('audio/')) {
return <HiMusicalNote className="h-8 w-8 text-pink-500" />
return <HiMusicalNote className={`${iconSize} text-pink-500`} />
}
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension || '')) {
return <HiArchiveBox className="h-8 w-8 text-orange-500" />
return <HiArchiveBox className={`${iconSize} text-orange-500`} />
}
return <HiDocument className="h-8 w-8 text-gray-500" />
return <HiDocument className={`${iconSize} text-gray-500`} />
}
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<HTMLDivElement, FileItemProps>((props, ref) => {
const {
item,
selected = false,
viewMode = 'grid',
onSelect,
onDoubleClick,
onCreateFolder,
onRename,
onMove,
onDelete,
@ -92,119 +136,290 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((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),
},
]
: []),
// Create Folder - sadece klasörler için
...(item.type === 'folder' && onCreateFolder
? [
{
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),
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 = (
<div className="py-1">
<div className="py-1 min-w-36">
{actionMenuItems.map((menuItem) => (
<div
key={menuItem.key}
className={classNames(
'flex items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700',
menuItem.dangerous && 'text-red-600 dark:text-red-400',
'flex items-center px-2 py-1 text-xs cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
menuItem.dangerous && 'text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20',
)}
onClick={() => {
menuItem.onClick()
setDropdownOpen(false)
}}
>
{menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-2" />}
{menuItem.icon === 'HiArrowDownTray' && <HiArrowDownTray className="h-4 w-4 mr-2" />}
{menuItem.icon === 'HiPencil' && <HiPencil className="h-4 w-4 mr-2" />}
{menuItem.icon === 'HiFolderPlus' && <HiFolderPlus className="h-4 w-4 mr-3 text-blue-500" />}
{menuItem.icon === 'HiEye' && <HiEye className="h-4 w-4 mr-3 text-gray-500" />}
{menuItem.icon === 'HiArrowDownTray' && <HiArrowDownTray className="h-4 w-4 mr-3 text-green-500" />}
{menuItem.icon === 'HiPencil' && <HiPencil className="h-4 w-4 mr-3 text-orange-500" />}
{menuItem.icon === 'HiArrowRightOnRectangle' && (
<HiArrowRightOnRectangle className="h-4 w-4 mr-2" />
<HiArrowRightOnRectangle className="h-4 w-4 mr-3 text-purple-500" />
)}
{menuItem.icon === 'HiTrash' && <HiTrash className="h-4 w-4 mr-2" />}
{menuItem.label}
{menuItem.icon === 'HiTrash' && <HiTrash className="h-4 w-4 mr-3" />}
<span className="flex-1">{menuItem.label}</span>
</div>
))}
</div>
)
// Resim preview komponenti
const ImagePreview = ({ src, alt }: { src: string; alt: string }) => {
const [imageError, setImageError] = useState(false)
return (
<div className="w-full h-full bg-gray-100 dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden">
{!imageError ? (
<img
src={src}
alt={alt}
className="max-w-full max-h-full object-cover"
onError={() => setImageError(true)}
/>
) : (
<div className="flex items-center justify-center w-full h-full">
{getFileIcon(item, false)}
</div>
)}
</div>
)
}
if (viewMode === 'list') {
return (
<div
ref={ref}
className={classNames(
'relative group p-4 border rounded-lg cursor-pointer transition-all duration-200',
'hover:border-blue-300 hover:shadow-md',
'relative group grid grid-cols-12 gap-4 p-3 border rounded-lg cursor-pointer transition-all duration-200',
'hover:border-blue-300 hover:bg-gray-50 dark:hover:bg-gray-700/50',
selected
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-md'
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800',
className,
)}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
{/* File Icon or Preview */}
<div className="col-span-1 flex items-center">
<div className="w-8 h-8">
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
<ImagePreview src={`/api/app/file-management/${item.id}/download-file`} alt={item.name} />
) : (
<div className="w-full h-full flex items-center justify-center">
{getFileIcon(item, false)}
</div>
)}
</div>
</div>
{/* File Name */}
<div className="col-span-4 flex items-center min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{item.name}
</p>
</div>
{/* File Type */}
<div className="col-span-2 flex items-center">
<span className="text-sm text-gray-500 dark:text-gray-400">
{getFileTypeLabel(item)}
</span>
</div>
{/* File Size */}
<div className="col-span-2 flex items-center">
{item.type === 'file' && item.size ? (
<span className="text-sm text-gray-500 dark:text-gray-400">
{formatFileSize(item.size)}
</span>
) : (
<span className="text-sm text-gray-500 dark:text-gray-400">-</span>
)}
</div>
{/* Modified Date */}
<div className="col-span-2 flex items-center">
<span className="text-sm text-gray-500 dark:text-gray-400">
{formatDate(item.modifiedAt)}
</span>
</div>
{/* Action Menu */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<Dropdown
onToggle={(open) => setDropdownOpen(open || false)}
renderTitle={
<div className="col-span-1 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity relative">
<button
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(!dropdownOpen)
}}
>
<HiEllipsisVertical className="h-4 w-4" />
</button>
}
>
{dropdownOpen && actionMenuItems.length > 0 && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-1 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</Dropdown>
</div>
</>
)}
</div>
</div>
)
}
// Grid view (varsayılan)
return (
<div
ref={ref}
className={classNames(
'relative group p-4 border rounded-xl cursor-pointer transition-all duration-300',
'hover:border-blue-400 hover:shadow-lg hover:-translate-y-1',
selected
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 shadow-lg'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm',
className,
)}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
{/* Action Menu */}
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
<button
className="p-1.5 rounded-full bg-white dark:bg-gray-700 shadow-md hover:shadow-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-all duration-200"
onClick={(e) => {
e.stopPropagation()
setDropdownOpen(!dropdownOpen)
}}
>
<HiEllipsisVertical className="h-4 w-4 text-gray-600 dark:text-gray-300" />
</button>
{dropdownOpen && actionMenuItems.length > 0 && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setDropdownOpen(false)}
/>
<div className="absolute right-0 top-full mt-2 z-20 bg-white dark:bg-gray-800 rounded-md shadow-lg border dark:border-gray-600">
{dropdownList}
</div>
</>
)}
</div>
{/* File/Folder Icon */}
<div className="flex justify-center mb-3">{getFileIcon(item)}</div>
{/* File/Folder Icon or Preview */}
<div className="flex justify-center mb-3">
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
<div className="w-16 h-16">
<ImagePreview src={`/api/app/file-management/${item.id}/download-file`} alt={item.name} />
</div>
) : (
<div className="flex justify-center">
{getFileIcon(item, true)}
</div>
)}
</div>
{/* File/Folder Name */}
{/* File/Folder Name and Details */}
<div className="text-center">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate mb-1">
{item.name}
</p>
{/* File Size */}
{item.type === 'file' && item.size && (
<p className="text-xs text-gray-500 dark:text-gray-400">{formatFileSize(item.size)}</p>
{/* File Size and Type */}
{item.type === 'file' && (
<div className="space-y-1">
<p className="text-xs text-gray-500 dark:text-gray-400">
{getFileTypeLabel(item)}
</p>
{item.size && (
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatFileSize(item.size)}
</p>
)}
</div>
)}
{/* Folder Child Count */}
@ -212,7 +427,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
'childCount' in item &&
typeof (item as any).childCount === 'number' && (
<p className="text-xs text-gray-500 dark:text-gray-400">
{(item as any).childCount} items
{(item as any).childCount} öğe
</p>
)}
</div>