Загрузка файлов в Catalyst-приложении с помощью стандартного средства Catalyst::Request::Upload. Работа с библиотекой lib::abs.
Задача: требуется организовать загрузку пользовательских файлов на сайт.
Реализация:
- Страница с полным списком товаров и вывод пригрепленного к товару изображения.
- Страница добавления нового товара.
- Страница редактирования товара и поле для загрузки изображений.
Пример очень простой, который демонстрирует только общие принципы работы с загружаемыми
пользовательскими файлами в Catalyst-приложении. В приведенном примере предполагалась загрузка только изображений, хотя тот же код можно использовать для загрузки файлов других типов: архивы, документы, и т.п.
Изменения в базе данных
Создаем новую таблицу в базе данных mysql, которая содержит информацию о товарах. В нашем случае она будет содержать только идентификатор товара, его название и название привязываемого к нему изображения.
|
1 2 3 4 5 6 7 8 |
CREATE TABLE `catalog` ( `id` INT(10) NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NULL DEFAULT NULL, `img_name` VARCHAR(255) NULL DEFAULT NULL, PRIMARY KEY (`id`) ) COLLATE='utf8_general_ci' ENGINE=InnoDB; |
Модель - доступ к файловому хранилищу
Зачем нам тут модель? Файлы, после получения от клиента - надо куда-то сохранять. Можно сохранять файлы прямо в специально отведенные директории, в рамках файловой структуры сайта.
Мне эта идея не нравится. Представим, что это не маленький интернет-магазин, а магазин с сотнями позиций, и каждую из них иллюстрирует несколько изображений, а к некоторым позициям добавляется видео с демонстрацией использования.
Еще хуже, если реализуется социальная сеть. Не так уж редко, один пользователь загружает больше тысячи фотографий себя любимого, и еще некоторое количество видео-файлов. Умножим это количество файлов на несколько тысяч пользователей (не говоря уж о том, что пользователей может быть несколько сотен тысяч).
Разместить такое количество данных в директории /myapp/root/images - занятие крайне не благодарное и опасное. Лучше использовать для хранения файлов специальные файловые хранилища и отдельные сервера, которые будут специализироваться на работе со статикой.
Вариантов реализации файлового хранилища для сайта, может быть огромное множество, в зависимости от потребностей и возможностей проекта. Но в любом случае, для работы с этим файловым хранилищем нужна модель.
Модель целиком и полностью отвечает за доступ к файлам, сохранение файлов, их удаление, проверку типов, назначение новых имен. Остальное приложение не знает, где хранятся файлы, как организован доступ к ним, какой протокол используется для работы с хранилищем и т.п. Если вы вдруг решите изменить принципы работы хранилища, вам понадобится переписать только код модели, а не всего приложения.
В моем примере модель реализуется самая простая, главное просто обозначить, что она есть. Проверки типов, имен файлов - не производятся. Если попытаться записать дважды файл, с одним и тем же именем, вероятнее всего, произойдет ошибка. Файлы сохраняются в отдельную директорию проекта. Как уже упоминалось выше, это не правильно "in real life", но для примера - вполне допустимо.
\lib\app\Model\FileStorage.pm :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package app::Model::FileStorage; use uni::perl ':dumper'; use Moose; use strict; use utf8; use lib::abs; use Path::Class::File; sub put { my ( $self, $upload ) = @_; $upload->copy_to('root/static/filestorage/images/'.$upload->filename); return 1; } sub get_file_url { my ( $self, $filename ) = @_; return "/static/filestorage/images/".$filename; } sub delete { my ( $self, $filename ) = @_; my $path = lib::abs::path('../../../root/static/filestorage/images/'); my $file = Path::Class::File->new($path, $filename); my $res = $file->remove(); return $res; } 1; |
Библиотека lib::abs, документация
Библиотека lib::abs помогает преобразовать относительные пути в абсолютные. Пример:
|
1 2 3 4 |
use lib::abs; my $path = lib::abs::path('../dev-lab.info/zvz.txt'); print $path."\n"; |
Результат выполнения:
|
1 2 |
> perl abs.pl C:/Documents and Settings/administrator/My Documents/dev-lab.info/vzv.txt |
Допустимый вариант использования:
|
1 2 |
use lib::abs qw(./lib ../bin); use lib::abs 'libs'; |
Указанные инструкции будут обрабатываться в тот же момент, когда начнется стадия выполнения BEGIN-блока, и повлияет на то, какие данные будут помещены в @INC. Но такой вариант применения lib::abs встречается значительно реже чем первый, с использованием функции path().
Контроллер - загрузка файлов, их удаление и просмотр
Особенности реализации.
Так же, как и модель, контроллер сильно упрощен. Отсутствуют проверки на ошибки, нет возможности добавлять несколько изображений к одному товару. При создании нового элемента, добавление картинки не реализуется.
\lib\app\Controller\Admin\Catalog.pm :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
package app::Controller::Admin::Catalog; use Moose; use namespace::autoclean; use app::Model::FileStorage; BEGIN { extends 'Catalyst::Controller'; } # вывод полного списка элементов каталога sub list :Path('/admin/catalog') :Args(0) { my ( $self, $c ) = @_; my $dbh = $c->model( 'DBI' )->dbh; my $filestorage = app::Model::FileStorage->new; my $catalog = $dbh->selectall_arrayref( "select * from catalog order by id", { Slice => {} } ); foreach my $element (@$catalog) { $element->{link} = $filestorage->get_file_url($element->{img_name}); } $c->stash( catalog => $catalog ); $c->stash->{template} = 'admin/catalog/list.tt'; $c->forward('View::TT'); } # добавление нового элемента каталога sub element_create :Path('/admin/catalog/create') :Args(0) { my ( $self, $c ) = @_; my $name = $c->request->params->{name} || ""; if (length($name) > 1) { my $dbh = $c->model('DBI')->dbh; my $sql = qq{INSERT INTO catalog(name, img_name) VALUES ('$name', '')}; my $rows = $dbh->do($sql); $c->response->redirect($c->uri_for('/admin/catalog')); } $c->stash->{template} = 'admin/catalog/element_create.tt'; $c->forward('View::TT'); } # редактирование элемента каталога, тут же может быть добавлена картинка sub element_edit :Chained('chain4') :PathPart('edit') :Args(0) { my ( $self, $c ) = @_; my $id = $c->stash->{id}; my $name = $c->request->params->{name} || ""; my $filestorage = app::Model::FileStorage->new; my $dbh = $c->model('DBI')->dbh; if (length($name) > 1) { my $upload = $c->req->upload('img_name'); my $res = $filestorage->put($upload); my $img_name = ($res ? $upload->filename : ''); my $sql = qq{UPDATE catalog SET name='$name', img_name = '$img_name' WHERE id='$id'}; my $rows = $dbh->do($sql); $c->response->redirect($c->uri_for('/admin/catalog')); } my $element = $dbh->selectrow_hashref(qq{SELECT id, name, img_name FROM catalog WHERE id = '$id'}); $element->{link} = $filestorage->get_file_url($element->{img_name}); $c->stash->{element} = $element; $c->stash->{template} = 'admin/catalog/element_edit.tt'; $c->forward('View::TT'); } # удаление элемента каталога и удаление связанной картинки sub element_delete :Chained('chain4') :PathPart('delete') :Args(0) { my ( $self, $c) = @_; my $id = $c->stash->{id}; my $dbh = $c->model('DBI')->dbh; my $filestorage = app::Model::FileStorage->new; my $element = $dbh->selectrow_hashref(qq{SELECT id, name, img_name FROM catalog WHERE id = '$id'}); my $res = $filestorage->delete($element->{img_name}); my $sql = qq{DELETE FROM catalog WHERE id = '$id'}; my $rows = $dbh->do($sql); $c->response->redirect($c->uri_for('/admin/catalog')); } sub chain1 :PathPart('') :Chained('/') :CaptureArgs(0) { my ( $self, $c) = @_; } sub chain2 :Chained('chain1') :PathPart('admin') :CaptureArgs(0) { my ( $self, $c) = @_; } sub chain3 :Chained('chain2') :PathPart('catalog') :CaptureArgs(0) { my ( $self, $c) = @_; } sub chain4 :Chained('chain2') :PathPart('catalog') :CaptureArgs(1) { my ( $self, $c, $id ) = @_; $c->stash->{id} = $id; } __PACKAGE__->meta->make_immutable; 1; |
В результате, данное приложение после запуска будет реагировать на следующие адресам:
- http://localhost:3000/admin/catalog
- http://localhost:3000/admin/catalog/create
- http://localhost:3000/admin/catalog/1/edit
- http://localhost:3000/admin/catalog/1/delete
Модуль Catalyst::Request::Upload, документация
Catalyst::Request::Upload - обрабатывает запросы на загрузку файлов в Catalyst. Чтобы задать место хранения временных файлов Catalyst, надо указать в конфиге:
|
1 |
__PACKAGE__->config( uploadtmp => '/path/to/tmpdir' ); |
По умолчанию, Catalyst будет использовать системную директорию для хранения временных файлов - temp .
$upload->new
Создание нового объекта Catalyst::Request::Upload. Объект создается автоматически, при вызове
my $upload = $c->req->upload('field');
$upload->copy_to
Копирует временный файл в указанную директорию. Копирование реализовано с помощью File::Copy. Возвращает true в случае успешного завершения операции, и false - в случае неудачи.
$upload->fh
Возвращает файловый дескриптор (IO::File) для временного файла.
$upload->link_to
Создает жесткую ссылку для временного файла. Возвращает true в случае успешного завершения операции, и false - в случае неудачи.
$upload->headers
Возвращает объект HTTP::Headers для запроса, во время которого был загружен файл.
Общий пример для нескольких методов, приведенных ниже:
|
1 2 |
print "INFO: ".$upload->filename." | ".$upload->size." | ". $upload->basename." | ".$upload->tempname." | ".$upload->type."\n"; |
Вывод (запуск скрипта был под windows):
|
1 2 |
INFO: ny_small.jpg | 47595 | ny_small.jpg | C:\DOCUME~1\administrator\LOCALS~1\Temp\Qq6DEOrd_O.jpg | image/jpeg |
$upload->size
Возвращает размер загруженного файла в байтах.
$upload->basename
Возвращает имя файла. При этом, имя будет обработано регулярным выражением basename =~ s|[^\w\.-]+|_|g , чтобы избежать появления имен со специальными символами, которые могут некорректно обрабатываться операционной системой.
$upload->tempname
Возвращает путь к временному файлу, который был создан Catalyst-приложением после получения запроса от клиента.
$upload->type
Возвращает Content-Type для отправленного клиентом файла.
Для получения более полной информации, смотрите официальную документацию на модуль Catalyst::Request::Upload.
Шаблоны - формы для загрузки файлов
1. Страница со списком товаров
root\src\admin\catalog\list.tt :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<table> [% FOREACH element = catalog %] <tr> <td> <a href="/admin/catalog/[% element.id %]/edit">[% element.name %]</a><br> <a href="/admin/catalog/[% element.id %]/delete">Удалить</a> <br><br> <img src="[% element.link %]"> </td> </tr> [% END %] </table> <br><br> <a href="/admin/catalog/create">Добавить новый товар</a> |
2. Страница создания нового товара
root\src\admin\catalog\element_create.tt :
|
1 2 3 4 5 6 7 8 9 |
<form action="/admin/catalog/create" method="POST"> Название товара <input type="text" size="20" name="name" maxlength="250"> <br> Картинка товара <input type="text" size="20" name="img_name" maxlength="250"> <br> <input value="Создать новую позицию" type="submit"> </form> |
3. Страница редактирования товара
root\src\admin\catalog\element_edit.tt :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<form action="/admin/catalog/[% element.id %]/edit" method="POST" enctype="multipart/form-data"> Название товара <input type="text" size="20" name="name" maxlength="250" value="[% element.name %]"> <br> Картинка товара <input type="file" size="20" name="img_name" maxlength="250" value="[% element.img_name %]"> <br> <input value="Обновить данные" type="submit"> </form> <br><br> <img src="[% element.link %]"> |
4. В каталоге root должен быть создан каталог static, в каталоге static создаем каталог filestorage, в каталоге filestorage создаем каталог images.
5. Для того, чтобы картинки отображались, в модуле \lib\app.pm правим конфигурационный блок:
|
1 2 3 4 5 6 7 8 9 10 11 |
... __PACKAGE__->config( 'Plugin::Static::Simple' => { include_path => [ '/static', app->config->{root}, ], ignore_dirs => [ qw/images css js help filestorage/ ], }, ); ... |
Полезные ссылки
search.cpan.org: Catalyst::Request::Upload