Writing GnomeVFS Modules

Mikael Hallendal, Imendio HB
Richard Hult, Imendio HB

This article was first published on IBM developerWorks' Linux Zone.

Introduction

This article explains how to extend GNOME by writing your own GnomeVFS modules. GnomeVFS was described in a previous article by the authors, so you should make sure to read it if you want to refresh your GnomeVFS knowledge.

Throughout the tutorial we will use an example consisting of a hard coded directory tree with text files. You will be able to browse the tree in Nautilus as well as drag text files to and from the location "tutorial:///".

What is a GnomeVFS module

A GnomeVFS module is a plugin that provides support for accessing different services as though they were part of your filesystem. For example, the HTTP modules lets you access web pages through "http:" URIs. There are a number of modules included with GnomeVFS, such as http, ftp and tar.

A GnomeVFS module can also do something less obvious, like fontilus does. Fontilus lets you browse the fonts available on your system in Nautilus at the location "fonts:///", and add and remove fonts by drag and drop. Another example of a GnomeVFS module is the handler for "applications:///" which lets you browse your application menu as a file system.


Nautilus showing the fonts using the fonts-method
Nautilus showing the fonts using the fonts-method

Writing the module

A GnomeVFS module is a shared object file (.so library) which is loaded at runtime. We will now go through the necessary steps to implement a simple module that can act as a starting point when you want to write a real module.

Getting started

When the module is first loaded the function vfs_module_init is called:

GnomeVFSMethod *
vfs_module_init (const char *method_name, const char *args)
{
       if (strcmp (method_name, "tutorial") == 0) {
               init_fake_tree ();
 
               return &method;
       }
 
       return NULL;
}

This is a good place to set up the module and initialize any variables or data needed in the module, we do that by calling init_fake_tree. This function should also return a pointer to a GnomeVFSMethod struct, specifying which functions to use for opening or closing a file, listing a directory, etc. Such a table of functions is often referred to as a "vtable", virtual table.

The vtable consists of quite a few functions, but depending on the complexity of the module, only some of them needs to be implemented. Since our example module will be a simple one, we don't have to implement that many.

static GnomeVFSMethod method = {
        sizeof (GnomeVFSMethod),
 
        do_open,                /* open */
        do_create,              /* create */
        do_close,               /* close */
        do_read,                /* read */
        do_write,               /* write */
        NULL,                   /* seek */
        NULL,                   /* tell */
        NULL,                   /* truncate_handle */
        do_open_directory,      /* open_directory */
        do_close_directory,     /* close_directory */
        do_read_directory,      /* read_directory */
        do_get_file_info,       /* get_file_info */
        NULL,                   /* get_file_info_from_handle */
        do_is_local,            /* is_local */
        do_make_directory,      /* make_directory */
        do_remove_directory,    /* remove_directory */
        NULL,                   /* move */
        do_unlink,              /* unlink */
        NULL,                   /* check_same_fs */
        NULL,                   /* set_file_info */
        NULL,                   /* truncate */
        NULL,                   /* find_directory */
        NULL,                   /* create_symbolic_link */
        NULL,                   /* monitor_add */
        NULL,                   /* monitor_cancel */
        NULL                    /* file_control */
};

As you can see, many of the functions are set to NULL, meaning that they are left unimplemented.

The ones we actually implement should be pretty much self-explanatory. GnomeVFS is modelled after the standard POSIX API so there shouldn't be too many surprises for those who are already familiar with regular C programming on a UNIX like system.

When the module is unloaded the function vfs_module_shutdown is called to let you clean up any memory you used in the module:

void
vfs_module_shutdown (GnomeVFSMethod* method)
{
       free_fake_tree ();
}

Our shutdown function frees the fake directory tree we built in the initialization function.

Reading directories

In order to do anything at all with a file system, you need to be able to enumerate the files and directories and retrieve their filenames and other information. We'll start our VFS module by implementing directory handling, and then try it out by browsing our filesystem with Nautilus.

The reading of the contents of a directory is split up into three parts, do_open_directory, do_read_directory, and close_directory. These are pretty straight-forward functions, so let's go ahead and implement them!

First, you'll notice that the three functions all have the same return type, GnomeVFSResult. This is true for almost all the VFS functions, and the return value for successful operations should be GNOME_VFS_OK, while there are many different error codes for failures. The whole list can be viewed in the API reference documentation.

In do_open_directory, we set up any data structures we need during the reading:

static GnomeVFSResult
do_open_directory  (GnomeVFSMethod           *method,
		    GnomeVFSMethodHandle    **method_handle,
		    GnomeVFSURI              *uri,
		    GnomeVFSFileInfoOptions   options,
		    GnomeVFSContext          *context)
{
       DirHandle   *handle;
       FakeNode    *file;
 
       handle = g_new0 (DirHandle, 1);
       handle->options = options;
 
       file = get_fake_node_from_uri (uri);
       if (file) {
               handle->gnode = file->gnode;
               handle->current_child = handle->gnode->children;
       } else {
               return GNOME_VFS_ERROR_NOT_FOUND;
       }
 
       *method_handle  = (GnomeVFSMethodHandle *) handle;
 
       return GNOME_VFS_OK;
}

The GnomeVFSMethodHandle argument is just a void pointer that you can use to pass any pointer that you see fit. In our example, we'll define a small struct that keeps the state while iterating through the contents of the directory.

The function do_read_directory will be called repeatedly until we return GNOME_VFS_ERROR_EOF to indicate that there are no more items:

static GnomeVFSResult
do_read_directory (GnomeVFSMethod       *method,
		   GnomeVFSMethodHandle *method_handle,
		   GnomeVFSFileInfo     *file_info,
		   GnomeVFSContext      *context)
{
       DirHandle *handle = (DirHandle *) method_handle;
       FakeNode  *file;
 
       if (!handle->current_child) {
               return GNOME_VFS_ERROR_EOF;
       }
 
       file = handle->current_child->data;
 
       if (file->directory) {
               file_info->type = GNOME_VFS_FILE_TYPE_DIRECTORY;
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_TYPE;
               file_info->mime_type = g_strdup ("x-directory/normal");
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;
       } else {
               file_info->type = GNOME_VFS_FILE_TYPE_REGULAR;
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_TYPE;
               file_info->mime_type = g_strdup ("text/plain");
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;
               file_info->size = file->size;
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_SIZE;
       }
 
       file_info->name = g_strdup (file->name);
 
       handle->current_child = handle->current_child->next;
 
       return GNOME_VFS_OK;
}

The struct with our state mentioned above will be passed to this function just like for do_open_directory.

For each item that we find in the directory, we fill in information about it in the GnomeVFSFileInfo struct that was passed to us. There is a lot of information that we can set, but fortunately we can just set the most important things like filename, file type, MIME type and size, and leave it at that. For a real VFS module, you might want to set things like modification time and permissions etc, as well.

Finally we have to close the directory:

static GnomeVFSResult
do_close_directory (GnomeVFSMethod       *method,
		    GnomeVFSMethodHandle *method_handle,
		    GnomeVFSContext      *context)
{
       DirHandle *handle = (DirHandle *) method_handle;
 
       g_free (handle);
 
       return GNOME_VFS_OK;
}

The do_close_directory function is very simple, it just gives us a chance to free up any resources used during the opening and reading.

Ok, so far so good. Now we need to implement file opening and reading, and when that is done we'll be able to test our module a bit.

Reading files

Just like for the directory handling, we need to implement open, read and close:

First we need to open the file. This is done in the do_open function:

static GnomeVFSResult
do_open (GnomeVFSMethod        *method,
	 GnomeVFSMethodHandle **method_handle,
	 GnomeVFSURI           *uri,
	 GnomeVFSOpenMode       mode,
	 GnomeVFSContext       *context)
{
       FakeNode   *file;
       FileHandle *handle;
 
       file = get_fake_node_from_uri (uri);
       if (file && file->directory) {
               return GNOME_VFS_ERROR_IS_DIRECTORY;
       }
 
       /* We don't support random mode. */
       if (mode & GNOME_VFS_OPEN_RANDOM) {
               return GNOME_VFS_ERROR_INVALID_OPEN_MODE;
       }
 
       if (mode & GNOME_VFS_OPEN_WRITE) {
		/* Only handle reading so far */
		/* Add the code to write here later in the tutorial */
		return GNOME_VFS_ERROR_READ_ONLY;
       } else if (mode & GNOME_VFS_OPEN_READ) {
               file = get_fake_node_from_uri (uri);
               if (!file) {
                       return GNOME_VFS_ERROR_NOT_FOUND;
               }
       } else {
               return GNOME_VFS_ERROR_INVALID_OPEN_MODE;
       }
 
       handle = g_new0 (FileHandle, 1);
       handle->fnode = file;
 
       *method_handle = (GnomeVFSMethodHandle *) handle;
 
       return GNOME_VFS_OK;
}

The do_open function works a lot like the directory handling case, but as you can see, it also has a GnomeVFSOpenMode flag, that is used for specifying the mode. It should be one of GNOME_VFS_OPEN_READ and GNOME_VFS_OPEN_WRITE, or both. There is also a GNOME_VFS_OPEN_RANDOM value that is outside the scope of this article.

We proceed to create a handle for storing the state of the read operation, again like for the directory handling described above.

If the URI points to a directory the type flag is set to GNOME_VFS_FILE_TYPE_DIRECTORY to indicate to the caller that they are trying to read a directory.

After the file is opened the do_read function will handle reading the actual data in the file:

static GnomeVFSResult
do_read (GnomeVFSMethod       *method,
	 GnomeVFSMethodHandle *method_handle,
	 gpointer              buffer,
	 GnomeVFSFileSize      bytes,
	 GnomeVFSFileSize     *bytes_read,
	 GnomeVFSContext      *context)
{
       FileHandle *handle = (FileHandle *) method_handle;
 
       if (!handle->str) {
               /* This is the first pass, get the content string. */
               handle->str = g_strdup (handle->fnode->content);
		handle->size = handle->fnode->size;
               handle->bytes_written = 0;
       }
 
       if (handle->bytes_written >= handle->len) {
	        /* The whole file is read, return EOF. */
	        *bytes_read = 0;
               return GNOME_VFS_ERROR_EOF;
	}
 
       *bytes_read = MIN (bytes, handle->size - handle->bytes_written);
 
       memcpy (buffer, handle->str + handle->bytes_written, *bytes_read);
 
       handle->bytes_written += *bytes_read;
 
       return GNOME_VFS_OK;
}

In the do_read function, we have to be a bit careful and make sure to handle the cases where we need to read more than the buffer can hold, in which case the function will get called again, repeatedly until the whole file is read. When the operation is completed, we return GNOME_VFS_ERROR_EOF to indicate that the end of the file was reached.

Finally the file has to be closed:

static GnomeVFSResult
do_close (GnomeVFSMethod       *method,
	  GnomeVFSMethodHandle *method_handle,
	  GnomeVFSContext      *context)
{
       FileHandle *handle = (FileHandle *) method_handle;
 
       g_free (handle->str);
       g_free (handle);
 
       return GNOME_VFS_OK;
}

The do_close function is just for cleaning up, so we free the handle that was created in do_open here.

There are still two more functions that we must write before we can start trying the module out, do_is_local and do_get_file_info:

static gboolean
do_is_local (GnomeVFSMethod    *method,
 	     const GnomeVFSURI *uri)
{
	return TRUE;
}

do_is_local is used to check if a URI is a local file system. Its intended use is for deciding whether or not to do things that could be potentially slow on remote file systems, for example generating thumbnails of pictures. If the file should be considered as being local you should return TRUE, otherwise FALSE.

Since we have a fake in-memory file system, we're advertising as a local system:

static GnomeVFSResult
do_get_file_info (GnomeVFSMethod          *method,
		  GnomeVFSURI             *uri,
		  GnomeVFSFileInfo        *file_info,
		  GnomeVFSFileInfoOptions  options,
		  GnomeVFSContext         *context)
{
       FakeNode *file;
 
       file = get_fake_node_from_uri (uri);
       if (!file) {
               return GNOME_VFS_ERROR_NOT_FOUND;
       }
 
       if (file->gnode == root) {
               /* Root directory. */
               file_info->name = g_strdup ("Tutorial");
       } else {
               file_info->name = g_strdup (file->name);
       }
 
       if (file->directory) {
	        file_info->type = GNOME_VFS_FILE_TYPE_DIRECTORY;
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_TYPE;
               file_info->mime_type = g_strdup ("x-directory/normal");
		file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;        
	} else {
               file_info->type = GNOME_VFS_FILE_TYPE_REGULAR;
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_TYPE;
               file_info->mime_type = g_strdup ("text/plain");
		file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_MIME_TYPE;
		file_info->size = file->size;
               file_info->valid_fields |= 
                   GNOME_VFS_FILE_INFO_FIELDS_SIZE;
       }
 
       return GNOME_VFS_OK;
}

The last function, do_get_file_info, is used to get the properties of a file, such as file size, permissions, creation time, MIME type, etc. Just like when reading a directory, we can fill in as much information as we want to, or can. For our example, we only set filename, MIME type and file size. Note the valid_fields variable, that is set accordingly to let the caller know which fields were filled in.

The GnomeVFSInfo struct has a member called valid_fields, this is a bit field which is used to define which other member variables are set in the info struct. You can see example on how to use it in the example.

Trying things out

At this point, we can actually test the module a bit! First we need to build and install the module. The easiest way to do this is to install it in the same prefix as the rest of your GNOME installation is ($prefix/lib/gnome-vfs-2.0/modules).

To establish the mapping between a function, 'tutorial:' and our module, we need to install (in $prefix/etc/gnome-vfs-2.0/modules) a simple configuration file called tutorial-method.conf, with the following line in:

tutorial: tutorial-method

The provided Makefile should install the files for you, if you do make install-1. to install the first example.

A very good tool for debugging and testing VFS modules can be found in the GnomeVFS sources.

If you have the GnomeVFS sources (see Resources list), check out the small program gnome-vfs/test/test-shell, which is a commandline shell that lets you browse the directories and files in all VFS modules. Let's try with an example:

./test-shell
 
gnome-vfs/test> cd tutorial:
 
tutorial:/ > ls
 
[documents]                             , type 'x-directory/normal'
test.txt                                , type 'text/plain'
 
tutorial:/ > cd documents
tutorial:/documents/ > ls
 
todo.txt                                , type 'text/plain'
 
tutorial:/documents/ > cat todo.txt
 
Buy milk
Wash dishes
 
tutorial:/documents/ > quit

For a more dashing demo, restart Nautilus (either by terminating the nautilus process, or by typing nautilus -q in a terminal) and then enter in tutorial: in the location bar. You should now be able to see the directories and files. If you open a file Nautilus will treat it as a regular text file, since that's the MIME type we set in our code.

If you only plan to support reading in your module, you should be able to go ahead and implementing it at this point. We will now show you how to make your module create and remove files and directories and how to write to files.

Writing and deleting files

First, we need to add write support for do_open(). This is really simple, we just need to check if the file exists, and if not, we create it. If you look above in the do_open function we will change the function to add support for writing:

if (mode & GNOME_VFS_OPEN_WRITE) {
               file = get_fake_node_from_uri (uri);
               if (file) {
		        g_free (file->content);
			file->content = NULL;
			file->size = 0;
               } else {
                       file = add_fake_node (uri, FALSE);
               }
       } else if (mode & GNOME_VFS_OPEN_READ) {

We also need to be able to create files, which means that we must implement do_create:

static GnomeVFSResult
do_create (GnomeVFSMethod        *method,
	   GnomeVFSMethodHandle **method_handle,
	   GnomeVFSURI           *uri,
	   GnomeVFSOpenMode       mode,
	   gboolean               exclusive,
	   guint                  perm,
	   GnomeVFSContext       *context)
{
	/* We cheat here and don't take perm or exclusive in consideration. */
	return do_open (method, method_handle, uri, mode, context);
}

This one differs from do_open in that the permissions to use for the file can be specified, and that there is an 'exclusive' parameter that, when set to TRUE, make the function fail if the file already exists. As you can see in the code, we don't use neither the permissions nor the exclusive parameter, to make things a bit easier for the example.

The next step is to implement do_write and do_unlink:

static GnomeVFSResult
do_write (GnomeVFSMethod       *method,
          GnomeVFSMethodHandle *method_handle,
          gconstpointer         buffer,
          GnomeVFSFileSize      bytes,
          GnomeVFSFileSize     *bytes_written,
          GnomeVFSContext      *context)
{
       FileHandle *handle = (FileHandle *) method_handle;
       FakeNode   *file;
 
       file = handle->fnode;
 
       file->content = g_memdup (buffer, bytes);
       file->size = bytes;
 
       *bytes_written = bytes;
 
       return GNOME_VFS_OK;
}

Writing is easy to implement for our simple file system, it's just a matter of taking the "fake file" from the handle, and copy the contents of buffer into it, and then we're done.

You can see that it works by copying a file in test-shell and then opening it. Note that you won't be able to view the contents of newly created files in Nautilus since our fake file system is per process, and the text viewer in Nautilus, and other viewers like Gedit, are run in separate processes. For a real module, this won't be the case, of course. Don't forget to install the new version of our example, with the command make install-2.

To be able to remove files you need to implement the function do_unlink:

static GnomeVFSResult
do_unlink (GnomeVFSMethod  *method,
	   GnomeVFSURI     *uri,
	   GnomeVFSContext *context)
{
       FakeNode *file;
 
       file = get_fake_node_from_uri (uri);
 
       if (!file) {
               return GNOME_VFS_ERROR_INVALID_URI;
       }
 
       /* Can't remove the root. */
       if (file->gnode == root) {
               return GNOME_VFS_ERROR_NOT_PERMITTED;
       }
 
       /* Can't remove directories. */
       if (file->directory) {
               return GNOME_VFS_ERROR_NOT_PERMITTED;
       }
 
       return remove_fake_node_by_uri (uri);
}

Deleting, or unlinking, files is also quite straight-forward. We perform a few checks to make sure that the URI actually points to a file, and then we remove it from the tree.

Creating and removing directories

The final pair of functions to implement is do_make_directory and do_remove_directory:

static GnomeVFSResult
do_make_directory (GnomeVFSMethod  *method,
		   GnomeVFSURI     *uri,
		   guint            perm,
		   GnomeVFSContext *context)
{
       GnomeVFSResult  result;
       FakeNode       *file;
 
       file = add_fake_node (uri, TRUE);
 
       if (file) {
               result = GNOME_VFS_OK;
       } else {
               result = GNOME_VFS_ERROR_NOT_PERMITTED;
       }
 
       return result;
}

Here we try to add the uri and if it succeeds we return GNOME_VFS_OK. If it doesn't succeed we return GNOME_VFS_ERROR_NOT_PERMITTED to signal that the couldn't create the directory pointed to by uri.

Since we don't have any access right checking for file system, we just ignore the perm argument.

static GnomeVFSResult
do_remove_directory (GnomeVFSMethod  *method,
		     GnomeVFSURI     *uri,
		     GnomeVFSContext *context)
{
       FakeNode *file;
 
       file = get_fake_node_from_uri (uri);
 
       if (!file) {
               return GNOME_VFS_ERROR_INVALID_URI;
       }
 
       /* Can't remove the root. */
       if (file->gnode == root) {
               return GNOME_VFS_ERROR_NOT_PERMITTED;
       }
 
       /* Can't remove non-empty directories. */
       if (g_node_n_children (file->gnode) > 0) {
               return GNOME_VFS_ERROR_NOT_PERMITTED;
       }
 
       return remove_fake_node_by_uri (uri);
}

First we check if uri exists. If it doesn't we return GNOME_VFS_ERROR_INVALID_URI, otherwise we check if the URI points to the root node which the user isn't permitted to remove. We are also restricting the removal of non-empty directories.

Things to consider

Thread safety

If you play around with the 'tutorial:' location in Nautilus, moving, creating and deleting files, you'll probably notice that it's fairly easy to make Nautilus crash. This is because we have not payed any attention to thread safety, that is, we are not protecting our data from concurrent reads and writes.

If you have any kind of data that can be accessed more than once at the same time, such as our fake file tree, then you must make sure that every access is protected by thread mutexes or locks. This can be done with GLib's thread related functionality, for example:

G_LOCK_DEFINE_STATIC (root);
static GNode *root = NULL;
 
/* And then protect accesses to the tree like this: */
 
G_LOCK (root);
...
G_UNLOCK (root);

Cancellable operations
A common case when writing GnomeVFS modules is that the files are actual files and not fake ones like in our example. Operations can take a long time and there must be a way for the user to cancel those operations. In order to avoid that, there are a few functions available to make your functions cancellable without a lot of extra work. Those functions are defined in libgnomevfs/gnome-vfs-cancellable-ops.h and should be fairly self-explanatory. You can also read more about them in the API documentation for GnomeVFS.

In our example, we haven't made the functions cancellable since the files are just read from memory, which is very fast.

Wrapping it up

Writing a basic GnomeVFS module is not that hard. The difficult parts is to get all the semantics right, so that it behaves like a real file system. If you are writing a module that will be exposed to the user as if it were a real file system, we encourage you to make an effort to get the details right, including thread safety. Also, think twice before starting to write a module; is it the right approach to the problem you are trying to solve?

In the final example, we have added locking for accessing the file system tree. You can try it out with make install-final. The result is a stable module, that doesn't crash Nautilus.

Conclusion

We have now created a simple GnomeVFS module by using an in memory directory tree with a couple of files. We have also tested our modules with the nice little tool test-shell and in Nautilus. You should now be able to implement your own module but keep in mind to make sure that it really is suited for being a GnomeVFS module.