@@ -3,6 +3,7 @@ package files
3
3
import (
4
4
"bytes"
5
5
"context"
6
+ "io"
6
7
"io/fs"
7
8
"sync"
8
9
@@ -140,20 +141,34 @@ type cacheEntry struct {
140
141
141
142
type fetcher func (context.Context , uuid.UUID ) (CacheEntryValue , error )
142
143
144
+ var _ fs.FS = (* CloseFS )(nil )
145
+ var _ io.Closer = (* CloseFS )(nil )
146
+
147
+ // CloseFS is a wrapper around fs.FS that implements io.Closer. The Close()
148
+ // method tells the cache to release the fileID. Once all open references are
149
+ // closed, the file is removed from the cache.
150
+ type CloseFS struct {
151
+ fs.FS
152
+
153
+ close func () error
154
+ }
155
+
156
+ func (f * CloseFS ) Close () error { return f .close () }
157
+
143
158
// Acquire will load the fs.FS for the given file. It guarantees that parallel
144
159
// calls for the same fileID will only result in one fetch, and that parallel
145
160
// calls for distinct fileIDs will fetch in parallel.
146
161
//
147
162
// Safety: Every call to Acquire that does not return an error must have a
148
163
// matching call to Release.
149
- func (c * Cache ) Acquire (ctx context.Context , fileID uuid.UUID ) (fs. FS , error ) {
164
+ func (c * Cache ) Acquire (ctx context.Context , fileID uuid.UUID ) (* CloseFS , error ) {
150
165
// It's important that this `Load` call occurs outside of `prepare`, after the
151
166
// mutex has been released, or we would continue to hold the lock until the
152
167
// entire file has been fetched, which may be slow, and would prevent other
153
168
// files from being fetched in parallel.
154
169
it , err := c .prepare (ctx , fileID ).Load ()
155
170
if err != nil {
156
- c .Release (fileID )
171
+ c .release (fileID )
157
172
return nil , err
158
173
}
159
174
@@ -163,11 +178,20 @@ func (c *Cache) Acquire(ctx context.Context, fileID uuid.UUID) (fs.FS, error) {
163
178
}
164
179
// Always check the caller can actually read the file.
165
180
if err := c .authz .Authorize (ctx , subject , policy .ActionRead , it .Object ); err != nil {
166
- c .Release (fileID )
181
+ c .release (fileID )
167
182
return nil , err
168
183
}
169
184
170
- return it .FS , err
185
+ var once sync.Once
186
+ return & CloseFS {
187
+ FS : it .FS ,
188
+ close : func () error {
189
+ // sync.Once makes the Close() idempotent, so we can call it
190
+ // multiple times without worrying about double-releasing.
191
+ once .Do (func () { c .release (fileID ) })
192
+ return nil
193
+ },
194
+ }, nil
171
195
}
172
196
173
197
func (c * Cache ) prepare (ctx context.Context , fileID uuid.UUID ) * lazy.ValueWithError [CacheEntryValue ] {
@@ -203,9 +227,12 @@ func (c *Cache) prepare(ctx context.Context, fileID uuid.UUID) *lazy.ValueWithEr
203
227
return entry .value
204
228
}
205
229
206
- // Release decrements the reference count for the given fileID, and frees the
230
+ // release decrements the reference count for the given fileID, and frees the
207
231
// backing data if there are no further references being held.
208
- func (c * Cache ) Release (fileID uuid.UUID ) {
232
+ //
233
+ // release should only be called after a successful call to Acquire using the Release()
234
+ // method on the returned *CloseFS.
235
+ func (c * Cache ) release (fileID uuid.UUID ) {
209
236
c .lock .Lock ()
210
237
defer c .lock .Unlock ()
211
238
0 commit comments