Update container copy to use FilePath (#1580)

- Replace `URL` with `FilePath` in `container copy` (#1557).
  In addition, make `copyIn`/`copyOut` API to use `String` for path as we
  need to preserve the trailing slash to the `LinuxContainer.copy`---i.e.,
  this trailing slash is used in `LinuxContainer.copy` to determine copy
  behavior.
This commit is contained in:
jwhur
2026-05-20 13:50:08 -07:00
committed by GitHub
parent 145867e6e2
commit df35f79ae0
3 changed files with 23 additions and 23 deletions
+1
View File
@@ -113,6 +113,7 @@ let package = Package(
"ContainerRuntimeClient",
"ContainerRuntimeLinuxClient",
"ContainerVersion",
.product(name: "SystemPackage", package: "swift-system"),
"ContainerXPC",
"TerminalProgress",
"Yams",
@@ -20,6 +20,7 @@ import ContainerResource
import Containerization
import ContainerizationError
import Foundation
import SystemPackage
extension Application {
public struct ContainerCopy: AsyncLoggableCommand {
@@ -63,40 +64,42 @@ extension Application {
switch (srcRef, dstRef) {
case (.container(let id, let path), .local(let localPath)):
let srcURL = URL(fileURLWithPath: path)
let destURL = URL(fileURLWithPath: localPath).standardizedFileURL
let srcPath = FilePath(path)
let destPath = FilePath((localPath as NSString).standardizingPath)
var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: destURL.path, isDirectory: &isDirectory)
let exists = FileManager.default.fileExists(atPath: destPath.string, isDirectory: &isDirectory)
if exists && isDirectory.boolValue {
let finalDest = destURL.appendingPathComponent(srcURL.lastPathComponent)
try await client.copyOut(id: id, source: srcURL, destination: finalDest)
guard let lastComponent = srcPath.lastComponent else {
throw ContainerizationError(.invalidArgument, message: "source path has no last component: \(path)")
}
let finalDest = destPath.appending(lastComponent)
try await client.copyOut(id: id, source: path, destination: finalDest.string)
} else if localPath.hasSuffix("/") {
try await client.copyOut(id: id, source: srcURL, destination: destURL)
try await client.copyOut(id: id, source: path, destination: destPath.string)
var resultIsDir: ObjCBool = false
if FileManager.default.fileExists(atPath: destURL.path, isDirectory: &resultIsDir),
if FileManager.default.fileExists(atPath: destPath.string, isDirectory: &resultIsDir),
!resultIsDir.boolValue
{
try? FileManager.default.removeItem(at: destURL)
try? FileManager.default.removeItem(atPath: destPath.string)
throw ContainerizationError(
.invalidArgument,
message: "destination is not a directory: \(localPath)")
}
} else {
try await client.copyOut(id: id, source: srcURL, destination: destURL)
try await client.copyOut(id: id, source: path, destination: destPath.string)
}
case (.local(let localPath), .container(let id, let path)):
let srcURL = URL(fileURLWithPath: localPath).standardizedFileURL
let srcPath = FilePath((localPath as NSString).standardizingPath)
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: srcURL.path, isDirectory: &isDirectory) else {
guard FileManager.default.fileExists(atPath: srcPath.string, isDirectory: &isDirectory) else {
throw ContainerizationError(.notFound, message: "source path does not exist: \(localPath)")
}
if localPath.hasSuffix("/") && !isDirectory.boolValue {
throw ContainerizationError(.invalidArgument, message: "source path is not a directory: \(localPath)")
}
let destURL = URL(fileURLWithPath: path)
try await client.copyIn(id: id, source: srcURL, destination: destURL, createParents: true)
try await client.copyIn(id: id, source: srcPath.string, destination: path, createParents: true)
case (.container, .container):
throw ContainerizationError(.invalidArgument, message: "copying between containers is not supported")
case (.local, .local):
@@ -312,15 +312,11 @@ public struct ContainerClient: Sendable {
}
/// Copy a file or directory from the host into the container.
public func copyIn(id: String, source: URL, destination: URL, mode: UInt32 = 0o644, createParents: Bool = true) async throws {
public func copyIn(id: String, source: String, destination: String, mode: UInt32 = 0o644, createParents: Bool = true) async throws {
let request = XPCMessage(route: .containerCopyIn)
let destinationPath =
destination.hasDirectoryPath && !destination.path.hasSuffix("/")
? "\(destination.path)/"
: destination.path
request.set(key: .id, value: id)
request.set(key: .sourcePath, value: source.path)
request.set(key: .destinationPath, value: destinationPath)
request.set(key: .sourcePath, value: source)
request.set(key: .destinationPath, value: destination)
request.set(key: .fileMode, value: UInt64(mode))
request.set(key: .createParents, value: createParents)
@@ -336,11 +332,11 @@ public struct ContainerClient: Sendable {
}
/// Copy a file or directory from the container to the host.
public func copyOut(id: String, source: URL, destination: URL, createParents: Bool = true) async throws {
public func copyOut(id: String, source: String, destination: String, createParents: Bool = true) async throws {
let request = XPCMessage(route: .containerCopyOut)
request.set(key: .id, value: id)
request.set(key: .sourcePath, value: source.path)
request.set(key: .destinationPath, value: destination.path)
request.set(key: .sourcePath, value: source)
request.set(key: .destinationPath, value: destination)
request.set(key: .createParents, value: createParents)
do {