1
1
package nix
2
2
3
3
import (
4
- "bytes"
5
4
"encoding/json"
6
5
"fmt"
7
6
"os"
8
7
"os/exec"
9
- "path/filepath"
8
+ "regexp"
9
+ "strings"
10
+ "time"
10
11
11
12
"github.com/pkg/errors"
12
13
"go.jetpack.io/devbox/internal/debug"
13
14
"go.jetpack.io/devbox/internal/xdg"
15
+ "go.jetpack.io/pkg/filecache"
14
16
)
15
17
16
18
var (
@@ -32,6 +34,11 @@ func (i *Info) String() string {
32
34
}
33
35
34
36
func Search (url string ) (map [string ]* Info , error ) {
37
+ if strings .HasPrefix (url , "runx:" ) {
38
+ // TODO implement runx search. Also, move this check outside this function: nix package
39
+ // should not be handling runx logic.
40
+ return map [string ]* Info {}, nil
41
+ }
35
42
return searchSystem (url , "" /* system */ )
36
43
}
37
44
@@ -113,11 +120,15 @@ type searchSystemCache struct {
113
120
114
121
const (
115
122
// searchSystemCacheSubDir is a sub-directory of the XDG cache directory
116
- searchSystemCacheSubDir = "devbox/nix"
123
+ searchSystemCacheSubDir = "devbox/nix-search "
117
124
searchSystemCacheFileName = "search-system-cache.json"
118
125
)
119
126
120
- var cache = searchSystemCache {}
127
+ type CachedSearchResult struct {
128
+ Results map [string ]* Info `json:"results"`
129
+ // Query is added easier to grep for debuggability
130
+ Query string `json:"query"`
131
+ }
121
132
122
133
// SearchNixpkgsAttribute is a wrapper around searchSystem that caches results.
123
134
// NOTE: we should be very conservative in where we use this function. `nix search`
@@ -126,63 +137,64 @@ var cache = searchSystemCache{}
126
137
// once `nix search` returns a valid result, it will always be the very same result.
127
138
// Hence we can cache it locally and answer future queries fast, by not calling `nix search`.
128
139
func SearchNixpkgsAttribute (query string ) (map [string ]* Info , error ) {
129
- if cache .QueryToInfo == nil {
130
- contents , err := readSearchSystemCacheFile ()
131
- if err != nil {
140
+ key := cacheKey (query )
141
+
142
+ // Check if the query was already cached, and return the result if so
143
+ cache := filecache .New ("devbox/nix" , filecache .WithCacheDir (xdg .CacheSubpath ("" )))
144
+ if cachedResults , err := cache .Get (key ); err == nil {
145
+ var results map [string ]* Info
146
+ if err := json .Unmarshal (cachedResults , & results ); err != nil {
132
147
return nil , err
133
148
}
134
- cache .QueryToInfo = contents
135
- }
136
-
137
- if result := cache .QueryToInfo [query ]; result != nil {
138
- return result , nil
149
+ return results , nil
150
+ } else if ! filecacheNeedsUpdate (err ) {
151
+ return nil , err // genuine error
139
152
}
140
153
141
- info , err := searchSystem (query , "" /*system*/ )
154
+ // If not cached, or an update is needed, then call searchSystem
155
+ infos , err := searchSystem (query , "" /*system*/ )
142
156
if err != nil {
143
157
return nil , err
144
158
}
145
159
146
- cache .QueryToInfo [query ] = info
147
- if err := writeSearchSystemCacheFile (cache .QueryToInfo ); err != nil {
148
- return nil , err
149
- }
150
-
151
- return info , nil
152
- }
153
-
154
- func readSearchSystemCacheFile () (map [string ]map [string ]* Info , error ) {
155
- contents , err := os .ReadFile (xdg .CacheSubpath (filepath .Join (searchSystemCacheSubDir , searchSystemCacheFileName )))
160
+ // Save the results to the cache
161
+ marshalled , err := json .Marshal (infos )
156
162
if err != nil {
157
- if os .IsNotExist (err ) {
158
- // If the file doesn't exist, return an empty map. This will hopefully be filled and written to disk later.
159
- return make (map [string ]map [string ]* Info ), nil
160
- }
161
163
return nil , err
162
164
}
163
-
164
- var result map [string ]map [string ]* Info
165
- if err := json .Unmarshal (contents , & result ); err != nil {
165
+ // TODO savil: add a SetForever API that does not expire. Time based expiration is not needed here
166
+ // because we're caching results that are guaranteed to be stable.
167
+ // TODO savil: Make filecache.cache a public struct so it can be passed into other functions
168
+ const oneYear = 12 * 30 * 24 * time .Hour
169
+ if err := cache .Set (key , marshalled , oneYear ); err != nil {
166
170
return nil , err
167
171
}
168
- return result , nil
172
+
173
+ return infos , nil
169
174
}
170
175
171
- func writeSearchSystemCacheFile (contents map [string ]map [string ]* Info ) error {
172
- // Print as a human-readable JSON file i.e. use nice indentation and newlines.
173
- buf := bytes.Buffer {}
174
- enc := json .NewEncoder (& buf )
175
- enc .SetIndent ("" , " " )
176
- err := enc .Encode (contents )
177
- if err != nil {
178
- return err
179
- }
176
+ // read as: filecache.NeedsUpdate(err)
177
+ // TODO savil: this should be implemented in the filecache package
178
+ func filecacheNeedsUpdate (err error ) bool {
179
+ return err == filecache .NotFound || err == filecache .Expired
180
+ }
180
181
181
- dir := xdg .CacheSubpath (searchSystemCacheSubDir )
182
- if err := os .MkdirAll (dir , 0o755 ); err != nil {
183
- return err
182
+ // cacheKey sanitizes the search query to be a valid unix filename.
183
+ // This cache key is used as the filename to store the cache value, and having a
184
+ // representation of the query is important for debuggability.
185
+ func cacheKey (input string ) string {
186
+ // Replace disallowed characters with underscores.
187
+ re := regexp .MustCompile (`[:/#+]` )
188
+ sanitized := re .ReplaceAllString (input , "_" )
189
+
190
+ // Remove any remaining invalid characters.
191
+ sanitized = regexp .MustCompile (`[^\w\.-]` ).ReplaceAllString (sanitized , "" )
192
+
193
+ // Ensure the filename doesn't exceed the maximum length.
194
+ const maxLen = 255
195
+ if len (sanitized ) > maxLen {
196
+ sanitized = sanitized [:maxLen ]
184
197
}
185
198
186
- path := filepath .Join (dir , searchSystemCacheFileName )
187
- return os .WriteFile (path , buf .Bytes (), 0o644 )
199
+ return sanitized
188
200
}
0 commit comments