Files
container/Sources/ContainerizationOCI/Content/LocalContentStore.swift
T
+6 3407cc3f16 initial commit
Co-authored-by: Aditya Ramani <a_ramani@apple.com>
Co-authored-by: Agam Dua <agam_dua@apple.com>
Co-authored-by: Danny Canter <danny_canter@apple.com>
Co-authored-by: Dmitry Kovba <dkovba@apple.com>
Co-authored-by: Eric Ernst <eric_ernst@apple.com>
Co-authored-by: Evan Hazlett <ehazlett@apple.com>
Co-authored-by: Gilbert Song <gilbertsong@apple.com>
Co-authored-by: Hugh Bussell <hbussell@apple.com>
Co-authored-by: John Logan <john_logan@apple.com>
Co-authored-by: Kathryn Baldauf <k_baldauf@apple.com>
Co-authored-by: Madhu Venugopal <mvenugopal@apple.com>
Co-authored-by: Michael Crosby <michael_crosby@apple.com>
Co-authored-by: Sidhartha Mani <sidhartha_mani@apple.com>
Co-authored-by: Tanweer Noor <tnoor@apple.com>
Co-authored-by: Ximena Perez Diaz <xperez528@gmail.com>
Co-authored-by: Yibo Zhuang <yzhuang@apple.com>
2025-06-05 16:15:21 -07:00

160 lines
6.2 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the containerization project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
// swiftlint:disable unused_optional_binding
import ContainerizationError
import ContainerizationExtras
import Crypto
import Foundation
public actor LocalContentStore: ContentStore {
private static let encoder = JSONEncoder()
private let _basePath: URL
private let _ingestPath: URL
private let _blobPath: URL
private let _lock: AsyncLock
private var activeIngestSessions: AsyncSet<String> = AsyncSet([])
public init(path: URL) throws {
let ingestPath = path.appendingPathComponent("ingest")
let blobPath = path.appendingPathComponent("blobs/sha256")
let fileManager = FileManager.default
try fileManager.createDirectory(at: ingestPath, withIntermediateDirectories: true)
try fileManager.createDirectory(at: blobPath, withIntermediateDirectories: true)
self._basePath = path
self._ingestPath = ingestPath
self._blobPath = blobPath
self._lock = AsyncLock()
Self.encoder.outputFormatting = .sortedKeys
}
public func get(digest: String) throws -> Content? {
let d = digest.trimmingDigestPrefix
let path = self._blobPath.appendingPathComponent(d)
do {
return try LocalContent(path: path)
} catch let err as ContainerizationError {
switch err.code {
case .notFound:
return nil
default:
throw err
}
}
}
public func get<T: Decodable & Sendable>(digest: String) throws -> T? {
guard let content: Content = try self.get(digest: digest) else {
return nil
}
return try content.decode()
}
public func delete(keeping: [String]) async throws -> ([String], UInt64) {
let fileManager = FileManager.default
let all = try fileManager.contentsOfDirectory(at: self._blobPath, includingPropertiesForKeys: nil)
let allDigests = Set(all.map { $0.lastPathComponent })
let toDelete = allDigests.subtracting(keeping)
return try await self.delete(digests: Array(toDelete))
}
@discardableResult
public func delete(digests: [String]) async throws -> ([String], UInt64) {
let store = AsyncStore<([String], UInt64)>()
try await self._lock.withLock { context in
let fileManager = FileManager.default
var deleted: [String] = []
var deletedBytes: UInt64 = 0
for toDelete in digests {
let p = self._blobPath.appendingPathComponent(toDelete)
guard let content = try? LocalContent(path: p) else {
continue
}
deletedBytes += try content.size()
try fileManager.removeItem(at: p)
deleted.append(toDelete)
}
await store.set((deleted, deletedBytes))
}
return await store.get() ?? ([], 0)
}
@discardableResult
public func ingest(_ body: @Sendable @escaping (URL) async throws -> Void) async throws -> [String] {
let (id, tempPath) = try await self.newIngestSession()
try await body(tempPath)
return try await self.completeIngestSession(id)
}
public func newIngestSession() async throws -> (id: String, ingestDir: URL) {
let id = UUID().uuidString
let temporaryPath = self._ingestPath.appendingPathComponent(id)
let fileManager = FileManager.default
try fileManager.createDirectory(atPath: temporaryPath.path, withIntermediateDirectories: true)
await self.activeIngestSessions.insert(id)
return (id, temporaryPath)
}
@discardableResult
public func completeIngestSession(_ id: String) async throws -> [String] {
guard await activeIngestSessions.contains(id) else {
throw ContainerizationError(.internalError, message: "Invalid session id \(id)")
}
await activeIngestSessions.remove(id)
let temporaryPath = self._ingestPath.appendingPathComponent(id)
let fileManager = FileManager.default
defer {
try? fileManager.removeItem(at: temporaryPath)
}
let tempDigests: [URL] = try fileManager.contentsOfDirectory(at: temporaryPath, includingPropertiesForKeys: nil)
return try await self._lock.withLock { context in
var moved: [String] = []
let fileManager = FileManager.default
do {
try tempDigests.forEach {
let digest = $0.lastPathComponent
let target = self._blobPath.appendingPathComponent(digest)
// only ingest if not exists
if !fileManager.fileExists(atPath: target.path) {
try fileManager.moveItem(at: $0, to: target)
moved.append(digest)
}
}
} catch {
moved.forEach {
try? fileManager.removeItem(at: self._blobPath.appendingPathComponent($0))
}
throw error
}
return tempDigests.map { $0.lastPathComponent }
}
}
public func cancelIngestSession(_ id: String) async throws {
guard let _ = await self.activeIngestSessions.remove(id) else {
return
}
let temporaryPath = self._ingestPath.appendingPathComponent(id)
let fileManager = FileManager.default
try? fileManager.removeItem(at: temporaryPath)
}
}