Super Simple Photo Gallery
Let's create an extremely simple (but fun!) photogallery. First things first, we need to make sure we have everything.
Our packing list:
DBIx::Class::InflateColumn::FS
-
This will be used to store our file pointer in the database, write the image to the file system, and later, give us a Path::Class::File object to manipulate our images later on.
Imager
-
The all and the everything for our image manipulation needs. If not installed already, make sure you install libjpeg so that JPEG files can be manipulated by Imager.
MIME::Types
File::Mimeinfo
DateTime
Catalyst::Controller::HTML::FormFu
Once you have installed these modules, move on to the next step.
Let's get started already!
Ok. So here comes the fun stuff. We're going to assume you know how to create a Catalyst application skeleton, controllers, and models at the very least. If not, you have some documentation to read first :-)
Create a controller in your application called Photos as follows:
perl script/myapp_create.pl controller Photos
Next, you'll want to set up a table called Photos in your RDBMS of choice. Here, I've used MySQL, but you could use SQLite , Postresql or any other RDBMS supported by DBIx::Class. The necessary columns you will need are as follows:
- photoid
-
type: INT size: 11 null: no other: PRIMARY KEY auto_increment
- name
-
type: VARCHAR size: 255 null: no other:
- mime
-
type: VARCHAR size: 255 null: no other:
- mime
-
type: VARCHAR size: 255 null: no other:
- uploaded
-
type: DATETIME size: null: no other: (If you want, include the InflateColumn::DateTime component to be able to manipulate this column as if it were a DateTime object)
- path
-
type: TEXT size: null: no other:
- caption
-
type: TEXT size: null: no other:
Now, create your model:
./script/myapp_create.pl model DB DBIC::Schema MyApp::Schema 'DBI:mysql:database=myapp' 'user' 'password'
So now you have your basic tables set up. Next, we need to generate our Schema files so DBIx::Class can interact with our database objects. I use this script to update/create my schema definition from the database. Copy/paste this script into a file called myapp_update_schema.pl (replace "myapp" with whatever you named this application) and stick it in the script/ directory of your application:
#!/usr/bin/perl use FindBin; use DBIx::Class::Schema::Loader qw| make_schema_at |; make_schema_at( "MyApp::Schema", { debug => 1, use_namespaces => 0, components => [qw/ InflateColumn::DateTime InflateColumn::FS PK::Auto/], dump_directory => "$FindBin::Bin/../lib" }, [ "dbi:mysql:myapp", "user", "password" ] );
now run this script:
perl script/myapp_update_schema.pl MyApp::Schema::Photos->load_components( "InflateColumn::DateTime", "InflateColumn::FS", ... MyApp::Schema::Photos->set_primary_key("photoid"); MyApp::Schema::Photos->add_unique_constraint("photos_pkey", ["photoid"]); Dumping manual schema for MyApp::Schema to directory /tmp/MyApp/script/../lib ... Schema dump completed.
If there are no errors, proceed!
Adding the InflateColumn::FS magic
Open up lib/Schema/Photos.pm. Since we used ::Loader, there will be a good portion of the file that you're not supposed to manually edit. This is marked by these lines:
# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-12-01 02:57:02 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:A/YrsvGDAWKpDaGlJ3Dwqg
You can safely modify anything below this though. Add these lines to the file:
use MyApp; __PACKAGE__->add_columns( "path", { data_type => 'TEXT', is_fs_column => 1, fs_column_path => MyApp->path_to( 'root', 'static', 'photos' ) . "" } );
This tells DBIx::Class that the column "path" is now a Path::Class::File object and we can call Path::Class::File methods on it upon retrieval.
Great! Now let's edit our controller.
The Photos Controller
Edit lib/Controller/Photos.pm to match this file:
package MyApp::Controller::Photos; use strict; use warnings; use parent 'Catalyst::Controller::HTML::FormFu'; use DateTime; use Imager; use MIME::Types; use File::MimeInfo (); use MyApp; __PACKAGE__->mk_accessors(qw(thumbnail_size)); =head1 NAME MyApp::Controller::Photos - Catalyst Controller =head1 DESCRIPTION Catalyst Controller. =head1 METHODS =cut =head2 index display the photos =cut sub index : Path : Args(0) { my ( $self, $c ) = @_; my @photos = $c->model('DB::Photos')->all; $c->stash->{photos} = \@photos; $c->stash->{template} = "photos/index.tt2"; } =head2 get_photos set up photo stash =cut sub get_photos : Chained('/') PathPart('photo') CaptureArgs(1) { my ( $self, $c, $photoid ) = @_; my $photo = $c->model('DB::Photos')->find($photoid); if ( $photo == undef ) { $c->stash->{error_msg} = "No such photo."; } else { $c->stash->{photo} = $photo; } } =head2 add_photo Add a photo to the database =cut sub add_photo : Path('/photo/add') FormConfig('photos/add.yml') { my ( $self, $c ) = @_; $c->stash->{template} = "photos/add.tt2"; my $form = $c->stash->{form}; my $mime = MIME::Types->new; ## comment out this block if you're not using the Authorization plugin if ( $c->can('check_user_roles') && !$c->check_user_roles('admin') ) { $c->flash->{error_msg} = "You don't have the proper permissions to add photos here"; $c->res->redirect( $c->uri_for('/photos') ); } if ( $form->submitted_and_valid ) { my $photo = $c->model('DB::Photos')->create( { name => $form->param('photo_name'), path => $c->req->upload('photo')->fh, caption => $form->param('caption'), uploaded => DateTime->now, mime => $mime->mimeTypeOf( $c->req->upload('photo')->basename ), } ); $c->stash->{status_msg} = "Successfully uploaded!"; $c->stash->{photo} = $photo; $c->detach; } } =head2 generate_thumbnail this method generates a thumbnail of a given image =cut sub generate_thumbnail : Chained('get_photos') PathPart('thumbnail') Args(0) { my ( $self, $c ) = @_; my $photo = $c->stash->{photo}; my $size = $self->thumbnail_size; my $mimeinfo = File::MimeInfo->new; my $data = $photo->path->open('r') or die "Error: $!"; my $img = Imager->new; $img->read( fh => $data ) or die $img->errstr; my $scaled = $img->scale( xheight => $size ); my $out; $scaled->write( type => $mimeinfo->extensions( $photo->mime ), data => \$out ) or die $scaled->errstr; $c->res->content_type( $photo->mime ); $c->res->content_length( -s $out ); $c->res->header( "Content-Disposition" => "inline; filename=" . $mimeinfo->extensions( $photo->mime ) ); binmode $out; $c->res->body($out); } =head2 view_image hackish method to view an image full-size =cut sub view_image : Chained('get_photos') PathPart('generate') Args(0) { my ( $self, $c ) = @_; my $photo = $c->stash->{photo}; my $mimeinfo = File::MimeInfo->new; my $data = $photo->path->open('r') or die "Error: $!"; my $img = Imager->new; $img->read( fh => $data ) or die $img->errstr; my $out; $img->write( type => $mimeinfo->extensions( $photo->mime ), data => \$out ) or die $img->errstr; $c->res->content_type( $photo->mime ); $c->res->content_length( -s $out ); $c->res->header( "Content-Disposition" => "inline; filename=" . $mimeinfo->extensions( $photo->mime ) ); binmode $out; $c->res->body($out); } =head2 view_photo view an individual photo =cut sub view_photo : Chained("get_photos") PathPart('view') Args(0) { my ( $self, $c ) = @_; my $photo = $c->stash->{photo}; $c->stash->{template} = "photos/view.tt2"; } =head2 delete_photo delete a photo or photos =cut sub delete_photo : Chained("get_photos") PathPart('delete') Args(0) { my ( $self, $c ) = @_; my $photo = $c->stash->{photo}; $c->stash->{template} = 'photos/delete.tt2'; if ( $c->check_user_roles("admin") ) { if ( $c->req->param('delete') eq 'yes' ) { $photo->delete; $c->stash->{status_msg} = "Photo " . $photo->photoid . " deleted!"; $c->detach; } } else { $c->flash->{error_msg} = "You don't have proper permissions to delete images."; $c->res->redirect("/"); } } 1;
The view:
- root/photos/add.tt2
-
<h2>Upload your photos</h2> [% UNLESS form.submitted_and_valid %] [% form %] [% ELSE %] <p>Success!</p> <p>Here's your image: <img src="[% c.uri_for("/photo/$photo.photoid/thumbnail") %]" /> [% END %]
- root/photos/delete.tt2
-
[% UNLESS c.req.param('delete') %] <h2>Deleting [% photo.name %]</h2> <div id="photo"> <img src="[% c.uri_for("/photo/$photo.photoid/generate") %]" alt="[% photo.name %]" /> </div> <p>ARE YOU SURE YOU WANT TO DELETE THIS PHOTO?</p> <p><a href="[% c.uri_for("/photo/$photo.photoid/delete", { delete=> 'yes' } ) %]">yes</a> I do</p> <p><a href="[% c.uri_for("/photos") %]">no</a> I don't</p> <div>«<a href="[% c.uri_for("/photos") %]">back</a> [% ELSE %] <p>Deleted!</p> [% END %]
- root/photos/index.tt2
-
[% USE table (photos, rows=3) %] <h2>Puppy Photos</h2> <div id="puppy-list"> <table> [% FOREACH column = table.cols %] <tr> [% FOREACH photo = column %] <td> [% IF photo %] <a href="[% c.uri_for("/photo/$photo.photoid/view") %]"> <img src="[% c.uri_for("/photo/$photo.photoid/thumbnail")%]" /> </a> [% ELSE %] <img src="[% c.uri_for("/static/photos/blank.gif")%]" width="150" height="100"/> [% END %] </td> [% END %] </tr> [% END %] <table> </div>
- root/photos/view.tt2
-
<h2>[% photo.name %]</h2> <div id="photo"> <img src="[% c.uri_for("/photo/$photo.photoid/generate") %]" alt="[% photo.name %]" /> </div> <p>[% photo.caption %]</p> <div>«<a href="[% c.uri_for("/photos") %]">back</a>
FormFu
And last but not least, the required HTML::FormFu files:
- root/forms/photos/add.yml
-
--- indicator: submit elements: - type: Text name: photo_name label: Name this photo container_tag: div - type: File name: photo label: Your photo container_tag: div - type: Textarea name: caption label: Photo caption container_tag: div - type: Submit name: submit value: Upload! constraints: SingleValue filters: HTMLScrubber
Voila! There you have it. Go nuts.
AUTHOR
Devin Austin
devin.austin@gmail.com
http://www.codedright.net
LICENSE
This library is free software, you can redistribute it and/or modify it under the same terms as Perl itself.