More fun scripting in Swift with Xcode: files and deep traversal

Apple’s support note suggesting that repairing permissions on your Home folder could fix various problems is fascinating me. The problem which it raises is how to discover whether there is a permissions problem. In some cases – when a particular Preference file isn’t working correctly – there are sufficient clues as to know where to look. But checking permissions on more than a handful of files gets very tedious.

This is a classic task for a scripting solution: scan a chosen folder, and report those files whose permissions seem out of kilter. An excellent starting point will be ~/Library/Preferences, where most of these mishaps seem to occur, and /Library/Preferences is often worth a look too.

But what should we deem to be acceptable permissions? What about locked files? Thankfully FileManager (formerly NSFileManager) provides two valuable calls: isReadableFile() and isWritableFile(). Both also traverse symbolic links, which is valuable when dealing with sandboxed apps and their containers.

Preference files in ~/Library/Preferences should surely all be writable by an app running with that same user’s ID. Those in /Library/Preferences are a bit more difficult, though, as some may only be writable by processes running as root. They should in the main still be readable by an app running with an admin user’s ID.

Apple’s scheme for repairing permissions goes far beyond the ~/Library/Preferences folder, though, so we should also offer checks on ~/Library and the whole Home folder. We’re then into tiger country, particularly when looking through the files in ~/Documents, which could quite correctly have other owners, be read-only, or have the immutable bit set to lock them.

These options are set using two popup menus, the first offering the choice of folders, the second whether to require that files are only readable, or are writable too.

Working rather more methodically, for once, I have put the code to scan the chosen folder into a function scanFolder, which is passed the URL of the folder to be scanned and whether to test for files being writable. It sets three variables: theOutString, a String which is then placed in the output text scroller area to list those files which fail the test, and two counters, to tally the total number of items scanned, and how many files fail the readable/writable test.

At its core is a FileManager enumerator which processes each of the items found when performing a deep traversal of the folder:
let fm = FileManager.default
let thePath = theDir.path
if let theDirEnumerator = fm.enumerator(atPath: thePath) {
for item in theDirEnumerator {

noting that the enumerator takes the path from the URL, rather than the URL itself.

Then the iteration itself:
if let path = NSURL(fileURLWithPath: item as! String, relativeTo: theDir as URL).path {
which gives us an absolute path from the relative path in item.

Now test whether writable/readable:
if writable {
if !(fm.isWritableFile(atPath: path)) {
theOutString += (path + "\n")
theCount += 1
}
} else {
if !(fm.isReadableFile(atPath: path)) {
theOutString += (path + "\n")
theCount += 1
}
}
totalCount += 1
}
}

Once the iterations are complete, write the totals out too:
theOutString = theOutString + "Found \(theCount) files out of \(totalCount) scanned in " + thePath
if writable {
theOutString = theOutString + " which are not writable by this user.\n"
} else {
theOutString = theOutString + " which are not readable by this user.\n"
}
let attrX = NSAttributedString(string: theOutString as String)
outputText.textStorage?.setAttributedString(attrX)
}

In pretty form:

permscan04

 

All the action function for the button has to do now is set up the values passed to that function, and call it:
@IBAction func scanHomeButton(_ sender: Any) {
let doWritable = (writableMenu.indexOfSelectedItem == 0)
let fm = FileManager.default
let homeDir = fm.homeDirectoryForCurrentUser
var chosenPath = NSURL(fileURLWithPath: theLibPrefs, relativeTo: homeDir as URL)
if (pathMenu.indexOfSelectedItem == 1) {
chosenPath = NSURL(fileURLWithPath: theLib, relativeTo: homeDir as URL)
} else if (pathMenu.indexOfSelectedItem == 2) {
chosenPath = homeDir as NSURL
} else if (pathMenu.indexOfSelectedItem == 3) {
chosenPath = NSURL(fileURLWithPath: theMainLibPrefs)
}
scanFolder(theDir: chosenPath as URL, writable: doWritable)
}

permscan03

If you want to look beyond testing for isWritableFile() or isReadableFile(), things do get a bit more complex. DirectoryEnumerators give full access to fileAttributes, but these are returned as a dictionary. To ease access to individual attributes, NSDictionary even provides a set of methods for obtaining the most commonly-accessed attributes such as Posix permissions and file type. Thankfully, we don’t need to use those here.

FileManager is one of the strengths of scripting with Swift. Its versatility in working with URLs and paths, either absolute or relative, is often very useful, but it is worth getting used to converting between the two, as you can guarantee that the function call you want will require the one which is not to hand. That can occasionally get a little irritating, but is soon addressed.

It is, though, disappointing how few otherwise good books on Swift programming for macOS or iOS cover this area. Working with files and folders is pretty fundamental stuff, after all.