mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
recursively move nested folders
This commit is contained in:
1 parent
f1dd230828
commit
9ea88cfc40
3 files changed
+453
-123
No files matched your search
@@ -86,6 +86,29 @@ const FileExplorerColumn = ({
|
||||
const snap = useStorageExplorerStateSnapshot()
|
||||
const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
|
||||
|
||||
// Helper function to get the path for this column
|
||||
const getColumnPath = () => {
|
||||
// Use the index prop directly instead of trying to find it dynamically
|
||||
const columnIndex = index
|
||||
|
||||
if (columnIndex >= 0) {
|
||||
// Column index 0 = root level (empty path)
|
||||
// Column index 1 = first folder level (e.g., "foldher")
|
||||
// Column index 2 = second folder level (e.g., "foldher/nested-folder")
|
||||
const path =
|
||||
columnIndex === 0
|
||||
? ''
|
||||
: snap.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// Drop target for column background
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'storage-item',
|
||||
@@ -110,14 +133,12 @@ const FileExplorerColumn = ({
|
||||
if (canUpdateStorage) {
|
||||
const targetDirectory = getColumnPath()
|
||||
|
||||
console.log('Column background drop - Moving item to:', targetDirectory)
|
||||
|
||||
// Use the drag & drop function that doesn't interfere with the modal
|
||||
snap.moveFilesDragAndDrop([draggedItem], targetDirectory)
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver({ shallow: true }),
|
||||
isOver: monitor.isOver({ shallow: false }),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -138,40 +159,6 @@ const FileExplorerColumn = ({
|
||||
setIsDraggedOver(false)
|
||||
}, [column.id])
|
||||
|
||||
// Helper function to get the path for this column
|
||||
const getColumnPath = () => {
|
||||
// Use the index prop directly instead of trying to find it dynamically
|
||||
const columnIndex = index
|
||||
console.log(
|
||||
'getColumnPath called - Column index:',
|
||||
columnIndex,
|
||||
'Opened folders:',
|
||||
snap.openedFolders.map((f) => f.name)
|
||||
)
|
||||
console.log(
|
||||
'All columns:',
|
||||
snap.columns.map((col, idx) => `${idx}: ${col.name} (${col.id})`)
|
||||
)
|
||||
console.log('Current column:', column.name, 'ID:', column.id, 'Index prop:', index)
|
||||
|
||||
if (columnIndex >= 0) {
|
||||
// Column index 0 = root level (empty path)
|
||||
// Column index 1 = first folder level (e.g., "foldher")
|
||||
// Column index 2 = second folder level (e.g., "foldher/nested-folder")
|
||||
const path =
|
||||
columnIndex === 0
|
||||
? ''
|
||||
: snap.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
console.log('Calculated column path:', path, '(column index:', columnIndex, ')')
|
||||
return path
|
||||
}
|
||||
console.log('No column index found, returning empty path')
|
||||
return ''
|
||||
}
|
||||
|
||||
const haveSelectedItems = selectedItems.length > 0
|
||||
const columnItemsId = column.items.map((item) => item.id)
|
||||
const columnFiles = column.items.filter((item) => item.type === STORAGE_ROW_TYPES.FILE)
|
||||
@@ -233,9 +220,7 @@ const FileExplorerColumn = ({
|
||||
onDragLeave={(event) => {
|
||||
// Only reset if we're actually leaving the column (not just moving to a child element)
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
|
||||
console.log('Leaving column, resetting drag states')
|
||||
setIsDraggedOver(false)
|
||||
setIsInternalDragOver(false)
|
||||
}
|
||||
}}
|
||||
onClick={(event) => {
|
||||
@@ -308,67 +293,6 @@ const FileExplorerColumn = ({
|
||||
onLoadNextPage={() => onColumnLoadMore(index, column)}
|
||||
/>
|
||||
|
||||
{/* List View Background Drag & Drop (separate from content) */}
|
||||
{snap.view === STORAGE_VIEWS.LIST && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 pointer-events-none',
|
||||
isInternalDragOver && 'pointer-events-auto'
|
||||
)}
|
||||
onDragOver={(event) => {
|
||||
// Handle list view background drag over for internal file moves
|
||||
if (event.dataTransfer.types.includes('application/json')) {
|
||||
event.preventDefault()
|
||||
if (!isInternalDragOver) {
|
||||
setIsInternalDragOver(true)
|
||||
// For list view, the target is the current folder level
|
||||
const currentPath = snap.openedFolders.map((folder) => folder.name).join('/')
|
||||
setMoveToPath(currentPath)
|
||||
console.log('List view background drag over - Target path:', currentPath)
|
||||
}
|
||||
}
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
// Handle list view background drop for internal file moves
|
||||
if (event.dataTransfer.types.includes('application/json')) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
const draggedItem = JSON.parse(event.dataTransfer.getData('application/json'))
|
||||
if (draggedItem && draggedItem.type === STORAGE_ROW_TYPES.FILE) {
|
||||
// Check if this is a drop to the same location (same opened folders)
|
||||
const draggedItemPath = snap.openedFolders
|
||||
.slice(0, draggedItem.columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
const currentPath = snap.openedFolders.map((folder) => folder.name).join('/')
|
||||
|
||||
if (draggedItemPath === currentPath) {
|
||||
console.log('Same location drop detected in list view, ignoring move operation')
|
||||
return
|
||||
}
|
||||
|
||||
const targetDirectory = moveToPath
|
||||
console.log('List view background drop - Target directory:', targetDirectory)
|
||||
|
||||
// Use the new drag & drop function that doesn't interfere with the modal
|
||||
snap.moveFilesDragAndDrop([draggedItem], targetDirectory)
|
||||
|
||||
// Reset drag states
|
||||
setIsDraggedOver(false)
|
||||
setIsInternalDragOver(false)
|
||||
setMoveToPath('')
|
||||
|
||||
console.log('List view file move completed')
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse dragged item in list view:', error)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Drag drop upload CTA for when column is empty */}
|
||||
{!(snap.isSearching && itemSearchString.length > 0) &&
|
||||
column.items.length === 0 &&
|
||||
@@ -404,11 +328,9 @@ const FileExplorerColumn = ({
|
||||
folderIsEmpty={isEmpty}
|
||||
onDragLeave={() => {
|
||||
setIsDraggedOver(false)
|
||||
setIsInternalDragOver(false)
|
||||
}}
|
||||
onDrop={() => {
|
||||
setIsDraggedOver(false)
|
||||
setIsInternalDragOver(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
openFolder,
|
||||
downloadFolder,
|
||||
selectRangeItems,
|
||||
foldersBeingMoved,
|
||||
} = snap
|
||||
const { show } = useContextMenu()
|
||||
const { onCopyUrl } = useCopyUrl()
|
||||
@@ -146,6 +147,18 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview?.id, item.id)
|
||||
const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
|
||||
|
||||
// Check if this folder is currently being moved
|
||||
const isBeingMoved =
|
||||
item.type === STORAGE_ROW_TYPES.FOLDER &&
|
||||
(() => {
|
||||
const folderPath = openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.concat(item.name)
|
||||
.join('/')
|
||||
return foldersBeingMoved.has(folderPath)
|
||||
})()
|
||||
|
||||
// Drag source for files and folders
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'storage-item',
|
||||
@@ -164,26 +177,48 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
accept: 'storage-item',
|
||||
canDrop: (draggedItem: any) => {
|
||||
// Only allow drops on folders
|
||||
if (item.type !== STORAGE_ROW_TYPES.FOLDER) return false
|
||||
if (item.type !== STORAGE_ROW_TYPES.FOLDER) {
|
||||
// Not a folder, canDrop returning false
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't allow dropping on itself
|
||||
if (draggedItem.id === item.id) return false
|
||||
// Don't allow dropping on itself - compare paths instead of IDs
|
||||
const draggedItemPath = snap.openedFolders
|
||||
.slice(0, draggedItem.columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
const targetItemPath = snap.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
|
||||
const draggedItemFullPath =
|
||||
draggedItemPath.length > 0 ? `${draggedItemPath}/${draggedItem.name}` : draggedItem.name
|
||||
const targetItemFullPath =
|
||||
targetItemPath.length > 0 ? `${targetItemPath}/${item.name}` : item.name
|
||||
|
||||
if (draggedItemFullPath === targetItemFullPath) {
|
||||
// Dropping on itself (same path), canDrop returning false
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't allow dropping a folder into its own subfolder (circular reference)
|
||||
if (draggedItem.type === STORAGE_ROW_TYPES.FOLDER) {
|
||||
const draggedItemPath = snap.openedFolders
|
||||
.slice(0, draggedItem.columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
const targetPath = snap.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.concat(item.name)
|
||||
.join('/')
|
||||
|
||||
return !targetPath.startsWith(draggedItemPath + '/')
|
||||
const wouldCreateCircularReference = targetPath.startsWith(draggedItemFullPath + '/')
|
||||
|
||||
if (wouldCreateCircularReference) {
|
||||
// Would create circular reference, canDrop returning false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// canDrop returning true
|
||||
return true
|
||||
},
|
||||
drop: (draggedItem: any) => {
|
||||
@@ -202,18 +237,20 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
.join('/')
|
||||
|
||||
if (draggedItemPath === targetDirectory) {
|
||||
console.log('Same location drop detected on folder, ignoring move operation')
|
||||
// Same location drop detected on folder, ignoring move operation
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Folder drop - Moving item to:', targetDirectory)
|
||||
|
||||
// Use the drag & drop function that doesn't interfere with the modal
|
||||
snap.moveFilesDragAndDrop([draggedItem], targetDirectory)
|
||||
} else {
|
||||
// 'Drop conditions not met:', {
|
||||
// isFolder: item.type === STORAGE_ROW_TYPES.FOLDER,
|
||||
// canUpdateFiles,
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver({ shallow: true }),
|
||||
isOver: monitor.isOver({ shallow: false }),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -392,7 +429,11 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('h-full border-b border-default', isDragging && 'opacity-50')}
|
||||
className={cn(
|
||||
'h-full border-b border-default',
|
||||
isDragging && 'opacity-50',
|
||||
isBeingMoved && 'opacity-50'
|
||||
)}
|
||||
data-item-type={item.type.toLowerCase()}
|
||||
data-item-name={item.name}
|
||||
onContextMenu={(event) => {
|
||||
|
||||
@@ -50,7 +50,35 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
import { lookupMime } from 'lib/mime'
|
||||
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui'
|
||||
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress, Progress } from 'ui'
|
||||
|
||||
// Custom progress toast component for folder moves
|
||||
const FolderMoveProgressToast = ({
|
||||
current,
|
||||
total,
|
||||
folderName,
|
||||
}: {
|
||||
current: number
|
||||
total: number
|
||||
folderName: string
|
||||
}) => {
|
||||
const progress = total > 0 ? Math.round((current / total) * 100) : 0
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 w-full">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex w-full justify-between">
|
||||
<p className="text-foreground text-sm">Moving folder "{folderName}"...</p>
|
||||
<p className="text-foreground-light text-sm font-mono">{`${progress}%`}</p>
|
||||
</div>
|
||||
<Progress value={progress} className="w-full" />
|
||||
<small className="text-foreground-light text-xs">
|
||||
Moved {current} / {total} items...
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type UploadProgress = {
|
||||
percentage: number
|
||||
@@ -158,6 +186,29 @@ function createStorageExplorerState({
|
||||
state.selectedItemsToMove = items
|
||||
},
|
||||
|
||||
// Track folders that are currently being moved (for visual feedback)
|
||||
foldersBeingMoved: new Set<string>(),
|
||||
addFolderBeingMoved: (folderPath: string) => {
|
||||
state.foldersBeingMoved.add(folderPath)
|
||||
},
|
||||
removeFolderBeingMoved: (folderPath: string) => {
|
||||
state.foldersBeingMoved.delete(folderPath)
|
||||
},
|
||||
// Progress tracking for folder moves
|
||||
folderMoveProgress: new Map<
|
||||
string,
|
||||
{ current: number; total: number; toastId: string | number }
|
||||
>(),
|
||||
setFolderMoveProgress: (
|
||||
folderPath: string,
|
||||
progress: { current: number; total: number; toastId: string | number }
|
||||
) => {
|
||||
state.folderMoveProgress.set(folderPath, progress)
|
||||
},
|
||||
removeFolderMoveProgress: (folderPath: string) => {
|
||||
state.folderMoveProgress.delete(folderPath)
|
||||
},
|
||||
|
||||
selectedBucket: {} as Bucket,
|
||||
setSelectedBucket: (bucket: Bucket) => {
|
||||
state.selectedBucket = bucket
|
||||
@@ -1410,13 +1461,69 @@ function createStorageExplorerState({
|
||||
const formattedNewPathToFile = newPaths.join('/')
|
||||
let numberOfFilesMovedFail = 0
|
||||
|
||||
const toastId = toast(`Moving ${items.length} file${items.length > 1 ? 's' : ''}...`, {
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
duration: Infinity,
|
||||
})
|
||||
// Check if any of the items are folders that need special handling
|
||||
const folders = items.filter((item) => item.type === STORAGE_ROW_TYPES.FOLDER)
|
||||
const files = items.filter((item) => item.type === STORAGE_ROW_TYPES.FILE)
|
||||
|
||||
// Show appropriate toast message based on what's being moved
|
||||
let toastId: string | number | undefined
|
||||
|
||||
if (folders.length > 0 && files.length === 0) {
|
||||
// For folders only, show progress toast
|
||||
if (folders.length === 1) {
|
||||
// Single folder - show progress toast
|
||||
const folder = folders[0]
|
||||
const folderPath = state.openedFolders
|
||||
.slice(0, folder.columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
const fullFolderPath =
|
||||
folderPath.length > 0 ? `${folderPath}/${folder.name}` : folder.name
|
||||
|
||||
// Initialize progress tracking
|
||||
state.setFolderMoveProgress(fullFolderPath, { current: 0, total: 0, toastId: 'temp' })
|
||||
|
||||
toastId = toast(
|
||||
<FolderMoveProgressToast current={0} total={0} folderName={folder.name} />,
|
||||
{
|
||||
duration: Infinity,
|
||||
}
|
||||
)
|
||||
|
||||
// Update the progress tracking with the actual toast ID
|
||||
state.setFolderMoveProgress(fullFolderPath, { current: 0, total: 0, toastId })
|
||||
} else {
|
||||
// Multiple folders - show simple toast
|
||||
toastId = toast(`Moving ${folders.length} folders...`, {
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
duration: Infinity,
|
||||
})
|
||||
}
|
||||
} else if (folders.length === 0 && files.length > 0) {
|
||||
toastId = toast(`Moving ${files.length} file${files.length > 1 ? 's' : ''}...`, {
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
duration: Infinity,
|
||||
})
|
||||
} else {
|
||||
toastId = toast(`Moving ${items.length} item${items.length > 1 ? 's' : ''}...`, {
|
||||
description: STORAGE_PROGRESS_INFO_TEXT,
|
||||
duration: Infinity,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle folder moves first (create new folder, move contents, delete original)
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
await state.moveFolderWithContents(folder, formattedNewPathToFile)
|
||||
} catch (error: any) {
|
||||
numberOfFilesMovedFail += 1
|
||||
toast.error(`Failed to move folder ${folder.name}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular file moves
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
files.map(async (item) => {
|
||||
const pathToFile = state.openedFolders
|
||||
.slice(0, item.columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
@@ -1441,12 +1548,12 @@ function createStorageExplorerState({
|
||||
)
|
||||
|
||||
if (numberOfFilesMovedFail === items.length) {
|
||||
toast.error('Failed to move files')
|
||||
toast.error('Failed to move items')
|
||||
} else {
|
||||
toast(
|
||||
`Successfully moved ${
|
||||
items.length - numberOfFilesMovedFail
|
||||
} files to ${formattedNewPathToFile.length > 0 ? formattedNewPathToFile : 'the root of your bucket'}`
|
||||
} items to ${formattedNewPathToFile.length > 0 ? formattedNewPathToFile : 'the root of your bucket'}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1456,6 +1563,266 @@ function createStorageExplorerState({
|
||||
await state.refetchAllOpenedFolders()
|
||||
},
|
||||
|
||||
// New function to handle folder moves by creating new folder and moving contents
|
||||
moveFolderWithContents: async (folder: StorageItemWithColumn, targetPath: string) => {
|
||||
const folderName = folder.name
|
||||
const sourcePath = state.openedFolders
|
||||
.slice(0, folder.columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
|
||||
const sourceFullPath = sourcePath.length > 0 ? `${sourcePath}/${folderName}` : folderName
|
||||
const targetFullPath = targetPath.length > 0 ? `${targetPath}/${folderName}` : folderName
|
||||
|
||||
// Add folder to the "being moved" set for visual feedback
|
||||
state.addFolderBeingMoved(targetFullPath)
|
||||
|
||||
// Create a placeholder file to create the new folder
|
||||
const placeholderContent = new Blob([''], { type: 'text/plain' })
|
||||
|
||||
try {
|
||||
// Check if supabaseClient is available
|
||||
if (!state.supabaseClient) {
|
||||
throw new Error('Supabase Client is not available')
|
||||
}
|
||||
|
||||
// Check if selectedBucket and projectRef are available
|
||||
if (!state.selectedBucket || !state.selectedBucket.id) {
|
||||
throw new Error('Selected bucket is not available')
|
||||
}
|
||||
|
||||
if (!state.projectRef) {
|
||||
throw new Error('Project reference is not available')
|
||||
}
|
||||
|
||||
// Create the new folder by uploading a placeholder file
|
||||
const { data: uploadData, error: uploadError } = await (
|
||||
await state.supabaseClient()
|
||||
).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.upload(targetFullPath + '/.folder-placeholder', placeholderContent, {
|
||||
upsert: false,
|
||||
})
|
||||
|
||||
if (uploadError) {
|
||||
throw new Error(`Failed to create target folder: ${uploadError.message}`)
|
||||
}
|
||||
|
||||
// Recursively discover all files and folders in the source directory
|
||||
const getAllFilesRecursively = async (
|
||||
path: string
|
||||
): Promise<Array<{ path: string; name: string; isFolder: boolean }>> => {
|
||||
const allItems: Array<{ path: string; name: string; isFolder: boolean }> = []
|
||||
|
||||
const { data: items, error: listError } = await (await state.supabaseClient()).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.list(path, {
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
sortBy: { column: 'name', order: 'asc' },
|
||||
})
|
||||
|
||||
if (listError) {
|
||||
console.error(`Failed to list contents of ${path}:`, listError)
|
||||
return allItems
|
||||
}
|
||||
|
||||
if (!items) return allItems
|
||||
|
||||
for (const item of items) {
|
||||
if (item.name === '.folder-placeholder') continue
|
||||
|
||||
const itemPath = `${path}/${item.name}`
|
||||
|
||||
// Check if this is a folder by trying to list its contents
|
||||
try {
|
||||
const { data: subItems } = await (await state.supabaseClient()).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.list(itemPath, { limit: 1 })
|
||||
|
||||
if (subItems && subItems.length > 0) {
|
||||
// This is a folder, recursively get its contents
|
||||
const subFolderItems = await getAllFilesRecursively(itemPath)
|
||||
allItems.push(...subFolderItems)
|
||||
} else {
|
||||
// This is a file
|
||||
allItems.push({ path: itemPath, name: item.name, isFolder: false })
|
||||
}
|
||||
} catch (error) {
|
||||
// If listing fails, assume it's a file
|
||||
allItems.push({ path: itemPath, name: item.name, isFolder: false })
|
||||
}
|
||||
}
|
||||
|
||||
return allItems
|
||||
}
|
||||
|
||||
// Get all files recursively
|
||||
const allItemsToMove = await getAllFilesRecursively(sourceFullPath)
|
||||
const filesToMove = allItemsToMove.filter((item) => !item.isFolder)
|
||||
|
||||
// Update progress tracking with total count
|
||||
const initialProgressData = state.folderMoveProgress.get(sourceFullPath)
|
||||
if (initialProgressData) {
|
||||
state.setFolderMoveProgress(sourceFullPath, {
|
||||
...initialProgressData,
|
||||
total: filesToMove.length,
|
||||
})
|
||||
}
|
||||
|
||||
// Move all files from source to target
|
||||
let movedCount = 0
|
||||
for (const fileItem of filesToMove) {
|
||||
// Calculate the relative path from the source folder
|
||||
const relativePath = fileItem.path.substring(sourceFullPath.length + 1) // +1 for the '/'
|
||||
const toPath = `${targetFullPath}/${relativePath}`
|
||||
|
||||
try {
|
||||
const moveParams = {
|
||||
projectRef: state.projectRef,
|
||||
bucketId: state.selectedBucket.id,
|
||||
from: fileItem.path,
|
||||
to: toPath,
|
||||
}
|
||||
|
||||
await moveStorageObject(moveParams)
|
||||
movedCount++
|
||||
|
||||
// Update progress and toast
|
||||
const progressData = state.folderMoveProgress.get(sourceFullPath)
|
||||
if (progressData) {
|
||||
const newProgress = { ...progressData, current: movedCount }
|
||||
state.setFolderMoveProgress(sourceFullPath, newProgress)
|
||||
|
||||
// Update the toast with new progress
|
||||
toast.loading(
|
||||
<FolderMoveProgressToast
|
||||
current={movedCount}
|
||||
total={filesToMove.length}
|
||||
folderName={folderName}
|
||||
/>,
|
||||
{ id: progressData.toastId }
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to move file ${fileItem.path}:`, error)
|
||||
// Continue with other files even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
// Show completion toast if we have progress tracking
|
||||
const finalProgressData = state.folderMoveProgress.get(sourceFullPath)
|
||||
if (finalProgressData && filesToMove.length > 0) {
|
||||
toast.success(
|
||||
<FolderMoveProgressToast
|
||||
current={filesToMove.length}
|
||||
total={filesToMove.length}
|
||||
folderName={folderName}
|
||||
/>,
|
||||
{ id: finalProgressData.toastId, duration: 3000 }
|
||||
)
|
||||
}
|
||||
|
||||
// Now delete all files from the source folder to effectively "delete" the folder
|
||||
const sourceFilePaths = filesToMove.map((fileItem) => fileItem.path)
|
||||
|
||||
if (sourceFilePaths.length > 0) {
|
||||
// Delete files in batches to avoid overwhelming the API
|
||||
const batchSize = 10
|
||||
for (let i = 0; i < sourceFilePaths.length; i += batchSize) {
|
||||
const batch = sourceFilePaths.slice(i, i + batchSize)
|
||||
const { error: deleteError } = await (await state.supabaseClient()).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.remove(batch)
|
||||
|
||||
if (deleteError) {
|
||||
console.error(`Failed to delete batch of files:`, deleteError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('Source folder is empty, no files to delete')
|
||||
}
|
||||
|
||||
// Recursively clean up the entire source folder hierarchy
|
||||
const cleanupFolderRecursively = async (path: string) => {
|
||||
try {
|
||||
const { data: items, error: listError } = await (await state.supabaseClient()).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.list(path, {
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
sortBy: { column: 'name', order: 'asc' },
|
||||
})
|
||||
|
||||
if (listError || !items) return
|
||||
|
||||
// Remove all items in this directory
|
||||
if (items.length > 0) {
|
||||
const itemPaths = items.map((item) => `${path}/${item.name}`)
|
||||
const { error: removeError } = await (await state.supabaseClient()).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.remove(itemPaths)
|
||||
|
||||
if (removeError) {
|
||||
console.error(`Failed to remove items from ${path}:`, removeError)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to remove common hidden files
|
||||
const hiddenFiles = [
|
||||
`${path}/.DS_Store`,
|
||||
`${path}/Thumbs.db`,
|
||||
`${path}/.empty`,
|
||||
`${path}/.folder-placeholder`,
|
||||
]
|
||||
|
||||
try {
|
||||
await (await state.supabaseClient()).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.remove(hiddenFiles)
|
||||
} catch (hiddenError) {
|
||||
// Hidden file removal errors are normal
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning up ${path}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the source folder hierarchy recursively
|
||||
await cleanupFolderRecursively(sourceFullPath)
|
||||
|
||||
// Delete the target folder placeholder
|
||||
const { error: deleteTargetError } = await (await state.supabaseClient()).storage
|
||||
.from(state.selectedBucket.id)
|
||||
.remove([targetFullPath + '/.folder-placeholder'])
|
||||
|
||||
if (deleteTargetError) {
|
||||
console.error('Failed to delete target folder placeholder:', deleteTargetError)
|
||||
}
|
||||
|
||||
// Remove folder from "being moved" set
|
||||
state.removeFolderBeingMoved(targetFullPath)
|
||||
|
||||
// Clean up progress tracking
|
||||
state.removeFolderMoveProgress(sourceFullPath)
|
||||
|
||||
try {
|
||||
await state.refetchAllOpenedFolders()
|
||||
console.log('Storage explorer refresh completed')
|
||||
} catch (refreshError) {
|
||||
console.error('Failed to refresh storage explorer:', refreshError)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Remove folder from "being moved" set on error
|
||||
state.removeFolderBeingMoved(targetFullPath)
|
||||
|
||||
// Clean up progress tracking on error
|
||||
state.removeFolderMoveProgress(sourceFullPath)
|
||||
|
||||
throw new Error(`Failed to move folder ${folderName}: ${error.message}`)
|
||||
}
|
||||
},
|
||||
|
||||
deleteFiles: async ({
|
||||
files,
|
||||
isDeleteFolder = false,
|
||||
|
||||
Reference in new issue
Block a user