I have a native macOS app, Messer, which allows to convert/resize/pad image files. One feature I built into it is auto-converting files in a specific folder.
This feature works well on most folders, even though it might require permissions to access them. For example, selecting the Downloads
folder to auto-convert triggers a permission dialog for the user to grant access.
The same goes for the Desktop
folder, however, even if the permission has been granted on the Desktop
folder, I'm getting an error when trying to copy a file inside a sub-directory.
The error output:
2022-04-27 13:35:04.255243 0200 Messer[1254:11983] open on /Users/osp/Desktop/posts/a-plus-content-4.png: Operation not permitted
Messer, could not copy file Error Domain=NSCocoaErrorDomain Code=513 "“a-plus-content-4.png” couldn’t be copied because you don’t have permission to access “com.ospfranco.messer”." UserInfo={NSSourceFilePathErrorKey=/Users/osp/Desktop/posts/a-plus-content-4.png, NSUserStringVariant=(
Copy
), NSDestinationFilePath=/var/folders/qn/vyvn49j90jv9_77vq77wzvw00000gn/T/com.ospfranco.messer/a-plus-content-4.png, NSFilePath=/Users/osp/Desktop/posts/a-plus-content-4.png, NSUnderlyingError=0x60000206dda0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}
Which is thrown when I try to copy the file to the temporary directory:
func secureCopyItem(at srcURL: URL, to dstURL: URL) {
do {
if FileManager.default.fileExists(atPath: dstURL.path) {
try FileManager.default.removeItem(at: dstURL)
}
try FileManager.default.copyItem(at: srcURL, to: dstURL)
} catch let error {
print("Messer, could not copy file \(error)")
SentrySDK.capture(error: error)
}
}
Here you can see the app has full disk access:
Yet the app still crashes! If I choose any other folder the copy operation works normally. All the entitlements and permissions seem to be there. Any idea what could be wrong?
EDIT 1:
It seems it's not quite clear I'm really copying from a desktop URL to a temporary directory, so here is some of the surrounding code:
if url.isImage && url.isLocalFile {
let newFileUrl = FileConstants.tempUrl.appendingPathComponent(url.lastPathComponent)
FileManager.default.secureCopyItem(at: url, to: newFileUrl)
self.initPipeline(urls: [newFileUrl], autoSave: true)
}
The temp URL in a different constants file:
static let tempUrl: URL = {
return FileManager.default.temporaryDirectory
}()
I can promise you this same code run with another file in another directory (e.g. Downloads) is correct and works without any error.
Edit 2:
After reading a bit some other answers, it could be a problem with how I'm accessing the resource, I allow the user to select a folder via UI and then save this path in my application. However, due to permissions, it could be that the resource is only accessible for a period of time after the user has selected the folder and not indefinitely. So after some time when I try to access the resources in the folder, macOS simply blocks any of my attempts to read the files.
I have tried using some resource lock method and it returns false on the passed URL:
let scopedResourceLocked = url.startAccessingSecurityScopedResource()
print("SCOPED RESOURCE LOCKEd \(scopedResourceLocked)") // prints false
let newFileUrl = FileConstants.tempUrl.appendingPathComponent(url.lastPathComponent)
FileManager.default.secureCopyItem(at: url, to: newFileUrl)
url.stopAccessingSecurityScopedResource()
Edit 3:
I just tried reselecting the folder (via NSOpenPanel) and converting a file and it worked. So I guess there is no way to achieve this in a consistent manner without user interaction. After a while macOS will lock/protect the files in the folder again.
CodePudding user response:
Thanks a lot to @vadian for pointing me in the right direction. As it turns out, having full disk access is not enough, even if the user has given permission to a specific folder.
Once you get the URL from NSOpenPanel, you need to create a Bookmark and save the binary data. On further app openings you hydrate the binary data. From this bookmark object you can create a URL again, on which you need to call startAccessingSecurityScopedResource
and stopAccessingSecurityScopedResource
to access any previous folder the user has granted access to (even if you are accessing a file or subfolder inside of that folder.
Basically followed this tutorial: