Write extend file attributes swift example
Asked Answered
T

1

23

I am looking for a solution to add extended file attributes for a file in swift. I checked this link Write extended file attributes, but the solutions are in objective c and I need a solution for swift.

Thought answered 13/7, 2016 at 5:19 Comment(1)
This is discussed at length in https://mcmap.net/q/584569/-swift-3-set-finder-label-colorTeddman
S
56

Here is a possible implementation in Swift 5 as an extension for URL, with methods to get, set, list, and remove extended attributes of a file. (Swift 2, 3, and 4 code can be found in the edit history.)

extension URL {

    /// Get extended attribute.
    func extendedAttribute(forName name: String) throws -> Data  {

        let data = try self.withUnsafeFileSystemRepresentation { fileSystemPath -> Data in

            // Determine attribute size:
            let length = getxattr(fileSystemPath, name, nil, 0, 0, 0)
            guard length >= 0 else { throw URL.posixError(errno) }

            // Create buffer with required size:
            var data = Data(count: length)

            // Retrieve attribute:
            let result =  data.withUnsafeMutableBytes { [count = data.count] in
                getxattr(fileSystemPath, name, $0.baseAddress, count, 0, 0)
            }
            guard result >= 0 else { throw URL.posixError(errno) }
            return data
        }
        return data
    }

    /// Set extended attribute.
    func setExtendedAttribute(data: Data, forName name: String) throws {

        try self.withUnsafeFileSystemRepresentation { fileSystemPath in
            let result = data.withUnsafeBytes {
                setxattr(fileSystemPath, name, $0.baseAddress, data.count, 0, 0)
            }
            guard result >= 0 else { throw URL.posixError(errno) }
        }
    }

    /// Remove extended attribute.
    func removeExtendedAttribute(forName name: String) throws {

        try self.withUnsafeFileSystemRepresentation { fileSystemPath in
            let result = removexattr(fileSystemPath, name, 0)
            guard result >= 0 else { throw URL.posixError(errno) }
        }
    }

    /// Get list of all extended attributes.
    func listExtendedAttributes() throws -> [String] {

        let list = try self.withUnsafeFileSystemRepresentation { fileSystemPath -> [String] in
            let length = listxattr(fileSystemPath, nil, 0, 0)
            guard length >= 0 else { throw URL.posixError(errno) }

            // Create buffer with required size:
            var namebuf = Array<CChar>(repeating: 0, count: length)

            // Retrieve attribute list:
            let result = listxattr(fileSystemPath, &namebuf, namebuf.count, 0)
            guard result >= 0 else { throw URL.posixError(errno) }

            // Extract attribute names:
            let list = namebuf.split(separator: 0).compactMap {
                $0.withUnsafeBufferPointer {
                    $0.withMemoryRebound(to: UInt8.self) {
                        String(bytes: $0, encoding: .utf8)
                    }
                }
            }
            return list
        }
        return list
    }

    /// Helper function to create an NSError from a Unix errno.
    private static func posixError(_ err: Int32) -> NSError {
        return NSError(domain: NSPOSIXErrorDomain, code: Int(err),
                       userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(err))])
    }
}

Example usage:

let fileURL = URL(fileURLWithPath: "/path/to/file")

let attr1 = "com.myCompany.myAttribute"
let attr2 = "com.myCompany.otherAttribute"

let data1 = Data([1, 2, 3, 4])
let data2 = Data([5, 6, 7, 8, 9])

do {
    // Set attributes:
    try fileURL.setExtendedAttribute(data: data1, forName: attr1)
    try fileURL.setExtendedAttribute(data: data2, forName: attr2)

    // List attributes:
    let list = try fileURL.listExtendedAttributes()
    print(list)
    // ["com.myCompany.myAttribute", "com.myCompany.otherAttribute", "other"]

    let data1a = try fileURL.extendedAttribute(forName: attr1)
    print(data1a as NSData)
    // <01020304>

    // Remove attributes
    for attr in list {
        try fileURL.removeExtendedAttribute(forName: attr)
    }

} catch let error {
    print(error.localizedDescription)
}
Sloth answered 13/7, 2016 at 6:4 Comment(37)
Thanks for the answer. I just copy pasted your code and ran the application. No errors printed but i could not confirm the attribute set from command line. I got "No such xattr: com.myCompany.myAttribute" errorThought
@prabhu: Have you double-checked that the attribute name in the code is the same as the attribute name on the command line? With xattr /path/to/file you can list all existing attributes of the file. – It worked for me, I tested the code before posting :)Sloth
yes it is same as in your example. I am not sure if I am missing anything. But I just copy pasted all you posted. No changesThought
@prabhu: Hopefully you replaced /path/to/file by the real path of an existing file?Sloth
Hey it is working. All I need to do is to set the attribute after i write contents to the file and "close it". I will accept the answer. ThanksThought
Curious to know if i can write a bool value to an attribute. It would be helpful if you could show a sample to read the attribute from a file. Thanks in advanceThought
that is vey helpful. Thanks for your time (y)Thought
Hi Martin. I checked this in my simulator and it is working fine. But in device, I exported the file and mail it. Then I tried to check if the file contains the attribute in command line and it says "No such xattr: com.myCompany.myAttribute" error. Any idea?Thought
waiting for your response :)Thought
@prabhu: In what format did you export and mail the file? You would have to choose a format which preserves extended attributes (if such a format exists).Sloth
Is there a function to remove all the stuff I added with "setExtendedAttribute"? I don't want to write 0 bytes, but keep the name in use.Calotte
@Peter71: The removexattr system call removes an attribute and its value. If you want to keep the attribute name then you have to set its value to zero bytes.Sloth
I used: func removeExtendedAttribute(forName name: String) -> Bool { var fileSystemPath = [Int8](count: Int(MAXPATHLEN), repeatedValue: 0) guard self.getFileSystemRepresentation(&fileSystemPath, maxLength: fileSystemPath.count) else { return false } let result = removexattr(&fileSystemPath, name, 0) return result == 0 } but it delivers an error.Calotte
@Peter71: I have tested it and it works. (The & is not necessary.) – I have taken the liberty and added that to the answer :)Sloth
how can I use this for removing xattr from a directory and all it its containing sub-directories, filesBargello
To fix the "withUnsafeMutableBytes is deprecated" warning in Swift 5, change $0 to $0.baseAddress.Impure
@MariánČerný: Thanks for the notice, I have updated the code for Swift 5.Sloth
@ayaio: I had recently updated the code for Swift 5, the Swift 4 version is in the edit historySloth
@MartinR Hello Martin, I'm trying to solve https://mcmap.net/q/584570/-macos-and-swift-4-reading-urlresourcekey-customiconkey-and-urlresourcekey-thumbnailkey-file-attributes/2227743 but I'm out of my comfort zone. I was trying to use your extension (with $0 for Swift 4 instead of $0.baseAddress) but I only get an empty array when listing attributes. Would you mind looking at this person's question? I'm interested by your take on this. :) (sorry about the previous comment, I didn't read the comments and edits before asking). Also, sorry if I'm bothering you, I'll understand if you're not interested. :)Deltoro
@ayaio: No problem, but I do not have an answer at the moment.Sloth
@MartinR I managed to get the attributes from the file if it's in the filesystem (doesn't work if the file is copied in the app bundle, not sure why). I get the resource fork data with try url.extendedAttribute(forName: "com.apple.ResourceFork"), thanks to your extension. Now, to find how to decode this data and extract the custom icon from it... // Thank you for your work. :)Deltoro
When running the code and adding an attribute to a file there's automatically another attribute added: "com.apple.quarantine"Evildoer
@MikeNathas: Sorry, I could not reproduce that behavior. – The com.apple.quarantine attribute is added to files downloaded from the Internet, see for example apple.stackexchange.com/q/104712/30895.Sloth
I know that's why I'm wondering why this attribute is added. This code will show the attributes before (no attribute) and after adding an attribute with your code (2 new attributes): com.apple.quarantine and newAttr. pastebin.com/BGbqeaAHEvildoer
@MikeNathas: Does that also happen if you add attributes with the “xattr” command-line tool?Sloth
No, with xattr -w newAttr "test" test.txt only the newAttr attribute is addedEvildoer
@MikeNathas: Sorry, but I still cannot reproduce that behaviour. I have tried that code with various files in the Documents folder, and no com.apple.quarantine attribute was added for files which did not have that attribute before.Sloth
@MikeNathas: Xcode 11.3.1 (Swift 5) on macOS 10.14 and 10.15.Sloth
OK it seems only to happen when you run the code in playgroundEvildoer
@martin Thanks for this, it was really useful. Do you happen to know if any way to get a list of all tags, like the one in the Finder side bar or in the Files app?Backstitch
I used this script in order to get a list of all the tags of a file (or folder) and it worked fine. Example : "let fileURL = URL(fileURLWithPath: "/Volumes/testVolume", isDirectory: true)" AND : "let attr1 = "com.apple.metadata:_kMDItemUserTags". After that, you 'll need to cast the result to utf8 to make it readable : "let resultatdata1a = (String(decoding: data1a, as: UTF8.self))". I am using swift version 5. I had to disable sandbox to allow this operation.Downcomer
Thanks a lot, this works fine! The only note that there's "native" (Swift) POSIXError in Foundation:Lavallee
developer.apple.com/documentation/foundation/posixerrorLavallee
@GrigoryEntin: Thanks for the notice. My posixError is just a helper method to create an NSError. In order to use Swift.POSIXError I ended up with something like POSIXError(POSIXErrorCode(rawValue: errno)!, userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(errno))]) with forced unwrapping. I am not sure if that is the best way ...Sloth
@MartinR I see/get your point. Just to mention it, I feel like userInfo is not necessary there - POSIXError provides the proper description automatically (from what I can see).Lavallee
@GrigoryEntin: You are right, POSIXError(POSIXErrorCode(rawValue: errno)!) would be sufficient. But I don't like the forced unwrapping. Of course one can provide a default value POSIXError(POSIXErrorCode(rawValue: errno) ?? .EINVAL), but why? We know that errno is an error code from a system call.Sloth
@MartinR I agreed with the argument for force unwrapping. Just thinking out loud, I wonder why (Swift) POSIXError exists at all. I can imagine that it would be used on the caller side, e.g. for matching/analysis of the errors... Even with this very code, I match against POSIXError.ENOATTR to catch non-existing attributes (so far I use POSIXError with fallback as you proposed). All in all, yes, it looks like a deficiency from API perspective that we have to use .Code instead of just Int for construction, still, it's not all black and white to me...Lavallee

© 2022 - 2024 — McMap. All rights reserved.