Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: Copy Directory by simone-panico · Pull Request #563 · apple/containerization · GitHub
Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: Copy Directory by simone-panico · Pull Request #563 · apple/containerization · GitHub
Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: Copy Directory by simone-panico · Pull Request #563 · apple/containerization · GitHub
Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: Copy Directory by simone-panico · Pull Request #563 · apple/containerization · GitHub
Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: Copy Directory by simone-panico · Pull Request #563 · apple/containerization · GitHub
Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: Copy Directory by simone-panico · Pull Request #563 · apple/containerization · GitHub
Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: Copy Directory by simone-panico · Pull Request #563 · apple/containerization · GitHub
Skip to content
Closed
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
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion Package.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ let package = Package(
.library(name: "ContainerizationOS", targets: ["ContainerizationOS"]),
.library(name: "ContainerizationExtras", targets: ["ContainerizationExtras"]),
.library(name: "ContainerizationArchive", targets: ["ContainerizationArchive"]),
.library(name: "_ContainerizationTar", targets: ["_ContainerizationTar"]),
.executable(name: "cctl", targets: ["cctl"]),
],
dependencies: [
Expand All@@ -42,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.29.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/apple/swift-system.git", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"),
Expand All@@ -64,6 +65,7 @@ let package = Package(
"ContainerizationOS",
"ContainerizationIO",
"ContainerizationExtras",
"_ContainerizationTar",
.target(name: "ContainerizationEXT4", condition: .when(platforms: [.macOS])),
],
exclude: [
Expand DownExpand Up@@ -259,5 +261,23 @@ let package = Package(
.target(
name: "CShim"
),
.target(
name: "_ContainerizationTar",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
],
path: "Sources/ContainerizationTar"
),
.testTarget(
name: "ContainerizationTarTests",
dependencies: [
"_ContainerizationTar",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "_NIOFileSystem", package: "swift-nio"),
]
),
]
)
48 changes: 48 additions & 0 deletions Sources/Containerization/LinuxContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -1089,6 +1089,54 @@ extension LinuxContainer {
}
}
}

/// Copy a directory from the host into the container.
public func copyDirIn(
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("copyDirIn")

let guestPath = URL(filePath: self.root).appending(path: destination.path)
try await state.vm.withAgent { agent in
try await agent.copyDirIn(
from: source,
to: guestPath,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}

/// Copy a directory from the container to the host.
public func copyDirOut(
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("copyDirOut")

let guestPath = URL(filePath: self.root).appending(path: source.path)
try await state.vm.withAgent { agent in
try await agent.copyDirOut(
from: guestPath,
to: destination,
createParents: createParents,
chunkSize: chunkSize,
progress: progress
)
}
}
}
}

extension VirtualMachineInstance {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

//
// DO NOT EDIT.
// swift-format-ignore-file
Expand Down
34 changes: 18 additions & 16 deletions Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
//===----------------------------------------------------------------------===//
// 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.
// 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.
//===----------------------------------------------------------------------===//

// DO NOT EDIT.
// swift-format-ignore-file
// swiftlint:disable all
Expand DownExpand Up@@ -863,6 +847,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyInInit: Sendable {
/// Create parent directories if they don't exist.
public var createParents: Bool = false

/// If true, the payload is a TAR archive to extract at the destination.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand All@@ -886,6 +873,9 @@ public struct Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: Sendable {
/// Source path in the guest.
public var path: String = String()

/// If true, create a TAR archive of the directory and stream it.
public var isDirectory: Bool = false

public var unknownFields = SwiftProtobuf.UnknownStorage()

public init() {}
Expand DownExpand Up@@ -2879,6 +2869,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
1: .same(proto: "path"),
2: .same(proto: "mode"),
3: .standard(proto: "create_parents"),
4: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2890,6 +2881,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
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) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2905,13 +2897,17 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyInInit: SwiftProtobuf.Messag
if self.createParents != false {
try visitor.visitSingularBoolField(value: self.createParents, fieldNumber: 3)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 4)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand DownExpand Up@@ -2940,6 +2936,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
public static let protoMessageName: String = _protobuf_package + ".CopyOutRequest"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "path"),
2: .standard(proto: "is_directory"),
]

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
Expand All@@ -2949,6 +2946,7 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
// 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.decodeSingularBoolField(value: &self.isDirectory) }()
default: break
}
}
Expand All@@ -2958,11 +2956,15 @@ extension Com_Apple_Containerization_Sandbox_V3_CopyOutRequest: SwiftProtobuf.Me
if !self.path.isEmpty {
try visitor.visitSingularStringField(value: self.path, fieldNumber: 1)
}
if self.isDirectory != false {
try visitor.visitSingularBoolField(value: self.isDirectory, fieldNumber: 2)
}
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.isDirectory != rhs.isDirectory {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,6 @@ service SandboxContext {
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);
// Delete an existing process inside the container.
Expand DownExpand Up@@ -245,13 +244,17 @@ message CopyInInit {
uint32 mode = 2;
// Create parent directories if they don't exist.
bool create_parents = 3;
// If true, the payload is a TAR archive to extract at the destination.
bool is_directory = 4;
}

message CopyInResponse {}

message CopyOutRequest {
// Source path in the guest.
string path = 1;
// If true, create a TAR archive of the directory and stream it.
bool is_directory = 2;
}

message CopyOutChunk {
Expand Down
38 changes: 38 additions & 0 deletions Sources/Containerization/VirtualMachineAgent.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,24 @@ public protocol VirtualMachineAgent: Sendable {
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the host into the guest.
func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

/// Copy a dir from the guest to the host.
func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws

// Process lifecycle
func createProcess(
Expand DownExpand Up@@ -139,4 +157,24 @@ extension VirtualMachineAgent {
) async throws {
throw ContainerizationError(.unsupported, message: "copyOut")
}

public func copyDirIn(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirIn")
}

public func copyDirOut(
from source: URL,
to destination: URL,
createParents: Bool,
chunkSize: Int,
progress: ProgressHandler?
) async throws {
throw ContainerizationError(.unsupported, message: "copyDirOut")
}
}
Loading