Paths are the most fundamental element of the Filesystem API. They represent
filesystem paths in a very abstract sense, and provide a high-level protocol
for working with paths without having to manipulate Strings. Here are some
examples using the methods that FSPath
provides:
"absolute path"
FSPath / 'plonk' / 'feep'
=> /plonk/feep
"relative path"
FSPath * 'plonk' / 'feep'
=> plonk/feep
"relative path with extension"
FSPath * 'griffle' , 'txt'
=> griffle.txt
"changing the extension"
FSPath * 'griffle.txt' , 'jpeg'
=> griffle.jpeg
"parent directory"
(FSPath / 'plonk' / 'griffle') parent
=> /plonk
"resolving a relative path"
(FSPath / 'plonk' / 'griffle') resolve: (FSPath * '..' / 'feep')
=> /plonk/feep
"resolving an absolute path"
(FSPath / 'plonk' / 'griffle') resolve: (FSPath / 'feep')
=> /feep
"resolving a string"
(FSPath * 'griffle') resolve: 'plonk'
=> griffle/plonk
"comparing"
(FSPath / 'plonk') contains: (FSPath / 'griffle' / 'nurp')
=> false
A filesystem is an interface to some hierarchy of directories and files. "The filesystem," provided by the host operating system, is embodied by FSDiskFilesystem and it's platform-specific subclasses. But other kinds of Filesystems are also possible. FSMemoryFilesystem provides a RAM disk-a filesystem where all files are stored as ByteArrays in the image. FSZipFilesystem represents the contents of a zip file.
Each filesystem has its own working directory, which it uses to resolve any relative paths that are passed to it. Some examples:
fs := FSMemoryFilesystem new. fs workingDirectory: (FSPath / 'plonk'). griffle := FSPath / 'plonk' / 'griffle'. nurp := FSPath * 'nurp'.
fs resolve: nurp => /plonk/nurp
fs createDirectory: (FSPath / 'plonk') => "/plonk created" (fs writeStreamOn: griffle) close. => "/plonk/griffle created" fs isFile: griffle. => true fs isDirectory: griffle => false fs copy: griffle to: nurp => "/plonk/griffle copied to /plonk/nurp" fs exists: nurp => true fs delete: griffle => "/plonk/griffle" deleted fs isFile: griffle => false fs isDirectory: griffle => false
Paths and filesystems are the lowest level of the Filesystem API. An
FSReference
combines a path and a filesystem into a single object which
provides a simpler protocol for working with files. It implements the same
operations as FSFilesystem
, but without the need to track paths and
filesystem separately:
fs := FSMemoryFilesystem new. griffle := fs referenceTo: (FSPath / 'plonk' / 'griffle'). nurp := fs referenceTo: (FSPath * 'nurp').
griffle isFile
griffle isDirectory
griffle parent ensureDirectory.
griffle writeStreamDo: [:s]
griffle copyTo: nurp
griffle delete
References also implement the path protocol, with methods like #/
,
#parent
and #resolve:
Locators could be considered late-bound references. They're left deliberately
fuzzy, and only resolved to a concrete reference when some file operation
needs to be performed. Instead of a filesystem and path, locators are made up
of an origin
and a path. An origin is an abstract filesystem location, such
as the user's home directory, the image file, or the VM executable. When it
receives a message like #isFile, a locator will first resolve its origin, then
resolve its path against the origin.
Locators make it possible to specify things like "an item named 'package-cache' in the same directory as the image file" and have that specification remain valid even if the image is saved and moved to another directory, possibly on a different computer.
locator := FSLocator image / 'package-cache'. locator printString => '{image}/package-cache' locator resolve => /Users/colin/Projects/Mason/package-cache locator isFile => false locator isDirectory => true
The following origins are currently supported:
Applications may also define their own origins, but the system will not be able to resolve them automatically. Instead, the user will be asked to manually choose a directory. This choice is then cached so that future resolution requests won't require user interaction.
References and Locators also provide simple methods for dealing with whole directory trees:
This will answer an array of references to all the files and directories in the directory tree rooted at the receiver. If the receiver is a file, the array will contain a single reference, equal to the receiver.
This method is similar to #allChildren, but it answers an array of FSDirectoryEntries, rather than references.
This will perform a deep copy of the receiver, to a location specified by the argument. If the receiver is a file, the file will be copied; if a directory, the directory and its contents will be copied recursively. The argument must be a reference that doesn't exist; it will be created by the copy.
This will perform a recursive delete of the receiver. If the receiver is a file, this has the same effect as #delete.
The above methods are sufficient for many common tasks, but application developers may find that they need to perform more sophisticated operations on directory trees.
The visitor protocol is very simple. A visitor needs to implement
#visitFile:
and #visitDirectory:
. The actual traversal of the filesystem
is handled by a guide. A guide works with a visitor, crawling the filesystem
and notifying the visitor of the files and directories it discovers. There are
three Guide classes, FSPreorderGuide
, FSPostorderGuide
and
FSBreadthFirstGuide
, which traverse the filesystem in different orders. To
arrange for a guide to traverse the filesystem with a particular visitor is
simple. Here's an example:
FSBreadthFirstGuide show: aReference to: aVisitor
The enumeration methods described above are implemented with visitors; see
FSCopyVisitor
, FSDeleteVisitor
and FSCollectVisitor
for examples.