The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

DBI::Filesystem - Store a filesystem in a relational database

SYNOPSIS

 use DBI::Filesystem;

 # Preliminaries. Create the mount point:
 mkdir '/tmp/mount';

 # Create the databas:
 system "mysqladmin -uroot create test_filesystem"; 
 system "mysql -uroot -e 'grant all privileges on test_filesystem.* to $ENV{USER}@localhost' mysql";

 # (Usually you would do this in the shell.)
 # (You will probably need to add the admin user's password)

 # Create the filesystem object
 $fs = DBI::Filesystem->new('dbi:mysql:test_filesystem',{initialize=>1});

 # Mount it on the mount point.
 # This call will block until the filesystem is mounted by another
 # process by calling "fusermount -u /tmp/mount"
 $fs->mount('/tmp/mount');

 # Alternatively, manipulate the filesystem directly from within Perl.
 # Any of these methods could raise a fatal error, so always wrap in
 # an eval to catch those errors.
 eval {
   # directory creation
   $fs->create_directory('/dir1');
   $fs->create_directory('/dir1/subdir_1a');

   # file creation
   $fs->create_file('/dir1/subdir_1a/test.txt');

   # file I/O
   $fs->write('/dir1/subdir_1a/test.txt','This is my favorite file',0);
   my $data = $fs->read('/dir1/subdir_1a/test.txt',100,0);

   # reading contents of a directory
   my @entries = $fs->getdir('/dir1');

   # fstat file/directory
   my @stat = $fs->stat('/dir1/subdir_1a/test.txt');

   #chmod/chown file
   $fs->chmod('/dir1/subdir_1a/test.txt',0600);
   $fs->chown('/dir1/subdir_1a/test.txt',1001,1001); #uid,gid

   # rename file/directory
   $fs->rename('/dir1'=>'/dir2');

   # create a symbolic link
   $fs->symlink('/dir2' => '/dir1');

   # create a hard link
   $fs->link('/dir2/subdir_1a/test.txt' => '/dir2/hardlink.txt');

   # read symbolic link
   my $target = $fs->read_symlink('/dir1/symlink.txt');

   # unlink a file
   $fs->unlink_file('/dir2/subdir_1a/test.txt');

   # remove a directory
   $fs->remove_directory('/dir2/subdir_1a');

   # get the inode (integer) that corresponds to a file/directory
   my $inode = $fs->path2inode('/dir2');

   # get the path(s) that correspond to an inode
   my @paths = $fs->inode2paths($inode);
 };
 if ($@) { warn "file operation failed with $@"; }
 

DESCRIPTION

This module can be used to create a fully-functioning "Fuse" userspace filesystem on top of a relational database. Unlike other filesystem-to-DBM mappings, such as Fuse::DBI, this one creates and manages a specific schema designed to support filesystem operations. If you wish to mount a filesystem on an arbitrary DBM schema, you probably want Fuse::DBI, not this.

Most filesystem functionality is implemented, including hard and soft links, sparse files, ownership and access modes, UNIX permission checking and random access to binary files. Very large files (up to multiple gigabytes) are supported without performance degradation.

Why would you use this? The main reason is that it allows you to use DBMs functionality such as accessibility over the network, database replication, failover, etc. In addition, the underlying DBI::Filesystem module can be extended via subclassing to allow additional functionality such as arbitrary access control rules, searchable file and directory metadata, full-text indexing of file contents, etc.

Before mounting the DBMS, you must have created the database and assigned yourself sufficient privileges to read and write to it. You must also create an empty directory to serve as the mount point.

A convenient front-end to this library is provided by sqlfs.pl, which is installed along with this library.

Unsupported Features

The following features are not implemented:

 * statfs -- df on the filesystem will not provide any useful information
            on free space or other filesystem information.

 * extended attributes -- Extended attributes are not supported.

 * nanosecond times -- atime, mtime and ctime are accurate only to the
            second.

 * ioctl -- none are supported

 * poll  -- polling on the filesystem to detect file update events will not work.

 * lock  -- file handle locking among processes running on the local machine 
            works, but protocol-level locking, which would allow cooperative 
            locks on different machines talking to the same database server, 
            is not implemented.

You must be the superuser in order to create a file system with the suid and dev features enabled, and must invoke this commmand with the mount options "allow_other", "suid" and/or "dev":

   -o dev,suid,allow_other

Supported Database Management Systems

DBMSs differ in what subsets of the SQL language they support, supported datatypes, date/time handling, and support for large binary objects. DBI::Filesystem currently supports MySQL, PostgreSQL and SQLite. Other DBMSs can be supported by creating a subclass file named, e.g. DBI::Filesystem:Oracle, where the last part of the class name corresponds to the DBD driver name ("Oracle" in this example). See DBI::Filesystem::SQLite, DBI::Filesystem::mysql and DBI::Filesystem:Pg for an illustration of the methods that need to be defined/overridden.

Fuse Installation Notes

For best performance, you will need to run this filesystem using a version of Perl that supports IThreads. Otherwise it will fall back to non-threaded mode, which will introduce occasional delays during directory listings and have notably slower performance when reading from more than one file simultaneously.

If you are running Perl 5.14 or higher, you *MUST* use at least 0.15 of the Perl Fuse module. At the time this was written, the version of Fuse 0.15 on CPAN was failing its regression tests on many platforms. I have found that the easiest way to get a fully operational Fuse module is to clone and compile a patched version of the source, following this recipe:

 $ git clone git://github.com/dpavlin/perl-fuse.git
 $ cd perl-fuse
 $ perl Makefile.PL
 $ make test   (optional)
 $ sudo make install

HIGH LEVEL METHODS

The following methods are most likely to be needed by users of this module.

$fs = DBI::Filesystem->new($dsn,{options...})

Create the new DBI::Filesystem object. The mandatory first argument is a DBI data source, in the format "dbi:<driver>:<other_arguments>". The other arguments may include the database name, host, port, and security credentials. See the documentation for your DBMS for details.

Non-mandatory options are contained in a hash reference with one or more of the following keys:

 initialize          If true, then initialize the database schema. Many
                     DBMSs require you to create the database first.

 ignore_permissions  If true, then Unix permission checking is not
                     performed when creating/reading/writing files.

 allow_magic_dirs    If true, allow SQL statements in "magic" directories
                     to be executed (see below).

WARNING: Initializing the schema quietly destroys anything that might have been there before!

$boolean = $fs->ignore_permissions([$boolean]);

Get/set the ignore_permissions flag. If ignore_permissions is true, then all permission checks on file and directory access modes are disabled, allowing you to create files owned by root, etc.

$boolean = $fs->allow_magic_dirs([$boolean]);

Get/set the allow_magic_dirs flag. If true, then directories whose names begin with "%%" will be searched for a dotfile named ".query" that contains a SQL statement to be run every time a directory listing is required from this directory. See getdir() below.

$fs->mount($mountpoint, [\%fuseopts])

This method will mount the filesystem on the indicated mountpoint using Fuse and block until the filesystem is unmounted using the "fusermount -u" command or equivalent. The mountpoint must be an empty directory unless the "nonempty" mount option is passed.

You may pass in a hashref of options to pass to the Fuse module. Recognized options and their defaults are:

 debug        Turn on verbose debugging of Fuse operations [false]
 threaded     Turn on threaded operations [true]
 nullpath_ok  Allow filehandles on open files to be used even after file
               is unlinked [true]
 mountopts    Comma-separated list of mount options

Mount options to be passed to Fuse are described at http://manpages.ubuntu.com/manpages/precise/man8/mount.fuse.8.html. In addition, you may pass the usual mount options such as "ro", etc. They are presented as a comma-separated list as shown here:

 $fs->mount('/tmp/foo',{debug=>1,mountopts=>'ro,nonempty'})

Common mount options include:

Fuse specific nonempty Allow mounting over non-empty directories if true [false] allow_other Allow other users to access the mounted filesystem [false] fsname Set the filesystem source name shown in df and /etc/mtab auto_cache Enable automatic flushing of data cache on open [false] hard_remove Allow true unlinking of open files [true] nohard_remove Activate alternate semantics for unlinking open files (see below)

General ro Read-only filesystem dev Allow device-special files nodev Do not allow device-special files suid Allow suid files nosuid Do not allow suid files exec Allow executable files noexec Do not allow executable files atime Update file/directory access times noatime Do not update file/directory access times

Some options require special privileges. In particular allow_other must be enabled in /etc/fuse.conf, and the dev and suid options can only be used by the root user.

The "hard_remove" mount option is passed by default. This option allows files to be unlinked in one process while another process holds an open filehandle on them. The contents of the file will not actually be deleted until the last open filehandle is closed. The downside of this is that certain functions will fail when called on filehandles connected to unlinked files, including fstat(), ftruncate(), chmod(), and chown(). If this is an issue, then pass option "nohard_remove". This will activate Fuse's alternative semantic in which unlinked open files are renamed to a hidden file with a name like ".fuse_hiddenXXXXXXX'. The hidden file is removed when the last filehandle is closed.

$boolean = $fs->mounted([$boolean])

This method returns true if the filesystem is currently mounted. Subclasses can change this value by passing the new value as the argument.

Fuse hook functions

This module defines a series of short hook functions that form the glue between Fuse's function-oriented callback hooks and this module's object-oriented methods. A typical hook function looks like this:

 sub e_getdir {
    my $path = fixup(shift);
    my @entries = eval {$Self->getdir($path)};
    return $Self->errno($@) if $@;
    return (@entries,0);
 }

The preferred naming convention is that the Fuse callback is named "getdir", the function hook is named e_getdir(), and the method is $fs->getdir(). The DBI::Filesystem object is stored in a singleton global named $Self. The hook fixes up the path it receives from Fuse, and then calls the getdir() method in an eval{} block. If the getdir() method raises an error such as "file not found", the error message is passed to the errno() method to turn into a ERRNO code, and this is returned to the caller. Otherwise, the hook returns the results in the format proscribed by Fuse.

If you are subclassing DBI::Filesystem, there is no need to define new hook functions. All hooks described by Fuse are already defined or generated dynamically as needed. Simply create a correctly-named method in your subclass.

These are the hooks that are defined:

 e_getdir       e_open           e_access      e_unlink     e_removexattr
 e_getattr      e_release        e_rename      e_rmdir
 e_fgetattr     e_flush          e_chmod       e_utime
 e_mkdir        e_read           e_chown       e_getxattr
 e_mknod        e_write          e_symlink     e_setxattr
 e_create       e_truncate       e_readlink    e_listxattr

These hooks will be created as needed if a subclass implements the corresponding methods:

 e_statfs       e_lock            e_init 
 e_fsync        e_opendir         e_destroy 
 e_readdir      e_utimens
 e_releasedir   e_bmap 
 e_fsyncdir     e_ioctl 
 e_poll

$inode = $fs->mknod($path,$mode,$rdev)

This method creates a file or special file (pipe, device file, etc). The arguments are the path of the file to create, the mode of the file, and the device number if creating a special device file, or 0 if not. The return value is the inode of the newly-created file, an unique integer ID, which is actually the primary key of the metadata table in the underlying database.

The path in this, and all subsequent methods, is relative to the mountpoint. For example, if the filesystem is mounted on /tmp/foobar, and the file you wish to create is named /tmp/foobar/dir1/test.txt, then pass "dir1/test.txt". You can also include a leading slash (as in "/dir1/test.txt") which will simply be stripped off.

The mode is a bitwise combination of file type and access mode as described for the st_mode field in the stat(2) man page. If you provide just the access mode (e.g. 0666), then the method will automatically set the file type bits to indicate that this is a regular file. You must provide the file type in the mode in order to create a special file.

The rdev field contains the major and minor device numbers for device special files, and is only needed when creating a device special file or pipe; ordinarily you can omit it. The rdev field is described in stat(2).

Various exceptions can arise during this call including invalid paths, permission errors and the attempt to create a duplicate file name. These will be presented as fatal errors which can be trapped by an eval {}. See $fs->errno() for a list of potential error messages.

Like other file-manipulation methods, this will die with a "permission denied" message if the current user does not have sufficient privileges to write into the desired directory. To disable permission checking, set ignore_permissions() to a true value:

 $fs->ignore_permissions(1)

Unless explicitly provided, the mode will be set to 0100777 (all permissions set).

$inode = $fs->mkdir($path,$mode)

Create a new directory with the specified path and mode and return the inode of the newly created directory. The path and mode are the same as those described for mknod(), except that the filetype bits for $mode will be set to those for a directory if not provided. Like mknod() this method may raise a fatal error, which should be trapped by an eval{}.

Unless explicitly provided, the mode will be set to 0040777 (all permissions set).

$fs->rename($oldname,$newname)

Rename a file or directory. Raises a fatal exception if unsuccessful.

$fs->unlink($path)

Unlink the file or symlink located at $path. If this is the last reference to the file (via hard links or filehandles) then the contents of the file and its inode will be permanently removed. This will raise a fatal exception on any errors.

$fs->rmdir($path)

Remove the directory at $path. This method will fail under a variety of conditions, raising a fatal exception. Common errors include attempting to remove a file rather than a directory or removing a directory that is not empty.

$fs->link($oldpath,$newpath)

Create a hard link from the file at $oldpath to $newpath. If an error occurs the method will die. Note that this method will allow you to create a hard link to directories as well as files. This is disallowed by the "ln" command, and is generally a bad idea as you can create a filesystem with path loops.

$fs->symlink($oldpath,$newpath)

Create a soft (symbolic) link from the file at $oldpath to $newpath. If an error occurs the method will die. It is safe to create symlinks that involve directories.

$path = $fs->readlink($path)

Read the symlink at $path and return its target. If an error occurs the method will die.

@entries = $fs->getdir($path)

Given a directory in $path, return a list of all entries (files, directories) contained within that directory. The '.' and '..' paths are also always returned. This method checks that the current user has read and execute permissions on the directory, and will raise a permission denied error if not (trap this with an eval{}).

Experimental feature: If the directory begins with the magic characters "%%" then getdir will look for a dotfile named ".query" within the directory. ".query" must contain a SQL query that returns a series of one or more inodes. These will be used to populate the directory automagically. The query can span multiple lines, and lines that begin with "#" will be ignored.

Here is a simple example which will run on all DBMSs. It displays all files with size greater than 2 Mb:

 select inode from metadata where size>2000000

Another example, which uses MySQL-specific date/time math to find all .jpg files created/modified within the last day:

 select m.inode from metadata as m,path as p
     where p.name like '%.jpg'
       and (now()-interval 1 day) <= m.mtime
       and m.inode=p.inode

(The date/time math syntax is very slightly different for PostgreSQL and considerably different for SQLite)

An example that uses extended attributes to search for all documents authored by someone with "Lincoln" in the name:

 select m.inode from metadata as m,xattr as x
    where x.name == 'user.Author'
     and x.value like 'Lincoln%'
     and m.inode=x.inode
    

The files contained within the magic directories can be read and written just like normal files, but cannot be removed or renamed. Directories are excluded from magic directories. If two or more files from different parts of the filesystem have name clashes, the filesystem will append a number to their end to distinguish them.

If the SQL contains an error, then the error message will be contained within a file named "SQL_ERROR".

$boolean = $fs->isdir($path)

Convenience method. Returns true if the path corresponds to a directory. May raise a fatal error if the provided path is invalid.

$fs->chown($path,$uid,$gid)

This method changes the user and group ids for the indicated path. It raises a fatal exception on errors.

$fs->chmod($path,$mode)

This method changes the access mode for the file or directory at the indicated path. The mode in this case is just the three octal word access mode, not the combination of access mode and path type used in mknod().

@stat = $fs->fgetattr($path,$inode)

Return the 13-element file attribute list returned by Perl's stat() function, describing an existing file or directory. You may pass the path, and/or the inode of the file/directory. If both are passed, then the inode takes precedence.

The returned list will contain:

   0 dev      device number of filesystem
   1 ino      inode number
   2 mode     file mode  (type and permissions)
   3 nlink    number of (hard) links to the file
   4 uid      numeric user ID of file's owner
   5 gid      numeric group ID of file's owner
   6 rdev     the device identifier (special files only)
   7 size     total size of file, in bytes
   8 atime    last access time in seconds since the epoch
   9 mtime    last modify time in seconds since the epoch
  10 ctime    inode change time in seconds since the epoch (*)
  11 blksize  preferred block size for file system I/O
  12 blocks   actual number of blocks allocated

@stat = $fs->getattr($path)

Similar to fgetattr() but only the path is accepted.

$inode = $fs->open($path,$flags,$info)

Open the file at $path and return its inode. $flags are a bitwise OR-ing of the access mode constants including O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, and $info is a hash reference containing flags from the Fuse module. The latter is currently ignored.

This method checks read/write permissions on the file and containing directories, unless ignore_permissions is set to true. The open method also increments the file's inuse counter, ensuring that even if it is unlinked, its contents will not be removed until the last open filehandle is closed.

The flag constants can be obtained from POSIX.

$fh->release($inode)

Release a file previously opened with open(), decrementing its inuse count. Be careful to balance calls to open() with release(), or the file will have an inconsistent use count.

$data = $fs->read($path,$length,$offset,$inode)

Read $length bytes of data from the file at $path, starting at position $offset. You may optionally pass an inode to the method to read from a previously-opened file.

On success, the requested data will be returned. Otherwise a fatal exception will be raised (which can be trapped with an eval{}).

Note that you do not need to open the file before reading from it. Permission checking is not performed in this call, but in the (optional) open() call.

$bytes = $fs->write($path,$data,$offset,$inode)

Write the data provided in $data into the file at $path, starting at position $offset. You may optionally pass an inode to the method to read from a previously-opened file.

On success, the number of bytes written will be returned. Otherwise a fatal exception will be raised (which can be trapped with an eval{}).

Note that the file does not previously need to have been opened in order to write to it, and permission checking is not performed at this level. This checking is performed in the (optional) open() call.

$fs->flush( [$path,[$inode]] )

Before data is written to the database, it is cached for a while in memory. flush() will force data to be written to the database. You may pass no arguments, in which case all cached data will be written, or you may provide the path and/or inode to an existing file to flush just the unwritten data associated with that file.

$fs->truncate($path,$length)

Shorten the contents of the file located at $path to the length indicated by $length.

$fs->ftruncate($path,$length,$inode)

Like truncate() but you may provide the inode instead of the path. This is called by Fuse to truncate an open file.

$fs->utime($path,$atime,$mtime)

Update the atime and mtime of the indicated file or directory to the values provided. You must have write permissions to the file in order to do this.

$fs->access($path,$access_mode)

This method checks the current user's permissions for a file or directory. The arguments are the path to the item of interest, and the mode is one of the following constants:

 F_OK   check for existence of file

or a bitwise OR of one or more of:

 R_OK   check that the file can be read
 W_OK   check that the file can be written to
 X_OK   check that the file is executable

These constants can be obtained from the POSIX module.

$errno = $fs->errno($message)

Most methods defined by this module are called within an eval{} to trap errors. On an error, the message contained in $@ is passed to errno() to turn it into a UNIX error code. The error code is then returned to the Fuse module.

The following is the limited set of mappings performed:

  Eval{} error message       Unix Errno   Context
  --------------------       ----------   -------

  not found                  ENOENT       Path lookups
  file exists                EEXIST       Path creation
  is a directory             EISDIR       Attempt to open/read/write a directory
  not a directory            ENOTDIR      Attempt to list entries from a file
  length beyond end of file  EINVAL       Truncate file to longer than current length
  not empty                  ENOTEMPTY    Attempt to remove a directory that is in use
  permission denied          EACCESS      Access modes don't allow requested operation

The full error message usually has further detailed information. For example the full error message for "not found" is "$path not found" where $path contains the requested path.

All other errors, including problems in the underlying DBI database layer, result in an error code of EIO ("I/O error"). These constants can be obtained from POSIX.

$result = $fs->setxattr($path,$name,$val,$flags)

This method sets the extended attribute named $name to the value indicated by $val for the file or directory in $path. The Fuse documentation states that $flags will be one of XATTR_REPLACE or XATTR_CREATE, but in my testing I have only seen the value 0 passed.

On success, the method returns 0.

$val = $fs->getxattr($path,$name)

Reads the extended attribute named $name from the file or directory at $path and returns the value. Will return undef if the attribute not found.

Note that when the filesystem is mounted, the Fuse interface provides no way to distinguish between an attribute that does not exist versus one that does exist but has value "0". The only workaround for this is to use "attr -l" to list the attributes and look for the existence of the desired attribute.

@attribute_names = $fs->listxattr($path)

List all xattributes for the file or directory at the indicated path and return them as a list.

$fs->removexattr($path,$name)

Remove the attribute named $name for path $path. Will raise a "no such attribute" error if then if the attribute does not exist.

LOW LEVEL METHODS

The following methods may be of interest for those who wish to understand how this module works, or want to subclass and extend this module.

$fs->initialize_schema

This method is called to initialize the database schema. The database must already exist and be writable by the current user. All previous data will be deleted from the database.

The default schema contains three tables:

 metadata -- Information about the inode used for the stat() call. This
             includes its length, modification and access times, 
             permissions, and ownership. There is one row per inode,
             and the inode is the table's primary key.

 path     -- Maps paths to inodes. Each row is a distinct component
             of a path and contains the name of the component, the 
             inode of the parent component, and the inode corresponding
             to the component. This is illustrated below.

 extents  -- Maps inodes to the contents of the file. Each row consists
             of the inode of the file, the block number of the data, and
             a blob containing the data in that block.

For the mysql adapter, here is the current schema:

metadata:

 +--------+------------+------+-----+---------------------+----------------+
 | Field  | Type       | Null | Key | Default             | Extra          |
 +--------+------------+------+-----+---------------------+----------------+
 | inode  | int(10)    | NO   | PRI | NULL                | auto_increment |
 | mode   | int(10)    | NO   |     | NULL                |                |
 | uid    | int(10)    | NO   |     | NULL                |                |
 | gid    | int(10)    | NO   |     | NULL                |                |
 | rdev   | int(10)    | YES  |     | 0                   |                |
 | links  | int(10)    | YES  |     | 0                   |                |
 | inuse  | int(10)    | YES  |     | 0                   |                |
 | size   | bigint(20) | YES  |     | 0                   |                |
 | mtime  | timestamp  | NO   |     | 0000-00-00 00:00:00 |                |
 | ctime  | timestamp  | NO   |     | 0000-00-00 00:00:00 |                |
 | atime  | timestamp  | NO   |     | 0000-00-00 00:00:00 |                |
 +--------+------------+------+-----+---------------------+----------------+

path:

 +--------+--------------+------+-----+---------+-------+
 | Field  | Type         | Null | Key | Default | Extra |
 +--------+--------------+------+-----+---------+-------+
 | inode  | int(10)      | NO   |     | NULL    |       |
 | name   | varchar(255) | NO   |     | NULL    |       |
 | parent | int(10)      | YES  | MUL | NULL    |       |
 +--------+--------------+------+-----+---------+-------+

extents:

 +----------+---------+------+-----+---------+-------+
 | Field    | Type    | Null | Key | Default | Extra |
 +----------+---------+------+-----+---------+-------+
 | inode    | int(10) | YES  | MUL | NULL    |       |
 | block    | int(10) | YES  |     | NULL    |       |
 | contents | blob    | YES  |     | NULL    |       |
 +----------+---------+------+-----+---------+-------+

The metadata table is straightforward. The meaning of most columns can be inferred from the stat(2) manual page. The only columns that may be mysterious are "links" and "inuse". "links" describes the number of distinct paths involving a file or directory. Files start out with one link and are incremented by one every time a hardlink is created (symlinks don't count). Directories start out with two links (one for '..' and the other for '.') and are incremented by one every time a file or subdirectory is added to the directory. The "inuse" column is incremented every time a file is opened for reading or writing, and decremented when the file is closed. It is used to prevent the content from being deleted if the file is still in use.

The path table is organized to allow rapid translation from a pathname to an inode. Each entry in the tree is identified by its inode, its name, and the inode of its parent directory. The inode of the root "/" node is hard-coded to 1. The following steps show the effect of creating subdirectories and files on the path table:

After initial filesystem initialization there is only one entry in paths corresponding to the root directory. The root has no parent:

 +-------+------+--------+
 | inode | name | parent |
 +-------+------+--------+
 |     1 | /    |   NULL |
 +-------+------+--------+

$ mkdir directory1 +-------+------------+--------+ | inode | name | parent | +-------+------------+--------+ | 1 | / | NULL | | 2 | directory1 | 1 | +-------+------------+--------+

$ mkdir directory1/subdir_1_1

 +-------+------------+--------+
 | inode | name       | parent |
 +-------+------------+--------+
 |     1 | /          |   NULL |
 |     2 | directory1 |      1 |
 |     3 | subdir_1_1 |      2 |
 +-------+------------+--------+

$ mkdir directory2

 +-------+------------+--------+
 | inode | name       | parent |
 +-------+------------+--------+
 |     1 | /          |   NULL |
 |     2 | directory1 |      1 |
 |     3 | subdir_1_1 |      2 |
 |     4 | directory2 |      1 |
 +-------+------------+--------+

$ touch directory2/file1.txt

 +-------+------------+--------+
 | inode | name       | parent |
 +-------+------------+--------+
 |     1 | /          |   NULL |
 |     2 | directory1 |      1 |
 |     3 | subdir_1_1 |      2 |
 |     4 | directory2 |      1 |
 |     5 | file1.txt  |      4 |
 +-------+------------+--------+

$ ln directory2/file1.txt link_to_file1.txt

 +-------+-------------------+--------+
 | inode | name              | parent |
 +-------+-------------------+--------+
 |     1 | /                 |   NULL |
 |     2 | directory1        |      1 |
 |     3 | subdir_1_1        |      2 |
 |     4 | directory2        |      1 |
 |     5 | file1.txt         |      4 |
 |     5 | link_to_file1.txt |      1 |
 +-------+-------------------+--------+

Notice in the last step how creating a hard link establishes a second entry with the same inode as the original file, but with a different name and parent.

The inode for path /directory2/file1.txt can be found with this recursive-in-spirit SQL fragment:

 select inode from path where name="file1.txt" 
              and parent in 
                (select inode from path where name="directory2" 
                              and parent in
                                (select 1)
                )

The extents table provides storage of file (and symlink) contents. During testing, it turned out that storing the entire contents of a file into a single BLOB column provided very poor random access performance. So instead the contents are now broken into blocks of constant size 4096 bytes. Each row of the table corresponds to the inode of the file, the block number (starting at 0), and the data contained within the block. In addition to dramatically better read/write performance, this scheme allows sparse files (files containing "holes") to be stored efficiently: Blocks that fall within holes are completely absent from the table, while those that lead into a hole are shorter than the full block length.

The logical length of the file is stored in the metadata size column.

If you have subclassed DBI::Filesystem and wish to adjust the default schema (such as adding indexes), this is the place to do it. Simply call the inherited initialize_schema(), and then alter the tables as you please.

$ok = $fs->check_schema

This method is called when opening a preexisting database. It checks that the metadata, path and extents tables exist in the database and have the expected relationships. Returns true if the check passes.

$version = $fs->schema_version

This method returns the schema version understood by this module. It is used when opening up a sqlfs databse to check whether database was created by an earlier or later version of the software. The schema version is distinct from the library version since updates to the library do not always necessitate updates to the schema.

Versions are small integers beginning at 1.

$version = $fs->get_schema_version

This returns the schema version known to a preexisting database.

$fs->set_schema_version($version)

This sets the databases's schema version to the indicated value.

$fs->check_schema_version

This checks whether the schema version in a preexisting database is compatible with the version known to the library. If the version is from an earlier version of the library, then schema updating will be attempted. If the database was created by a newer version of the software, the method will raise a fatal exception.

$fs->_update_schema_from_A_to_B

Every update to this library that defines a new schema version has a series of methods named _update_schema_from_A_to_B(), where A and B are sequential version numbers. For example, if the current schema version is 3, then the library will define the following methods:

 $fs->_update_schema_from_1_to_2
 $fs->_update_schema_from_2_to_3

These methods are only of interests to people who want to write adapters for DBMS engines that are not currently supported, such as Oracle.

This method returns the blocksize (currently 4096 bytes) used for writing and retrieving file contents to the extents table. Because 4096 is a typical value used by libc, altering the value in subclasses will probably degrade performance. Also be aware that altering the blocksize will render filesystems created with other blocksize values unreadable.

$count = $fs->flushblocks

This method returns the maximum number of blocks of file contents data that can be stored in memory before it is written to disk. Because all blocks are written to the database in a single transaction, this can have a dramatic performance effect and it is worth trying different values when tuning the module for new DBMSs.

The default is 64.

$fixed_path = fixup($path)

This is an ordinary function (not a method!) that removes the initial slash from paths passed to this module from Fuse. The root directory (/) is not changed:

 Before      After fixup()
 ------      -------------
 /foo        foo
 /foo/bar    foo/bar
 /          /

To call this method from subclasses, invoke it as DBI::Filesystem::fixup().

$dsn = $fs->dsn

This method returns the DBI data source passed to new(). It cannot be changed.

$dbh = $fs->dbh

This method opens a connection to the database defined by dsn() and returns the database handle (or raises a fatal exception). The database handle will have its RaiseError and AutoCommit flags set to true. Since the mount function is multithreaded, there will be one database handle created per thread.

$inode = $fs->create_inode($type,$mode,$rdev,$uid,$gid)

This method creates a new inode in the database. An inode corresponds to a file, directory, symlink, pipe or block special device, and has a unique integer ID defining it as its primary key. Arguments are the type of inode to create, which is used to check that the passed mode is correct ('f'=file, 'd'=directory,'l'=symlink; anything else is ignored), the mode of the inode, which is a combination of type and access permissions as described in stat(2), the device ID if a special file, and the desired UID and GID.

The return value is the newly-created inode ID.

You will ordinarily use the mknod() and mkdir() methods to create files, directories and special files.

$id = $fs->last_inserted_inode($dbh)

After a new inode is inserted into the database, this method returns its ID. Unique inode IDs are generated using various combinations of database autoincrement and sequence semantics, which vary from DBMS to DBMS, so you may need to override this method in subclasses.

The default is simply to call DBI's last_insert_id method:

 $dbh->last_insert_id(undef,undef,undef,undef)

$self->create_path($inode,$path)

After creating an inode, you can associate it with a path in the filesystem using this method. It will raise an error if unsuccessful.

$inode=$self->create_inode_and_path($path,$type,$mode,$rdev)

Create an inode and associate it with the indicated path, returning the inode ID. Arguments are the path, the file type (one of 'd', 'f', or 'l' for directory, file or symbolic link). As usual, this may exit with a fatal error.

Given an inode, this deletes it and its contents, but only if the file is no longer in use. It will die with an exception if the changes cannot be committed to the database.

$boolean = $fs->check_path($name,$inode,$uid,$gid)

Given a directory's name, inode, and the UID and GID of the current user, this will traverse all containing directories checking that their execute permissions are set. If the directory and all of its parents are executable by the current user, then returns true.

$fs->check_perm($inode,$access_mode)

Given a file or directory's inode and the access mode (a bitwise OR of R_OK, W_OK, X_OK), checks whether the current user is allowed access. This will return if access is allowed, or raise a fatal error potherwise.

$fs->touch($inode,$field)

This updates the file/directory indicated by $inode to the current time. $field is one of 'atime', 'ctime' or 'mtime'.

$inode = $fs->path2inode($path)

($inode,$parent_inode,$name) = $self->path2inode($path)

This method takes a filesystem path and transforms it into an inode if the path is valid. In a scalar context this method return just the inode. In a list context, it returns a three element list consisting of the inode, the inode of the containing directory, and the basename of the file.

This method does permission and access path checking, and will die with a "permission denied" error if either check fails. In addition, passing an invalid path will return a "path not found" error.

@paths = $fs->inode2paths($inode)

Given an inode, this method returns the path(s) that correspond to it. There may be multiple paths since file inodes can have hard links. In addition, there may be NO path corresponding to an inode, if the file is open but all externally accessible links have been unlinked.

Be aware that the path table is indexed to make path to inode searches fast, not the other way around. If you build a content search engine on top of DBI::Filesystem and rely on this method, you may wish to add an index to the path table's "inode" field.

$groups = $fs->get_groups($uid,$gid)

This method takes a UID and GID, and returns the primary and supplemental groups to which the user is assigned, and is used during permission checking. The result is a hashref in which the keys are the groups to which the user belongs.

$ctx = $fs->get_context

This method is a wrapper around the fuse_get_context() function described in Fuse. If called before the filesystem is mounted, then it fakes the call, returning a context object based on the information in the current process.

SUBCLASSING

Subclass this module as you ordinarily would by creating a new package that has a "use base DBI::Filesystem". You can then tell the command-line sqlfs.pl tool to load your subclass rather than the original by providing a --module (or -M) option, as in:

 $ sqlfs.pl -MDBI::Filesystem::MyClass <database> <mtpt>

AUTHOR

Copyright 2013, Lincoln D. Stein <lincoln.stein@gmail.com>

LICENSE

This package is distributed under the terms of the Perl Artistic License 2.0. See http://www.perlfoundation.org/artistic_license_2_0.

1 POD Error

The following errors were encountered while parsing the POD:

Around line 2168:

Unknown directive: =Head2