@@ -140,20 +140,33 @@ type cacheEntry struct {
140140
141141type fetcher func (context.Context , uuid.UUID ) (CacheEntryValue ,error )
142142
143+ var _ fs.FS = (* CloseFS )(nil )
144+
145+ // CloseFS is a wrapper around fs.FS that implements io.Closer. The Close()
146+ // method tells the cache to release the fileID. Once all open references are
147+ // closed, the file is removed from the cache.
148+ type CloseFS struct {
149+ fs.FS
150+
151+ close func ()
152+ }
153+
154+ func (f * CloseFS )Close () {f .close () }
155+
143156// Acquire will load the fs.FS for the given file. It guarantees that parallel
144157// calls for the same fileID will only result in one fetch, and that parallel
145158// calls for distinct fileIDs will fetch in parallel.
146159//
147160// Safety: Every call to Acquire that does not return an error must have a
148161// matching call to Release.
149- func (c * Cache )Acquire (ctx context.Context ,fileID uuid.UUID ) (fs. FS ,error ) {
150- // It's important that this `Load` call occurs outsideof `prepare`, after the
162+ func (c * Cache )Acquire (ctx context.Context ,fileID uuid.UUID ) (* CloseFS ,error ) {
163+ // It's important that this `Load` call occurs outside `prepare`, after the
151164// mutex has been released, or we would continue to hold the lock until the
152165// entire file has been fetched, which may be slow, and would prevent other
153166// files from being fetched in parallel.
154167it ,err := c .prepare (ctx ,fileID ).Load ()
155168if err != nil {
156- c .Release (fileID )
169+ c .release (fileID )
157170return nil ,err
158171}
159172
@@ -163,11 +176,19 @@ func (c *Cache) Acquire(ctx context.Context, fileID uuid.UUID) (fs.FS, error) {
163176}
164177// Always check the caller can actually read the file.
165178if err := c .authz .Authorize (ctx ,subject ,policy .ActionRead ,it .Object );err != nil {
166- c .Release (fileID )
179+ c .release (fileID )
167180return nil ,err
168181}
169182
170- return it .FS ,err
183+ var once sync.Once
184+ return & CloseFS {
185+ FS :it .FS ,
186+ close :func () {
187+ // sync.Once makes the Close() idempotent, so we can call it
188+ // multiple times without worrying about double-releasing.
189+ once .Do (func () {c .release (fileID ) })
190+ },
191+ },nil
171192}
172193
173194func (c * Cache )prepare (ctx context.Context ,fileID uuid.UUID )* lazy.ValueWithError [CacheEntryValue ] {
@@ -203,9 +224,12 @@ func (c *Cache) prepare(ctx context.Context, fileID uuid.UUID) *lazy.ValueWithEr
203224return entry .value
204225}
205226
206- //Release decrements the reference count for the given fileID, and frees the
227+ //release decrements the reference count for the given fileID, and frees the
207228// backing data if there are no further references being held.
208- func (c * Cache )Release (fileID uuid.UUID ) {
229+ //
230+ // release should only be called after a successful call to Acquire using the Release()
231+ // method on the returned *CloseFS.
232+ func (c * Cache )release (fileID uuid.UUID ) {
209233c .lock .Lock ()
210234defer c .lock .Unlock ()
211235