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"
"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
mu sync.Mutex
cache map[string]fs.Node
populated bool
}
type lazyByIDNode struct {
@@ -381,6 +383,9 @@ type lazyByIDShardNode struct {
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]
}
+11
View File
@@ -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) {