Skip to content

Commit a2a1add

Browse files
authored
Add totalAllocatedSize to ContentStore (#760)
This PR adds `totalAllocatedSize()` to the `ContentStore` protocol so it can be used to get the on-disk footprint without reaching past the abstraction. `LocalContentStore` implements it by walking its base path, covering both committed blobs and active ingest sessions.
1 parent 6cb6658 commit a2a1add

3 files changed

Lines changed: 95 additions & 0 deletions

File tree

Sources/ContainerizationOCI/Content/ContentStoreProtocol.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,8 @@ public protocol ContentStore: Sendable {
6060
/// Cancels a previously started ingest session corresponding to `id`.
6161
/// The contents from the ingest directory corresponding to the session are removed.
6262
func cancelIngestSession(_ id: String) async throws
63+
64+
/// Total bytes allocated on disk for the content store, covering
65+
/// committed blobs and any active ingest sessions.
66+
func totalAllocatedSize() async throws -> UInt64
6367
}

Sources/ContainerizationOCI/Content/LocalContentStore.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,29 @@ public actor LocalContentStore: ContentStore {
198198
let fileManager = FileManager.default
199199
try? fileManager.removeItem(at: temporaryPath)
200200
}
201+
202+
/// Total bytes allocated on disk for the content store, covering
203+
/// committed blobs and any active ingest sessions.
204+
public func totalAllocatedSize() throws -> UInt64 {
205+
let fileManager = FileManager.default
206+
guard
207+
let enumerator = fileManager.enumerator(
208+
at: self._basePath,
209+
includingPropertiesForKeys: [.totalFileAllocatedSizeKey],
210+
options: [.skipsHiddenFiles]
211+
)
212+
else {
213+
throw ContainerizationError(.internalError, message: "failed to enumerate content store at \(self._basePath.path)")
214+
}
215+
var size: UInt64 = 0
216+
for case let fileURL as URL in enumerator {
217+
guard let values = try? fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey]),
218+
let fileSize = values.totalFileAllocatedSize
219+
else {
220+
continue
221+
}
222+
size += UInt64(fileSize)
223+
}
224+
return size
225+
}
201226
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import Testing
19+
20+
@testable import ContainerizationOCI
21+
22+
@Suite
23+
struct LocalContentStoreTests {
24+
private static let digestA = String(repeating: "a", count: 64)
25+
private static let digestB = String(repeating: "b", count: 64)
26+
27+
@Test func totalAllocatedSizeReportsZeroForEmptyStore() async throws {
28+
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
29+
defer { try? FileManager.default.removeItem(at: dir) }
30+
31+
let store = try LocalContentStore(path: dir)
32+
let size = try await store.totalAllocatedSize()
33+
#expect(size == 0)
34+
}
35+
36+
@Test func totalAllocatedSizeReflectsCommittedBlobs() async throws {
37+
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
38+
defer { try? FileManager.default.removeItem(at: dir) }
39+
40+
let store = try LocalContentStore(path: dir)
41+
let payload = Data(repeating: 0xAB, count: 64 * 1024)
42+
43+
try await store.ingest { tempDir in
44+
try payload.write(to: tempDir.appendingPathComponent(Self.digestA))
45+
}
46+
47+
let size = try await store.totalAllocatedSize()
48+
#expect(size >= UInt64(payload.count))
49+
}
50+
51+
@Test func totalAllocatedSizeIncludesInFlightIngest() async throws {
52+
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
53+
defer { try? FileManager.default.removeItem(at: dir) }
54+
55+
let store = try LocalContentStore(path: dir)
56+
let payload = Data(repeating: 0xCD, count: 32 * 1024)
57+
58+
let session = try await store.newIngestSession()
59+
try payload.write(to: session.ingestDir.appendingPathComponent(Self.digestB))
60+
61+
let size = try await store.totalAllocatedSize()
62+
#expect(size >= UInt64(payload.count))
63+
64+
try await store.cancelIngestSession(session.id)
65+
}
66+
}

0 commit comments

Comments
 (0)