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 {