Writing file systems in Go with FUSE
Motivation
Some time ago, I decided I wanted to solve my own storage needs better, and I realized that I can’t just rely on synchronizing files. I needed a filesystem that combines the best of three worlds: local files, network file systems, and file synchronization. This project is called Bazil, as in bazillion bytes.
To make Bazil possible, I needed to be able to easily write a filesystem in Go. And now you can, too, with bazil.org/fuse.
What we’ll build today is an example Go application that serves a Zip archive as a filesystem:
|
|
FUSE
FUSE (Filesystem In Userpace) is a Linux kernel filesystem that sends the incoming requests over a file descriptor to userspace. Historically, these have been served with a C library of the same name, but ultimately FUSE is just a protocol. Since then, the protocol has been implemented for other platforms such as OS X, FreeBSD and OpenBSD.
bazil.org/fuse is a reimplementation of that protocol in pure Go.
Structure of Unix filesystems
Unix filesystems consist of inodes (“index nodes”). These nodes are files, directories, etc. Directories contain directory entries (dirent) that point to child inodes. A directory entry is identified by its name, and carries very little metadata. The inode manages both the metadata (including things like access control) and the content of the file.
Open files are identified in userspace with file descriptors, which are just safe references to kernel objects known as handles.
Go API
Our FUSE library is split into two parts. The low-level protocol is in
bazil.org/fuse
while the
higher-level, optional, state machine keeping track of object
lifetimes is
bazil.org/fuse/fs
.
Each file system has a root entry. The interface
fs.FS
has a method
Root
that returns an
fs.Node
.
To access a file (see its metadata, open it, etc), the kernel looks it
up by name by sending a
fuse.LookupRequest
to the FUSE server, stating the parent directory and basename. This
request is served by a
Lookup
method on the parent
fs.Node
. The method
returns an fs.Node
, and
the result is cached in the kernel and reference counted. Dropping a
cache entry sends a
ForgetRequest
, and
when the reference count reaches zero,
Forget
gets
called.
Files are renamed with
Rename
, deleted
with Remove
, and
so on.
Kernel file handles are created for example by opening a file.
Opening an existing file sends an
OpenRequest
, you
guessed it, served by
Open
. All methods
creating new handles return a
Handle
. Handles are
closed by a combination of
Flush
and
Release
.
The default Open
action, if the method is not implemented, is to use the
fs.Node
also as a
Handle
; this tends to
work well for stateless read-only files.
Reads from a Handle
are
served by Read
,
writes with
Write
, and apart
from all the extra data available these look similar to
io.ReaderAt
and
io.WriterAt
. Note that file
size changes via
Setattr
, not
based on Write
,
and Attr
needs to return
the correct Size
.
Listing a directory happens by reading an open file handle that is a
directory. Instead of file contents, the read returns marshaled
directory entries. The
ReadDir
method
implements a slightly higher-level API, where you return a slice of
directory entries.
And so on. Learning to write a file system requires a decent understanding of the kernel data structures and their state changes on an abstract level, but the actual Go parts of it are quite simple. So let’s dive into the code.
zipfs
As our example project, we’ll write a filesystem that shows a read-only view of the contents of a Zip archive.
The full source code is available at https://github.com/bazillion/zipfs
Skeleton
Let’s start with a skeleton with a argument parsing:
|
|
Mounting is a bit cumbersome due to OSXFUSE behaving very differently from Linux; there are several stages where errors may show up.
|
|
Filesystem
On to the actual file system. We just hold a pointer to the zip archive:
|
|
And we need to provide the Root
method:
|
|
Directories
Zip files contain a list of files, but typical zip archivers include entries for the directories, with a name ending in a slash. We rely on this behavior later.
Let’s define our Dir
type, and implement the mandatory Attr
method. We use the *zip.File
to serve directory metadata.
|
|
Directory entry lookup
For our filesystem to contain anything useful, we need to be able to find entries by name. We just iterate over the zip entries, matching paths:
|
|
Files
Our Lookup
above returned File
types when the matched entry did
not end in a slash. Let’s define type File
, using the same zipAttr
helper as for directories:
|
|
Files are not very useful unless you can open them:
|
|
Handles
|
|
We hold an “open file” inside our handle. In this case, it’s just a
helper type in archive/zip
, but in another filesystem this might be
a *os.File
, a network connection, or such. We should be careful to
close them:
|
|
And then let’s handle actual Read
operations:
|
|
Readdir
At this point, our files are accessible by cat
and such, but you
need to know their names. Let’s add support for ReadDir
:
|
|
Testing zipfs
Prepare a zip file:
|
|
Mount it:
|
|
Lookup directory entries:
|
|
Read file contents:
|
|
Readdir (the “total 0” is not correct, but that doesn’t matter):
|
|
Unmount (for OS X, use umount mnt
):
|
|
That’s it! For a longer and more featureful examples to read, see https://github.com/bazillion/bolt-mount (screencast of a code walkthrough) and all of the projects importing fuse.
Resources
Bazil is a distributed file system designed for single-person disconnected operation. It lets you share your files across all your computers, with or without cloud services.
FUSE is a Linux kernel filesystem that makes calls to userspace to serve filesystem content.
Confusingly also known as FUSE is the C library for implementing userspace FUSE filesystems.
bazil.org/fuse is a Go library for writing filesystems. See also GoDoc for
fuse
andfuse/fs
OSXFUSE is a FUSE kernel implementation for OS X.
bolt-mount
is a more comprehensive example filesystem, including write operations. See also a screencast of a code walkthrough.Writing a file system in Go is an earlier talk that explains FUSE a bit more.
FUSE questions are welcome on the bazil-dev Google Group or on IRC channel #go-nuts on irc.freenode.net.