@@ -58,6 +58,12 @@ public enum FileSystemError: Swift.Error {
5858
5959 /// An unspecific operating system error.
6060 case unknownOSError
61+
62+ /// File or folder already exists at destination.
63+ ///
64+ /// This is thrown when copying or moving a file or directory but the destination
65+ /// path already contains a file or folder.
66+ case alreadyExistsAtDestination
6167}
6268
6369extension FileSystemError {
@@ -179,11 +185,16 @@ public protocol FileSystem: class {
179185 /// Change file mode.
180186 func chmod( _ mode: FileMode , path: AbsolutePath , options: Set < FileMode . Option > ) throws
181187
182-
183188 /// Returns the file info of the given path.
184189 ///
185190 /// The method throws if the underlying stat call fails.
186191 func getFileInfo( _ path: AbsolutePath ) throws -> FileInfo
192+
193+ /// Copy a file or directory.
194+ func copy( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws
195+
196+ /// Move a file or directory.
197+ func move( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws
187198}
188199
189200/// Convenience implementations (default arguments aren't permitted in protocol
@@ -408,6 +419,18 @@ private class LocalFileSystem: FileSystem {
408419 try setMode ( path: ( path as! URL ) . path)
409420 }
410421 }
422+
423+ func copy( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws {
424+ guard exists ( sourcePath) else { throw FileSystemError . noEntry }
425+ guard !exists( destinationPath) else { throw FileSystemError . alreadyExistsAtDestination }
426+ try FileManager . default. copyItem ( at: sourcePath. asURL, to: destinationPath. asURL)
427+ }
428+
429+ func move( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws {
430+ guard exists ( sourcePath) else { throw FileSystemError . noEntry }
431+ guard !exists( destinationPath) else { throw FileSystemError . alreadyExistsAtDestination }
432+ try FileManager . default. moveItem ( at: sourcePath. asURL, to: destinationPath. asURL)
433+ }
411434}
412435
413436// FIXME: This class does not yet support concurrent mutation safely.
@@ -675,6 +698,45 @@ public class InMemoryFileSystem: FileSystem {
675698 public func chmod( _ mode: FileMode , path: AbsolutePath , options: Set < FileMode . Option > ) throws {
676699 // FIXME: We don't have these semantics in InMemoryFileSystem.
677700 }
701+
702+ public func copy( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws {
703+ // Get the source node.
704+ guard let source = try getNode ( sourcePath) else {
705+ throw FileSystemError . noEntry
706+ }
707+
708+ // Create directory to destination parent.
709+ guard let destinationParent = try getNode ( destinationPath. parentDirectory) else {
710+ throw FileSystemError . noEntry
711+ }
712+
713+ // Check that the parent is a directory.
714+ guard case . directory( let contents) = destinationParent. contents else {
715+ throw FileSystemError . notDirectory
716+ }
717+
718+ guard contents. entries [ destinationPath. basename] == nil else {
719+ throw FileSystemError . alreadyExistsAtDestination
720+ }
721+
722+ contents. entries [ destinationPath. basename] = source
723+ }
724+
725+ public func move( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws {
726+ // Get the source parent node.
727+ guard let sourceParent = try getNode ( sourcePath. parentDirectory) else {
728+ throw FileSystemError . noEntry
729+ }
730+
731+ // Check that the parent is a directory.
732+ guard case . directory( let contents) = sourceParent. contents else {
733+ throw FileSystemError . notDirectory
734+ }
735+
736+ try copy ( from: sourcePath, to: destinationPath)
737+
738+ contents. entries [ sourcePath. basename] = nil
739+ }
678740}
679741
680742/// A rerooted view on an existing FileSystem.
@@ -767,6 +829,14 @@ public class RerootedFileSystemView: FileSystem {
767829 public func chmod( _ mode: FileMode , path: AbsolutePath , options: Set < FileMode . Option > ) throws {
768830 try underlyingFileSystem. chmod ( mode, path: path, options: options)
769831 }
832+
833+ public func copy( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws {
834+ try underlyingFileSystem. copy ( from: formUnderlyingPath ( sourcePath) , to: formUnderlyingPath ( sourcePath) )
835+ }
836+
837+ public func move( from sourcePath: AbsolutePath , to destinationPath: AbsolutePath ) throws {
838+ try underlyingFileSystem. move ( from: formUnderlyingPath ( sourcePath) , to: formUnderlyingPath ( sourcePath) )
839+ }
770840}
771841
772842/// Public access to the local FS proxy.
0 commit comments