FileManager.removeItem(atPath:) fails with "You don't have permission to access the file" error when trying to remove non-empty directory on NAS

A user of my app reported that when trying to remove a file it always fails with the error "file couldn't be removed because you don't have permission to access it (Cocoa Error Domain 513)". After some testing, we found out that it's caused by trying to delete non-empty directories.

I'm using FileManager.removeItem(atPath:) which has worked fine for many years, but it seems that with their particular NAS, it doesn't work.

I could work around this by checking if the file is a directory, and if it is, enumerating the directory and remove each contained file before removing the directory itself. But shouldn't this already be taken care of? In the source code of FileManager I see that for Darwin platforms it calls

removefile(pathPtr, state, removefile_flags_t(REMOVEFILE_RECURSIVE))

so it seems that it should already work. Is the REMOVEFILE_RECURSIVE flag perhaps ignored by the device? But then, is the misleading "you don't have permission to access the file" error thrown by the device or by macOS?

For the FileManager source code, see https://github.com/swiftlang/swift-foundation/blob/1d5d70997410fc8b7700c8648b10d6fc28194202/Sources/FoundationEssentials/FileManager/FileOperations.swift#L444

Actually, what prompted me to write this post and I forgot to mention is that the user told me that they can remove the folders without errors in the Finder. So I was also wondering if the Finder is using its own recursive folder delete function, and if I should be doing so as well to be safe?

It's more likely that Finder is calling a network-specific file operation for the SMB or AFP connection.

You said you "could" work around the problem by manually doing a recursive delete. But does that mean that this actually works? Permissions can be complicated, especially on a Linux-based file server that the user could have configured in an unusual fashion.

The user sent me a video showing how removing regular files and empty directories works, but removing non-empty directories doesn't work. They even created a new directory, put a file in there and tried to remove the directory: it didn't work, and after moving the contained file away, it worked.

So I was also wondering if the Finder is using its own recursive folder delete function.

It is, though it's not necessarily because it's inherently "better". The Finder implements most of its own I/O routes because:

  1. It's been around for a VERY long time, predating most of the API alternatives. For example, "copyfile" wasn't added until 10.5, before which we didn't have any copy API the Finder could have used.

  2. For historical reasons, it implements semantics which the larger system doesn't implement.

That last point is probably what's going on here. Unix semantics allow open files to be deleted, but the Finder does NOT allow that because MacOS Classic did not allow that. That means its "delete" call goes through a different (private) syscall path than the standard syscall. The user of that syscall also means that it's doing its own directory iteration, based on its own internal structures.

And if I should be doing so as well to be safe?

The word "safe" there is very tricky. Writing your own file operations can be much trickier than it looks, particularly when network file systems are involved.

One question here:

Is the REMOVEFILE_RECURSIVE flag perhaps ignored by the device?

No, but how long is the deepest file path? removefile() has a flag (REMOVEFILE_ALLOW_LONG_PATHS) to handle sub-paths that exceed PATH_MAX, but FileManager doesn't use it because it won't work properly if multiple threads call removefile.

The user sent me a video showing how removing regular files and empty directories works, but removing non-empty directories doesn't work. They even created a new directory, put a file in there, and tried to remove the directory: it didn't work, and after moving the contained file away, it worked.

Are you able to remove the sub-directory they added?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for your insights.

how long is the deepest file path?

Not long. In the video they showed how the removed directory is only 2 levels deep and every filename is 10-20 characters long.

Are you able to remove the sub-directory they added?

Like I mentioned, with FileManager.removeItem(atPath:) I wasn't able to, but with my custom function below that recursively removes files before the containing folder and I sent to them in a test app today, it worked (and no error alerts were shown).

(Note: before macOS 10.15, the code will leave empty sub-directories and try to remove the top-level directory at the end.)

func removeFileRecursively(atPath path: String) -> Bool {
    if case let url = URL(fileURLWithPath: path, isDirectory: false), (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true {
        var options: FileManager.DirectoryEnumerationOptions = []
        if #available(macOS 10.15, *) {
            options.insert(.includesDirectoriesPostOrder)
        }
        guard let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isDirectoryKey, .isUserImmutableKey], options: options, errorHandler: { url, error in
            NSAlert(error: error).runModal()
            return true
        }) else {
            return false
        }
        while let url = enumerator.nextObject() as? URL {
            let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true
            let shouldRemove = if #available(macOS 10.15, *) {
                !isDirectory || enumerator.isEnumeratingDirectoryPostOrder
            } else {
                !isDirectory
            }
            if shouldRemove {
                do {
                    if try url.resourceValues(forKeys: [.isUserImmutableKey]).isUserImmutable == true {
                        try (url as NSURL).setResourceValues([.isUserImmutableKey: false])
                    }
                    if #available(macOS 10.15, *) {
                        let result = if isDirectory {
                            rmdir(url.path)
                        } else {
                            unlink(url.path)
                        }
                        if result < 0 {
                            throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
                        }
                    } else {
                        try FileManager.default.removeItem(at: url)
                    }
                } catch {
                    NSAlert(error: error).runModal()
                    return false
                }
            }
        }
    }
    do {
        try FileManager.default.removeItem(atPath: path)
    } catch {
        NSAlert(error: error).runModal()
        return false
    }
    return true
}

Like I mentioned, with FileManager.removeItem(atPath:) I wasn't able to, but with my custom function below that recursively removes files before the containing folder and I sent to them in a test app today, it worked (and no error alerts were shown).

Did it work before you added this:

if try url.resourceValues(forKeys: [.isUserImmutableKey]).isUserImmutable == true {
	try (url as NSURL).setResourceValues([.isUserImmutableKey: false])
}

By design, the FileManager won't remove objects marked immutable, which would also prevent directory removal. That's intentional, as the goal of the immutable bit was to allow user to mark file as "safe" from normal deletion.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

By design, the FileManager won't remove objects marked immutable, which would also prevent directory removal.

Good guess, but that wasn't the issue, even though it would have made a lot of sense. It's unfortunate I left that part of the code in there, but the user actually tested my App Store app with and without unlocking locked files (the app contains a switch for that), which didn't help, and they sent me plenty of screenshots and videos which show that the files are not locked.

Good guess, but that wasn't the issue, even though it would have made a lot of sense. It's unfortunate I left that part of the code in there, but the user actually tested my App Store app with and without unlocking locked files (the app contains a switch for that), which didn't help, and they sent me plenty of screenshots and videos which show that the files are not locked.

So, I took another pass at this and I think I found the problem. There is actually a known bug (r.129627798) similar to this in our smb server that was fixed in macOS 26. Basically, the way delete works in smb is that the client accesses the file with a "DELETE_ON_CLOSE" option, then closes the file to trigger the delete. The server tracks open file references and then deletes the file when the count reaches 0. Unfortunately, there was an issue in our file closing logic which could cause the server to keep the count "high" longer than it should, leading to the problem you're seeing.

I've mentioned that this was happening on our server, but part of our own testing showed that other servers were failing under the same conditions, so the bug isn't unique to us. I'd also highlight that NAS boxes are often updated very infrequently (if at all), increasing the likelihood of problems.

I think I can also explain this:

Actually, what prompted me to write this post and I forgot to mention is that the user told me that they can remove the folders without errors in the Finder.

Unfortunately, I think this is a case of one problem hiding another. The Finder's current delete logic on smb is significantly slower and more complicated than it really should be. I haven't tested this myself, but I suspect that a comparison between NSFileManager and the Finder will show that NSFileManager is considerably faster.

This is an issue we're working on (r.153221920). However, I think that extra complexity and delay end up hiding/preventing the problem you're seeing. I suspect the same dynamic is playing out with your own implementation. It's probably faster than the Finder and significantly slower than NSFileManager, but it also ends up generating a different series of SMB operations. The combination of those factors means that it masks the problem the same way the Finder does.

In terms of what you do about it, that's a harder question to answer. My initial thought was that you should:

Unfortunately, having done a bit more testing on our own bug, I'm not convinced that will work. Based on the testing I've done using rm and the Finder, the Finder is initially able to delete the directory, but doing rm first ends up creating a directory state that the Finder can't delete either. For the moment at least, I think your best option is to just ship your own delete function.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

FileManager.removeItem(atPath:) fails with "You don't have permission to access the file" error when trying to remove non-empty directory on NAS
 
 
Q