dropbox: fix ChangeNotify when the root's case differs from Dropbox's - fixes #9692
Dropbox is case insensitive and the path_display it returns in change notifications may not match the case of the configured root. Before this change the root was trimmed with a case sensitive prefix match, so when the cases differed the full path was passed to the ChangeNotify callback and the notification was ignored. This trims the root case insensitively while preserving the display case of the remaining path.
This commit is contained in:
committed by
Nick Craig-Wood
parent
bdeb95ae01
commit
4af64270cc
@@ -392,8 +392,8 @@ type Fs struct {
|
||||
sharing sharing.ContextClient // as above, but for generating sharing links
|
||||
users users.ContextClient // as above, but for accessing user information
|
||||
team team.ContextClient // for the Teams API
|
||||
slashRoot string // root with "/" prefix, lowercase
|
||||
slashRootSlash string // root with "/" prefix and postfix, lowercase
|
||||
slashRoot string // root with "/" prefix
|
||||
slashRootSlash string // root with "/" prefix and postfix
|
||||
pacer *fs.Pacer // To pace the API calls
|
||||
ns string // The namespace we are using or "" for none
|
||||
batcher *batcher.Batcher[*files.UploadSessionFinishArg, *files.FileMetadata]
|
||||
@@ -1672,6 +1672,34 @@ func (f *Fs) changeNotifyCursor(ctx context.Context) (cursor string, err error)
|
||||
return startCursor.Cursor, nil
|
||||
}
|
||||
|
||||
// trimPrefixFold returns s with the leading prefix removed, matching
|
||||
// case insensitively rune by rune so folded runes of differing UTF-8
|
||||
// length still match.
|
||||
//
|
||||
// If s is exactly the root that prefix names (prefix without its
|
||||
// trailing "/") it returns "". If prefix doesn't match it returns s
|
||||
// unchanged.
|
||||
func trimPrefixFold(s, prefix string) string {
|
||||
if strings.HasSuffix(prefix, "/") && strings.EqualFold(s, strings.TrimSuffix(prefix, "/")) {
|
||||
return ""
|
||||
}
|
||||
|
||||
offset := 0
|
||||
for prefix != "" {
|
||||
if offset >= len(s) {
|
||||
return s
|
||||
}
|
||||
sRune, sSize := utf8.DecodeRuneInString(s[offset:])
|
||||
prefixRune, prefixSize := utf8.DecodeRuneInString(prefix)
|
||||
if !strings.EqualFold(string(sRune), string(prefixRune)) {
|
||||
return s
|
||||
}
|
||||
offset += sSize
|
||||
prefix = prefix[prefixSize:]
|
||||
}
|
||||
return s[offset:]
|
||||
}
|
||||
|
||||
func (f *Fs) changeNotifyRunner(ctx context.Context, notifyFunc func(string, fs.EntryType), startCursor string) (newCursor string, err error) {
|
||||
cursor := startCursor
|
||||
var res *files.ListFolderLongpollResult
|
||||
@@ -1731,17 +1759,18 @@ func (f *Fs) changeNotifyRunner(ctx context.Context, notifyFunc func(string, fs.
|
||||
switch info := entry.(type) {
|
||||
case *files.FolderMetadata:
|
||||
entryType = fs.EntryDirectory
|
||||
entryPath = strings.TrimPrefix(info.PathDisplay, f.slashRootSlash)
|
||||
entryPath = info.PathDisplay
|
||||
case *files.FileMetadata:
|
||||
entryType = fs.EntryObject
|
||||
entryPath = strings.TrimPrefix(info.PathDisplay, f.slashRootSlash)
|
||||
entryPath = info.PathDisplay
|
||||
case *files.DeletedMetadata:
|
||||
entryType = fs.EntryObject
|
||||
entryPath = strings.TrimPrefix(info.PathDisplay, f.slashRootSlash)
|
||||
entryPath = info.PathDisplay
|
||||
default:
|
||||
fs.Errorf(entry, "dropbox ChangeNotify: ignoring unknown EntryType %T", entry)
|
||||
continue
|
||||
}
|
||||
entryPath = trimPrefixFold(entryPath, f.slashRootSlash)
|
||||
|
||||
if entryPath != "" {
|
||||
notifyFunc(f.opt.Enc.ToStandardPath(entryPath), entryType)
|
||||
|
||||
@@ -86,6 +86,71 @@ func TestInternalGetMetadataCancellation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type changeNotifyClient struct {
|
||||
files.ContextClient
|
||||
entries []files.IsMetadata
|
||||
}
|
||||
|
||||
func (c changeNotifyClient) ListFolderLongpollContext(context.Context, *files.ListFolderLongpollArg) (*files.ListFolderLongpollResult, error) {
|
||||
return files.NewListFolderLongpollResult(true), nil
|
||||
}
|
||||
|
||||
func (c changeNotifyClient) ListFolderContinueContext(context.Context, *files.ListFolderContinueArg) (*files.ListFolderResult, error) {
|
||||
return files.NewListFolderResult(c.entries, "next", false), nil
|
||||
}
|
||||
|
||||
func TestTrimPrefixFold(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
path string
|
||||
prefix string
|
||||
want string
|
||||
}{
|
||||
{name: "exact casing", path: "/Docs/Sub", prefix: "/Docs/", want: "Sub"},
|
||||
{name: "different casing", path: "/docs/Sub", prefix: "/Docs/", want: "Sub"},
|
||||
{name: "different UTF-8 lengths", path: "/K/Sub", prefix: "/K/", want: "Sub"},
|
||||
{name: "exact root", path: "/docs", prefix: "/Docs/", want: ""},
|
||||
{name: "sibling boundary", path: "/Docs2/File", prefix: "/Docs/", want: "/Docs2/File"},
|
||||
{name: "root remote", path: "/File", prefix: "/", want: "File"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.want, trimPrefixFold(test.path, test.prefix))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeNotifyTrimsRootCaseInsensitively(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := changeNotifyClient{entries: []files.IsMetadata{
|
||||
&files.FolderMetadata{Metadata: files.Metadata{PathDisplay: "/docs/Sub"}},
|
||||
&files.FileMetadata{Metadata: files.Metadata{PathDisplay: "/docs/Sub/File.TXT"}},
|
||||
&files.DeletedMetadata{Metadata: files.Metadata{PathDisplay: "/docs/Gone.md"}},
|
||||
}}
|
||||
f := &Fs{
|
||||
ci: fs.GetConfig(ctx),
|
||||
srv: client,
|
||||
svc: client,
|
||||
slashRootSlash: "/Docs/",
|
||||
pacer: fs.NewPacer(ctx, pacer.NewDefault()),
|
||||
}
|
||||
|
||||
type notification struct {
|
||||
path string
|
||||
entryType fs.EntryType
|
||||
}
|
||||
var notifications []notification
|
||||
cursor, err := f.changeNotifyRunner(ctx, func(path string, entryType fs.EntryType) {
|
||||
notifications = append(notifications, notification{path: path, entryType: entryType})
|
||||
}, "start")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "next", cursor)
|
||||
assert.Equal(t, []notification{
|
||||
{path: "Sub", entryType: fs.EntryDirectory},
|
||||
{path: "Sub/File.TXT", entryType: fs.EntryObject},
|
||||
{path: "Gone.md", entryType: fs.EntryObject},
|
||||
}, notifications)
|
||||
}
|
||||
|
||||
func TestInternalCheckPathLength(t *testing.T) {
|
||||
rep := func(n int, r rune) (out string) {
|
||||
rs := make([]rune, n)
|
||||
|
||||
Reference in New Issue
Block a user