diff --git a/frontend/src/main.js b/frontend/src/main.js index 9dfee25..f4ce6c2 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -573,9 +573,10 @@ window.handleContextMenu = async (action) => { const item = selectedItems[0]; const newName = prompt(`Enter new name for ${item.name}:`, item.name); if (newName && newName !== item.name) { - const pathParts = item.path.split('/'); + const sep = item.path.includes('\\') ? '\\' : '/'; + const pathParts = item.path.split(/[/\\]/); pathParts[pathParts.length - 1] = newName; - const newPath = pathParts.join('/'); + const newPath = pathParts.join(sep); await Rename(currentConn, item.path, newPath); refreshCurrentDir(); @@ -595,9 +596,18 @@ window.handleContextMenu = async (action) => { window.navigateUp = () => { if (!currentPath || currentPath === '/') return; - const parts = currentPath.split('/').filter(p => p); + const startsWithSlash = currentPath.startsWith('/'); + const sep = currentPath.includes('\\') ? '\\' : '/'; + const parts = currentPath.split(/[/\\]/).filter(p => p); parts.pop(); - currentPath = parts.length ? '/' + parts.join('/') : ''; + if (parts.length === 0) { + currentPath = ''; + } else { + currentPath = (startsWithSlash ? '/' : '') + parts.join(sep); + if (!startsWithSlash && parts.length === 1 && parts[0].endsWith(':')) { + currentPath += sep; + } + } loadDirectory(currentConn, currentPath); }; @@ -641,9 +651,18 @@ window.loadTransferDestDirectory = async (connId, path) => { tr.innerHTML = `📁 ..`; tr.style.cursor = 'pointer'; tr.ondblclick = () => { - const parts = path.split('/').filter(p => p); + const startsWithSlash = path.startsWith('/'); + const sep = path.includes('\\') ? '\\' : '/'; + const parts = path.split(/[/\\]/).filter(p => p); parts.pop(); - loadTransferDestDirectory(connId, parts.length ? '/' + parts.join('/') : ''); + let newPath = ''; + if (parts.length > 0) { + newPath = (startsWithSlash ? '/' : '') + parts.join(sep); + if (!startsWithSlash && parts.length === 1 && parts[0].endsWith(':')) { + newPath += sep; + } + } + loadTransferDestDirectory(connId, newPath); }; tbody.appendChild(tr); } diff --git a/internal/protocols/s3/s3.go b/internal/protocols/s3/s3.go index cbded41..3bcd7e2 100644 --- a/internal/protocols/s3/s3.go +++ b/internal/protocols/s3/s3.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" appcfg "goexplore/internal/config" "goexplore/internal/explorer" ) @@ -243,11 +244,61 @@ func (e *S3Explorer) Delete(path string) error { return err } + // Try to delete the exact object (might be a file or a folder marker) _, err = e.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ Bucket: aws.String(bucket), Key: aws.String(subpath), }) - return err + if err != nil { + // Ignore error; it might just be a prefix with no exact marker + } + + // Also treat subpath as a folder prefix and delete all matching objects + prefix := subpath + if !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + + var continuationToken *string + for { + listInput := &s3.ListObjectsV2Input{ + Bucket: aws.String(bucket), + Prefix: aws.String(prefix), + ContinuationToken: continuationToken, + } + listOutput, err := e.client.ListObjectsV2(context.TODO(), listInput) + if err != nil { + return err + } + + if len(listOutput.Contents) > 0 { + var objectsToDelete []types.ObjectIdentifier + for _, obj := range listOutput.Contents { + objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{ + Key: obj.Key, + }) + } + + _, err = e.client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{ + Bucket: aws.String(bucket), + Delete: &types.Delete{ + Objects: objectsToDelete, + Quiet: aws.Bool(true), + }, + }) + if err != nil { + return err + } + } + + if listOutput.IsTruncated != nil && *listOutput.IsTruncated { + continuationToken = listOutput.NextContinuationToken + } else { + break + } + } + + return nil } func (e *S3Explorer) Rename(src, dst string) error {