1
0

query.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. package geodata
  2. import "strings"
  3. // Categories returns the database's categories, filtered by a case-insensitive
  4. // substring of the category code. A non-positive limit returns all of them:
  5. // the category index is small even for the largest databases, and the panel
  6. // filters it client-side so typing in the search box costs no requests.
  7. func (s *Store) Categories(name, query string, offset, limit int) (GeoCategoryPage, error) {
  8. idx, err := s.index(name)
  9. if err != nil {
  10. return GeoCategoryPage{}, err
  11. }
  12. matched := idx.categories
  13. if query = strings.ToLower(strings.TrimSpace(query)); query != "" {
  14. matched = make([]GeoCategory, 0, len(idx.categories))
  15. for _, category := range idx.categories {
  16. if strings.Contains(category.Code, query) {
  17. matched = append(matched, category)
  18. }
  19. }
  20. }
  21. page := GeoCategoryPage{Total: len(matched), Items: []GeoCategory{}}
  22. from, to := categoryBounds(len(matched), offset, limit)
  23. page.Items = append(page.Items, matched[from:to]...)
  24. return page, nil
  25. }
  26. // Entries returns one page of a category's rules, filtered by a
  27. // case-insensitive substring of the rule value. The page is scanned out of the
  28. // file on each call: a category such as geosite's category-ads-all holds well
  29. // over a hundred thousand rules, and holding those in memory to serve one
  30. // screenful of them is what the panel cannot afford.
  31. func (s *Store) Entries(name, code, query string, offset, limit int) (GeoEntryPage, error) {
  32. idx, err := s.index(name)
  33. if err != nil {
  34. return GeoEntryPage{}, err
  35. }
  36. code = strings.ToLower(strings.TrimSpace(code))
  37. if _, ok := idx.byCode[code]; !ok {
  38. return GeoEntryPage{}, ErrUnknownCategory
  39. }
  40. if _, err := s.resolve(name); err != nil {
  41. return GeoEntryPage{}, err
  42. }
  43. if offset < 0 {
  44. offset = 0
  45. }
  46. if limit <= 0 || limit > MaxPageSize {
  47. limit = MaxPageSize
  48. }
  49. spans := idx.spans[code]
  50. if len(spans) == 0 {
  51. return GeoEntryPage{}, ErrUnknownCategory
  52. }
  53. s.scan.Lock()
  54. defer s.scan.Unlock()
  55. records, err := s.recordsLocked(name, code, spans)
  56. if err != nil {
  57. return GeoEntryPage{}, err
  58. }
  59. return scanEntries(records, idx.kind, code, strings.ToLower(strings.TrimSpace(query)), offset, limit)
  60. }
  61. // Lookup reports whether a category exists in the database, without paying for
  62. // the entry data. The code is matched verbatim apart from case: this backs the
  63. // routing-token validator, and the core does not forgive a stray space either,
  64. // so trimming one here would hide the very typo the validator exists to find.
  65. func (s *Store) Lookup(name, code string) (GeoCategory, error) {
  66. idx, err := s.index(name)
  67. if err != nil {
  68. return GeoCategory{}, err
  69. }
  70. category, ok := idx.byCode[strings.ToLower(code)]
  71. if !ok {
  72. return GeoCategory{}, ErrUnknownCategory
  73. }
  74. return category, nil
  75. }
  76. func categoryBounds(total, offset, limit int) (int, int) {
  77. if limit <= 0 {
  78. if offset < 0 {
  79. offset = 0
  80. }
  81. if offset > total {
  82. offset = total
  83. }
  84. return offset, total
  85. }
  86. return sliceBounds(total, offset, limit)
  87. }
  88. func sliceBounds(total, offset, limit int) (int, int) {
  89. if offset < 0 {
  90. offset = 0
  91. }
  92. if offset > total {
  93. offset = total
  94. }
  95. if limit <= 0 || limit > MaxPageSize {
  96. limit = MaxPageSize
  97. }
  98. to := offset + limit
  99. if to > total {
  100. to = total
  101. }
  102. return offset, to
  103. }
  104. func (s *Store) recordsLocked(name, code string, spans []byteSpan) ([][]byte, error) {
  105. info, err := s.resolve(name)
  106. if err != nil {
  107. return nil, err
  108. }
  109. key := fileKey{name: name, size: info.Size(), modTime: info.ModTime().UnixNano()}
  110. if s.hot.key == key && s.hot.code == code {
  111. return s.hot.records, nil
  112. }
  113. records, err := readSpans(s.dir, name, spans)
  114. if err != nil {
  115. return nil, err
  116. }
  117. s.hot = hotRecord{key: key, code: code, records: records}
  118. return records, nil
  119. }