
Building a custom file system with FUSE (Filesystem in Userspace) and Go is a fantastic way to create a new file system that can interact with existing operating systems.
FUSE is a software framework that allows you to create a file system in userspace, without the need to modify the kernel.
This tutorial will guide you through the process of building a custom file system using FUSE and Go.
To get started, you'll need to install the FUSE library for Go, which can be done with the command `go get github.com/hanwen/go-fuse/fuse`.
File System
A filesystem is a crucial component of a FUSE implementation in Go. It's where the magic happens, allowing you to interact with files and directories.
To implement a basic filesystem, you'll need to create implementations for Attr and Setattr, which remain pretty much the same.
To write to a file, you'll implement the HandleWriter interface, and to read the contents, you'll implement the HandleReadAller interface. There's also a more generic HandleReader interface for handling ReadRequest with size and offset.
The mount point should point to a directory, similar to how mount works. Once mounted, you can cd into the directory and create directories and write files.
Discover more: Golang Write
Abort File System

If you're dealing with a buggy or crashed FUSE filesystem, a hung file system can be a real pain.
The fusectl filesystem is your best friend in this situation, as it allows you to write into /sys/fs/fuse/connection/$ID/abort to clean up the mess.
This will cause reads from the FUSE device to fail, and all callers will receive an ENOTCONN error on their pending syscalls.
The FUSE connection ID can be found in the Stat_t result for a file in the mount, specifically in the Dev field.
Attr IsRegular
The Attr IsRegular function is a valuable tool in understanding file types. It reports whether the FileInfo describes a regular file.
A regular file is a fundamental concept in file systems, and IsRegular helps you identify them.
In the context of file systems, regular files are the most common type of file and can contain any type of data.
The IsRegular function is particularly useful when working with files, as it allows you to determine the type of file you're dealing with.
InodeNotifyStoreCache

InodeNotifyStoreCache is a function that tells the kernel to store data into an inode's cache. It's similar to InodeNotify, but instead of just invalidating a data region, it gives the kernel updated data directly.
This call is used to update the kernel's view of the file system, making sure it has the latest information. It's a way for the kernel to refresh its cache and ensure that it's working with the most up-to-date data.
InodeNotifyStoreCache is a key part of how the file system stays in sync, especially in situations where data is changing rapidly. It helps the kernel keep track of what's happening, even when things are moving fast.
InodeRetrieveCache
InodeRetrieveCache is a crucial function in the file system that retrieves data from the kernel's inode cache.
The kernel's inode cache is a temporary storage area where frequently accessed inode data is stored.
This function asks the kernel to return data from its cache for a specific inode at a given offset and length.
If the kernel cache has fewer consecutive data starting at the offset, that's the amount that's returned.
In particular, if inode data at the offset is not cached, the function returns a value of 0, indicating that it's okay.
RawFileSystem

RawFileSystem is an interface close to the FUSE wire protocol.
Unless you really know what you're doing, you shouldn't implement this, but rather the interfaces associated with fs.InodeEmbedder. The details of getting interactions with open files, renames, and threading right are somewhat tricky and not very interesting.
Each FUSE request results in a corresponding method called by Server.
Several calls may be made simultaneously, because the server typically calls each method in separate goroutine.
A null implementation is provided by NewDefaultRawFileSystem.
Logfs
Logfs is a simple filesystem that logs function calls to understand how bazil/fuse works. This idea is loosely based on the Big Brother Filesystem.
It's built on top of the Unix filesystem, which is made up of inodes that can represent files, directories, etc. Inodes store all the metadata and permissions.
The skeleton code to handle CLI flags is stolen from the bazil/fuse example. This code parses the cli flags to get a mount point to start the FUSE server.
The fs.go file provides the entry point for the filesystem, which is the root directory that gets mounted. It has an inodeCount that gets incremented every time a file or directory is added.
The EntryGetter interface is defined, which will be implemented for files and directories. This interface allows us to differentiate them by their Type, which is now a DT_File.
Error Handling
Error Handling is crucial when working with fuse golang. If a FUSE API call is canceled, it should return EINTR.
When a FUSE API call is canceled, the outstanding request data is not reused, so the API call may return EINTR without ensuring that child contexts have successfully completed. This can have implications for the overall stability of your application.
(*Context) Err
The (*Context) Err scenario is a specific error handling situation that arises when a FUSE API call is canceled. This is signaled by closing the `cancel` channel.
In this case, the API call should return EINTR. The outstanding request data is not reused, so the API call may return EINTR without ensuring that child contexts have successfully completed.
AttrTimeout

When working with attributes, it's essential to understand how to handle timeouts. The AttrTimeout function returns the TTL in nanoseconds of the attribute data.
A TTL of zero means the attribute data never expires. This can be useful in certain scenarios where you want to ensure the data remains valid indefinitely.
In practice, I've found that understanding TTLs helps prevent unexpected errors caused by expired data. By setting a reasonable TTL, you can avoid these issues and ensure your application runs smoothly.
If you're working with a TTL that's too short, you may experience errors due to expired data. This can be frustrating, especially if you're not aware of the TTL's existence.
File Operations
File Operations are crucial for a functional filesystem. You can read, write, and list files with ease.
To read the contents of a file, you need to implement the HandleReader interface. This allows you to handle ReadRequest with size, offset, etc. A more generic HandleReader interface is also available.
You can also implement the HandleWriter interface to allow writing to the file. The WriteRequest includes the data size, which you use to update the file attribute. To read the contents of a file, you can use the HandleReader interface.
How To Build
Building a file system can be a bit tricky, but don't worry, I've got you covered. You'll need to install some prerequisites depending on your operating system.
To build on Windows, you'll need WinFsp and gcc. You can get gcc from Mingw-builds. Set your CPATH to C:\Program Files (x86)\WinFsp\inc\fuse and then run go install -v ./fuse ./examples/memfs.
On macOS, you'll need macFUSE and command line tools. You can build with just one command: go install -v ./fuse ./examples/memfs ./examples/passthrough.
For Linux, you'll need libfuse-dev and libfuse3-dev. You can build FUSE with go install -v ./fuse ./examples/memfs ./examples/passthrough, and FUSE3 with go install -tags=fuse3 -v ./fuse ./examples/memfs ./examples/passthrough.
On FreeBSD, you'll need fusefs-libs and fusefs-libs3. Building is similar to Linux: go install -v ./fuse ./examples/memfs ./examples/passthrough for FUSE, and go install -tags=fuse3 -v ./fuse ./examples/memfs ./examples/passthrough for FUSE3.
Some operating systems, like NetBSD, don't require any prerequisites. You can build with just one command: go install -v ./fuse ./examples/memfs ./examples/passthrough.

On OpenBSD, you'll also need to make some system changes to use FUSE. You can build with the same command as NetBSD, but you'll need to be root to use FUSE and cgofuse.
Here's a quick reference guide to building on different operating systems:
How To Use
To use a user mode file system, you need to implement fuse.FileSystemInterface. This interface is required for a file system to function properly.
You can make implementation simpler by embedding fuse.FileSystemBase, which provides default implementations for all operations. This can save you a lot of time and effort.
To mount a file system, you must instantiate a fuse.FileSystemHost using fuse.NewFileSystemHost. This is a crucial step that enables the file system to interact with the underlying operating system.
There are several pre-built file systems available that you can use as a starting point. Here are a few examples:
- Hellofs is an extremely simple file system that runs on all operating systems.
- Memfs is an in-memory file system that also runs on all operating systems.
- Passthrough is a file system that passes all operations to the underlying file system, but it's not compatible with Windows.
- Notifyfs is a file system that can issue file change notifications, but it's only compatible with Windows.
Attr IsSymlink
The Attr IsSymlink function is a useful tool in file operations. It's used to determine whether a file is a symbolic link or not.
IsSymlink reports whether the FileInfo describes a symbolic link. This means it checks the file's metadata to see if it's a link to another file or directory.
To use IsSymlink, you'll need to call the function on an Attr object. This function returns a boolean value, indicating whether the file is a symbolic link or not.
You can use IsSymlink to make decisions in your code based on whether a file is a symbolic link or not. For example, you might want to handle symbolic links differently than regular files.
Set Attr Timeout
Setting an attribute timeout is crucial for file operations, especially when dealing with large files or slow networks.
The default attribute timeout is 1 second, but this can be adjusted to suit your needs.
A longer timeout can help prevent errors when working with files that take a while to access.
However, be cautious not to set the timeout too high, as this can lead to unnecessary delays and resource waste.
For example, setting the timeout to 30 seconds can help prevent errors when working with large files, but setting it to 1 hour may be excessive.
Directory Entry Lookup

Directory entry lookup is a crucial operation in file systems. It allows us to find entries by name, which is essential for a filesystem to contain anything useful.
To implement directory entry lookup, we iterate over the zip entries, matching paths. This is achieved through the `Lookup` function, which takes a `LookupRequest` and a `LookupResponse` as input, and returns a `Node` and an error.
The `Lookup` function first constructs the full path by concatenating the directory path with the requested file name. If the current directory has a file associated with it, the path is also updated to include the file name.
We then iterate over the zip entries and check if the current entry matches the requested path. If it does, we return a new `File` or `Dir` node, depending on whether the entry is a file or a directory. If the entry is a directory, we also update its file pointer to point to the current file.
If the requested path does not match any entry, we return an error with the code `ENOENT`.
Here's a summary of the `Lookup` function's behavior:
(*Connection) ReadOp
The (*Connection) ReadOp function is a crucial part of the file operations process. It consumes the next op from the kernel process and returns the op and a context that should be used for work related to the op.
If there's an error, you'll need to call c.Reply with the returned context later. This is a responsibility you'll want to keep in mind when using this function.
The ReadOp function delivers ops in exactly the order they are received from /dev/fuse. This is a key point to remember when working with file operations.
You should not call ReadOp multiple times concurrently. This is an important restriction to follow to ensure the function works as intended.
Files
Files are a crucial part of any filesystem, and our Lookup function returns File types when the matched entry does not end in a slash.
To define a type File, we use the same zipAttr helper as for directories, which is a struct that holds the file's attributes. The file attribute is a pointer to a zip.File, and the var_fs.Node is a pointer to a File.
Files are not very useful unless you can open them, so we need to implement the HandleWriter interface to allow writing to the file. This involves implementing the WriteRequest function, which updates the file attribute with the data size.
To read the contents of the file, we implement the HandleReadAller interface, which returns the file's contents. We can also implement the more generic HandleReader interface, which allows us to handle ReadRequest with size, offset, etc.
Here's a list of the interfaces we've implemented for file operations:
- HandleWriter: allows writing to the file
- HandleReadAller: reads the contents of the file
- HandleReader: handles ReadRequest with size, offset, etc.
With these interfaces implemented, our filesystem can support almost all common operations, including writing to and reading from files.
Server and Mount
Creating a FUSE server and mounting it to a directory is a straightforward process. You can use the `NewServer` function to create a FUSE server and attach it to a specific `mountPoint` directory.
To initiate the FUSE loop, you'll need to call the `Serve` method on your server instance. This method runs the FUSE loop and should be run in a goroutine, especially in testing scenarios.
The `MountedFileSystem` type represents the status of a mount operation, and it has a method to wait for unmounting. This is useful for managing the lifecycle of your file system.
Mounting a file system on a given directory using a supplied server is done with the `Mount` function. It blocks until the file system is successfully mounted.
Notifications
Notifications play a crucial role in a FUSE filesystem, and there are two main types: DeleteNotify and EntryNotify.
DeleteNotify notifies the kernel that an entry is removed from a directory.
You should not hold any FUSE filesystem locks while using DeleteNotify, as this can lead to deadlock.
EntryNotify should be used if the existence status of an entry within a directory changes.
Holding FUSE filesystem locks while using EntryNotify can also cause deadlock, so it's essential to avoid this.
In many cases, DeleteNotify is equivalent to EntryNotify, except when the directory is in use by a process.
On a similar theme: How to Update a Github Using Golang
Configuration and Settings

Fuse Go, a framework for building event-driven systems, requires careful configuration to get the most out of it.
The configuration file, `fuse.yaml`, is the central hub for Fuse Go's settings. It allows you to customize various aspects of the framework, such as the event bus and messaging system.
To start, you'll need to create a `fuse.yaml` file in the root directory of your project. This file is where you'll define the configuration for your Fuse Go application.
The `fuse.yaml` file is divided into several sections, each representing a different aspect of the framework. The `event-bus` section, for example, allows you to configure the event bus, including the type of event bus to use and the maximum number of threads.
By carefully configuring the `fuse.yaml` file, you can fine-tune your Fuse Go application to meet the specific needs of your project.
EntryTimeout
EntryTimeout is a critical setting in your configuration. It determines the timeout in nanoseconds for a directory entry, which is essentially the time it takes to check if a file exists within a directory.
The EntryTimeout setting is specific to directory entries, and it's measured in nanoseconds, giving you precise control over how long the system waits for a file to exist or not exist within a directory.
KernelSettings
KernelSettings is a crucial part of adapting to the kernel driver's features.
The KernelSettings function returns the Init message from the kernel, which filesystems can use to adapt to the availability of kernel features.
This message should not be altered, as it's essential for filesystems to work correctly.
By understanding how KernelSettings works, you can ensure your filesystems are optimized for the kernel's capabilities.
Connection and Files
A connection to the fuse kernel process is represented by the type Connection. It's used to receive and reply to requests from the kernel.
To interact with the fuse kernel process, you need a connection. This connection is established through the fuse kernel process.
The fusectl filesystem provides a way to clean up situations where a caller is hung due to a buggy or crashed FUSE filesystem. By writing to /sys/fs/fuse/connection/$ID/abort, reads from the FUSE device fail, and all callers receive ENOTCONN on their pending syscalls.
You can find the connection ID in the Dev field of the Stat_t result for a file in the mount. This ID is used to identify the connection and perform operations on it.
To read the contents of a file, you can implement the HandleReadAller interface. This interface is used to handle ReadRequest with size, offset, etc.
Here's a summary of the interfaces you can use to interact with files:
The Attr and Setattr implementations remain the same for files as they do for directories. This allows you to interact with files in a similar way to directories.
Status and Conversion
The ToStatus function is a useful tool in fuse golang, allowing you to extract an errno number from Go error objects. It's a simple yet effective way to handle errors and ensure your code runs smoothly.
If ToStatus fails to extract the errno number, it logs an error and returns ENOSYS, indicating that the function is not supported. This ensures that your code doesn't crash unexpectedly and provides valuable feedback for debugging purposes.
ToStatus

ToStatus is a function that extracts an errno number from Go error objects. It's a crucial tool for handling errors in a Go program.
If ToStatus fails, it logs an error and returns ENOSYS, indicating that the system call is not implemented. This is a clear signal that something has gone wrong.
ConvertExpirationTime
In the world of status and conversion, having the right tools to manage your data is crucial.
The ConvertExpirationTime function is a vital tool for doing just that. It's used to convert an absolute cache expiration time to a relative time from now.
This is particularly useful for consumption by the fuse kernel module.
Convert File Mode
Converting file modes can be a bit tricky, but fortunately, Go provides two helpful functions to make the process smoother.
The ConvertFileMode function returns an os.FileMode with the Go mode and permission bits set according to the Linux mode and permission bits.
In practice, this means you can easily convert Linux file modes to Go's os.FileMode type.
The ConvertGoMode function, on the other hand, does the opposite - it returns an integer with the Linux mode and permission bits set according to the Go mode and permission bits.
This function is useful when you need to work with Linux file modes in your Go code.
You might enjoy: Golang Bits
Featured Images: pexels.com

