DBIx::Class::InflateColumn::FS and X-Sendfile

In this article I will explain how to elegantly store user submitted HTML code in the filesystem for later delivery with X-Sendfile.

Storing pages

We are using DBIx::Class::InflateColumn::FS to store content in the filesystem.

Setting up your schema result class

Code:

MyApp/Schema/Result/Pages.pm

    __PACKAGE__->load_components(qw/InflateColumn::FS Core/);
    __PACKAGE__->add_columns(
        id => {
            data_type         => 'INT',
            is_auto_increment => 1,
        },
        file => {
            data_type      => 'TEXT',
            is_fs_column   => 1,
            fs_column_path => '/var/www/myapp/root/pages',
        },
    );

Passing the content to the schema

If you pass a filehandle to the file column, filesystem management will be handled by DBIx::Class::InflateColumn::FS. So somewhere in your Controller you probably want code like this:

Code:

MyApp/Controller/Pages.pm

    use IO::File;
    use Carp;

    sub create :Path('create') Args(0) {
        my ($self, $c) = @_;

        # user submitted html code in a forms textarea
        # validate form with module of your choice
        ...

        my $validated_page_code = ...

        # create a temp file
        $fh = IO::File->new_tmpfile;
        print {$fh} $validated_page_code
            or croak 'Couldn't write to temporary file';
        close $fh
            or croak 'Couldn't close temporary file';

        # store page
        $c->model('DB::Pages')->create({ file => $fh });
    }

Delivering pages

Now that the page is stored in our filesystem let's have a look how to deliver the file using X-Sendfile.

X-Sendfile lets the webserver deal with delivery of static files.

The X-Sendfile features is supported by Apache, Lighttpd and nginx (nginx is calling it X-Accel-Redirect though). Here is an Apache configuration.

First, install mod_xsendfile. On Debian based systems use the package system.

    aptitude install libapache2-mod-xsendfile

then enable the module in your apache configuration

    XSendfile On

Now Apache looks for the presence of a X-Sendfile header and delivers the file referenced in the header.

Setting the X-Sendfile header.

You might want to put this code into a Role and let your Controller consume it.

Code:

    sub sendfile {
        my ($self, $c, $file, $content_type) = @_;

        my $engine = $ENV{CATALYST_ENGINE} || '';

        # Catalyst development server
        if ( $engine =~ /^HTTP/ ) {
            if ( $file->stat && -f _ && -r _ ) {
                $c->res->body( $file->openr );
            }
        }

        # Deployment with FastCGI
        elsif ( $engine eq 'FastCGI' ) {
            $c->res->header('X-Sendfile', $file);
            $c->res->body("foo"); # MASSIVE HACK: bypass RenderView
        }

        # unknown engine
        else {
            die "Unknown engine: " . $engine;
        }

        $c->res->content_type($content_type);
        $c->res->content_length( $file->stat->size );
        $c->res->status(200);
        $c->detach;
    }

the 'send' action

Also in your controller add a send action that sets the X-Sendfile header for the requested file.

Code:

    sub send : Path('send') Args(1) {
        my ( $self, $c, $page_id ) = @_;

        # get the requested page from your model
        my $page = $c->model('DB::Pages')->find($page_id);

        # display error page if requested page doesn't exist
        $c->detach('/error404') unless $page;

        # set X-Sendfile header
        $self->sendfile($c, $page->file, 'text/html');
    }

Further Reading

This article is based on the Catayst Cookbook entry Controller With Fileupload.

AUTHOR

davewood: David Schmidt <davewood@gmx.at>