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.
Write extend file attributes swift example
This is discussed at length in https://mcmap.net/q/584569/-swift-3-set-finder-label-color –
Teddman
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)
}
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 –
Thought
@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 changes –
Thought
@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. Thanks –
Thought
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 –
Thought
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, files –
Bargello
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 history –
Sloth
@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/BGbqeaAH –
Evildoer
@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 added –
Evildoer @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 playground –
Evildoer
@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
@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.