sam
sam

Reputation: 3479

How to get a file reference URL in Swift?

I cannot get a file reference URL to work in Swift. First, Swift's URL type doesn't even define a method to convert a file path URL to a file reference URL. NSURL has such a method: fileReferenceURL() -> URL?, but I cannot figure out how to get it to work.

What I tried:

let myURL = URL(string: "file:///Users/Me/Desktop/CoolFolder")!
let refURL = (myURL as NSURL).fileReferenceURL()!
NSLog("\(myURL)")   // file:///Users/Me/Desktop/CoolFolder (ok)
NSLog("\(refURL)")  // file:///Users/Me/Desktop/CoolFolder (wha?!?)
logFolderContents() // Logs contents of folder pointed to by refURL
// ...
// Rename CoolFolder -> UncoolFolder in Finder.
// ...
logFolderContents() // Fails!
// ...
// Rename UncoolFolder -> CoolFolder.
// ...
logFolderContents() // Successfully logs contents again.

Basically, refURL continues to point at the original file path. The call to fileReferenceURL() seemingly did nothing.

How can I get a file reference URL in Swift?

Update: User @Sam points out in the comments that this appears to be a Swift 3 regression bug as documented here: https://bugs.swift.org/browse/SR-2728. Please visit the link and vote on this bug. The thread also contains a workaround.

Upvotes: 3

Views: 6100

Answers (1)

Thomas Tempelmann
Thomas Tempelmann

Reputation: 12043

Sadly, the bug still is not fixed as of April 2021. This makes comparing file URLs for equality quite difficult.

With ObjC this works well:

NSString *p1 = @"/etc/hosts";
NSString *p2 = @"/private/etc/hosts";
    
NSURL *url1 = [NSURL fileURLWithPath:p1];
NSURL *url2 = [NSURL fileURLWithPath:p2];
    
NSURL *ref1 = url1.fileReferenceURL;
NSURL *ref2 = url2.fileReferenceURL;
    
BOOL equal = [ref1 isEqual:ref2];

In Swift, I got practically the same result (i.e. a 64 bit in containing both the volume ID and the file ID) with this code:

let p1 = "/etc/hosts"
let p2 = "/private/etc/hosts"

let url1 = URL(fileURLWithPath: p1)
let url2 = URL(fileURLWithPath: p2)

// This does not work because the refs do not use fileIDs as they should
// See https://bugs.swift.org/browse/SR-2728
let ref1bad = (url1 as NSURL).fileReferenceURL()!
let ref2bad = (url2 as NSURL).fileReferenceURL()!
let equalBad = ref1bad == ref2bad
print("\(ref1bad) = \(ref2bad): \(equalBad)")

// This works:
var ref1: NSObject?
var ref2: NSObject?
if let resourceValues = try? url1.resourceValues(forKeys: [.fileResourceIdentifierKey]) {
    ref1 = resourceValues.fileResourceIdentifier as? NSObject
}
if let resourceValues = try? url2.resourceValues(forKeys: [.fileResourceIdentifierKey]) {
    ref2 = resourceValues.fileResourceIdentifier as? NSObject
}
let equal = ref1!.isEqual(ref2) // the use of "!" is probably not good here
print("\(String(describing: ref1)) = \(String(describing: ref2)): \(equal)")

If you wanted a real file reference URL, you'd have to reconstruct the URL from the two 32 bit values in the object (which appears to be of type NSData) and use the first as the fileID and the second as the volumeID. Writing the code for that exceeds my currently rather meager Swift skills, though.

Upvotes: 1

Related Questions