Basic Filemanager paths/URLs

macOS 10.12; Swift 4

This post started as an offshoot of the NSOpenPanel/NSSavePanel post. Frequently I want to navigate to certain locations – eg, my apps ApplicationSupport folder, or the user’s document folder – and I could never remember how to get to them. (Spoiler: I think this API has been updated a lot since I last looked at it in any detail; fileManager.urls(for: .documentDirectory, in: .userDomainMask) gives easy access to most locations, and thanks to enums and code completion, there’s not much to remember.)

Filepaths and URLs

String-based filepaths vs. URL
Once upon a time, the macOS operating system – OSX as it was then – used filepaths to say where files lived or should live. Filepaths are simply strings, and you could type them out and compose them out of substrings.

file://Users/yourName/Desktop/filename.jpg

for instance is the path for a file named filename.jpg which lives on your desktop.

Filepaths are easy to construct and parse, but they’re also strings. NSURL has been around since macOS 10.0, but many developers (me included) opted for filepaths where both options are available, simply because they removed an additional layer of obfuscation. Apple, however, is very clear on how you should proceed: URL objects are the preferred way to refer to local files. (Source: Filemanager Documentation). Or, in slightly more detail:
When specifying the location of files, you can use either NSURL or NSString objects. The use of the NSURL class is generally preferred for specifying file-system items because they can convert path information to a more efficient representation internally. You can also obtain a bookmark from an NSURL object, which is similar to an alias and offers a more sure way of locating the file or directory later.

And in case you worry, URL conforms to Codable out of the box, so using strings is no advantage there.

The FileSystem Programming Guide goes into more detail:

All of the following entries are valid references to a file called MyFile.txt in a user’s Documents directory:
Path-based URL: file://localhost/Users/steve/Documents/MyFile.txt
File reference URL: file:///.file/id=6571367.2773272/
String-based path: /Users/steve/Documents/MyFile.txt

and here we finally find the relevant information about URLs:

You create URL objects using the NSURL methods and convert them to file reference URLs only when needed. Path-based URLs are easier to manipulate, easier to debug, and are generally preferred by classes such as NSFileManager. An advantage of file reference URLs is that they are less fragile than path-based URLs while your app is running. If the user moves a file in the Finder, any path-based URLs that refer to the file immediately become invalid and must be updated to the new path. However, as long as the file moved to another location on the same disk, its unique ID does not change and any file reference URLs remain valid.

Important: Although they are safe to use while your app is running, file reference URLs are not safe to store and reuse between launches of your app because a file’s ID may change if the system is rebooted. If you want to store the location of a file persistently between launches of your app, create a bookmark as described in Locating Files Using Bookmarks.

(The bad news – and this is the last I will do in regard to File reference URLs for the moment – is that it seems as if they have become a NSURL-only feature, as described – with workaround – in this bug report.)

While I am attempting to use URL – as Apple has very clearly stated one should – I was suprised to find that a lot of the involved methods are still path- (String) based: FileManager’s changeCurrentDirectoryPath, for instance, does not seem to have a URL equivalent. [I would not be suprised if we’re supposed to use a very different URL-based equivalent; I just have not identified it]. Elsewhere, paths have been deprecated: NSOpenPanel’s filename property has been deprecated in 10.6, for instance, making NSOpenPanel.url the only choice. (url is a more useful property than filename anyway, since filename used to return nil if more than one file was selected, while url returns the first item in the urls array.)

I want to point out one last property of URLs, because that’s the sort of thing that can come back and bite you (it’s becoming more of a general Swift pattern): many of its functions have two versions, eg deletePathExtension() – a mutating function that changes the URL you perform this on – and deletingPathExtension() which returns a new URL minus the path extension (.txt etc).

First a gigantic warning in 47pt fonts with bats around it:

Turning sandboxing on or off can have interesting effects on your code. An example:

let openPanel = NSOpenPanel()
openPanel.directoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)

Without sandboxing:

ScreenShot2018-02-11at20.02.04-2018-02-7-11-27.png

With sandboxing, default configuration:

ScreenShot2018-02-11at20.06.58-2018-02-7-11-27.png

(NSVBSavePanel appears to be an internal subclass of NSSavePanel (?) which is used in sandboxed apps; why an openPanel should use it is beyond me; the one that complains about viewVibrancy on every run is NSVBOpenPanel, so this might be a bug twice over.

The currentDirectory path returns /Users/username/Library/Containers/domainIdentifier.App-Name/Data, but that’s not a valid location here. (You can manually navigate to it in your app; you just cannot set it as the default location for an NSOpenPanel.)

_With_ read/write access, you end up in your user folder (~/username/) which also isn’t the location I expected.

The behaviour of ‘you can write code that would work perfectly fine outside the sandbox, but if you’re trying to do something you have no permission to, your app will crash at runtime’ is one Apple warns you about in the sandbox programming guide; trying to open files when you have no permission to open files does legitimately fall under that category. And given some of the difficulties with sandboxing – if you want to automatically re-open a file a user opened previously, you need to do some extra steps, for instance), you probably could not have implemented permissions in the same manner as you fine on iOS – where the user is asked what level of access they wish to permit and can change that later in Settings – but it would be nice if this failed with ‘sorry, you do not have permission’ instead of ‘failed to init an openPanel’.

So anyway, here are, in no particular order, useful locations in/things to do with the filesystem:

0) You can print/show a URL to the user using URL.absoluteString (this contains the file:// prefix), but if you want to derive a valid filepath from a URL, you need to use URL.path.

1) user home directory: FileManager.default.homeDirectoryForCurrentUser

Other common locations 2) There are several ways of reaching common locations. The old-fashioned way is

2a) desktop: let desktopPathWithTilde: NSString = "~/Desktop"
let expandedDesktopPath = desktopPathWithTilde.expandingTildeInPath
let desktopURL = URL(fileURLWithPath: expandedDesktopPath)

(Only NSString has the expandingTildeInPath method, if you leave this as a String, the compiler will complain.)

This also works for “~/Documents”, regardless of whether their location is in the cloud or not (in 10.12, both desktop and documents are moved to iCloud, but in my app – sandboxed without iCloud permission – I could get there just fine.)

With the help of an imageView and

func setImage(for url: URL){
imageView.image = NSImage(contentsOf: url)
}

@IBOutlet weak var imageView: NSImageView!

I have tested that my sandboxed app, with no user permissions set for accessing files [most of the time, it needs at least read-only, but I *did* run it with none and this worked; several times], nonetheless is perfectly able to open a file and display it. That’s not how I expected sandboxing to work, and it’s quite possibly not how sandboxing is supposed to work; with a good chance that this will not always work, but it makes it much harder to notice that something is amiss, because if everything works as expected, it’s not obvious that you failed to set the correct permissions.

This method – ~/Directory works for all of the top-level directories: “Desktop”, “Documents”, “Downloads”, “Library”, “Movies”, “Music”, “Pictures” You can list them with the following:

do {
let files = try FileManager.default.contentsOfDirectory(atPath: ".")
print(files)
}
catch {
print(error)
}

If you are using this URL to set the initial location for an NSSavePanel or NSOpenPanel not much can go wrong – they default to the user’s home directory if the URL is nil (and URL(fileURLWithPath:) returns an optional) – but you’re still using a string, and if you pass in “Desktopp” or “documents”, that’s a hard error to debug.

2b)
You will occasionally find

let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]

This produces a path (= String) you need to cast to a URL.

2c) A better option is

let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first
(optional); [0] should be safe for most uses, but .first is safer and since NSOpenPanel.directoryURL, for instance, is an optional, this is not even more hassle to use.

(sandboxed; read-only or read/write permission; without permission this crashes)
file:///Users/username/Library/Containers/domainIdentifier.App-Name/Data/Documents/
(not-sandboxed)
file:///Users/username/Documents/

It’s interesting that both of these URLs crash the NSOpenPanel if permissions are not set correctly (presumably because they are trying to retrieve information through the FileManager, which should check permission), while the manually constructed paths occasionally worked regardless of sandbox entitlements. That’s radar-worthy. (Most of the time, they crash; but I ran at several instances where they didn’t.)

SearchPathDirectory is an enum with many wonderful cases, all listed in the documentation: here you can find Applications, Library, ApplicationSupport, Trash, and many more.
SearchPathDomainMask gives you not only .userDomainMask but localDomainMask (= currentMachine), networkDomainMask, and allDomainMask.

Between these, most of the interesting directories should not only be easily reachable, but mostly self-evident. (A listing of library directories can be found here.)

Temporary Directory

Temporary Directory
3) temporary directory for this application (this is scratch space; it will, as far as I can see, not be backed up by TimeMachine, nor is it visible to the user); use to temporarily save files and in-between steps.

let temp = fileManager.temporaryDirectory

sandboxed:
file:///var/folders/_2/mrld8j392r337qmb4sgtk1940000gn/T/domainIdentifier.App-Name/
not-sandboxed:
file:///var/folders/_2/mrld8j392r337qmb4sgtk1940000gn/T/

NSHipster has a more in-depth article about temporary directories/files; I don’t want to fall into this particular rabbithole at this moment, and I make no guarantees about the quality of the code (though based on past experience, I would expect it to skew towards ‘excellent’). This article on Cocoa With Love also goes into more depth; but it was written in 2009 and not updated since.

The following is, however, still valid and important:

The correct directory is almost always the one returned by the Foundation function NSTemporaryDirectory. This directory is unique for each user (so each user has write permissions to it).
If you choose this directory though, it is important to know that it is cleaned out automatically every 3 days but otherwise persists between application launches and reboots. This directory is in a hashed location (not predictable in advance) and is therefore safe against security issues associated with predicatable locations.
If you need a location that can persist longer than 3 days, you probably want to use the Application Support directory or a Cache directory instead. Both of these locations are permanent (until your application deletes the contents) but Application Support is considered “important data” and backed up by Time Machine, whereas Cache is considered “user deletable at any time” and is not backed up. The backup point is important: don’t store large amounts of temporary data in a location that will fill someone’s backup drive.

4) Composing URLs

The URL class has the ability to compose URLs from, or decompose URLs into, URL components.

Since URLs are not only pointers to locations in the file system, but used for internet addresses, the URL class contains a whole host of components you will never need for file system operations (like host and password). But this, too, is a topic for a separate post (which I probably will never make). Have, instead, the NSHipster post on this, which includes listings of all URL components. It’s worth playing with them and getting a feel for them, or a slightly different approach from SametDEDE.

The most useful operation here is probably removing extensions from a URL and adding different extensions to it:

print(panelURL) //myFile.png
let shortenedURL = panelURL.deletingPathExtension()
print(shortenedURL) //myFile
let lengthenedURL = shortenedURL.appendingPathExtension("txt")
print(lengthenedURL) //myFile.txt

5) Worth pointing out may be that isFileURL does not check whether a URL belongs to a file, merely whether it is part of the file system (as opposed to on the Internet); directories also return true for this. (check hasDirectoryPath instead if you need to know). Ask me why I felt this was worth pointing out. Ooops.

As you may guess, this is just a brief (hah!) introduction to the topic, but all I needed to know for the moment.