fix: add cross-platform path delimiter support for file renaming and navigation logic #9

Merged
jamie merged 2 commits from v1.1.4 into main 2026-06-10 23:45:02 +01:00
2 changed files with 77 additions and 7 deletions
+25 -6
View File
@@ -573,9 +573,10 @@ window.handleContextMenu = async (action) => {
const item = selectedItems[0]; const item = selectedItems[0];
const newName = prompt(`Enter new name for ${item.name}:`, item.name); const newName = prompt(`Enter new name for ${item.name}:`, item.name);
if (newName && newName !== 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; pathParts[pathParts.length - 1] = newName;
const newPath = pathParts.join('/'); const newPath = pathParts.join(sep);
await Rename(currentConn, item.path, newPath); await Rename(currentConn, item.path, newPath);
refreshCurrentDir(); refreshCurrentDir();
@@ -595,9 +596,18 @@ window.handleContextMenu = async (action) => {
window.navigateUp = () => { window.navigateUp = () => {
if (!currentPath || currentPath === '/') return; 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(); 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); loadDirectory(currentConn, currentPath);
}; };
@@ -641,9 +651,18 @@ window.loadTransferDestDirectory = async (connId, path) => {
tr.innerHTML = `<td>📁 ..</td>`; tr.innerHTML = `<td>📁 ..</td>`;
tr.style.cursor = 'pointer'; tr.style.cursor = 'pointer';
tr.ondblclick = () => { 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(); 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); tbody.appendChild(tr);
} }
+51
View File
@@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials" "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"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
appcfg "goexplore/internal/config" appcfg "goexplore/internal/config"
"goexplore/internal/explorer" "goexplore/internal/explorer"
) )
@@ -243,13 +244,63 @@ func (e *S3Explorer) Delete(path string) error {
return err return err
} }
// Try to delete the exact object (might be a file or a folder marker)
_, err = e.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ _, err = e.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
Bucket: aws.String(bucket), Bucket: aws.String(bucket),
Key: aws.String(subpath), Key: aws.String(subpath),
}) })
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 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 { func (e *S3Explorer) Rename(src, dst string) error {
bucket1, subpath1, _, err := e.getBucketAndPath(src) bucket1, subpath1, _, err := e.getBucketAndPath(src)
if err != nil { if err != nil {