@@ -26,6 +26,7 @@ import (
2626 "github.com/go-git/go-git/v5/plumbing"
2727 githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
2828 "github.com/go-git/go-git/v5/storage/memory"
29+ "github.com/gofrs/flock"
2930 "github.com/pkg/errors"
3031 "github.com/sirupsen/logrus"
3132 "gopkg.in/yaml.v3"
@@ -136,6 +137,16 @@ func (m *Module) RegisterExtensions(ctx context.Context, log logrus.FieldLogger,
136137// Manifest downloads the module if not already downloaded and returns a parsed
137138// configuration.TemplateRepositoryManifest of this module.
138139func (m * Module ) Manifest (ctx context.Context ) (configuration.TemplateRepositoryManifest , error ) {
140+ lockDir := FSLockDir (m .PathSlug ())
141+ lock , err := exclusiveLockDirectory (lockDir )
142+ if err != nil {
143+ return configuration.TemplateRepositoryManifest {},
144+ errors .Wrapf (err , "failed to lock module cache directory %q" , lockDir )
145+ }
146+
147+ //nolint:errcheck // Why: Unlock error can be safely ignored here
148+ defer lock .Unlock ()
149+
139150 fs , err := m .GetFS (ctx )
140151 if err != nil {
141152 return configuration.TemplateRepositoryManifest {}, errors .Wrap (err , "failed to download fs" )
@@ -182,21 +193,20 @@ func (m *Module) GetFS(ctx context.Context) (billy.Filesystem, error) {
182193 return m .fs , nil
183194 }
184195
185- cacheDir := filepath .Join (StencilCacheDir (), "module_fs" , ModuleCacheDirectory (m .URI , m .Version ))
186- logrus .Debug ("cacheDir" , cacheDir )
196+ cacheDir := m .FSCacheDir ()
187197
188- if useModuleCache (cacheDir ) {
198+ if useCache (cacheDir ) {
189199 m .fs = osfs .New (cacheDir )
190200 return m .fs , nil
191201 }
192202
193203 err = os .RemoveAll (cacheDir )
194204 if err != nil {
195- return nil , errors .Wrapf (err , "failed to remove stale cache %q" , cacheDir )
205+ return nil , errors .Wrapf (err , "failed to remove stale module cache directory %q" , cacheDir )
196206 }
197207
198208 if err = os .MkdirAll (cacheDir , 0o755 ); err != nil {
199- return nil , errors .Wrap (err , "failed to create cache directory" )
209+ return nil , errors .Wrapf (err , "failed to create module cache directory %q" , cacheDir )
200210 }
201211
202212 m .fs = osfs .New (cacheDir )
@@ -237,26 +247,88 @@ func (m *Module) GetFS(ctx context.Context) (billy.Filesystem, error) {
237247 return m .fs , nil
238248}
239249
240- // useModuleCache determines if the specified path should be used as a module cache.
241- func useModuleCache (path string ) bool {
250+ func (m * Module ) PathSlug () string {
251+ return PathSlug (m .URI , m .Version )
252+ }
253+
254+ func (m * Module ) FSCacheDir () string {
255+ return FSCacheDir (m .PathSlug ())
256+ }
257+
258+ // exclusiveLockDirectory creates a new flock lock for the specified directory.
259+ func exclusiveLockDirectory (dir string ) (* flock.Flock , error ) {
260+ lock := flock .New (filepath .Join (dir , "ex_dir.lock" ))
261+ for {
262+ locked , err := lock .TryLock ()
263+ if err != nil {
264+ if ! errors .Is (err , os .ErrNotExist ) {
265+ return nil , err
266+ }
267+
268+ if err = os .MkdirAll (dir , 0o755 ); err != nil {
269+ return nil , errors .Wrapf (err , "failed to create directory %q" , dir )
270+ }
271+ continue
272+ }
273+
274+ if locked {
275+ break
276+ }
277+ }
278+
279+ return lock , nil
280+ }
281+
282+ // useCache determines if the specified path should be used as a module cache.
283+ func useCache (path string ) bool {
242284 info , err := os .Stat (path )
243285 if err != nil || time .Since (info .ModTime ()) > ModuleCacheTTL {
244286 return false
245287 }
246288
289+ if files , err := os .ReadDir (path ); err != nil || len (files ) == 0 {
290+ return false
291+ }
292+
247293 return true
248294}
249295
250- // ModuleCacheDirectory generates a directory name for the module from the given URI and optional branch.
251- func ModuleCacheDirectory (uri , branch string ) string {
252- if branch == "" {
253- branch = "v0.0.0"
296+ // PathSlug returns a unique identifier for a module
297+ // using the given URI and versioning constraints.
298+ func PathSlug (uri , version string ) string {
299+ if version == "" {
300+ version = "v0.0.0"
254301 }
255302
256- return regexp .MustCompile (`[^a-zA-Z0-9@]+` ).ReplaceAllString (uri + "@" + branch , "_" )
303+ return regexp .MustCompile (`[^a-zA-Z0-9<>=.\[\]\- @]+` ).ReplaceAllString (uri + "@" + version , "_" )
257304}
258305
259306// StencilCacheDir returns the directory where stencil caches its data.
260307func StencilCacheDir () string {
261308 return filepath .Join (os .TempDir (), "stencil_cache" )
262309}
310+
311+ // CacheDir returns the cache directory for a module based on its type and ID.
312+ func CacheDir (cacheType , moduleID string ) string {
313+ return filepath .Join (StencilCacheDir (), cacheType , moduleID )
314+ }
315+
316+ // FSCacheDir returns the cache directory for a module based on its ID.
317+ func FSCacheDir (moduleID string ) string {
318+ return CacheDir ("module_fs" , moduleID )
319+ }
320+
321+ // VersionCacheDir returns the version cache directory for a module based on its ID.
322+ func VersionCacheDir (moduleID string ) string {
323+ return CacheDir ("module_version" , moduleID )
324+ }
325+
326+ // VersionLockDir returns the lock directory for a module version based on its ID.
327+ func VersionLockDir (moduleID string ) string {
328+ return CacheDir ("module_version_lock" , moduleID )
329+ }
330+
331+ // FSLockDir returns the lock directory for a module filesystem based on its ID.
332+ func FSLockDir (moduleID string ) string {
333+ return CacheDir ("module_fs_lock" , moduleID )
334+ }
0 commit comments