Files
rclone/backend/mega/mega_internal_test.go
Nick Craig-Wood 80928a93c4 mega: fix deleted and moved files still showing in listings - fixes #9554
The mega backend keeps the whole account as an in-memory tree which go-mega
reconciles asynchronously from the server's event stream. After an upload,
delete or move, the optimistic local update could be undone moments later when
go-mega replayed the corresponding server event, re-adding a node to its parent.
In a long-running process such as mount or serve this showed up as a deleted
file lingering in directory listings (a "ghost") until the next event poll, or
reappearing after a move.

Wait for the server to confirm uploads, deletes and moves so the in-memory tree
is settled before returning, matching what Rmdir and Purge already do. The move
wait was also previously started after the server-side move so only the rename
was covered.
2026-06-29 10:51:27 +01:00

64 lines
2.0 KiB
Go

package mega
import (
"bytes"
"context"
"testing"
"time"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/fstest/fstests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// InternalTestGhostAfterRemove checks that a file removed in a long-running
// process disappears from listings straight away rather than lingering as a
// ghost until go-mega's next event poll reconciles the in-memory tree.
//
// The ghost is a race between the upload's server event being replayed and
// the delete, so it puts, removes and lists in a tight loop to make it show
// up reliably.
func (f *Fs) InternalTestGhostAfterRemove(t *testing.T) {
ctx := context.Background()
// Exercise the default soft-delete path - the hard_delete=true path
// hits a separate bug in go-mega's Delete which leaves the node in its
// parent's children regardless of what rclone does.
if f.opt.HardDelete {
oldHardDelete := f.opt.HardDelete
f.opt.HardDelete = false
defer func() { f.opt.HardDelete = oldHardDelete }()
}
dir := "ghost-test"
require.NoError(t, f.Mkdir(ctx, dir))
defer func() { _ = f.Rmdir(ctx, dir) }()
contents := []byte("ghost test contents")
for i := range 10 {
remote := dir + "/test.txt"
src := object.NewStaticObjectInfo(remote, time.Now(), int64(len(contents)), true, nil, nil)
obj, err := f.Put(ctx, bytes.NewReader(contents), src)
require.NoError(t, err)
require.NoError(t, obj.Remove(ctx))
// The file must be gone from the listing straight away - if the
// in-memory tree isn't settled it lingers as a ghost.
entries, err := f.List(ctx, dir)
require.NoError(t, err)
names := make([]string, 0, len(entries))
for _, entry := range entries {
names = append(names, entry.Remote())
}
assert.NotContains(t, names, remote, "file should be gone immediately after remove (iteration %d)", i)
}
}
// InternalTest dispatches the backend specific internal tests
func (f *Fs) InternalTest(t *testing.T) {
t.Run("GhostAfterRemove", f.InternalTestGhostAfterRemove)
}
var _ fstests.InternalTester = (*Fs)(nil)