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.
Asked
Active
Viewed 7,309 times
1 Answers
53
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)
}

Martin R
- 529,903
- 94
- 1,240
- 1,382
-
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" error – prabhu Jul 13 '16 at 06:57
-
@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 :) – Martin R Jul 13 '16 at 07:02
-
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 changes – prabhu Jul 13 '16 at 07:06
-
@prabhu: Hopefully you replaced `/path/to/file` by the real path of an existing file? – Martin R Jul 13 '16 at 07:07
-
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. Thanks – prabhu Jul 13 '16 at 07:10
-
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 advance – prabhu Jul 13 '16 at 07:12
-
-
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? – prabhu Jul 14 '16 at 08:07
-
-
1@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). – Martin R Jul 19 '16 at 09:43
-
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. – Peter71 Sep 02 '16 at 11:45
-
@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. – Martin R Sep 02 '16 at 11:50
-
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. – Peter71 Sep 02 '16 at 18:37
-
@Peter71: I have tested it and it works. (The `&` is not necessary.) – I have taken the liberty and added that to the answer :) – Martin R Sep 02 '16 at 18:50
-
-
how can I use this for removing xattr from a directory and all it its containing sub-directories, files – moghya Mar 11 '19 at 07:11
-
2To fix the "`withUnsafeMutableBytes` is deprecated" warning in Swift 5, change `$0` to `$0.baseAddress`. – Marián Černý Apr 02 '19 at 09:17
-
1@MariánČerný: Thanks for the notice, I have updated the code for Swift 5. – Martin R Apr 02 '19 at 09:43
-
1@ayaio: I had recently updated the code for Swift 5, the Swift 4 version is in the [edit history](https://stackoverflow.com/revisions/09b23c4b-c7dd-45b8-bbdd-fc5334eb845c/view-source) – Martin R Apr 03 '19 at 20:28
-
@MartinR Hello Martin, I'm trying to solve https://stackoverflow.com/q/55497492/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. :) – Eric Aya Apr 03 '19 at 20:30
-
-
@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. :) – Eric Aya Apr 04 '19 at 07:23
-
When running the code and adding an attribute to a file there's automatically another attribute added: "com.apple.quarantine" – Mike Nathas Jan 16 '20 at 12:28
-
@MikeNathas: Sorry, I could not reproduce that behavior. – The com.apple.quarantine attribute is added to files downloaded from the Internet, see for example https://apple.stackexchange.com/q/104712/30895. – Martin R Jan 20 '20 at 20:19
-
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. https://pastebin.com/BGbqeaAH – Mike Nathas Jan 22 '20 at 15:16
-
@MikeNathas: Does that also happen if you add attributes with the “xattr” command-line tool? – Martin R Jan 22 '20 at 15:35
-
No, with `xattr -w newAttr "test" test.txt` only the newAttr attribute is added – Mike Nathas Jan 22 '20 at 15:40
-
@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. – Martin R Jan 22 '20 at 19:48
-
-
-
@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? – mralexhay Apr 12 '20 at 13:57
-
1I 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. – Fredo Oct 10 '21 at 23:35
-
Thanks a lot, this works fine! The only note that there's "native" (Swift) POSIXError in Foundation: – Grigory Entin Jan 15 '22 at 15:50
-
-
@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 ... – Martin R Jan 15 '22 at 18:16
-
@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). – Grigory Entin Jan 15 '22 at 19:01
-
@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. – Martin R Jan 15 '22 at 19:04
-
@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... – Grigory Entin Jan 15 '22 at 19:25