From c8a16d3f3ee1ec085e47bb91de1fced6dd85cab2 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Sat, 19 Sep 2026 23:47:18 +0200 Subject: [PATCH] fix: case-insensitive ids via query language and caching --- mount.go | 83 +++++++++++++++++++++++++++++++++++---------------- mount_test.go | 11 +++++++ 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/mount.go b/mount.go index 4a7abc9..95973ad 100644 --- a/mount.go +++ b/mount.go @@ -9,6 +9,7 @@ import ( "path/filepath" "sort" "strings" + "sync" "time" "bazil.org/fuse" @@ -53,7 +54,7 @@ type mountedItemMetadata struct { const mountMetadataFileName = ".docspell-cli-metadata.json" -const docspellIDCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" +const docspellIDCharacters = "abcdefghijklmnopqrstuvwxyz0123456789-_.@" func runMountCommand(args []string, cfg config, password string) error { options, err := parseMountOptions(args, cfg) @@ -360,15 +361,16 @@ type lazyYearNode struct { } type lazyMonthNode struct { - query string - client *DocspellClient - limit int + items *lazyItemDirNode } type lazyItemDirNode struct { - query string - client *DocspellClient - limit int + query string + client *DocspellClient + limit int + mu sync.Mutex + cache map[string]fs.Node + populated bool } type lazyByIDNode struct { @@ -378,9 +380,12 @@ type lazyByIDNode struct { } type lazyByIDShardNode struct { - query string - client *DocspellClient - limit int + query string + client *DocspellClient + limit int + mu sync.Mutex + cache map[string]fs.Node + populated bool } func combineItemQueries(base, constraint string) string { @@ -400,11 +405,11 @@ func (d *lazyYearNode) Lookup(ctx context.Context, name string) (fs.Node, error) if len(name) != 2 || name < "01" || name > "12" { return nil, fuse.ENOENT } - return &lazyMonthNode{ + return &lazyMonthNode{items: &lazyItemDirNode{ query: combineItemQueries(d.query, fmt.Sprintf("date:%04d-%s", d.year, name)), client: d.client, limit: d.limit, - }, nil + }}, nil } func (d *lazyYearNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { @@ -421,11 +426,11 @@ func (d *lazyMonthNode) Attr(ctx context.Context, a *fuse.Attr) error { } func (d *lazyMonthNode) Lookup(ctx context.Context, name string) (fs.Node, error) { - return (&lazyItemDirNode{query: d.query, client: d.client, limit: d.limit}).Lookup(ctx, name) + return d.items.Lookup(ctx, name) } func (d *lazyMonthNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { - return (&lazyItemDirNode{query: d.query, client: d.client, limit: d.limit}).ReadDirAll(ctx) + return d.items.ReadDirAll(ctx) } func (d *lazyItemDirNode) Attr(ctx context.Context, a *fuse.Attr) error { @@ -433,7 +438,12 @@ func (d *lazyItemDirNode) Attr(ctx context.Context, a *fuse.Attr) error { return nil } -func (d *lazyItemDirNode) children(ctx context.Context) (map[string]fs.Node, error) { +func (d *lazyItemDirNode) children(ctx context.Context, refresh bool) (map[string]fs.Node, error) { + d.mu.Lock() + defer d.mu.Unlock() + if d.populated && !refresh { + return d.cache, nil + } items, err := d.client.SearchItems(ctx, d.query, d.limit) if err != nil { return nil, fmt.Errorf("search items: %w", err) @@ -445,7 +455,9 @@ func (d *lazyItemDirNode) children(ctx context.Context) (map[string]fs.Node, err collectMountFiles(children, child) } } - return children, nil + d.cache = children + d.populated = true + return d.cache, nil } func collectMountFiles(children map[string]fs.Node, node fs.Node) { @@ -471,7 +483,7 @@ func collectMountFiles(children map[string]fs.Node, node fs.Node) { } func (d *lazyItemDirNode) Lookup(ctx context.Context, name string) (fs.Node, error) { - children, err := d.children(ctx) + children, err := d.children(ctx, false) if err != nil { return nil, err } @@ -483,7 +495,7 @@ func (d *lazyItemDirNode) Lookup(ctx context.Context, name string) (fs.Node, err } func (d *lazyItemDirNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { - children, err := d.children(ctx) + children, err := d.children(ctx, true) if err != nil { return nil, err } @@ -496,7 +508,7 @@ func (d *lazyByIDNode) Attr(ctx context.Context, a *fuse.Attr) error { } func (d *lazyByIDNode) Lookup(ctx context.Context, name string) (fs.Node, error) { - if len(name) != 2 { + if !isDocspellIDPrefix(name) { return nil, fuse.ENOENT } return &lazyByIDShardNode{ @@ -516,12 +528,29 @@ func (d *lazyByIDNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { return entries, nil } +func isDocspellIDPrefix(prefix string) bool { + if len(prefix) != 2 { + return false + } + for _, char := range prefix { + if !strings.ContainsRune(docspellIDCharacters, char) { + return false + } + } + return true +} + func (d *lazyByIDShardNode) Attr(ctx context.Context, a *fuse.Attr) error { a.Mode = os.ModeDir | 0555 return nil } -func (d *lazyByIDShardNode) children(ctx context.Context) (map[string]fs.Node, error) { +func (d *lazyByIDShardNode) children(ctx context.Context, refresh bool) (map[string]fs.Node, error) { + d.mu.Lock() + defer d.mu.Unlock() + if d.populated && !refresh { + return d.cache, nil + } items, err := d.client.SearchItems(ctx, d.query, d.limit) if err != nil { return nil, fmt.Errorf("search items: %w", err) @@ -531,16 +560,20 @@ func (d *lazyByIDShardNode) children(ctx context.Context) (map[string]fs.Node, e if !ok { return map[string]fs.Node{}, nil } + children := map[string]fs.Node{} for _, child := range byID.children { if shard, ok := child.(*dirNode); ok { - return shard.children, nil + children = shard.children + break } } - return map[string]fs.Node{}, nil + d.cache = children + d.populated = true + return d.cache, nil } func (d *lazyByIDShardNode) Lookup(ctx context.Context, name string) (fs.Node, error) { - children, err := d.children(ctx) + children, err := d.children(ctx, false) if err != nil { return nil, err } @@ -552,7 +585,7 @@ func (d *lazyByIDShardNode) Lookup(ctx context.Context, name string) (fs.Node, e } func (d *lazyByIDShardNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { - children, err := d.children(ctx) + children, err := d.children(ctx, true) if err != nil { return nil, err } @@ -656,7 +689,7 @@ func sanitizePathParts(parts []string) []string { } func idShard(id string) string { - id = sanitizeName(id) + id = strings.ToLower(sanitizeName(id)) if len(id) >= 2 { return id[:2] } diff --git a/mount_test.go b/mount_test.go index 82e8d54..6780d47 100644 --- a/mount_test.go +++ b/mount_test.go @@ -101,6 +101,9 @@ func TestLazyByIDShardFetchesMatchingPrefix(t *testing.T) { if len(entries) != 1 || entries[0].Name != "it-item-1.pdf" { t.Fatalf("unexpected shard entries: %#v", entries) } + if _, err := shard.(*lazyByIDShardNode).Lookup(context.Background(), "it-item-1.pdf"); err != nil { + t.Fatal(err) + } if len(queries) != 2 || queries[0] != "id:it*" || queries[1] != "id:it*" { t.Fatalf("unexpected queries: %v", queries) } @@ -130,6 +133,14 @@ func TestLazyByIDListsEveryPrefixWithoutSearching(t *testing.T) { if !found { t.Fatalf("expected 7j directory in %#v", entries) } + if _, err := byID.(*lazyByIDNode).Lookup(context.Background(), "7J"); err == nil { + t.Fatal("expected uppercase ID prefix lookup to fail") + } + for _, prefix := range []string{"-_", "_o", ".@"} { + if _, err := byID.(*lazyByIDNode).Lookup(context.Background(), prefix); err != nil { + t.Fatalf("expected %q ID prefix lookup to succeed: %v", prefix, err) + } + } } func TestBuildMountTreeGroupsByFolderAndDate(t *testing.T) {