Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/ContainerBuild/BuildFSSync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ actor BuildFSSync: BuildPipelineHandler {
pathInArchive: URL(fileURLWithPath: rel))
}

for await chunk in try tarURL.zeroCopyReader() {
for try await chunk in try tarURL.bufferedCopyReader() {
let part = BuildTransfer(
id: packet.id,
source: tarURL.path,
Expand Down
143 changes: 143 additions & 0 deletions Sources/ContainerBuild/URL+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,147 @@ extension URL {
}
}
}

func bufferedCopyReader(chunkSize: Int = 4 * 1024 * 1024) throws -> BufferedCopyReader {
try BufferedCopyReader(url: self, chunkSize: chunkSize)
}
}

/// A synchronous buffered reader that reads one chunk at a time from a file
/// Uses a configurable buffer size (default 4MB) and only reads when nextChunk() is called
/// Implements AsyncSequence for use with `for await` loops
public final class BufferedCopyReader: AsyncSequence {
public typealias Element = Data
public typealias AsyncIterator = BufferedCopyReaderIterator

private let inputStream: InputStream
private let chunkSize: Int
private var isFinished: Bool = false
private let reusableBuffer: UnsafeMutablePointer<UInt8>

/// Initialize a buffered copy reader for the given URL
/// - Parameters:
/// - url: The file URL to read from
/// - chunkSize: Size of each chunk to read (default: 4MB)
public init(url: URL, chunkSize: Int = 4 * 1024 * 1024) throws {
guard let stream = InputStream(url: url) else {
throw CocoaError(.fileReadNoSuchFile)
}
self.inputStream = stream
self.chunkSize = chunkSize
self.reusableBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: chunkSize)
self.inputStream.open()
}

deinit {
inputStream.close()
reusableBuffer.deallocate()
}

/// Create an async iterator for this sequence
public func makeAsyncIterator() -> BufferedCopyReaderIterator {
BufferedCopyReaderIterator(reader: self)
}

/// Read the next chunk of data from the file
/// - Returns: Data chunk, or nil if end of file reached
/// - Throws: Any file reading errors
public func nextChunk() throws -> Data? {
guard !isFinished else { return nil }

// Read directly into our reusable buffer
let bytesRead = inputStream.read(reusableBuffer, maxLength: chunkSize)

// Check for errors
if bytesRead < 0 {
if let error = inputStream.streamError {
throw error
}
throw CocoaError(.fileReadUnknown)
}

// If we read no data, we've reached the end
if bytesRead == 0 {
isFinished = true
return nil
}

// If we read less than the chunk size, this is the last chunk
if bytesRead < chunkSize {
isFinished = true
}

// Create Data object only with the bytes actually read
return Data(bytes: reusableBuffer, count: bytesRead)
}

/// Check if the reader has finished reading the file
public var hasFinished: Bool {
isFinished
}

/// Reset the reader to the beginning of the file
/// Note: InputStream doesn't support seeking, so this recreates the stream
/// - Throws: Any file opening errors
public func reset() throws {
inputStream.close()
// Note: InputStream doesn't provide a way to get the original URL,
// so reset functionality is limited. Consider removing this method
// or storing the original URL if reset is needed.
throw CocoaError(
.fileReadUnsupportedScheme,
userInfo: [
NSLocalizedDescriptionKey: "Reset not supported with InputStream-based implementation"
])
}

/// Get the current file offset
/// Note: InputStream doesn't provide offset information
/// - Returns: Current position in the file
/// - Throws: Unsupported operation error
public func currentOffset() throws -> UInt64 {
throw CocoaError(
.fileReadUnsupportedScheme,
userInfo: [
NSLocalizedDescriptionKey: "Offset tracking not supported with InputStream-based implementation"
])
}

/// Seek to a specific offset in the file
/// Note: InputStream doesn't support seeking
/// - Parameter offset: The byte offset to seek to
/// - Throws: Unsupported operation error
public func seek(to offset: UInt64) throws {
throw CocoaError(
.fileReadUnsupportedScheme,
userInfo: [
NSLocalizedDescriptionKey: "Seeking not supported with InputStream-based implementation"
])
}

/// Close the input stream explicitly (called automatically in deinit)
public func close() {
inputStream.close()
isFinished = true
}
}

/// AsyncIteratorProtocol implementation for BufferedCopyReader
public struct BufferedCopyReaderIterator: AsyncIteratorProtocol {
public typealias Element = Data

private let reader: BufferedCopyReader

init(reader: BufferedCopyReader) {
self.reader = reader
}

/// Get the next chunk of data asynchronously
/// - Returns: Next data chunk, or nil when finished
/// - Throws: Any file reading errors
public mutating func next() async throws -> Data? {
// Yield control to allow other tasks to run, then read synchronously
await Task.yield()
return try reader.nextChunk()
}
}