From df35f79ae05be97522d60ce71e83902c8d02b447 Mon Sep 17 00:00:00 2001 From: jwhur <57657645+JaewonHur@users.noreply.github.com> Date: Wed, 20 May 2026 13:50:08 -0700 Subject: [PATCH] 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. --- Package.swift | 1 + .../Container/ContainerCopy.swift | 29 ++++++++++--------- .../Client/ContainerClient.swift | 16 ++++------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Package.swift b/Package.swift index 5fed0c1a..442aa8b9 100644 --- a/Package.swift +++ b/Package.swift @@ -113,6 +113,7 @@ let package = Package( "ContainerRuntimeClient", "ContainerRuntimeLinuxClient", "ContainerVersion", + .product(name: "SystemPackage", package: "swift-system"), "ContainerXPC", "TerminalProgress", "Yams", diff --git a/Sources/ContainerCommands/Container/ContainerCopy.swift b/Sources/ContainerCommands/Container/ContainerCopy.swift index 8778b74d..c6bdef07 100644 --- a/Sources/ContainerCommands/Container/ContainerCopy.swift +++ b/Sources/ContainerCommands/Container/ContainerCopy.swift @@ -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): diff --git a/Sources/Services/ContainerAPIService/Client/ContainerClient.swift b/Sources/Services/ContainerAPIService/Client/ContainerClient.swift index 838d86bd..291e8cf2 100644 --- a/Sources/Services/ContainerAPIService/Client/ContainerClient.swift +++ b/Sources/Services/ContainerAPIService/Client/ContainerClient.swift @@ -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 {