Skip to content

Commit 57741e8

Browse files
authored
feat: index extended filesystem attribute user.xdg.tags (#14)
* wip: adding xattr usser.xdg.tags support * feat: config setting for xattr indexing * fix: actually index/search properly
1 parent 3b3b795 commit 57741e8

File tree

7 files changed

+93
-18
lines changed

7 files changed

+93
-18
lines changed

cmd/dsearch/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ var (
6565
searchExifLatMax float64
6666
searchExifLonMin float64
6767
searchExifLonMax float64
68+
searchXattrTags string
6869
)
6970

7071
var rootCmd = &cobra.Command{
@@ -200,6 +201,7 @@ func init() {
200201
searchCmd.Flags().Float64Var(&searchExifLatMax, "exif-lat-max", 0, "maximum GPS latitude")
201202
searchCmd.Flags().Float64Var(&searchExifLonMin, "exif-lon-min", 0, "minimum GPS longitude")
202203
searchCmd.Flags().Float64Var(&searchExifLonMax, "exif-lon-max", 0, "maximum GPS longitude")
204+
searchCmd.Flags().StringVar(&searchXattrTags, "xattr-tags", "", "tags in user.xdg.tags xattr")
203205

204206
indexFilesCmd.Flags().IntVar(&filesLimit, "limit", 100, "maximum number of files to list")
205207

@@ -383,6 +385,7 @@ func runSearch(cmd *cobra.Command, args []string) error {
383385
ExifLatMax: searchExifLatMax,
384386
ExifLonMin: searchExifLonMin,
385387
ExifLonMax: searchExifLonMax,
388+
XattrTags: searchXattrTags,
386389
}
387390

388391
result, err := client.SearchWithOptions(clientOpts)
@@ -442,6 +445,7 @@ func runSearch(cmd *cobra.Command, args []string) error {
442445
ExifLatMax: searchExifLatMax,
443446
ExifLonMin: searchExifLonMin,
444447
ExifLonMax: searchExifLonMax,
448+
XattrTags: searchXattrTags,
445449
}
446450

447451
result, err = idx.SearchWithOptions(indexerOpts)

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/danielgtaylor/huma/v2 v2.35.0
1111
github.com/fsnotify/fsnotify v1.9.0
1212
github.com/go-chi/chi/v5 v5.2.5
13+
github.com/pkg/xattr v0.4.12
1314
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
1415
github.com/spf13/cobra v1.10.2
1516
go.etcd.io/bbolt v1.4.3

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
9696
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
9797
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
9898
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
99+
github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=
100+
github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
99101
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
100102
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
101103
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -123,6 +125,7 @@ golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
123125
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
124126
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
125127
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
128+
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
126129
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
127130
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
128131
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

internal/api/handlers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type SearchInput struct {
5757
ExifLatMax float64 `query:"exif_lat_max" doc:"Maximum GPS latitude" example:"41.0"`
5858
ExifLonMin float64 `query:"exif_lon_min" doc:"Minimum GPS longitude" example:"-74.0"`
5959
ExifLonMax float64 `query:"exif_lon_max" doc:"Maximum GPS longitude" example:"-73.0"`
60+
XattrTags string `query:"xattr_tags" doc:"Tags" example:"+must,should,-must-not"`
6061
}
6162

6263
type SearchOutput struct {
@@ -122,6 +123,7 @@ func RegisterHandlers(srv *Server, api huma.API) {
122123
ExifLatMax: input.ExifLatMax,
123124
ExifLonMin: input.ExifLonMin,
124125
ExifLonMax: input.ExifLonMax,
126+
XattrTags: input.XattrTags,
125127
}
126128

127129
result, err := srv.Indexer.SearchWithOptions(opts)

internal/client/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ type SearchOptions struct {
133133
ExifLatMax float64
134134
ExifLonMin float64
135135
ExifLonMax float64
136+
XattrTags string
136137
}
137138

138139
func Search(query string, limit int) (*bleve.SearchResult, error) {
@@ -224,6 +225,9 @@ func SearchWithOptions(opts *SearchOptions) (*bleve.SearchResult, error) {
224225
if opts.ExifLonMax != 0 {
225226
params["exif_lon_max"] = opts.ExifLonMax
226227
}
228+
if opts.XattrTags != "" {
229+
params["xattr_tags"] = opts.XattrTags
230+
}
227231

228232
result, err := sendRequest("search", params)
229233
if err != nil {

internal/config/config.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,27 @@ import (
1313
)
1414

1515
type IndexPath struct {
16-
Path string `toml:"path"`
17-
MaxDepth int `toml:"max_depth"`
18-
ExcludeHidden bool `toml:"exclude_hidden"`
19-
ExcludeDirs []string `toml:"exclude_dirs"`
20-
ExtractExif bool `toml:"extract_exif"`
21-
Watch *bool `toml:"watch,omitempty"` // nil = true (default), false = skip fsnotify
16+
Path string `toml:"path"`
17+
MaxDepth int `toml:"max_depth"`
18+
ExcludeHidden bool `toml:"exclude_hidden"`
19+
ExcludeDirs []string `toml:"exclude_dirs"`
20+
ExtractExif bool `toml:"extract_exif"`
21+
ExtractXattrTags bool `toml:"extract_xattr_tags"`
22+
Watch *bool `toml:"watch,omitempty"` // nil = true (default), false = skip fsnotify
2223

2324
excludeDirsMap map[string]bool
2425
excludeDirsRegex []*regexp.Regexp
2526
}
2627

2728
type Config struct {
28-
IndexPath string `toml:"index_path"`
29-
ListenAddr string `toml:"listen_addr"`
30-
MaxFileBytes int64 `toml:"max_file_bytes"`
31-
WorkerCount int `toml:"worker_count"`
32-
IndexPaths []IndexPath `toml:"index_paths"`
33-
TextExts []string `toml:"text_extensions"`
34-
IndexAllFiles bool `toml:"index_all_files"`
29+
IndexPath string `toml:"index_path"`
30+
ListenAddr string `toml:"listen_addr"`
31+
MaxFileBytes int64 `toml:"max_file_bytes"`
32+
WorkerCount int `toml:"worker_count"`
33+
IndexPaths []IndexPath `toml:"index_paths"`
34+
TextExts []string `toml:"text_extensions"`
35+
IndexAllFiles bool `toml:"index_all_files"`
36+
IndexXattrTags bool `toml:"index_xattr_tags"`
3537

3638
RootDir string `toml:"root_dir,omitempty"`
3739
MaxDepth int `toml:"max_depth,omitempty"`
@@ -121,11 +123,12 @@ func Default() *Config {
121123
}
122124

123125
cfg := &Config{
124-
IndexPath: getDefaultIndexPath(),
125-
ListenAddr: ":43654",
126-
MaxFileBytes: 2 * 1024 * 1024,
127-
WorkerCount: workerCount,
128-
IndexAllFiles: true,
126+
IndexPath: getDefaultIndexPath(),
127+
ListenAddr: ":43654",
128+
MaxFileBytes: 2 * 1024 * 1024,
129+
WorkerCount: workerCount,
130+
IndexAllFiles: true,
131+
IndexXattrTags: true,
129132
IndexPaths: []IndexPath{
130133
{
131134
Path: home,

internal/indexer/indexer.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package indexer
22

33
import (
4+
"bytes"
45
"crypto/sha256"
6+
"encoding/csv"
57
"encoding/hex"
68
"fmt"
79
"io"
810
"mime"
911
"os"
1012
"path/filepath"
13+
"slices"
1114
"strings"
1215
"sync"
1316
"sync/atomic"
@@ -25,6 +28,7 @@ import (
2528
_ "github.com/blevesearch/bleve/v2/analysis/tokenizer/single"
2629
"github.com/blevesearch/bleve/v2/mapping"
2730
query "github.com/blevesearch/bleve/v2/search/query"
31+
"github.com/pkg/xattr"
2832
"github.com/rwcarlsen/goexif/exif"
2933
)
3034

@@ -47,6 +51,7 @@ type Document struct {
4751
ExifFNumber float64 `json:"exif_fnumber,omitempty"`
4852
ExifExposure string `json:"exif_exposure,omitempty"`
4953
ExifFocalLen float64 `json:"exif_focal_length,omitempty"`
54+
XattrTags []string `json:"xattr_tags,omitempty"`
5055
}
5156

5257
type Indexer struct {
@@ -85,6 +90,7 @@ type SearchOptions struct {
8590
ExifLatMax float64 `json:"exif_lat_max,omitempty"`
8691
ExifLonMin float64 `json:"exif_lon_min,omitempty"`
8792
ExifLonMax float64 `json:"exif_lon_max,omitempty"`
93+
XattrTags string `json:"xattr_tags,omitempty"`
8894
}
8995

9096
func New(cfg *config.Config) (*Indexer, error) {
@@ -306,6 +312,10 @@ func buildIndexMapping() mapping.IndexMapping {
306312
exifFocalField.Store = true
307313
docMapping.AddFieldMappingsAt("exif_focal_length", exifFocalField)
308314

315+
xattrTagsField := bleve.NewKeywordFieldMapping()
316+
xattrTagsField.Store = true
317+
docMapping.AddFieldMappingsAt("xattr_tags", xattrTagsField)
318+
309319
m.DefaultMapping = docMapping
310320
return m
311321
}
@@ -389,9 +399,26 @@ func (i *Indexer) readDocument(path string, info os.FileInfo) (*Document, error)
389399
i.extractExifData(path, doc)
390400
}
391401

402+
if i.config.IndexXattrTags {
403+
i.extractXattrTags(path, doc)
404+
}
405+
392406
return doc, nil
393407
}
394408

409+
func (i *Indexer) extractXattrTags(path string, doc *Document) {
410+
tags, err := xattr.Get(path, "user.xdg.tags")
411+
if err != nil || len(tags) == 0 {
412+
return
413+
}
414+
parsedTags, _ := csv.NewReader(bytes.NewReader(tags)).Read()
415+
if len(parsedTags) > 0 {
416+
doc.XattrTags = parsedTags
417+
slices.Sort(doc.XattrTags)
418+
doc.XattrTags = slices.Compact(doc.XattrTags)
419+
}
420+
}
421+
395422
func isImageFile(contentType string) bool {
396423
return strings.HasPrefix(contentType, "image/")
397424
}
@@ -652,6 +679,37 @@ func (i *Indexer) SearchWithOptions(opts *SearchOptions) (*bleve.SearchResult, e
652679
filters = append(filters, lonQuery)
653680
}
654681

682+
if i.config.IndexXattrTags && opts.XattrTags != "" {
683+
tags, _ := csv.NewReader(strings.NewReader(opts.XattrTags)).Read()
684+
if len(tags) > 0 {
685+
tagsQuery := bleve.NewBooleanQuery()
686+
for _, tag := range tags {
687+
if len(tag) == 0 {
688+
continue
689+
}
690+
691+
addFn := tagsQuery.AddShould
692+
switch tag[0] {
693+
case '-':
694+
tag = tag[1:]
695+
addFn = tagsQuery.AddMustNot
696+
case '+':
697+
tag = tag[1:]
698+
addFn = tagsQuery.AddMust
699+
}
700+
701+
if len(tag) == 0 {
702+
continue
703+
}
704+
705+
tagQuery := bleve.NewTermQuery(tag)
706+
tagQuery.SetField("xattr_tags")
707+
addFn(tagQuery)
708+
}
709+
filters = append(filters, tagsQuery)
710+
}
711+
}
712+
655713
// Combine main query with filters
656714
var finalQuery query.Query
657715
if len(filters) > 0 {

0 commit comments

Comments
 (0)