В первой статье я рассказывал о том, как написать простое веб-приложение с применением Sinatra и DataMapper. В этот раз мы добавим множество новых фич и улучшим код в целом.
В этом разделе мы подготовим почву для всех будущих изысканий.
Итак, в прошлый раз мы получили приложение со следующей файловой структурой:
При этом весь код находится в одном файле myapp.rb. На мой взгляд, это весьма неудобно и некрасиво, поэтому в этот раз мы начнем с создания папок и файлов, часть из которых пригодится нам по ходу повествования. У меня получилась следующая структура:
В папке files у нас у нас лежат файлы, доступные извне. В папке lib — библиотеки. Папка tasks будет содержать rake-задачи (rake-tasks). В папку manual мы будем класть файлы, минуя веб-интерфейс. Public — хранилище файлов, отдаваемых браузеру напрямую. В test положим, как ни странно, тесты. Наконец, views нам уже знакома — там находятся шаблоны для генерации HTML страниц (пока он у нас всего один).
Теперь давайте установим Sinatra локально (прямо в папку с приложением) — это позволит нам не заботиться об установки Sinatra gem'а. Для этого загрузим код Синатры из git-репозитория. Для этого у вас должен быть установлен git:
Основной код нашего приложения разместим в файле init.rb, предварительно изменив его начало на такое:
Этот код подключает Синатру и конфигурационный файл config.rb из папки lib — его-то мы сейчас и создадим и напишем в нем следующий (пока ничего не делающий) код:
Затем нам пригодится Rakefile — специальный файл для утилиты rake, который указывает ей, где брать задачи (tasks). Файл под именем Rakefile (без расширения), расположенный в корневой директории, должен содержать следующее:
В нём мы просто загружаем все rake-файлы из каталога lib/tasks и его подкаталогов (для этого применяется регулярное выражение /**/*.rake).
Код приложения, лежащий в одном файле — это не только неудобно, но и неправильно :) Поэтому сейчас мы начнем с того, что вынесем наш класс StoredFile в отдельный, простите за каламбур, файл. В папке lib у нас должен появиться файл stored_file.rb со следующим содержимым:
А в конец бесчисленных require в init.rb мы добавим строку
Функциональность нашего приложения не изменилась ни на йоту, но выглядит оно теперь немного поприличнее.
В первой версии приложения мы использовали для генерации HTML страниц язык Erb (Embedded RuBy), который представляет собой HTML-код с включенными фрагментами Ruby-кода. Этот подход традиционен в Rails, но мы-то занимаемся маргинальными решениями :) В связи с этим я принял решение применять в качестве языка для шаблонов Haml.
Haml — это язык разметки, проповедующий красоту, как главное божество шаблона. И действительно, код на Haml, на мой взгляд, выглядит симпатичнее (а поговаривают, что и работает он быстрее). Еще одним важным свойством Haml'а является то, что в он не позволит вам не закрыть тег (потому что вы вообще не будете использовать HTML-теги в явной форме)
Установка проста:
Давайте создадим файл list.haml в нашем каталоге views и займемся его наполнением. Haml похож на Python тем, что использует отступы в коде в качестве ограничителей блоков (в HTML для этого используются закрывающие теги, а в Erb ключевое слово end). Таким образом, HTML-код
На Haml выглядит следующим образом:
Что, согласитесь, гораздо короче.
Немного о синтаксисе, который нам пригодится:
Создает тег <TAG /> или <TAG></TAG> — в зависимости от тега (для hr будет выбран первый вариант, для a — второй)
Создает тег <TAG>CONTENT</TAG>.
Превратится в <tag>CONTENT<tag>. При этом CONTENT может тоже содержать оператор % и любые другие операторы.
Для задания аттрибутов используется тот же синтаксис, что и для хешей в Ruby:
Станет <span class=«header» id=«news_135» style=«float:left»>Лось съел 3 килограмма собачьей еды и залаял!</span>
На самом деле, для задания класса и id существует более простой синтаксис (весьма напоминающий CSS):
А для создания div'ов с нужным классом, всё еще проще:
Теперь вмешаем сюда Ruby. Для выполнения строки кода (аналог <% %> в Erb) поставьте перед ней знак "-":
Для превращения результата в строку используется оператор "=":
Выведет текущее время.
Разумеется, можно использовать = в комплекте с операторами тегов:
И последнее, что нам нужно знать для написания нашего Haml-шаблона, это оператор "!!!". По умолчанию он создает DOCTYPE Transitional. А большего нам от него и не надо.
У меня получился следующий шаблон для отображения списка файлов и формы загрузки нового файла:
Не могу не отметить, что получившийся шаблон весьма элегантен (особенно, если у вас есть текстовый редактор с подсветкой Haml-синтаксиса).
Давайте представим, что нам могут понадобится еще и другие представления для нашего приложения (а они нам понадобятся) — не будем же мы в каждом писать общую шапку с "!!!", "%html" и так далее. Для решения этой проблемы в Sinatra, как и в Rails, есть механизм макетов (layouts). Макет можно рассматривать, как шаблон верхнего уровня — в макет вставляется содержимое шаблона и уже вместе они уходят пользователю.
Для использования макета нам надо просто поместить файл layout.haml в папку views и он автоматически начнет использоваться нашим приложением. Кстати, точно также мы могли бы создать макет layout.erb, который бы использовался для Erb шаблонов.
Из шаблона в макет перекочует следующий код:
Новой тут является только строчка "= yield" в конце. Она, как, опять же, и в случае с Erb макетами, вызывает обработку Haml-шаблона в месте своего вызова (то есть внутри div'а с классом main). Код, который мы разместили в layout.haml, само собой, надо удалить из list.haml.
Всё! Теперь мы солидные люди и используем Haml в качестве языка для шаблонов. Осталась самая малость — указать Sinatra на то, что надо выводить не list.erb, а list.haml. Для этого идем в наш init.rb, и заменяем строчку «erb :list», на «haml :list». Вот теперь всё — мы используем Haml и радуемся жизни.
Хотя, я, пожалуй, готов поставить немного денег на то, что у вас ничего не получилось и Sinatra выдал вам сообщение об ошибке, связанной с отсупами в файле шаблона или макета. Дело вот в чем: Haml работает только с шаблонами и макетами, в которых в качестве отступа используется 2 пробела — именно пробела, а не знака табуляции, к примеру. И именно 2. Исправьте это и, скорее всего, ошибка исчезнет. В частности, если вы копировали код прямо со страницы, то у вас совершенно точно присутствуют знаки табуляции вместо пробелов.
Я уже объяснял, почему пользователям не стоит давать возможность знать id файла, который они скачивают, поэтому останавливаться на этом не буду. Теперь пришла пора решить эту проблему. В качестве адресов для скачивания мы будем использовать сорокасимвольный SHA-1 дайджест (digest) от имени файла в качестве его идентификатора. Для этого проделаем следующее:
Добавим в stored_file.rb новое поле для хранения дайджеста:
В конце файла у нас написано «DataMapper.auto_upgrade!», а значит, база данных будет автоматически обновлена DataMapper'ом.
Откроем файл init.rb и изменим все блоки кроме первого (get '/'):
Осталось изменить наш шаблон:
Готово! Теперь наши ссылки выглядят вот так:
Это совсем простой пункт. Количество скачиваний и размер файла мы будем хранить в БД, а значит надо расширить наше описание класса StoredFile:
Upload и download файла выглядят теперь так:
Отобразим мы новые данные следующим образом:
Этот раздел не имеет прямого отношения к изучению основных технологий, но зато позволяет чуть больше вжиться в новую «систему ценностей». Согласитесь, что удалять файл без каких-либо подтверждений — не самое лучшее решение даже для домашнего использования: кликнуть можно и случайно. Поэтому добавим-ка мы подтверждение при попытке удалить файл.
В папке public создадим файл script.js со следующим элементарным содержимым:
Затем подключим этот JavaScript-файл макете:
А в нашем шаблоне внесем небольшую правку:
Таким образом, при нажатии кнопки «Удалить» у нас будут спрашивать, уверены ли мы, а это лишний шанс одуматься.
Вернемся к оригинальной постановке задачи: файлообменник с удобным интерфейсом, работающий на домашнем сервере. Находясь дома, не так уж удобно заходить на какой-то «сайт» и загружать файл — гораздо проще кинуть его в расшаренную папку, чтобы он сам добавился к нам в список.
К счастью это несложно, если мы будем использовать Rake — аналог Make для Ruby. Эта утилита позволяет создавать так называемые задачи (tasks), которые выполняются командой rake ИМЯ_ЗАДАЧИ. При запуске rake ищет файл Rakefile в текущей директории и выполняет указанные в нем команды, потом ищет задачу с указанным именем и выполняет её.
Что же такое rake task? Это не более чем специальным образом оформленный Ruby-код. В нашем случае, rake task будет брать все файлы из каталога manual и имитировать их загрузку обычным образом (то есть копировать их в папку files и создавать необходимые записи в БД).
На случай, если у вас вдруг отсутствует Rake (что маловероятно), установка проводиться традиционным для Ruby образом:
Итак, наш файл Rakefile, который мы создали в начале сообщает Rake, что ему необходимо загрузить все файлы с расширением .rake из каталога lib/tasks. Значит, именно там мы и создадим наш файл, который назовем manual_monitor.rake. Напишем же мы в него следующее:
Сначала мы подключаем ftools (файловые утилиты) и наш stored_file.rb (описывающий класс StoredFile — понадобится нам для работы с БД. Далее идет описание задачи check_files. Как можно догадаться, в общем виде задача описывается так:
Необязательная команда desc перед задачей позволяет задать произвольное текстовое описание для задачи (его можно просмотреть по команде «rake --tasks»)
Внутри самой задачи мы делаем почти то же самое, что и в коде, загружающем файл через броузер, но с парой отличий:
Можно проверить, как работает наша задача, поместив в папку manual какой-либо файл и вызвав команду
Rake должен сообщить, что файл скопирован успешно. Через веб-интерфейс мы можем убедиться, что так и есть.
Теперь нам надо научиться вызывать нашу команду автоматически. Я рассматривал два варианта: через crontab и посредством Ruby-демона. Мы с вами, конечно, извращенцы и маргиналы, но изобретать велосипед (и просто так расходовать системные ресурсы) в этом случае я не стал и просто воспользовался crontab'ом. Впрочем, если кому-то интересен вариант запуска через Ruby-демона, могу рассказать и про него.
Мой crontab выглядит так
То есть команда выполняется каждую минуту.
Изначально я планировал уложиться в одну статью, но практика показала, что даже двух мало. В третьей части я планирую рассказать про:
Вот сейчас написал список и уже сомневаюсь, что и трех частей мне будет достаточно… Но если вы хотите узнать что-то еще — пишите в комментариях, разберёмся :)
В очередной раз, спасибо за внимание!
Предварительная подготовка
В этом разделе мы подготовим почву для всех будущих изысканий.
Итак, в прошлый раз мы получили приложение со следующей файловой структурой:
/ myapp.rb views/ list.erb files/
При этом весь код находится в одном файле myapp.rb. На мой взгляд, это весьма неудобно и некрасиво, поэтому в этот раз мы начнем с создания папок и файлов, часть из которых пригодится нам по ходу повествования. У меня получилась следующая структура:
/ files/ lib/ tasks/ manual/ public/ test/ views/
В папке files у нас у нас лежат файлы, доступные извне. В папке lib — библиотеки. Папка tasks будет содержать rake-задачи (rake-tasks). В папку manual мы будем класть файлы, минуя веб-интерфейс. Public — хранилище файлов, отдаваемых браузеру напрямую. В test положим, как ни странно, тесты. Наконец, views нам уже знакома — там находятся шаблоны для генерации HTML страниц (пока он у нас всего один).
Теперь давайте установим Sinatra локально (прямо в папку с приложением) — это позволит нам не заботиться об установки Sinatra gem'а. Для этого загрузим код Синатры из git-репозитория. Для этого у вас должен быть установлен git:
cd lib git clone git://github.com/bmizerany/sinatra.git rm -rf sinatra/.git rm sinatra/.gitignore cd ..
Основной код нашего приложения разместим в файле init.rb, предварительно изменив его начало на такое:
require 'rubygems' require File.expand_path(File.dirname(__FILE__) + '/lib/sinatra/lib/sinatra') require File.expand_path(File.dirname(__FILE__) + '/lib/config') require 'ftools' require 'dm-core' require 'dm-validations' require 'dm-timestamps'
Этот код подключает Синатру и конфигурационный файл config.rb из папки lib — его-то мы сейчас и создадим и напишем в нем следующий (пока ничего не делающий) код:
configure do # Здесь будут параметры конфигурации для Синатры end
Затем нам пригодится Rakefile — специальный файл для утилиты rake, который указывает ей, где брать задачи (tasks). Файл под именем Rakefile (без расширения), расположенный в корневой директории, должен содержать следующее:
require 'rake' require 'rake/testtask' require 'rake/rdoctask' Dir["#{File.dirname(__FILE__)}/lib/tasks/**/*.rake"].sort.each { |ext| load ext }
В нём мы просто загружаем все rake-файлы из каталога lib/tasks и его подкаталогов (для этого применяется регулярное выражение /**/*.rake).
Разделяем код
Код приложения, лежащий в одном файле — это не только неудобно, но и неправильно :) Поэтому сейчас мы начнем с того, что вынесем наш класс StoredFile в отдельный, простите за каламбур, файл. В папке lib у нас должен появиться файл stored_file.rb со следующим содержимым:
require 'dm-core' require 'dm-validations' require 'dm-timestamps' DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/files.sqlite3") class StoredFile include DataMapper::Resource property :id, Integer, :serial => true # primary serial key property :filename, String, :nullable => false # cannot be null property :created_at, DateTime default_scope(:default).update(:order => [:created_at.desc]) end DataMapper.auto_upgrade!
А в конец бесчисленных require в init.rb мы добавим строку
require 'lib/stored_file'
Функциональность нашего приложения не изменилась ни на йоту, но выглядит оно теперь немного поприличнее.
Viva la Haml!
В первой версии приложения мы использовали для генерации HTML страниц язык Erb (Embedded RuBy), который представляет собой HTML-код с включенными фрагментами Ruby-кода. Этот подход традиционен в Rails, но мы-то занимаемся маргинальными решениями :) В связи с этим я принял решение применять в качестве языка для шаблонов Haml.
Haml — это язык разметки, проповедующий красоту, как главное божество шаблона. И действительно, код на Haml, на мой взгляд, выглядит симпатичнее (а поговаривают, что и работает он быстрее). Еще одним важным свойством Haml'а является то, что в он не позволит вам не закрыть тег (потому что вы вообще не будете использовать HTML-теги в явной форме)
Установка проста:
sudo gem install haml
Давайте создадим файл list.haml в нашем каталоге views и займемся его наполнением. Haml похож на Python тем, что использует отступы в коде в качестве ограничителей блоков (в HTML для этого используются закрывающие теги, а в Erb ключевое слово end). Таким образом, HTML-код
<div> <span>Some text</span> </div> <ul> <li>Item 1</li> <li>Item 2</li> </ul>
На Haml выглядит следующим образом:
%div %span Some text %ul %li Item 1 %li Item 2
Что, согласитесь, гораздо короче.
Немного о синтаксисе, который нам пригодится:
%TAG
Создает тег <TAG /> или <TAG></TAG> — в зависимости от тега (для hr будет выбран первый вариант, для a — второй)
%TAG CONTENT
Создает тег <TAG>CONTENT</TAG>.
%TAG CONTENT
Превратится в <tag>CONTENT<tag>. При этом CONTENT может тоже содержать оператор % и любые другие операторы.
Для задания аттрибутов используется тот же синтаксис, что и для хешей в Ruby:
%span{ :class => "header", :id => "news_135", :style => "float:left"} Лось съел 3 килограмма собачьей еды и залаял!
Станет <span class=«header» id=«news_135» style=«float:left»>Лось съел 3 килограмма собачьей еды и залаял!</span>
На самом деле, для задания класса и id существует более простой синтаксис (весьма напоминающий CSS):
%span.header#news_135 Еще один желтый заголовок
А для создания div'ов с нужным классом, всё еще проще:
.content = %div.content = %div{ :class => "content" } = <div class="content">
Теперь вмешаем сюда Ruby. Для выполнения строки кода (аналог <% %> в Erb) поставьте перед ней знак "-":
- str = @my_object.get_some_string
Для превращения результата в строку используется оператор "=":
- now = Time.now = now
Выведет текущее время.
Разумеется, можно использовать = в комплекте с операторами тегов:
%span#current_time= Time.now
И последнее, что нам нужно знать для написания нашего Haml-шаблона, это оператор "!!!". По умолчанию он создает DOCTYPE Transitional. А большего нам от него и не надо.
Пишем шаблон
У меня получился следующий шаблон для отображения списка файлов и формы загрузки нового файла:
!!! %html{ :xmlns => "http://www.w3.org/1999/xhtml" } %head %title Файлопомойка имени меня %meta{ :"http-equiv" => "content-type", :content => "text/html;charset=UTF-8" } %body .main - if @files.empty? %h1 Файлов нет. - else .list %h1 Список файлов %table{ :cellspacing => 0} %tr %th Файл %th Загружен %th Удалить - @files.each do |file| %tr %td.filename %a{ :href => "/#{file.id}", :title => file.filename }= file.filename %td.created_at= file.created_at.strftime("%d %b") %td.delete %a{ :href => "/#{file.id}/delete", :title => "Удалить" } Удалить .upload %h1 Добавить %form{ :name => "new_file", :enctype => "multipart/form-data", :method => "post", :action => "/" } %input{ :name => "file", :type => "file" } %br %input{ :type => "submit", :value => "Закачать" }
Не могу не отметить, что получившийся шаблон весьма элегантен (особенно, если у вас есть текстовый редактор с подсветкой Haml-синтаксиса).
Отделяем макет от шаблона
Давайте представим, что нам могут понадобится еще и другие представления для нашего приложения (а они нам понадобятся) — не будем же мы в каждом писать общую шапку с "!!!", "%html" и так далее. Для решения этой проблемы в Sinatra, как и в Rails, есть механизм макетов (layouts). Макет можно рассматривать, как шаблон верхнего уровня — в макет вставляется содержимое шаблона и уже вместе они уходят пользователю.
Для использования макета нам надо просто поместить файл layout.haml в папку views и он автоматически начнет использоваться нашим приложением. Кстати, точно также мы могли бы создать макет layout.erb, который бы использовался для Erb шаблонов.
Из шаблона в макет перекочует следующий код:
!!! %html{ :xmlns => "http://www.w3.org/1999/xhtml" } %head %title Файлопомойка имени меня %meta{ :"http-equiv" => "content-type", :content => "text/html;charset=UTF-8" } %body .main = yield
Новой тут является только строчка "= yield" в конце. Она, как, опять же, и в случае с Erb макетами, вызывает обработку Haml-шаблона в месте своего вызова (то есть внутри div'а с классом main). Код, который мы разместили в layout.haml, само собой, надо удалить из list.haml.
Всё! Теперь мы солидные люди и используем Haml в качестве языка для шаблонов. Осталась самая малость — указать Sinatra на то, что надо выводить не list.erb, а list.haml. Для этого идем в наш init.rb, и заменяем строчку «erb :list», на «haml :list». Вот теперь всё — мы используем Haml и радуемся жизни.
Хотя, я, пожалуй, готов поставить немного денег на то, что у вас ничего не получилось и Sinatra выдал вам сообщение об ошибке, связанной с отсупами в файле шаблона или макета. Дело вот в чем: Haml работает только с шаблонами и макетами, в которых в качестве отступа используется 2 пробела — именно пробела, а не знака табуляции, к примеру. И именно 2. Исправьте это и, скорее всего, ошибка исчезнет. В частности, если вы копировали код прямо со страницы, то у вас совершенно точно присутствуют знаки табуляции вместо пробелов.
Прячем ссылки от посторонних глаз
Я уже объяснял, почему пользователям не стоит давать возможность знать id файла, который они скачивают, поэтому останавливаться на этом не буду. Теперь пришла пора решить эту проблему. В качестве адресов для скачивания мы будем использовать сорокасимвольный SHA-1 дайджест (digest) от имени файла в качестве его идентификатора. Для этого проделаем следующее:
Добавим в stored_file.rb новое поле для хранения дайджеста:
property :sha, String
В конце файла у нас написано «DataMapper.auto_upgrade!», а значит, база данных будет автоматически обновлена DataMapper'ом.
Откроем файл init.rb и изменим все блоки кроме первого (get '/'):
post '/' do tempfile = params['file'][:tempfile] filename = params['file'][:filename] digest = Digest::SHA1.hexdigest(filename) # Вычисляем дайджест @file = StoredFile.create :filename => filename, :sha => digest # Записываем имя файла и дайждест в БД File.copy(tempfile.path, "./files/#{@file.id}.upload") redirect '/' end # download file get '/:sha' do # теперь ищем файлы по значению sha, а не по id @file = StoredFile.first :sha => params[:sha] send_file "./files/#{@file.id}.upload", :filename => @file.filename, :type => 'Application/octet-stream' end # delete file get '/:sha/delete' do # и удаляем тоже по sha, а не по id @file = StoredFile.first :sha => params[:sha] File.delete("./files/#{@file.id}.upload") @file.destroy redirect '/' end
Осталось изменить наш шаблон:
%td.filename %a{ :href => "/#{<strong>file.sha</strong>}", :title => file.filename }= file.filename %td.created_at= file.created_at.strftime("%d %b") %td.delete %a{ :href => "/#{<strong>file.sha</strong>}/delete", :title => "Удалить"} Удалить
Готово! Теперь наши ссылки выглядят вот так:
localhost:4567/1c62e8aa8072c8a3cd5df1ba49bb4513bc1d8a88
Добавляем счетчик скачиваний и указание размера файла
Это совсем простой пункт. Количество скачиваний и размер файла мы будем хранить в БД, а значит надо расширить наше описание класса StoredFile:
property :downloads, Integer, :default => 0 property :filesize, Integer, :nullable => false
Upload и download файла выглядят теперь так:
post '/' do tempfile = params['file'][:tempfile] filename = params['file'][:filename] digest = Digest::SHA1.hexdigest(filename) @file = StoredFile.create :filename => filename, :sha => digest, :filesize => File.size(tempfile.path) File.copy(tempfile.path, "./files/#{@file.id}.upload") redirect '/' end get '/:sha' do @file = StoredFile.first :sha => params[:sha] @file.downloads += 1 @file.save send_file "./files/#{@file.id}.upload", :filename => @file.filename, :type => 'Application/octet-stream' end
Отобразим мы новые данные следующим образом:
%tr %th Файл %th Загружен %th Скачан %th Удалить - @files.each do |file| %tr %td.filename %a{ :href => "/#{file.sha}", :title => file.filename }= file.filename = "(#{file.filesize/1024} Kb)" %td.created_at= file.created_at.strftime("%d %b") %td.downloads= file.downloads %td.delete %a{ :href => "/#{file.sha}/delete", :title => "Удалить" } Удалить
Немного JavaScript
Этот раздел не имеет прямого отношения к изучению основных технологий, но зато позволяет чуть больше вжиться в новую «систему ценностей». Согласитесь, что удалять файл без каких-либо подтверждений — не самое лучшее решение даже для домашнего использования: кликнуть можно и случайно. Поэтому добавим-ка мы подтверждение при попытке удалить файл.
В папке public создадим файл script.js со следующим элементарным содержимым:
function ORLY() { return confirm('Вы уверены'); }
Затем подключим этот JavaScript-файл макете:
%meta{ :"http-equiv" => "content-type", :content => "text/html;charset=UTF-8" } %script{ :type => "text/javascript", :src => "/script.js"} %body
А в нашем шаблоне внесем небольшую правку:
%td.delete %a{ :href => "/#{file.sha}/delete", :title => "Удалить", :onclick => "return ORLY();"} Удалить
Таким образом, при нажатии кнопки «Удалить» у нас будут спрашивать, уверены ли мы, а это лишний шанс одуматься.
Автоматически ищем и добавляем новые файлы
Вернемся к оригинальной постановке задачи: файлообменник с удобным интерфейсом, работающий на домашнем сервере. Находясь дома, не так уж удобно заходить на какой-то «сайт» и загружать файл — гораздо проще кинуть его в расшаренную папку, чтобы он сам добавился к нам в список.
К счастью это несложно, если мы будем использовать Rake — аналог Make для Ruby. Эта утилита позволяет создавать так называемые задачи (tasks), которые выполняются командой rake ИМЯ_ЗАДАЧИ. При запуске rake ищет файл Rakefile в текущей директории и выполняет указанные в нем команды, потом ищет задачу с указанным именем и выполняет её.
Что же такое rake task? Это не более чем специальным образом оформленный Ruby-код. В нашем случае, rake task будет брать все файлы из каталога manual и имитировать их загрузку обычным образом (то есть копировать их в папку files и создавать необходимые записи в БД).
На случай, если у вас вдруг отсутствует Rake (что маловероятно), установка проводиться традиционным для Ruby образом:
sudo gem install rake
Итак, наш файл Rakefile, который мы создали в начале сообщает Rake, что ему необходимо загрузить все файлы с расширением .rake из каталога lib/tasks. Значит, именно там мы и создадим наш файл, который назовем manual_monitor.rake. Напишем же мы в него следующее:
require 'ftools' require 'lib/datamapper/stored_file' desc "Checks directory 'manual' and uploads all files from it" task : manual_monitor do Dir["manual/*"].each do |path| file = File.new(path) filename = File.basename(file.path) digest = Digest::SHA1.hexdigest(filename) stored_file = StoredFile.create :filename => filename, :sha => digest, :filesize => File.size(file.path) File.move(path, "./files/#{stored_file.id}.upload") puts "File #{path} succesfully uploaded. ID = #{stored_file.id}" end end
Сначала мы подключаем ftools (файловые утилиты) и наш stored_file.rb (описывающий класс StoredFile — понадобится нам для работы с БД. Далее идет описание задачи check_files. Как можно догадаться, в общем виде задача описывается так:
task :TASK_NAME do CODE end
Необязательная команда desc перед задачей позволяет задать произвольное текстовое описание для задачи (его можно просмотреть по команде «rake --tasks»)
Внутри самой задачи мы делаем почти то же самое, что и в коде, загружающем файл через броузер, но с парой отличий:
- Мы работаем со всеми файлами из директории manual
- Мы перемещаем файл из manual в files (чтобы не обрабатывать его повторно)
Можно проверить, как работает наша задача, поместив в папку manual какой-либо файл и вызвав команду
rake manual_monitor
Rake должен сообщить, что файл скопирован успешно. Через веб-интерфейс мы можем убедиться, что так и есть.
Теперь нам надо научиться вызывать нашу команду автоматически. Я рассматривал два варианта: через crontab и посредством Ruby-демона. Мы с вами, конечно, извращенцы и маргиналы, но изобретать велосипед (и просто так расходовать системные ресурсы) в этом случае я не стал и просто воспользовался crontab'ом. Впрочем, если кому-то интересен вариант запуска через Ruby-демона, могу рассказать и про него.
Мой crontab выглядит так
*/1 * * * * cd /path/to/my/app/; rake manual_monitor
То есть команда выполняется каждую минуту.
Уфф!
Изначально я планировал уложиться в одну статью, но практика показала, что даже двух мало. В третьей части я планирую рассказать про:
- Нормальную авторизацию (чтобы разрешить всем скачивать файлы, но запретить кому попало их загружать и удалять)
- Вспомогательные методы (helpers)
- Как добавить задержку при скачивании (чтобы можно было убедиться, что ты пришел по верной ссылке и не скачивать стомегабайтный файл по GPRS)
- Конфигурирование
- Создание CSS с помощью SASS
- Тестирование кода
- Оценки производительности
Вот сейчас написал список и уже сомневаюсь, что и трех частей мне будет достаточно… Но если вы хотите узнать что-то еще — пишите в комментариях, разберёмся :)
В очередной раз, спасибо за внимание!