Adds kind fixes deleting broken symlinks

`delete()` and other functions would check `exists` to do certain behaviors, but `exists` will validate a symlink if the entry is a symlink, thus instead we check if the path is an actual entry now instead.
This commit is contained in:
Max Howell
2019-03-18 09:09:06 -04:00
parent 7e774b6cf5
commit 0e061f9cc8
6 changed files with 97 additions and 27 deletions

View File

@@ -258,9 +258,15 @@ for that as the check was deemed too expensive to be worthwhile.
equality check is required. equality check is required.
* There are several symlink paths on Mac that are typically automatically * There are several symlink paths on Mac that are typically automatically
resolved by Foundation, eg. `/private`, we attempt to do the same for resolved by Foundation, eg. `/private`, we attempt to do the same for
functions that you would expect it (notably `realpath`), but we do *not* for functions that you would expect it (notably `realpath`), we *do* the same for
`Path.init`, *nor* if you are joining a path that ends up being one of these `Path.init`, but *do not* if you are joining a path that ends up being one of
paths, (eg. `Path.root.join("var/private')`). these paths, (eg. `Path.root.join("var/private')`).
If a `Path` is a symlink but the destination of the link does not exist `exists`
returns `false`. This seems to be the correct thing to do since symlinks are
meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `kind` is `nil`.
## We do not provide change directory functionality ## We do not provide change directory functionality

View File

@@ -83,4 +83,22 @@ public extension Path {
#endif #endif
return self return self
} }
enum Kind {
case file, symlink, directory
}
var kind: Kind? {
var buf = stat()
guard lstat(string, &buf) == 0 else {
return nil
}
if buf.st_mode & S_IFMT == S_IFLNK {
return .symlink
} else if buf.st_mode & S_IFMT == S_IFDIR {
return .directory
} else {
return .file
}
}
} }

View File

@@ -25,11 +25,11 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func copy(to: Path, overwrite: Bool = false) throws -> Path { func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile, isFile { if overwrite, let tokind = to.kind, tokind != .directory, kind != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
#if os(Linux) && !swift(>=5.1) // check if fixed #if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, to.isFile { if !overwrite, to.kind != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -61,15 +61,15 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func copy(into: Path, overwrite: Bool = false) throws -> Path { func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if into.kind == nil {
try into.mkdir(.p) try into.mkdir(.p)
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile { if overwrite, let kind = rv.kind, kind != .directory {
try rv.delete() try FileManager.default.removeItem(at: rv.url)
} }
#if os(Linux) && !swift(>=5.1) // check if fixed #if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, rv.isFile { if !overwrite, rv.kind != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -95,7 +95,7 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func move(to: Path, overwrite: Bool = false) throws -> Path { func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile { if overwrite, let kind = to.kind, kind != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
try FileManager.default.moveItem(at: url, to: to.url) try FileManager.default.moveItem(at: url, to: to.url)
@@ -119,17 +119,21 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func move(into: Path, overwrite: Bool = false) throws -> Path { func move(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { switch into.kind {
case nil:
try into.mkdir(.p) try into.mkdir(.p)
} else if !into.isDirectory { fallthrough
case .directory?:
let rv = into/basename()
if overwrite, let rvkind = rv.kind, rvkind != .directory {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
case .file?, .symlink?:
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
let rv = into/basename()
if overwrite, rv.isFile {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
} }
/** /**
@@ -138,11 +142,12 @@ public extension Path {
*Path.swift* doesnt error if desired end result preexists. *Path.swift* doesnt error if desired end result preexists.
- Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission. - Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission.
- Note: This function will fail if the file or directory is locked - Note: This function will fail if the file or directory is locked
- Note: If entry is a symlink, deletes the symlink.
- SeeAlso: `lock()` - SeeAlso: `lock()`
*/ */
@inlinable @inlinable
func delete() throws { func delete() throws {
if exists { if kind != nil {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
} }
@@ -154,7 +159,7 @@ public extension Path {
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
if !exists { if kind == nil {
guard FileManager.default.createFile(atPath: string, contents: nil) else { guard FileManager.default.createFile(atPath: string, contents: nil) else {
throw CocoaError.error(.fileWriteUnknown) throw CocoaError.error(.fileWriteUnknown)
} }
@@ -228,14 +233,17 @@ public extension Path {
*/ */
@discardableResult @discardableResult
func symlink(into dir: Path) throws -> Path { func symlink(into dir: Path) throws -> Path {
if !dir.exists { switch dir.kind {
case nil, .symlink?:
try dir.mkdir(.p) try dir.mkdir(.p)
} else if !dir.isDirectory { fallthrough
case .directory?:
let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst
case .file?:
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst
} }
} }

View File

@@ -8,7 +8,10 @@ import Darwin
public extension Path { public extension Path {
//MARK: Filesystem Properties //MARK: Filesystem Properties
/// Returns true if the path represents an actual filesystem entry. /**
- Returns: `true` if the path represents an actual filesystem entry.
- Note: If `self` is a symlink the return value represents the destination.
*/
var exists: Bool { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)
} }

View File

@@ -69,9 +69,18 @@ class PathTests: XCTestCase {
XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar") XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar")
} }
func testExists() { func testExists() throws {
XCTAssert(Path.root.exists) XCTAssert(Path.root.exists)
XCTAssert((Path.root/"bin").exists) XCTAssert((Path.root/"bin").exists)
try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.exists)
XCTAssertFalse(try tmpdir.bar.symlink(as: tmpdir.foo).exists)
XCTAssertTrue(tmpdir.foo.kind == .symlink)
XCTAssertTrue(try tmpdir.bar.touch().symlink(as: tmpdir.baz).exists)
XCTAssertTrue(tmpdir.bar.kind == .file)
XCTAssertTrue(tmpdir.kind == .directory)
}
} }
func testIsDirectory() { func testIsDirectory() {
@@ -379,6 +388,21 @@ class PathTests: XCTestCase {
#if !os(Linux) #if !os(Linux)
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete()) XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
#endif #endif
// regression test: can delete a symlink that points to a non-existent file
let bar5 = try tmpdir.bar4.symlink(as: tmpdir.bar5)
XCTAssertEqual(bar5.kind, .symlink)
XCTAssertFalse(bar5.exists)
XCTAssertNoThrow(try bar5.delete())
XCTAssertEqual(bar5.kind, nil)
// test that deleting a symlink *only* deletes the symlink
let bar7 = try tmpdir.bar6.touch().symlink(as: tmpdir.bar7)
XCTAssertEqual(bar7.kind, .symlink)
XCTAssertTrue(bar7.exists)
XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.kind, nil)
XCTAssertEqual(tmpdir.bar6.kind, .file)
} }
} }
@@ -604,4 +628,14 @@ class PathTests: XCTestCase {
let baz: String.SubSequence? = "/a/b:1".split(separator: ":").first let baz: String.SubSequence? = "/a/b:1".split(separator: ":").first
_ = baz.flatMap(Path.init) _ = baz.flatMap(Path.init)
} }
func testKind() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssertEqual(tmpdir.kind, .directory)
XCTAssertEqual(foo.kind, .file)
XCTAssertEqual(bar.kind, .symlink)
}
}
} }

View File

@@ -27,6 +27,7 @@ extension PathTests {
("testInitializerForRelativePath", testInitializerForRelativePath), ("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testKind", testKind),
("testLock", testLock), ("testLock", testLock),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),