11package indexer
22
33import (
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
5257type 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
9096func 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+
395422func 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