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::TypesFile::MimeinfoDateTimeCatalyst::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: HTMLScrubberVoila! 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.