88 "os"
99 "path/filepath"
1010 "sort"
11+ "sync"
12+ "time"
1113
1214 "cdr.dev/slog"
1315)
@@ -16,21 +18,77 @@ import (
1618// copying the VSIX and extracting said VSIX to a tree structure in the form of
1719// publisher/extension/version to easily serve individual assets via HTTP.
1820type Local struct {
19- extdir string
20- logger slog.Logger
21+ listCache []extension
22+ listDuration time.Duration
23+ listExpiration time.Time
24+ listMutex sync.Mutex
25+ extdir string
26+ logger slog.Logger
2127}
2228
23- func NewLocalStorage (extdir string , logger slog.Logger ) (* Local , error ) {
24- extdir , err := filepath .Abs (extdir )
29+ type LocalOptions struct {
30+ // How long to cache the list of extensions with their manifests. Zero means
31+ // no cache.
32+ ListCacheDuration time.Duration
33+ ExtDir string
34+ }
35+
36+ func NewLocalStorage (options * LocalOptions , logger slog.Logger ) (* Local , error ) {
37+ extdir , err := filepath .Abs (options .ExtDir )
2538 if err != nil {
2639 return nil , err
2740 }
2841 return & Local {
29- extdir : extdir ,
30- logger : logger ,
42+ // TODO: Eject the cache when adding/removing extensions and/or add a
43+ // command to eject the cache?
44+ extdir : extdir ,
45+ listDuration : options .ListCacheDuration ,
46+ logger : logger ,
3147 }, nil
3248}
3349
50+ func (s * Local ) list (ctx context.Context ) []extension {
51+ var list []extension
52+ publishers , err := s .getDirNames (ctx , s .extdir )
53+ if err != nil {
54+ s .logger .Error (ctx , "Error reading publisher" , slog .Error (err ))
55+ }
56+ for _ , publisher := range publishers {
57+ ctx := slog .With (ctx , slog .F ("publisher" , publisher ))
58+ dir := filepath .Join (s .extdir , publisher )
59+
60+ extensions , err := s .getDirNames (ctx , dir )
61+ if err != nil {
62+ s .logger .Error (ctx , "Error reading extensions" , slog .Error (err ))
63+ }
64+ for _ , name := range extensions {
65+ ctx := slog .With (ctx , slog .F ("extension" , name ))
66+ versions , err := s .Versions (ctx , publisher , name )
67+ if err != nil {
68+ s .logger .Error (ctx , "Error reading versions" , slog .Error (err ))
69+ }
70+ if len (versions ) == 0 {
71+ continue
72+ }
73+
74+ // The manifest from the latest version is used for filtering.
75+ manifest , err := s .Manifest (ctx , publisher , name , versions [0 ])
76+ if err != nil {
77+ s .logger .Error (ctx , "Unable to read extension manifest" , slog .Error (err ))
78+ continue
79+ }
80+
81+ list = append (list , extension {
82+ manifest ,
83+ name ,
84+ publisher ,
85+ versions ,
86+ })
87+ }
88+ }
89+ return list
90+ }
91+
3492func (s * Local ) AddExtension (ctx context.Context , manifest * VSIXManifest , vsix []byte ) (string , error ) {
3593 // Extract the zip to the correct path.
3694 identity := manifest .Metadata .Identity
@@ -118,39 +176,23 @@ func (s *Local) Versions(ctx context.Context, publisher, name string) ([]Version
118176 return versions , err
119177}
120178
121- func (s * Local ) WalkExtensions (ctx context.Context , fn func (manifest * VSIXManifest , versions []Version ) error ) error {
122- publishers , err := s .getDirNames (ctx , s .extdir )
123- if err != nil {
124- s .logger .Error (ctx , "Error reading publisher" , slog .Error (err ))
179+ func (s * Local ) listWithCache (ctx context.Context ) []extension {
180+ s .listMutex .Lock ()
181+ defer s .listMutex .Unlock ()
182+ if s .listCache == nil || time .Now ().After (s .listExpiration ) {
183+ s .listExpiration = time .Now ().Add (s .listDuration )
184+ s .listCache = s .list (ctx )
125185 }
126- for _ , publisher := range publishers {
127- ctx := slog .With (ctx , slog .F ("publisher" , publisher ))
128- dir := filepath .Join (s .extdir , publisher )
129-
130- extensions , err := s .getDirNames (ctx , dir )
131- if err != nil {
132- s .logger .Error (ctx , "Error reading extensions" , slog .Error (err ))
133- }
134- for _ , extension := range extensions {
135- ctx := slog .With (ctx , slog .F ("extension" , extension ))
136- versions , err := s .Versions (ctx , publisher , extension )
137- if err != nil {
138- s .logger .Error (ctx , "Error reading versions" , slog .Error (err ))
139- }
140- if len (versions ) == 0 {
141- continue
142- }
143-
144- // The manifest from the latest version is used for filtering.
145- manifest , err := s .Manifest (ctx , publisher , extension , versions [0 ])
146- if err != nil {
147- s .logger .Error (ctx , "Unable to read extension manifest" , slog .Error (err ))
148- continue
149- }
186+ return s .listCache
187+ }
150188
151- if err = fn (manifest , versions ); err != nil {
152- return err
153- }
189+ func (s * Local ) WalkExtensions (ctx context.Context , fn func (manifest * VSIXManifest , versions []Version ) error ) error {
190+ // Walking through directories on disk and parsing manifest files takes several
191+ // minutes with many extensions installed, so if we already did that within
192+ // a specified duration, just load extensions from the cache instead.
193+ for _ , extension := range s .listWithCache (ctx ) {
194+ if err := fn (extension .manifest , extension .versions ); err != nil {
195+ return err
154196 }
155197 }
156198 return nil
0 commit comments