Skip to content

Commit 58e248f

Browse files
authored
Merge pull request #1547 from spevans/pr_fm_linkitem
2 parents 26e6335 + 850ba69 commit 58e248f

File tree

2 files changed

+105
-27
lines changed

2 files changed

+105
-27
lines changed

Foundation/FileManager.swift

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -530,22 +530,14 @@ open class FileManager : NSObject {
530530
}
531531
}
532532

533-
open func copyItem(atPath srcPath: String, toPath dstPath: String) throws {
533+
private func _copyOrLinkDirectoryHelper(atPath srcPath: String, toPath dstPath: String, _ body: (String, String, FileAttributeType) throws -> ()) throws {
534534
guard
535535
let attrs = try? attributesOfItem(atPath: srcPath),
536536
let fileType = attrs[.type] as? FileAttributeType
537537
else {
538538
return
539539
}
540540

541-
func copyNonDirectory(srcPath: String, dstPath: String, fileType: FileAttributeType) throws {
542-
if fileType == .typeSymbolicLink {
543-
try _copySymlink(atPath: srcPath, toPath: dstPath)
544-
} else if fileType == .typeRegular {
545-
try _copyRegularFile(atPath: srcPath, toPath: dstPath)
546-
}
547-
}
548-
549541
if fileType == .typeDirectory {
550542
try createDirectory(atPath: dstPath, withIntermediateDirectories: false, attributes: nil)
551543

@@ -560,12 +552,22 @@ open class FileManager : NSObject {
560552
if fileType == .typeDirectory {
561553
try createDirectory(atPath: dst, withIntermediateDirectories: false, attributes: nil)
562554
} else {
563-
try copyNonDirectory(srcPath: src, dstPath: dst, fileType: fileType)
555+
try body(src, dst, fileType)
564556
}
565557
}
566558
}
567559
} else {
568-
try copyNonDirectory(srcPath: srcPath, dstPath: dstPath, fileType: fileType)
560+
try body(srcPath, dstPath, fileType)
561+
}
562+
}
563+
564+
open func copyItem(atPath srcPath: String, toPath dstPath: String) throws {
565+
try _copyOrLinkDirectoryHelper(atPath: srcPath, toPath: dstPath) { (srcPath, dstPath, fileType) in
566+
if fileType == .typeSymbolicLink {
567+
try _copySymlink(atPath: srcPath, toPath: dstPath)
568+
} else if fileType == .typeRegular {
569+
try _copyRegularFile(atPath: srcPath, toPath: dstPath)
570+
}
569571
}
570572
}
571573

@@ -586,18 +588,15 @@ open class FileManager : NSObject {
586588
}
587589

588590
open func linkItem(atPath srcPath: String, toPath dstPath: String) throws {
589-
var isDir: ObjCBool = false
590-
if self.fileExists(atPath: srcPath, isDirectory: &isDir) {
591-
if !isDir.boolValue {
592-
// TODO: Symlinks should be copied instead of hard-linked.
591+
try _copyOrLinkDirectoryHelper(atPath: srcPath, toPath: dstPath) { (srcPath, dstPath, fileType) in
592+
if fileType == .typeSymbolicLink {
593+
try _copySymlink(atPath: srcPath, toPath: dstPath)
594+
} else if fileType == .typeRegular {
593595
try _fileSystemRepresentation(withPath: srcPath, andPath: dstPath, {
594596
if link($0, $1) == -1 {
595597
throw _NSErrorWithErrno(errno, reading: false, path: srcPath)
596598
}
597599
})
598-
} else {
599-
// TODO: Recurse through directories, copying them.
600-
NSUnimplemented("Recursive linking not yet implemented")
601600
}
602601
}
603602
}

TestFoundation/TestFileManager.swift

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class TestFileManager : XCTestCase {
2424
("test_contentsOfDirectoryAtPath", test_contentsOfDirectoryAtPath),
2525
("test_subpathsOfDirectoryAtPath", test_subpathsOfDirectoryAtPath),
2626
("test_copyItemAtPathToPath", test_copyItemAtPathToPath),
27+
("test_linkItemAtPathToPath", test_linkItemAtPathToPath),
2728
("test_homedirectoryForUser", test_homedirectoryForUser),
2829
("test_temporaryDirectoryForUser", test_temporaryDirectoryForUser),
2930
("test_creatingDirectoryWithShortIntermediatePath", test_creatingDirectoryWithShortIntermediatePath),
@@ -550,7 +551,13 @@ class TestFileManager : XCTestCase {
550551
XCTFail("Failed to clean up files")
551552
}
552553
}
553-
554+
555+
private func directoryExists(atPath path: String) -> Bool {
556+
var isDir: ObjCBool = false
557+
let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDir)
558+
return exists && isDir.boolValue
559+
}
560+
554561
func test_copyItemAtPathToPath() {
555562
let fm = FileManager.default
556563
let srcPath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)"
@@ -560,13 +567,7 @@ class TestFileManager : XCTestCase {
560567
ignoreError { try fm.removeItem(atPath: srcPath) }
561568
ignoreError { try fm.removeItem(atPath: destPath) }
562569
}
563-
564-
func directoryExists(atPath path: String) -> Bool {
565-
var isDir: ObjCBool = false
566-
let exists = fm.fileExists(atPath: path, isDirectory: &isDir)
567-
return exists && isDir.boolValue
568-
}
569-
570+
570571
func createDirectory(atPath path: String) {
571572
do {
572573
try fm.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
@@ -638,7 +639,85 @@ class TestFileManager : XCTestCase {
638639
// ignore
639640
}
640641
}
641-
642+
643+
func test_linkItemAtPathToPath() {
644+
let fm = FileManager.default
645+
let basePath = NSTemporaryDirectory() + "linkItemAtPathToPath/"
646+
let srcPath = basePath + "testdir\(NSUUID().uuidString)"
647+
let destPath = basePath + "testdir\(NSUUID().uuidString)"
648+
defer { ignoreError { try fm.removeItem(atPath: basePath) } }
649+
650+
func getFileInfo(atPath path: String, _ body: (String, Bool, UInt64, UInt64) -> ()) {
651+
guard let enumerator = fm.enumerator(atPath: path) else {
652+
XCTFail("Cant enumerate \(path)")
653+
return
654+
}
655+
while let item = enumerator.nextObject() as? String {
656+
let fname = "\(path)/\(item)"
657+
do {
658+
let attrs = try fm.attributesOfItem(atPath: fname)
659+
let inode = (attrs[.systemFileNumber] as? NSNumber)?.uint64Value
660+
let linkCount = (attrs[.referenceCount] as? NSNumber)?.uint64Value
661+
let ftype = attrs[.type] as? FileAttributeType
662+
663+
if inode == nil || linkCount == nil || ftype == nil {
664+
XCTFail("Unable to get attributes of \(fname)")
665+
return
666+
}
667+
let isDir = (ftype == .typeDirectory)
668+
body(item, isDir, inode!, linkCount!)
669+
} catch {
670+
XCTFail("Unable to get attributes of \(fname): \(error)")
671+
return
672+
}
673+
}
674+
}
675+
676+
ignoreError { try fm.removeItem(atPath: basePath) }
677+
XCTAssertNotNil(try? fm.createDirectory(atPath: "\(srcPath)/tempdir/subdir/otherdir/extradir", withIntermediateDirectories: true, attributes: nil))
678+
XCTAssertTrue(fm.createFile(atPath: "\(srcPath)/tempdir/tempfile", contents: Data(), attributes: nil))
679+
XCTAssertTrue(fm.createFile(atPath: "\(srcPath)/tempdir/tempfile2", contents: Data(), attributes: nil))
680+
XCTAssertTrue(fm.createFile(atPath: "\(srcPath)/tempdir/subdir/otherdir/extradir/tempfile2", contents: Data(), attributes: nil))
681+
682+
var fileInfos: [String: (Bool, UInt64, UInt64)] = [:]
683+
getFileInfo(atPath: srcPath, { name, isDir, inode, linkCount in
684+
fileInfos[name] = (isDir, inode, linkCount)
685+
})
686+
XCTAssertEqual(fileInfos.count, 7)
687+
XCTAssertNotNil(try? fm.linkItem(atPath: srcPath, toPath: destPath), "Unable to link directory")
688+
689+
getFileInfo(atPath: destPath, { name, isDir, inode, linkCount in
690+
guard let srcFileInfo = fileInfos.removeValue(forKey: name) else {
691+
XCTFail("Cant find \(name) in \(destPath)")
692+
return
693+
}
694+
let (srcIsDir, srcInode, srcLinkCount) = srcFileInfo
695+
XCTAssertEqual(srcIsDir, isDir, "Directory/File type mismatch")
696+
if isDir {
697+
XCTAssertEqual(srcLinkCount, linkCount)
698+
} else {
699+
XCTAssertEqual(srcInode, inode)
700+
XCTAssertEqual(srcLinkCount + 1, linkCount)
701+
}
702+
})
703+
704+
XCTAssertEqual(fileInfos.count, 0)
705+
// linkItem should fail a 2nd time
706+
XCTAssertNil(try? fm.linkItem(atPath: srcPath, toPath: destPath), "Copy overwrites a file/folder that already exists")
707+
708+
// Test 'linking' a symlink, which actually does a copy
709+
let srcLink = srcPath + "/testlink"
710+
let destLink = destPath + "/testlink"
711+
do {
712+
try fm.createSymbolicLink(atPath: srcLink, withDestinationPath: "linkdest")
713+
try fm.linkItem(atPath: srcLink, toPath: destLink)
714+
XCTAssertEqual(try fm.destinationOfSymbolicLink(atPath: destLink), "linkdest")
715+
} catch {
716+
XCTFail("\(error)")
717+
}
718+
XCTAssertNil(try? fm.linkItem(atPath: srcLink, toPath: destLink), "Creating link where one already exists")
719+
}
720+
642721
func test_homedirectoryForUser() {
643722
let filemanger = FileManager.default
644723
XCTAssertNil(filemanger.homeDirectory(forUser: "someuser"))

0 commit comments

Comments
 (0)