RiverViews Header
River Views

home :: perl :: dbic-ic-fs

Sun, 31 Jan 2010
use DBIx::Class::InflateColumn::FS instead

DBIx::Class::InflateColumn::File - DEPRECATED

It is time to stop using DBIx::Class::InflateColumn::File. This article gives some tips and examples for switching to DBIx::Class::InflateColumn::FS.

From DBIx::Class::InflateColumn::File:

DBIx::Class::InflateColumn::File - DEPRECATED (superseded by DBIx::Class::InflateColumn::FS)

Deprecation Notice

This component has a number of architectural deficiencies and is not recommended for further use. It will be retained for backwards compatibility, but no new functionality patches will be accepted. Please consider using the much more mature and actively supported DBIx::Class::InflateColumn::FS

I switched to DBIx::Class::InflateColumn::FS because I had problems with DBIx::Class::InflateColumn::File not deleting all files from the file system; and that was due to one of the “number of architectural deficiencies”. I am happy to see further development for file-based storage will be focussed on one package.

So let’s look at ways of using DBIx::Class::InflateColumn::FS instead

If you have been using DBIC::IC::File and want to migrate to DBIC::IC::FS, there are some changes to assigning column values.

The examples below assume IC::FS is being used in a Catalyst environment. We are also assuming the desired filename is already known (part of the $rec object) or we can use a random (UUID) filename.

Using IC::File required assigning a hashref to the file column, eg:

$rec->media_orig_file({ 
    handle   => $c->req->upload('myupload')->fh, 
    filename => $c->req->upload('myupload')->basename 
});

While IC::FS just wants a filehandle:

$rec->media_orig_file( $c->req->upload('myupload')->fh );

IC::FS also has a different way of naming the file on disk and specifying the directory to write it to.

The is_fs_column column type from IS::FS offers a couple of methods to control its behavior:

fs_file_name

Provides the file naming algorithm. Override this method to change it. This method is called with two parameters: The name of the column and the column_info object.

_fs_column_dirs

Returns the sub-directory components for a given file name. Override it to provide a deeper directory tree or change the algorithm.

The following goes in MyApp::Schema::Result::Media.pm; it assumes I already assigned values to user, name & mime_type. It also allows me to use a different filename depending on which column is being set.

sub fs_file_name {
    my ($self, $column, $column_info) = @_;
    my MIME::Type $img_mimetype = MIME::Types->new->type($self->mime_type); #'image/jpeg'
    my $size = $column eq 'media_thumb_file' ?
                    'thumb' :
               $column eq 'media_orig_file'  ?
                    'orig' :
                    '';
    return sprintf("%05d-%s_%s.%s",
        $self->user->id,
        $self->name,
        $size,
        $img_mimetype->subType)
}

Or if you want random file names but need to control the file extension, use a method similar to IC::FS->fs_file_name; it assumes I have assigned a value to mime_type.

sub fs_file_name {
    my ($self, $column, $column_info) = @_;
    my MIME::Type $img_mimetype = MIME::Types->new->type($self->mime_type); #'image/jpeg'
    return sprintf("%s.%s", DBIx::Class::UUIDColumns->get_uuid, $img_mimetype->subType)
}

Of course if you just want a random filename, no need to override the fs_file_name method at all. And if you need to use the filename from the upload param, but don’t want to create a column in your table for the name, then use (eg) a Moose attribute for it in your Result class. And set the filename attribute before setting the file column value.

has 'upload_filename' => ( is => 'rw' );

sub fs_file_name {
    my ($self, $column, $column_info) = @_;
    my MIME::Type $img_mimetype = MIME::Types->new->type($self->mime_type); #'image/jpeg'
    return sprintf("%s.%s", $self->upload_filename, $img_mimetype->subType)
}

and elsewhere…

$rec->upload_filename( $c->req->upload('myupload')->basename );
$rec->media_orig_file( $c->req->upload('myupload')->fh );

Note, I haven’t tried that method, but it should work fine. MyApp just uses a random (UUID) filename.

What directory will files be stored in?

There are two parts to configuring the directory where files will be written.

The first is defining which directory will be used for all files written for a given column. I’m using class method path_to that I setup for uses like this. There are plenty of other ways to specify the directory value to fs_column_path.

__PACKAGE__->add_columns(

  "media_thumb_file",
  {
    data_type       => "varchar",  size => 255,
    is_fs_column    => 1,
    fs_column_path  => __PACKAGE__->path_to('root','static','media'),
  },
  "media_orig_file",
  {
    data_type       => "varchar",  size => 255,
    is_fs_column    => 1,
    fs_column_path  => __PACKAGE__->path_to('root','data','media'),
  },

};

And second you specify which sub-directory will be used for the file. You can use the default for IC::FS, from the POD:

Within the path specified by fs_column_path, files are stored in sub-directories based on the first 2 characters of the unique file names. Up to 256 sub-directories will be created, as needed. Override _fs_column_dirs in a derived class to change this behavior.

Or use a variation of the default method; this uses two-level sub-directory structure:

sub _fs_column_dirs {
    shift;
    my $filename = shift;

    $filename =~ s/(..)(..).*/$1\/$2/;
    return $filename;
}

Or let’s use a value from $rec; mimics IC::File behavior:

sub _fs_column_dirs {
    return shift->id;
}

At some point you will need to refer to the file again, and one common use is specifying a URL which points to the file as a resource on your server. IC::FS doesn’t give us a method we can use for that, but it’s very simple to create one in our Result class.

sub fs_file_path {
    my ($self, $column) = @_;
    my $fh = $self->$column;
    return $fh->relative( $self->result_source->column_info($column)->{fs_column_path} )->stringify;
}

The fs_file_path method uses column_info to get the base directory for the file (as set in fs_column_path), then it uses $fh->relative method to get the end of the directory/file path. And that value can used for creating src attributes, eg. from a TT template:

[%  media = rec
    img_full_src   = c.uri_for("/static/media/", media.fs_file_path('media_full_file' ));
    img_thumb_src  = c.uri_for("/static/media/", media.fs_file_path('media_thumb_file'));
%]
<a href="[% img_full_src %]" title="[% media.name %]"
><img src="[% img_thumb_src %]"  width="[% media.width %]"  height="[% media.height %]" /></a>

First we assigned $rec to a local variable media. We call fs_file_path twice for two different image columns. One value we use for setting src to the thumbnail image, and the second is for setting the href in the anchor tag. (Note, you will need the latest version of Catalyst to avoid a path encoding bug with uri_for, at least when using the above syntax.)

More Advanced Usage

One of the more common reasons to use a filesystem-backed storage is for image and other media files. And when there is one media file there is often another since we’ll need to offer different sizes and maybe even different formats. So let’s get our Result::Media class to assist with making multiple formats simple to manage.

In one of your controller actions:

my $image = $c->req->upload('media_image');
my $name = $image->filename || 'image'; ## || $image->basename
$name =~ s/(.*)\.(.*?)$/$1/; ## remove file extension

my $media = $c->model('DBIC::Media')->create({ name => $name });
$media->set_image($image->fh);
$media->update;

And in Result::Media, create a method which stores both the original uploaded image and a thumbnail copy. We use Imager to handle creating the thumbnail image.

sub set_image {
    my $self = shift;
    my $img_fh = shift;

    Imager->set_file_limits(bytes => 10_000_000);   # limit to 10 million bytes of memory usage
    Imager->set_file_limits(width => 1024, height => 1024); # limit to 1024 x 1024

    $img_fh->seek(0,0);
    binmode $img_fh;

    my $img_orig = Imager->new;
    if ($img_orig->read(fh=>$img_fh)) {

        my $img_format = $img_orig->tags(name=>'i_format');
        my MIME::Type $img_mimetype  = MIME::Types->new->mimeTypeOf($img_format);

        ## set some other columns we use for metadata
        $self->mime_type("$img_mimetype"); # $img_mimetype needs to be stringified
        $self->size_width($img_orig->getwidth); 
        $self->size_height($img_orig->getheight);

        $img_fh->seek(0,0);
        $self->media_orig_file( $img_fh );

        ## Let's grab copy as thumbnail image, scaling and cropping along the way
        my $img_thumb = $img_orig->scale(
            xpixels   => 75,
            ypixels   => 100,
            type      => 'max',
            ## preview, mixing, normal - preview is faster than mixing which is much faster than normal
            qtype     => 'mixing',
        )->crop(width=>75, height=>100) or 
                $self->throw_exception("Could not scale/crop thumbnail image: " . $img_orig->errstr);


        my $img_thumb_fh = IO::File->new_tmpfile;
        $img_thumb->write(fh => $img_thumb_fh, type => $img_format) or 
                $self->throw_exception("Could not write file handle for thumbnail image: " . $img_thumb->errstr);

        $img_thumb_fh->seek(0,0);
        $self->media_thumb_file( $img_thumb_fh );

    } else {
        $self->throw_exception("Could not read image from file handle: " . $img_orig->errstr);
    }
}

So with just a few lines in our controller, we can add media files of different sizes and formats from one file upload.

Conclusions

Using DBIx::Class::InflateColumn::FS is easy, and there is enough flexibility to allow us to store files in way that meets individual file system requirements.

Through the use of _fs_column_dirs we have control of scalability issues from directory size limitations, and along with fs_file_name we have control over the filename so we can assign user-meaningful values or something more arbitrary like UUID values.

We can easily get the path for a file to use in a src attribute using a method like fs_file_path defined in our Result class.

There are other usage scenarios that may require different solutions. Eg. (as discussed in the mailing list) how does HTML::FormFu handle file uploads when used with HTML::FormFu::Model::DBIC? I would like to see seamless integration with HTML::FF, I don’t know what’s required to make that happen though.

Final Words… DBIx::Class::InflateColumn::FS is another great example of why the DBIx::Class and Catalyst projects are such a joy to work with.

/perl | permanent link

River Views
Views and comments from the Colo River in Australia.

Charlie Garrison
charlie@riverviews.com.au

Subscribe
Subscribe to a syndicated (RSS) feed of River Views.

blosxom logo