@@ -16,14 +16,29 @@ struct ContentStore {
1616 private static let digestPrefix = " sha256: "
1717
1818 let baseURL : URL
19+ private let pruneLockURL : URL
1920
2021 init ( ) throws {
2122 try self . init ( baseURL: Config ( ) . tartCacheDir. appendingPathComponent ( " content " , isDirectory: true ) )
2223 }
2324
2425 init ( baseURL: URL ) throws {
2526 self . baseURL = baseURL
27+ pruneLockURL = baseURL. appendingPathComponent ( " .gc.lock " )
2628 try FileManager . default. createDirectory ( at: baseURL, withIntermediateDirectories: true )
29+ if !FileManager. default. fileExists ( atPath: pruneLockURL. path) {
30+ _ = FileManager . default. createFile ( atPath: pruneLockURL. path, contents: Data ( ) )
31+ }
32+ }
33+
34+ /// Serializes reference publication with the final reference check and
35+ /// deletion of immutable cache entries across Tart processes.
36+ func withPruneLock< T> ( _ body: ( ) throws -> T ) throws -> T {
37+ let lock = try FileLock ( lockURL: pruneLockURL)
38+ try lock. lock ( )
39+ defer { try ? lock. unlock ( ) }
40+
41+ return try body ( )
2742 }
2843
2944 func contentURL( for contentDigest: String ) throws -> URL {
@@ -65,16 +80,18 @@ struct ContentStore {
6580 return lockURL
6681 }
6782
68- /// Returns a digest-addressed entry without rereading it. Pull verifies
69- /// content hashes before accepting a cache hit; clone only needs a cheap
70- /// structural check, like Tart's existing disk.img path .
83+ /// Returns an immutable digest-addressed entry without rereading it. Files
84+ /// are verified when installed and when deciding whether a pull is a cache
85+ /// hit; normal clone/run/push paths trust the store like Tart's disk.img.
7186 func contentURLIfPresent( for contentDigest: String ) throws -> URL ? {
7287 let url = try contentURL ( for: contentDigest)
7388
7489 guard FileManager . default. fileExists ( atPath: url. path) else {
7590 return nil
7691 }
7792
93+ try url. updateAccessDate ( )
94+
7895 return url
7996 }
8097
@@ -92,6 +109,66 @@ struct ContentStore {
92109 return url
93110 }
94111
112+ /// Returns immutable content files that no retained OCI record or local VM
113+ /// references. Callers may prune these like other cache entries.
114+ func prunables( excluding referencedContentDigests: Swift . Set < String > ) throws -> [ URL ] {
115+ let sha256URL = baseURL. appendingPathComponent ( " sha256 " , isDirectory: true )
116+ guard let enumerator = FileManager . default. enumerator (
117+ at: sha256URL,
118+ includingPropertiesForKeys: [ . isRegularFileKey] ,
119+ options: [ . skipsSubdirectoryDescendants]
120+ ) else {
121+ return [ ]
122+ }
123+
124+ return try enumerator. compactMap { element in
125+ guard let url = element as? URL ,
126+ try url. resourceValues ( forKeys: [ . isRegularFileKey] ) . isRegularFile == true else {
127+ return nil
128+ }
129+
130+ let contentDigest = " sha256: \( url. lastPathComponent) "
131+ guard ( try ? validatedDigestHex ( contentDigest) ) != nil ,
132+ !referencedContentDigests. contains ( contentDigest) else {
133+ return nil
134+ }
135+
136+ return url
137+ }
138+ }
139+
140+ /// Reconstructs one immutable disk file from transport chunks unless a
141+ /// validated copy is already present under its whole-file digest.
142+ func pullContent(
143+ registry: Registry ,
144+ chunks: [ OCIManifestLayer ] ,
145+ contentDigest: String ,
146+ concurrency: UInt ,
147+ progress: Progress
148+ ) async throws -> URL {
149+ if let existingURL = try existingContentURL ( for: contentDigest) {
150+ progress. completedUnitCount += chunks. reduce ( 0 ) { $0 + Int64( $1. size) }
151+ return existingURL
152+ }
153+
154+ let temporaryURL = try temporaryContentURL ( for: contentDigest)
155+
156+ do {
157+ try await DiskV2 . pull (
158+ registry: registry,
159+ diskLayers: chunks,
160+ diskURL: temporaryURL,
161+ concurrency: concurrency,
162+ progress: progress
163+ )
164+
165+ return try install ( temporaryURL, contentDigest: contentDigest)
166+ } catch {
167+ try ? FileManager . default. removeItem ( at: temporaryURL)
168+ throw error
169+ }
170+ }
171+
95172 /// Move a fully reconstructed temporary file into the cache after verifying
96173 /// its semantic identity. The caller should create the temporary file with
97174 /// temporaryContentURL(for:) or partialContentURL(for:) so rename stays on
0 commit comments