NSFilePresenter presentedsubitem didmoveto URL formatting

I have impemented NSFilePresenter presentedSubitem:at oldURL:didMoveTo newURL protocol method in a Mac app (Catalina 10.15.3 and Xcode 11.4.1) on a local drive without sandboxing. The method gets called correctly when a file within the presented directory gets moved.


However, the two urls passed are formatted differently. OldURL "file:///System/Volumes/Data/...", while newURL is "file:///Users/...".


func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) {

     print(oldURL) // file:///System/Volumes/Data/Users/...
     print(newURL) // file:///Users/...
}


This creates the following issue: I keep a tree of nodes with file information and traverse the tree to search for the node of the oldURL. The URL's in the node is stored in a "file://Users/..." format and the url's aren't considered euqal.
I've tried to compare the File Resource Identifier. However, oldURL's file resource identifier is nil.


extension URL {

    var identifier: NSObjectProtocol? {
        return (try? resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier
    }

    static func &=(lhs: URL, rhs: URL) -> Bool {
       
        guard let idl = lhs.identifier, let idr = rhs.identifier else {
            assertionFailure("volume does not support resource identifiers") // oldURL.identifier = nil
            return lhs == rhs
        }
        return idl.isEqual(idr)
    }
}


How do I compare two URLs where one has the prefix "file:///File/Volumes/Data/" and one has not in a fool proof way?

Answered by PuzzleMaster in 419907022

I see what you mean: If you store the file resource identifier, you can use the identifier from newURL and simply ignore oldURL. You really don't need it. Thanks so much, that is indeed the solution.


If I understand you correctly, the solution would be something like this in code.


import Cocoa

final class Controller: NSObject, NSFilePresenter {
    
    var presentedItemURL: URL?
    var presentedItemOperationQueue: OperationQueue = OperationQueue()
    
    var root: FileNode?
    
    // ...
    
    func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) {
        
        // Ignore oldURL. Find node based on resource identifier of newURL
        guard let node = root?.search(for: newURL) else { return }
        
        // Move node to now location
    }
}

final class FileNode: NSObject {
    
    var url: URL
    var identifier: NSObjectProtocol?
    @objc var children = [FileNode]()

    // ...
    
    func search(for target: URL) throws -> FileNode? {
        
        guard let identifier = self.url.identifier, let targetIdentifier = target.identifier else {
            throw Error.NoIdentifier
        }
        
        if identifier.isEqual(targetIdentifier) {
            return self
        } else {
            // Traverse tree
        }
        return nil
    }
}

extension URL {
    var identifier: NSObjectProtocol? {
        return (try? resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier
    }
}

I've (kind of) solved it by taking the relative paths and check if either's suffix is equal to the other relative path. There are a few edge cases where this doesn't work, so a more solid solution is welcome.


static func &=(lhs: URL, rhs: URL) -> Bool {
       
     guard let idl = lhs.identifier, let idr = rhs.identifier else {           
          if lhs.relativePath.hasSuffix(rhs.relativePath) || rhs.relativePath.hasSuffix(lhs.relativePath) { 
               return true
          } else { return lhs == rhs }
     }
     return idl.isEqual(idr)
}

However, the two urls passed are formatted differently.

oldURL
file:///System/Volumes/Data/...
, while
newURL
is
file:///Users/...
.

Blergh! This is fallout from 10.15’s read-only system volume support. The

Users
directory is held on the data volume, so it’s true path is
/System/Volumes/Data/Users
but there’s a firmlink on the system volume that makes it appear like it’s at
/Users
. WWDC 2019 Session 710 What’s New in Apple File Systems discusses this concept in depth (although be aware that there were significant changes between the first 10.15 beta, which is the subject of that talk, and the final 10.15 release).

However,

oldURL
’s file resource identifier is
nil
.

Right. You can’t get a file resource identifier for a non-existant file, and that’s the case here.

How do I compare two URLs where one has the prefix

file:///File/Volumes/Data/
and one has not in a fool proof way?

Using

.fileResourceIdentifierKey
is definitely the best option. Beyond that, things get tricky, and it’s hard to come up with a universally applicable solution.

However, there are options that you can apply in specific cases. Earlier you wrote:

I keep a tree of nodes with file information and traverse the tree to search for the node of the

oldURL
.

Why not store the

.fileResourceIdentifierKey
value in that tree?

ps While 10.15 adds new wrinkles, this problem isn’t unique to 10.15. All versions of macOS have various symlinks in

/
that point to
/private
, and it’s not uncommon to see URLs both with and without the leading
/private
. I regularly see with with temporary files because the temporary directory exists within
/private/var/folders
.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Accepted Answer

I see what you mean: If you store the file resource identifier, you can use the identifier from newURL and simply ignore oldURL. You really don't need it. Thanks so much, that is indeed the solution.


If I understand you correctly, the solution would be something like this in code.


import Cocoa

final class Controller: NSObject, NSFilePresenter {
    
    var presentedItemURL: URL?
    var presentedItemOperationQueue: OperationQueue = OperationQueue()
    
    var root: FileNode?
    
    // ...
    
    func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) {
        
        // Ignore oldURL. Find node based on resource identifier of newURL
        guard let node = root?.search(for: newURL) else { return }
        
        // Move node to now location
    }
}

final class FileNode: NSObject {
    
    var url: URL
    var identifier: NSObjectProtocol?
    @objc var children = [FileNode]()

    // ...
    
    func search(for target: URL) throws -> FileNode? {
        
        guard let identifier = self.url.identifier, let targetIdentifier = target.identifier else {
            throw Error.NoIdentifier
        }
        
        if identifier.isEqual(targetIdentifier) {
            return self
        } else {
            // Traverse tree
        }
        return nil
    }
}

extension URL {
    var identifier: NSObjectProtocol? {
        return (try? resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier
    }
}

One thing to keep in mind here is that the returned object is guaranteed to be an

NSObject
, and thus be hashable. That means you can keep a mapping from ID to whatever data type you need. For example, something like this:
struct URLMap {

    private var map: [NSObject: URL] = [:]

    mutating func add(_ url: URL) throws {
        let rv = try url.resourceValues(forKeys: [.fileResourceIdentifierKey])
        let id = rv.fileResourceIdentifier!
        let idObj = id as! NSObject
        self.map[idObj] = url
    }

    func oldURL(_ url: URL) throws -> URL? {
        let rv = try url.resourceValues(forKeys: [.fileResourceIdentifierKey])
        let id = rv.fileResourceIdentifier!
        let idObj = id as! NSObject
        return self.map[idObj]
    }
}

The only weirdness here is the force cast to create

idObj
. This is necessary because, while
NSObject
is hashable,
NSObjectProtocol
is not.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
NSFilePresenter presentedsubitem didmoveto URL formatting
 
 
Q