How can I tell if a symbolic link exists at a certain path?
Asked Answered
M

3

6

I have an original file, /path/to/foo.txt, and a symbolic link to it, /other/path/to/foo.txt. I delete /path/to/foo.txt, but leave the symbolic link in-place. How can I tell that the symbolic link still exists using Cocoa APIs?


I found this by using the standard/recommended FileManager.fileExists(atPath:). The problem here, for anyone unfamiliar with that API, is that it traverses symlinks. So, when I do this:

FileManager.default.fileExists(atPath: "/other/path/to/foo.txt")

it returns false, because it saw that I gave it a symlink and resolved it, and then saw that there is no file at the resolved path.

As the documentation says:

If the file at path is inaccessible to your app, perhaps because one or more parent directories are inaccessible, this method returns false. If the final element in path specifies a symbolic link, this method traverses the link and returns true or false based on the existence of the file at the link destination.

There doesn't seem to be an alternative in FileManager. So, I'm wondering if I can call a Cocoa API to tell if a symlink exists there, or if I'll have to resort to C or Bash APIs.

Mother answered 17/8, 2018 at 18:55 Comment(0)
S
4

Here's a simpler way: Fondation/FileManger/FileWrapper

let node = try FileWrapper(url: URL(fileURLWithPath: "/PATH/file.link"), options: .immediate)

node.isDirectory      >> false
node.isRegularFile    >> false
node.isSymbolicLink   >> true
Suicidal answered 12/10, 2019 at 9:29 Comment(1)
A problem with this approach that it may be prohibitively expensive, as it traverses the directory tree. As per doc "If url is a directory, this method recursively creates file wrappers for each node within that directory. Use the fileWrappers property to get the file wrappers of the nodes contained by the directory."Richert
Q
11

You don't need/use FileManager for this. And you should not be using string file paths for anything any more.

Start with a file URL — the URL version of your "/other/path/to/foo.txt". Now read the file's .isSymbolicLink resource key and see whether it's a symbolic link. If it is, but if the file pointed to doesn't exist, you know your link has gone bad.

I wrote a little test in a playground:

let url = URL(fileURLWithPath: "/Users/mattneubelcap/Desktop/test.txt")
if let ok = try? url.checkResourceIsReachable(), ok {
    let vals = url.resourceValues(forKeys: [.isSymbolicLinkKey])
    if let islink = vals.isSymbolicLink, islink {
        print("it's a symbolic link")
        let dest = url.resolvingSymlinksInPath()
        let report = dest != url ? "It exists" : "It doesn't exist"
        print(report)
    }
}
Queenhood answered 17/8, 2018 at 19:6 Comment(3)
Thanks! I actually am using a thin wrapper extension function that takes a URL and passing that, but I forewent that information for the sake of this question.Mother
Added actual code example. Finding out whether the link has gone bad is trickier than I thought; you have to ask whether the resolved path is the same as the link path.Queenhood
it seems FileManager figures out if it's gone bad, but that's not what I care about at the moment; I'll resolve it later in the program's execution. I just want to ensure the link is there, ready for future resolution.Mother
S
4

Here's a simpler way: Fondation/FileManger/FileWrapper

let node = try FileWrapper(url: URL(fileURLWithPath: "/PATH/file.link"), options: .immediate)

node.isDirectory      >> false
node.isRegularFile    >> false
node.isSymbolicLink   >> true
Suicidal answered 12/10, 2019 at 9:29 Comment(1)
A problem with this approach that it may be prohibitively expensive, as it traverses the directory tree. As per doc "If url is a directory, this method recursively creates file wrappers for each node within that directory. Use the fileWrappers property to get the file wrappers of the nodes contained by the directory."Richert
S
2

A solution to replace fileExists(atPath:) is to use attributesOfItem(atPath:) that returns the type of node (FileAttributeKey.type) and throws and error Code=260 if file/node doesn't exist.

So here my "interpretation" with that:

func nodeExists(atPath path: String) -> FileType? {
do {
    let attribs = try fm.attributesOfItem(atPath: path)
    if let type = attribs[FileAttributeKey.type] {
        switch (type as! FileAttributeType) {
            case FileAttributeType.typeDirectory: return .directory
            case FileAttributeType.typeRegular: return .file
            case FileAttributeType.typeSymbolicLink: return .symlink
            default:
                return nil
            }
        }
    } catch {
        if (error as NSError).code == 260 {
            return false
        } else {
            return nil
        }
    }
}

Way to dig the error code <<< Thanks to Ben Leggiero for the trick :-)

Suicidal answered 26/9, 2019 at 20:42 Comment(3)
Every Swift error can be cast to an NSError, so (error as NSError).codeMother
What is 260 ? Naming an error code has a lot more semantic value over the number.Misty
Number or Name it as you like :-)Suicidal

© 2022 - 2024 — McMap. All rights reserved.