Agent: Add single file copying (#463)

Add a streaming rpc to copy single files in and out. This can easily be
extended to support copying directories, but we need the guest agent to
be able to tar/untar, and we'll need to figure out what that model is.
This commit is contained in:
Danny Canter authored and GitHub committed 2026-01-02 08:57:40 -08:00
1 parent fb95398fb5
commit 44ecb87f8d
9 files changed
+1165 -8

No files matched your search

+54 -1
View File
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -830,6 +830,59 @@ extension LinuxContainer {
try await relayManager.start(port: port, socket: socket)
try await relayAgent.relaySocket(port: port, configuration: socket)
}
/// Default chunk size for file transfers (1MiB).
public static let defaultCopyChunkSize = 1024 * 1024
/// Copy a file from the host into the container.
public func copyIn(
from source: URL,
to destination: URL,
mode: UInt32 = 0o644,
createParents: Bool = true,
chunkSize: Int = defaultCopyChunkSize,
progress: ProgressHandler? = nil
) async throws {
try await self.state.withLock {
let state = try $0.startedState("copyIn")
let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyIn(
from: source,
to: guestPath,
mode: mode,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
/// Copy a file from the container to the host.
public func copyOut(
from source: URL,
to destination: URL,
createParents: Bool = true,
chunkSize: Int = defaultCopyChunkSize,
progress: ProgressHandler? = nil
) async throws {
try await self.state.withLock {
let state = try $0.startedState("copyOut")
let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}
extension VirtualMachineInstance {
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -79,6 +79,16 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtoc
callOptions: CallOptions?
) -> UnaryCall<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>
func copyIn(
callOptions: CallOptions?
) -> ClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>
func copyOut(
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
callOptions: CallOptions?,
handler: @escaping (Com_Apple_Containerization_Sandbox_V3_CopyOutChunk) -> Void
) -> ServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>
func createProcess(
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
callOptions: CallOptions?
@@ -337,6 +347,45 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextClientProtocol {
)
}
/// Copy a file from the host into the guest.
///
/// Callers should use the `send` method on the returned object to send messages
/// to the server. The caller should send an `.end` after the final message has been sent.
///
/// - Parameters:
/// - callOptions: Call options.
/// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response.
public func copyIn(
callOptions: CallOptions? = nil
) -> ClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse> {
return self.makeClientStreamingCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
)
}
/// Copy a file from the guest to the host.
///
/// - Parameters:
/// - request: Request to send to CopyOut.
/// - callOptions: Call options.
/// - handler: A closure called when each response is received from the server.
/// - Returns: A `ServerStreamingCall` with futures for the metadata and status.
public func copyOut(
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
callOptions: CallOptions? = nil,
handler: @escaping (Com_Apple_Containerization_Sandbox_V3_CopyOutChunk) -> Void
) -> ServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk> {
return self.makeServerStreamingCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? [],
handler: handler
)
}
/// Create a new process inside the container.
///
/// - Parameters:
@@ -771,6 +820,15 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientP
callOptions: CallOptions?
) -> GRPCAsyncUnaryCall<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>
func makeCopyInCall(
callOptions: CallOptions?
) -> GRPCAsyncClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>
func makeCopyOutCall(
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
callOptions: CallOptions?
) -> GRPCAsyncServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>
func makeCreateProcessCall(
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
callOptions: CallOptions?
@@ -980,6 +1038,28 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco
)
}
public func makeCopyInCall(
callOptions: CallOptions? = nil
) -> GRPCAsyncClientStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse> {
return self.makeAsyncClientStreamingCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
)
}
public func makeCopyOutCall(
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
callOptions: CallOptions? = nil
) -> GRPCAsyncServerStreamingCall<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk> {
return self.makeAsyncServerStreamingCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? []
)
}
public func makeCreateProcessCall(
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
callOptions: CallOptions? = nil
@@ -1307,6 +1387,42 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncClientProtoco
)
}
public func copyIn<RequestStream>(
_ requests: RequestStream,
callOptions: CallOptions? = nil
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse where RequestStream: Sequence, RequestStream.Element == Com_Apple_Containerization_Sandbox_V3_CopyInChunk {
return try await self.performAsyncClientStreamingCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
requests: requests,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
)
}
public func copyIn<RequestStream>(
_ requests: RequestStream,
callOptions: CallOptions? = nil
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Com_Apple_Containerization_Sandbox_V3_CopyInChunk {
return try await self.performAsyncClientStreamingCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn.path,
requests: requests,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCopyInInterceptors() ?? []
)
}
public func copyOut(
_ request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
callOptions: CallOptions? = nil
) -> GRPCAsyncResponseStream<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk> {
return self.performAsyncServerStreamingCall(
path: Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut.path,
request: request,
callOptions: callOptions ?? self.defaultCallOptions,
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? []
)
}
public func createProcess(
_ request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
callOptions: CallOptions? = nil
@@ -1570,6 +1686,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextClientInterc
/// - Returns: Interceptors to use when invoking 'writeFile'.
func makeWriteFileInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>]
/// - Returns: Interceptors to use when invoking 'copyIn'.
func makeCopyInInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>]
/// - Returns: Interceptors to use when invoking 'copyOut'.
func makeCopyOutInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>]
/// - Returns: Interceptors to use when invoking 'createProcess'.
func makeCreateProcessInterceptors() -> [ClientInterceptor<Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, Com_Apple_Containerization_Sandbox_V3_CreateProcessResponse>]
@@ -1639,6 +1761,8 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata {
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.setTime,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.setupEmulator,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.writeFile,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyIn,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.copyOut,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.createProcess,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.deleteProcess,
Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata.Methods.startProcess,
@@ -1715,6 +1839,18 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextClientMetadata {
type: GRPCCallType.unary
)
public static let copyIn = GRPCMethodDescriptor(
name: "CopyIn",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyIn",
type: GRPCCallType.clientStreaming
)
public static let copyOut = GRPCMethodDescriptor(
name: "CopyOut",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyOut",
type: GRPCCallType.serverStreaming
)
public static let createProcess = GRPCMethodDescriptor(
name: "CreateProcess",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess",
@@ -1858,6 +1994,12 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider: Ca
/// Write data to an existing or new file.
func writeFile(request: Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>
/// Copy a file from the host into the guest.
func copyIn(context: UnaryResponseCallContext<Com_Apple_Containerization_Sandbox_V3_CopyInResponse>) -> EventLoopFuture<(StreamEvent<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>) -> Void>
/// Copy a file from the guest to the host.
func copyOut(request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, context: StreamingResponseCallContext<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>) -> EventLoopFuture<GRPCStatus>
/// Create a new process inside the container.
func createProcess(request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, context: StatusOnlyCallContext) -> EventLoopFuture<Com_Apple_Containerization_Sandbox_V3_CreateProcessResponse>
@@ -2007,6 +2149,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextProvider {
userFunction: self.writeFile(request:context:)
)
case "CopyIn":
return ClientStreamingServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyInResponse>(),
interceptors: self.interceptors?.makeCopyInInterceptors() ?? [],
observerFactory: self.copyIn(context:)
)
case "CopyOut":
return ServerStreamingServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>(),
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? [],
userFunction: self.copyOut(request:context:)
)
case "CreateProcess":
return UnaryServerHandler(
context: context,
@@ -2237,6 +2397,19 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvide
context: GRPCAsyncServerCallContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_WriteFileResponse
/// Copy a file from the host into the guest.
func copyIn(
requestStream: GRPCAsyncRequestStream<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>,
context: GRPCAsyncServerCallContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse
/// Copy a file from the guest to the host.
func copyOut(
request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
responseStream: GRPCAsyncResponseStreamWriter<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>,
context: GRPCAsyncServerCallContext
) async throws
/// Create a new process inside the container.
func createProcess(
request: Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest,
@@ -2447,6 +2620,24 @@ extension Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvider {
wrapping: { try await self.writeFile(request: $0, context: $1) }
)
case "CopyIn":
return GRPCAsyncServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyInResponse>(),
interceptors: self.interceptors?.makeCopyInInterceptors() ?? [],
wrapping: { try await self.copyIn(requestStream: $0, context: $1) }
)
case "CopyOut":
return GRPCAsyncServerHandler(
context: context,
requestDeserializer: ProtobufDeserializer<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest>(),
responseSerializer: ProtobufSerializer<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>(),
interceptors: self.interceptors?.makeCopyOutInterceptors() ?? [],
wrapping: { try await self.copyOut(request: $0, responseStream: $1, context: $2) }
)
case "CreateProcess":
return GRPCAsyncServerHandler(
context: context,
@@ -2653,6 +2844,14 @@ public protocol Com_Apple_Containerization_Sandbox_V3_SandboxContextServerInterc
/// Defaults to calling `self.makeInterceptors()`.
func makeWriteFileInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_WriteFileRequest, Com_Apple_Containerization_Sandbox_V3_WriteFileResponse>]
/// - Returns: Interceptors to use when handling 'copyIn'.
/// Defaults to calling `self.makeInterceptors()`.
func makeCopyInInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyInChunk, Com_Apple_Containerization_Sandbox_V3_CopyInResponse>]
/// - Returns: Interceptors to use when handling 'copyOut'.
/// Defaults to calling `self.makeInterceptors()`.
func makeCopyOutInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>]
/// - Returns: Interceptors to use when handling 'createProcess'.
/// Defaults to calling `self.makeInterceptors()`.
func makeCreateProcessInterceptors() -> [ServerInterceptor<Com_Apple_Containerization_Sandbox_V3_CreateProcessRequest, Com_Apple_Containerization_Sandbox_V3_CreateProcessResponse>]
@@ -2740,6 +2939,8 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata {
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.setTime,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.setupEmulator,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.writeFile,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.copyIn,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.copyOut,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.createProcess,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.deleteProcess,
Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata.Methods.startProcess,
@@ -2816,6 +3017,18 @@ public enum Com_Apple_Containerization_Sandbox_V3_SandboxContextServerMetadata {
type: GRPCCallType.unary
)
public static let copyIn = GRPCMethodDescriptor(
name: "CopyIn",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyIn",
type: GRPCCallType.clientStreaming
)
public static let copyOut = GRPCMethodDescriptor(
name: "CopyOut",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CopyOut",
type: GRPCCallType.serverStreaming
)
public static let createProcess = GRPCMethodDescriptor(
name: "CreateProcess",
path: "/com.apple.containerization.sandbox.v3.SandboxContext/CreateProcess",
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -756,6 +756,137 @@ public struct Com_Apple_Containerization_Sandbox_V3_WriteFileResponse: Sendable
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_CopyInChunk: @unchecked Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
public var content: Com_Apple_Containerization_Sandbox_V3_CopyInChunk.OneOf_Content? = nil
/// Initialization message (must be first).
public var init_p: Com_Apple_Containerization_Sandbox_V3_CopyInInit {
get {
if case .init_p(let v)? = content {return v}
return Com_Apple_Containerization_Sandbox_V3_CopyInInit()
}
set {content = .init_p(newValue)}
}
/// File data chunk.
public var data: Data {
get {
if case .data(let v)? = content {return v}
return Data()
}
set {content = .data(newValue)}
}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public enum OneOf_Content: Equatable, @unchecked Sendable {
/// Initialization message (must be first).
case init_p(Com_Apple_Containerization_Sandbox_V3_CopyInInit)
/// File data chunk.
case data(Data)
}
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// Destination path in the guest.
public var path: String = String()
/// File mode (defaults to 0644 if not set).
public var mode: UInt32 = 0
/// Create parent directories if they don't exist.
public var createParents: Bool = false
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_CopyInResponse: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// Source path in the guest.
public var path: String = String()
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_CopyOutChunk: @unchecked Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
public var content: Com_Apple_Containerization_Sandbox_V3_CopyOutChunk.OneOf_Content? = nil
/// Initialization message with metadata (first chunk).
public var init_p: Com_Apple_Containerization_Sandbox_V3_CopyOutInit {
get {
if case .init_p(let v)? = content {return v}
return Com_Apple_Containerization_Sandbox_V3_CopyOutInit()
}
set {content = .init_p(newValue)}
}
/// File data chunk.
public var data: Data {
get {
if case .data(let v)? = content {return v}
return Data()
}
set {content = .data(newValue)}
}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public enum OneOf_Content: Equatable, @unchecked Sendable {
/// Initialization message with metadata (first chunk).
case init_p(Com_Apple_Containerization_Sandbox_V3_CopyOutInit)
/// File data chunk.
case data(Data)
}
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_CopyOutInit: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
/// Total file size in bytes.
public var totalSize: UInt64 = 0
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public struct Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: Sendable {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
@@ -2565,6 +2696,263 @@ extension Com_Apple_Containerization_Sandbox_V3_WriteFileResponse: SwiftProtobuf
}
}
extension Com_Apple_Containerization_Sandbox_V3_CopyInChunk: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".CopyInChunk"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "init"),
2: .same(proto: "data"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try {
var v: Com_Apple_Containerization_Sandbox_V3_CopyInInit?
var hadOneofValue = false
if let current = self.content {
hadOneofValue = true
if case .init_p(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.content = .init_p(v)
}
}()
case 2: try {
var v: Data?
try decoder.decodeSingularBytesField(value: &v)
if let v = v {
if self.content != nil {try decoder.handleConflictingOneOf()}
self.content = .data(v)
}
}()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
switch self.content {
case .init_p?: try {
guard case .init_p(let v)? = self.content else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
}()
case .data?: try {
guard case .data(let v)? = self.content else { preconditionFailure() }
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyInChunk, rhs: Com_Apple_Containerization_Sandbox_V3_CopyInChunk) -> Bool {
if lhs.content != rhs.content {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".CopyInInit"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.path) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.mode) }()
case 3: try { try decoder.decodeSingularBoolField(value: &self.createParents) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.mode != 0 {
try visitor.visitSingularUInt32Field(value: self.mode, fieldNumber: 2)
}
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyInInit, rhs: Com_Apple_Containerization_Sandbox_V3_CopyInInit) -> Bool {
if lhs.path != rhs.path {return false}
if lhs.mode != rhs.mode {return false}
if lhs.createParents != rhs.createParents {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_CopyInResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".CopyInResponse"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
// Load everything into unknown fields
while try decoder.nextFieldNumber() != nil {}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyInResponse, rhs: Com_Apple_Containerization_Sandbox_V3_CopyInResponse) -> Bool {
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.path) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest, rhs: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest) -> Bool {
if lhs.path != rhs.path {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_CopyOutChunk: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".CopyOutChunk"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "init"),
2: .same(proto: "data"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try {
var v: Com_Apple_Containerization_Sandbox_V3_CopyOutInit?
var hadOneofValue = false
if let current = self.content {
hadOneofValue = true
if case .init_p(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.content = .init_p(v)
}
}()
case 2: try {
var v: Data?
try decoder.decodeSingularBytesField(value: &v)
if let v = v {
if self.content != nil {try decoder.handleConflictingOneOf()}
self.content = .data(v)
}
}()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
switch self.content {
case .init_p?: try {
guard case .init_p(let v)? = self.content else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 1)
}()
case .data?: try {
guard case .data(let v)? = self.content else { preconditionFailure() }
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyOutChunk, rhs: Com_Apple_Containerization_Sandbox_V3_CopyOutChunk) -> Bool {
if lhs.content != rhs.content {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_CopyOutInit: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".CopyOutInit"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "total_size"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt64Field(value: &self.totalSize) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.totalSize != 0 {
try visitor.visitSingularUInt64Field(value: self.totalSize, fieldNumber: 1)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_CopyOutInit, rhs: Com_Apple_Containerization_Sandbox_V3_CopyOutInit) -> Bool {
if lhs.totalSize != rhs.totalSize {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Com_Apple_Containerization_Sandbox_V3_IpLinkSetRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".IpLinkSetRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@@ -24,6 +24,10 @@ service SandboxContext {
rpc SetupEmulator(SetupEmulatorRequest) returns (SetupEmulatorResponse);
// Write data to an existing or new file.
rpc WriteFile(WriteFileRequest) returns (WriteFileResponse);
// Copy a file from the host into the guest.
rpc CopyIn(stream CopyInChunk) returns (CopyInResponse);
// Copy a file from the guest to the host.
rpc CopyOut(CopyOutRequest) returns (stream CopyOutChunk);
// Create a new process inside the container.
rpc CreateProcess(CreateProcessRequest) returns (CreateProcessResponse);
@@ -225,6 +229,45 @@ message WriteFileRequest {
message WriteFileResponse {}
message CopyInChunk {
oneof content {
// Initialization message (must be first).
CopyInInit init = 1;
// File data chunk.
bytes data = 2;
}
}
message CopyInInit {
// Destination path in the guest.
string path = 1;
// File mode (defaults to 0644 if not set).
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
}
message CopyInResponse {}
message CopyOutRequest {
// Source path in the guest.
string path = 1;
}
message CopyOutChunk {
oneof content {
// Initialization message with metadata (first chunk).
CopyOutInit init = 1;
// File data chunk.
bytes data = 2;
}
}
message CopyOutInit {
// Total file size in bytes.
uint64 total_size = 1;
}
message IpLinkSetRequest {
string interface = 1;
bool up = 2;
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -46,6 +46,27 @@ public protocol VirtualMachineAgent: Sendable {
func sync() async throws
func writeFile(path: String, data: Data, flags: WriteFileFlags, mode: UInt32) async throws
// File transfer
/// Copy a file from the host into the guest.
func copyIn(
from source: URL,
to destination: URL,
mode: UInt32,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws
/// Copy a file from the guest to the host.
func copyOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws
// Process lifecycle
func createProcess(
id: String,
@@ -96,4 +117,25 @@ extension VirtualMachineAgent {
public func sync() async throws {
throw ContainerizationError(.unsupported, message: "sync")
}
public func copyIn(
from source: URL,
to destination: URL,
mode: UInt32,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyIn")
}
public func copyOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}
}
+91 -1
View File
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -409,6 +409,96 @@ extension Vminitd {
})
return response.result
}
/// Copy a file from the host into the guest.
public func copyIn(
from source: URL,
to destination: URL,
mode: UInt32,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
let fileHandle = try FileHandle(forReadingFrom: source)
defer { try? fileHandle.close() }
let attrs = try FileManager.default.attributesOfItem(atPath: source.path)
guard let fileSize = attrs[.size] as? Int64 else {
throw ContainerizationError(
.invalidArgument,
message: "copyIn: failed to get file size for '\(source.path)'"
)
}
await progress?([ProgressEvent(event: "add-total-size", value: fileSize)])
let call = client.makeCopyInCall()
try await call.requestStream.send(
.with {
$0.content = .init_p(
.with {
$0.path = destination.path
$0.mode = mode
$0.createParents = createParents
})
}
)
var totalSent: Int64 = 0
while true {
guard let data = try fileHandle.read(upToCount: chunkSize), !data.isEmpty else {
break
}
try await call.requestStream.send(.with { $0.content = .data(data) })
totalSent += Int64(data.count)
await progress?([ProgressEvent(event: "add-size", value: Int64(data.count))])
}
call.requestStream.finish()
_ = try await call.response
}
/// Copy a file from the guest to the host.
public func copyOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
let request = Com_Apple_Containerization_Sandbox_V3_CopyOutRequest.with {
$0.path = source.path
}
if createParents {
let parentDir = destination.deletingLastPathComponent()
try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true)
}
let fd = open(destination.path, O_WRONLY | O_CREAT | O_TRUNC, 0o644)
guard fd != -1 else {
throw ContainerizationError(
.internalError,
message: "copyOut: failed to open '\(destination.path)': \(String(cString: strerror(errno)))"
)
}
let fileHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
defer { try? fileHandle.close() }
let stream = client.copyOut(request)
for try await chunk in stream {
switch chunk.content {
case .init_p(let initMsg):
await progress?([ProgressEvent(event: "add-total-size", value: Int64(initMsg.totalSize))])
case .data(let data):
try fileHandle.write(contentsOf: data)
await progress?([ProgressEvent(event: "add-size", value: Int64(data.count))])
case .none:
break
}
}
}
}
extension Hosts {
+178 -1
View File
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -1436,4 +1436,181 @@ extension IntegrationSuite {
throw IntegrationError.assert(msg: "container with CAP_CHOWN should succeed, got exit code \(status.exitCode)")
}
}
func testCopyIn() async throws {
let id = "test-copy-in"
let bs = try await bootstrap(id)
// Create a temp file on the host with known content
let testContent = "Hello from the host! This is a copyIn test."
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("test-input.txt")
try testContent.write(to: hostFile, atomically: true, encoding: .utf8)
let buffer = BufferWriter()
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
config.bootLog = bs.bootLog
}
do {
try await container.create()
try await container.start()
// Copy the file into the container
try await container.copyIn(
from: hostFile,
to: URL(filePath: "/tmp/copied-file.txt")
)
// Verify the file exists and has correct content
let exec = try await container.exec("verify-copy") { config in
config.arguments = ["cat", "/tmp/copied-file.txt"]
config.stdout = buffer
}
try await exec.start()
let status = try await exec.wait()
try await exec.delete()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "cat command failed with status \(status)")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert output to UTF8")
}
guard output == testContent else {
throw IntegrationError.assert(
msg: "copied file content mismatch: expected '\(testContent)', got '\(output)'")
}
try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
func testCopyOut() async throws {
let id = "test-copy-out"
let bs = try await bootstrap(id)
let testContent = "Hello from the guest! This is a copyOut test."
let hostDestination = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("test-output.txt")
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
config.bootLog = bs.bootLog
}
do {
try await container.create()
try await container.start()
// Create a file inside the container
let exec = try await container.exec("create-file") { config in
config.arguments = ["sh", "-c", "echo -n '\(testContent)' > /tmp/guest-file.txt"]
}
try await exec.start()
let status = try await exec.wait()
try await exec.delete()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "failed to create file in guest, status \(status)")
}
// Copy the file out of the container
try await container.copyOut(
from: URL(filePath: "/tmp/guest-file.txt"),
to: hostDestination
)
// Verify the file was copied correctly
let copiedContent = try String(contentsOf: hostDestination, encoding: .utf8)
guard copiedContent == testContent else {
throw IntegrationError.assert(
msg: "copied file content mismatch: expected '\(testContent)', got '\(copiedContent)'")
}
try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
func testCopyLargeFile() async throws {
let id = "test-copy-large-file"
let bs = try await bootstrap(id)
// Create a 10MB file on the host with a repeating pattern
let fileSize = 10 * 1024 * 1024
let hostFile = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("large-file.bin")
// Generate data with a repeating pattern
let pattern = Data("ContainerizationCopyTest".utf8)
var testData = Data(capacity: fileSize)
while testData.count < fileSize {
testData.append(pattern)
}
testData = testData.prefix(fileSize)
try testData.write(to: hostFile)
let hostDestination = FileManager.default.uniqueTemporaryDirectory(create: true)
.appendingPathComponent("large-file-out.bin")
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
config.bootLog = bs.bootLog
}
do {
try await container.create()
try await container.start()
// Copy large file into the container
try await container.copyIn(
from: hostFile,
to: URL(filePath: "/tmp/large-file.bin")
)
// Copy it back out
try await container.copyOut(
from: URL(filePath: "/tmp/large-file.bin"),
to: hostDestination
)
// Verify the content matches
let copiedData = try Data(contentsOf: hostDestination)
guard copiedData.count == testData.count else {
throw IntegrationError.assert(
msg: "file size mismatch: expected \(testData.count), got \(copiedData.count)")
}
guard copiedData == testData else {
throw IntegrationError.assert(msg: "file content mismatch after round-trip copy")
}
try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
}
+4 -1
View File
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -305,6 +305,9 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("container capabilities OCI default", testCapabilitiesOCIDefault),
Test("container capabilities all capabilities", testCapabilitiesAllCapabilities),
Test("container capabilities file ownership", testCapabilitiesFileOwnership),
Test("container copy in", testCopyIn),
Test("container copy out", testCopyOut),
Test("container copy large file", testCopyLargeFile),
// Pods
Test("pod single container", testPodSingleContainer),
+149 -1
View File
@@ -1,5 +1,5 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors.
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -306,6 +306,154 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContextAsyncProvid
return .init()
}
// Chunk size for streaming file transfers (1MB).
private static let copyChunkSize = 1024 * 1024
func copyIn(
requestStream: GRPCAsyncRequestStream<Com_Apple_Containerization_Sandbox_V3_CopyInChunk>,
context: GRPC.GRPCAsyncServerCallContext
) async throws -> Com_Apple_Containerization_Sandbox_V3_CopyInResponse {
var fileHandle: FileHandle?
var path: String = ""
var totalBytes: Int = 0
do {
for try await chunk in requestStream {
switch chunk.content {
case .init_p(let initMsg):
path = initMsg.path
log.debug(
"copyIn",
metadata: [
"path": "\(path)",
"mode": "\(initMsg.mode)",
"createParents": "\(initMsg.createParents)",
])
if initMsg.createParents {
let fileURL = URL(fileURLWithPath: path)
let parentDir = fileURL.deletingLastPathComponent()
try FileManager.default.createDirectory(
at: parentDir,
withIntermediateDirectories: true
)
}
let mode = initMsg.mode > 0 ? mode_t(initMsg.mode) : mode_t(0o644)
let fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
guard fd != -1 else {
throw GRPCStatus(
code: .internalError,
message: "copyIn: failed to open file '\(path)': \(swiftErrno("open"))"
)
}
fileHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
case .data(let bytes):
guard let fh = fileHandle else {
throw GRPCStatus(
code: .failedPrecondition,
message: "copyIn: received data before init message"
)
}
if !bytes.isEmpty {
try fh.write(contentsOf: bytes)
totalBytes += bytes.count
}
case .none:
break
}
}
log.debug(
"copyIn complete",
metadata: [
"path": "\(path)",
"totalBytes": "\(totalBytes)",
])
return .init()
} catch {
log.error(
"copyIn",
metadata: [
"path": "\(path)",
"error": "\(error)",
])
if error is GRPCStatus {
throw error
}
throw GRPCStatus(
code: .internalError,
message: "copyIn: \(error)"
)
}
}
func copyOut(
request: Com_Apple_Containerization_Sandbox_V3_CopyOutRequest,
responseStream: GRPCAsyncResponseStreamWriter<Com_Apple_Containerization_Sandbox_V3_CopyOutChunk>,
context: GRPC.GRPCAsyncServerCallContext
) async throws {
let path = request.path
log.debug(
"copyOut",
metadata: [
"path": "\(path)"
])
do {
let fileURL = URL(fileURLWithPath: path)
let attrs = try FileManager.default.attributesOfItem(atPath: path)
guard let fileSize = attrs[.size] as? UInt64 else {
throw GRPCStatus(
code: .internalError,
message: "copyOut: failed to get file size for '\(path)'"
)
}
let fileHandle = try FileHandle(forReadingFrom: fileURL)
defer { try? fileHandle.close() }
// Send init message with total size.
try await responseStream.send(
.with {
$0.content = .init_p(.with { $0.totalSize = fileSize })
}
)
var totalSent: UInt64 = 0
while true {
guard let data = try fileHandle.read(upToCount: Self.copyChunkSize), !data.isEmpty else {
break
}
try await responseStream.send(.with { $0.content = .data(data) })
totalSent += UInt64(data.count)
}
log.debug(
"copyOut complete",
metadata: [
"path": "\(path)",
"totalBytes": "\(totalSent)",
])
} catch {
log.error(
"copyOut",
metadata: [
"path": "\(path)",
"error": "\(error)",
])
if error is GRPCStatus {
throw error
}
throw GRPCStatus(
code: .internalError,
message: "copyOut: \(error)"
)
}
}
func mount(request: Com_Apple_Containerization_Sandbox_V3_MountRequest, context: GRPC.GRPCAsyncServerCallContext)
async throws -> Com_Apple_Containerization_Sandbox_V3_MountResponse
{