fix: case-insensitive ids via query language and caching

This commit is contained in:
Jan Bader committed 2026-09-19 23:47:18 +02:00
1 parent 3b91a26b2c
commit c8a16d3f3e
2 files changed
+63 -19

No files matched your search

+52 -19
View File
@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"bazil.org/fuse" "bazil.org/fuse"
@@ -53,7 +54,7 @@ type mountedItemMetadata struct {
const mountMetadataFileName = ".docspell-cli-metadata.json" const mountMetadataFileName = ".docspell-cli-metadata.json"
const docspellIDCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" const docspellIDCharacters = "abcdefghijklmnopqrstuvwxyz0123456789-_.@"
func runMountCommand(args []string, cfg config, password string) error { func runMountCommand(args []string, cfg config, password string) error {
options, err := parseMountOptions(args, cfg) options, err := parseMountOptions(args, cfg)
@@ -360,15 +361,16 @@ type lazyYearNode struct {
} }
type lazyMonthNode struct { type lazyMonthNode struct {
query string items *lazyItemDirNode
client *DocspellClient
limit int
} }
type lazyItemDirNode struct { type lazyItemDirNode struct {
query string query string
client *DocspellClient client *DocspellClient
limit int limit int
mu sync.Mutex
cache map[string]fs.Node
populated bool
} }
type lazyByIDNode struct { type lazyByIDNode struct {
@@ -381,6 +383,9 @@ type lazyByIDShardNode struct {
query string query string
client *DocspellClient client *DocspellClient
limit int limit int
mu sync.Mutex
cache map[string]fs.Node
populated bool
} }
func combineItemQueries(base, constraint string) string { 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" { if len(name) != 2 || name < "01" || name > "12" {
return nil, fuse.ENOENT return nil, fuse.ENOENT
} }
return &lazyMonthNode{ return &lazyMonthNode{items: &lazyItemDirNode{
query: combineItemQueries(d.query, fmt.Sprintf("date:%04d-%s", d.year, name)), query: combineItemQueries(d.query, fmt.Sprintf("date:%04d-%s", d.year, name)),
client: d.client, client: d.client,
limit: d.limit, limit: d.limit,
}, nil }}, nil
} }
func (d *lazyYearNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 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) { 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) { 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 { 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 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) items, err := d.client.SearchItems(ctx, d.query, d.limit)
if err != nil { if err != nil {
return nil, fmt.Errorf("search items: %w", err) 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) 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) { 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) { 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 { if err != nil {
return nil, err 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) { func (d *lazyItemDirNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
children, err := d.children(ctx) children, err := d.children(ctx, true)
if err != nil { if err != nil {
return nil, err 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) { func (d *lazyByIDNode) Lookup(ctx context.Context, name string) (fs.Node, error) {
if len(name) != 2 { if !isDocspellIDPrefix(name) {
return nil, fuse.ENOENT return nil, fuse.ENOENT
} }
return &lazyByIDShardNode{ return &lazyByIDShardNode{
@@ -516,12 +528,29 @@ func (d *lazyByIDNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
return entries, nil 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 { func (d *lazyByIDShardNode) Attr(ctx context.Context, a *fuse.Attr) error {
a.Mode = os.ModeDir | 0555 a.Mode = os.ModeDir | 0555
return nil 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) items, err := d.client.SearchItems(ctx, d.query, d.limit)
if err != nil { if err != nil {
return nil, fmt.Errorf("search items: %w", err) 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 { if !ok {
return map[string]fs.Node{}, nil return map[string]fs.Node{}, nil
} }
children := map[string]fs.Node{}
for _, child := range byID.children { for _, child := range byID.children {
if shard, ok := child.(*dirNode); ok { 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) { 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 { if err != nil {
return nil, err 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) { func (d *lazyByIDShardNode) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
children, err := d.children(ctx) children, err := d.children(ctx, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -656,7 +689,7 @@ func sanitizePathParts(parts []string) []string {
} }
func idShard(id string) string { func idShard(id string) string {
id = sanitizeName(id) id = strings.ToLower(sanitizeName(id))
if len(id) >= 2 { if len(id) >= 2 {
return id[:2] return id[:2]
} }
+11
View File
@@ -101,6 +101,9 @@ func TestLazyByIDShardFetchesMatchingPrefix(t *testing.T) {
if len(entries) != 1 || entries[0].Name != "it-item-1.pdf" { if len(entries) != 1 || entries[0].Name != "it-item-1.pdf" {
t.Fatalf("unexpected shard entries: %#v", entries) 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*" { if len(queries) != 2 || queries[0] != "id:it*" || queries[1] != "id:it*" {
t.Fatalf("unexpected queries: %v", queries) t.Fatalf("unexpected queries: %v", queries)
} }
@@ -130,6 +133,14 @@ func TestLazyByIDListsEveryPrefixWithoutSearching(t *testing.T) {
if !found { if !found {
t.Fatalf("expected 7j directory in %#v", entries) 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) { func TestBuildMountTreeGroupsByFolderAndDate(t *testing.T) {