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
@@ -105,84 +112,77 @@ func searchSystem(url, system string) (map[string]*Info, error) {
105
112
return parseSearchResults (out ), nil
106
113
}
107
114
108
- // searchSystemCache is a machine-wide cache of search results. It is shared by all
109
- // Devbox projects on the current machine. It is stored in the XDG cache directory.
110
- type searchSystemCache struct {
111
- QueryToInfo map [ string ] map [ string ] * Info `json:"query_to_info "`
115
+ type CachedSearchResult struct {
116
+ Results map [ string ] * Info `json:"results"`
117
+ // Query is added easier to grep for debuggability
118
+ Query string `json:"query "`
112
119
}
113
120
114
- const (
115
- // searchSystemCacheSubDir is a sub-directory of the XDG cache directory
116
- searchSystemCacheSubDir = "devbox/nix"
117
- searchSystemCacheFileName = "search-system-cache.json"
118
- )
119
-
120
- var cache = searchSystemCache {}
121
-
122
121
// SearchNixpkgsAttribute is a wrapper around searchSystem that caches results.
123
122
// NOTE: we should be very conservative in where we use this function. `nix search`
124
123
// accepts generalized `installable regex` as arguments but is slow. For certain
125
124
// queries of the form `nixpkgs/<commit-hash>#attribute`, we can know for sure that
126
125
// once `nix search` returns a valid result, it will always be the very same result.
127
126
// Hence we can cache it locally and answer future queries fast, by not calling `nix search`.
128
127
func SearchNixpkgsAttribute (query string ) (map [string ]* Info , error ) {
129
- if cache .QueryToInfo == nil {
130
- contents , err := readSearchSystemCacheFile ()
131
- if err != nil {
128
+ key := cacheKey (query )
129
+
130
+ // Check if the query was already cached, and return the result if so
131
+ cache := filecache .New ("devbox/nix" , filecache .WithCacheDir (xdg .CacheSubpath ("" )))
132
+ if cachedResults , err := cache .Get (key ); err == nil {
133
+ var results map [string ]* Info
134
+ if err := json .Unmarshal (cachedResults , & results ); err != nil {
132
135
return nil , err
133
136
}
134
- cache .QueryToInfo = contents
137
+ return results , nil
138
+ } else if ! filecacheNeedsUpdate (err ) {
139
+ return nil , err // genuine error
135
140
}
136
141
137
- if result := cache .QueryToInfo [query ]; result != nil {
138
- return result , nil
139
- }
140
-
141
- info , err := searchSystem (query , "" /*system*/ )
142
+ // If not cached, or an update is needed, then call searchSystem
143
+ infos , err := searchSystem (query , "" /*system*/ )
142
144
if err != nil {
143
145
return nil , err
144
146
}
145
147
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 )))
148
+ // Save the results to the cache
149
+ marshalled , err := json .Marshal (infos )
156
150
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
151
return nil , err
162
152
}
163
-
164
- var result map [string ]map [string ]* Info
165
- if err := json .Unmarshal (contents , & result ); err != nil {
153
+ // TODO savil: add a SetForever API that does not expire. Time based expiration is not needed here
154
+ // because we're caching results that are guaranteed to be stable.
155
+ // TODO savil: Make filecache.cache a public struct so it can be passed into other functions
156
+ const oneYear = 12 * 30 * 24 * time .Hour
157
+ if err := cache .Set (key , marshalled , oneYear ); err != nil {
166
158
return nil , err
167
159
}
168
- return result , nil
160
+
161
+ return infos , nil
169
162
}
170
163
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
- }
164
+ // read as: filecache.NeedsUpdate(err)
165
+ // TODO savil: this should be implemented in the filecache package
166
+ func filecacheNeedsUpdate (err error ) bool {
167
+ return errors .Is (err , filecache .NotFound ) || errors .Is (err , filecache .Expired )
168
+ }
180
169
181
- dir := xdg .CacheSubpath (searchSystemCacheSubDir )
182
- if err := os .MkdirAll (dir , 0o755 ); err != nil {
183
- return err
170
+ // cacheKey sanitizes the search query to be a valid unix filename.
171
+ // This cache key is used as the filename to store the cache value, and having a
172
+ // representation of the query is important for debuggability.
173
+ func cacheKey (input string ) string {
174
+ // Replace disallowed characters with underscores.
175
+ re := regexp .MustCompile (`[:/#+]` )
176
+ sanitized := re .ReplaceAllString (input , "_" )
177
+
178
+ // Remove any remaining invalid characters.
179
+ sanitized = regexp .MustCompile (`[^\w\.-]` ).ReplaceAllString (sanitized , "" )
180
+
181
+ // Ensure the filename doesn't exceed the maximum length.
182
+ const maxLen = 255
183
+ if len (sanitized ) > maxLen {
184
+ sanitized = sanitized [:maxLen ]
184
185
}
185
186
186
- path := filepath .Join (dir , searchSystemCacheFileName )
187
- return os .WriteFile (path , buf .Bytes (), 0o644 )
187
+ return sanitized
188
188
}
0 commit comments