Предыдущие две статьи (раз и два) оказались гораздо популярнее, чем я мог ожидать. А теперь пришла пора третьей, завершающей статьи о файлообменнике на базе Sinatra и DataMapper.
В этот раз мы рассмотрим:
В предыдущий раз Kane заметил важную ошибку в приложении: ключом для загрузки файла является дайджест от его имени -, но что же случится, если мы загрузим два файла с одинаковыми именами? К сожалению, раз их дайджесты совпадают, в текущей версии мы даже не сможем сказать, какой из файлов будет отдаваться пользователю — первый или второй. Но, к счастью, эту ошибку легко исправить: мы просто добавим в ссылку на скачивание еще и id файла, который мы хотим скачать. Так что два одинаковых файла будут иметь разные ссылки вида:
Но пользователи всё равно не смогут определить ссылки для скачивания других файлов (единственное, что можно сделать — перебирать ID для того же дайджеста в надежде скачать одноименный файл). Для этого исправления нам придется поменять совсем немного: код в init.rb и шаблон list.haml.
init.rb:
list.haml:
Теперь файлы-тезки нам не страшны!
Представим себе ситауацию: ваш знакомый просит прислать ему фотографию, сделанную вами. Вы скидываете ее в свой файлообменник и присылаете ему ссылку на файлообменник, он начинает ее скачивать. А А теперь усложним задачу: размер файла 20 мегабайт, а знакомый сидит на GPRS. Естественно, знай он размер файла заранее, он не стал бы его скачивать, чтобы сэкономить дорогущий трафик. Решение: создадим страницу, которая будет показываться перед загрузкой и разместим на ней информацию об имени и размере файла.
Начнем с init.rb:
Итак: если в параметрах ссылки передано «nowait=true», то скачивание начинается мгновенно, в противном случае мы просто показываем шаблон download.haml.
А вот, собственно, и он:
download.haml:
В начале идет простой JavaScript, который ждет 10 секунд и редиректит нас на ту же ссылку, но с параметром «nowait=true», а затем сам текст, с указанием на имя файла и его размер.
Осталось только расширить шаблон списка файлов, чтобы он содержал две ссылки — для немедленного скачивания (ее будем использовать мы сами) и для скачивания с задержкой (эту ссылку мы будем отсылать по аське). Выглядит так:
list.haml:
Cтавим галочку и преходим к следующему пункту.
SASS — это часть пакета Haml, отвечающая за создание CSS файлов. С точки зрения синтаксиса SASS расположился между CSS и Haml: он использует схему с селекторами и аттрибутами (CSS), но при этом в качестве ограничителя используются отступы (Haml), а не фигурные скобки.
SASS-файл состоит из набора правил:
Где SELECTOR (S) — один или несколько обычных CSS-селекторов (класс, id, имя тега), а PROPRETY_X/VALUE_X — названия и значения CSS-свойств. Весьма похоже на CSS, но есть и некоторые отличия:
Но вернемся к нашим баранам: SASS файл можно использовать двояко — можно получить из него CSS файл и подключить его к приложению, а можно использовать встроенный в Синатру SASS-шаблонизатор для генерации CSS «на лету». Мы воспользуемся вторым методом не смотря на его бессмысленность:)
init.rb:
layout.haml:
Ну, а мой файл style.sass вы можете посмотреть по этой ссылке.
Теперь у нашего приложения появилось какое-никакое оформление.
Пришла пора приделать к нашему приложению нормальный механизм аутентификации. Мы хотим, чтобы все желающие могли скачивать файлы по прямым ссылкам, но upload и удаление файлов, а также просмотр общего списка должны быть доступны только после ввода пароля (для простоты установим один, жестко закодированный пароль).
Я решил задачу следующим образом: взял модуль HTTP-аутентификации (код по ссылке), положил его в папку lib и внес следующие изменения в init.rb:
Если вкратце, то блок «helpers do … end» выполняется в контексте всех наших блоков — обработчиков URL’ов, то есть мы делаем модуль Sinatra: Authorization доступным внутри приложения. В этом же блоке можно определять методы, которые можно будет использовать в шаблонах и основном приложении (так называемые хелперы — вспомогательные методы, которые позволяют избежать повторений одного и того же кода в шаблонах).
Итак, наше приложение достигло промышленных высот и готово к развертыванию на рабочем сервере. Напомню, что сейчас мы запускаем его командой «ruby init.rb» и оно работает пока открыта консоль с ruby — естественно, это несерьезно — веб-приложение должно запускаться веб-сервером. В качестве веб-сервера я выбираю thin — компактный и чрезвычайно быстрый сервер для Ruby-приложений. Установка проста:
Теперь пришла пора создать несколько папок в каталоге нашего приложения.
В папку config перенесем файл config.rb из папки lib (одновременно поправив к нему путь в init.rb). Для настройки thin нам понадобится файл, который мы назовем thin.yml — создадим его в папке config и запишем следующее:
Мы говорим thin’у, что надо работать в production окружении, сделав chdir в корневой каталог приложения, разместить PID файл в папке tmp, взять Rackup-файл (о нём ниже) в папке config, лог вести в log/thin.log, поддерживать до 1024 одновременных соединений с таймаутом в 30 секунд, держать до 512 постоянных соединений и работать в качестве демона (то есть независимо от наличия залогиненого пользователя в системе).
Теперь о rackup-файле: по сути, это конфигурационный файл для Rack — интерфейса между Ruby и веб-сервером (в нашем случае thin). Этот файл содержит всего две строки:
Первая строка подключает init.rb (то есть, наше приложение), вторая говорит Rack, что надо запустить thin на 3000-м порту и передать ему синатровское приложение.
Дело сделано! Теперь приложение запускается вот такой командой
Мы просто передаем thin’у конфигурационный файл.
Остановка происходит командой
Этот раздел я специально оставил напоследок так как понимаю, что мало кто будет тестировать приложения под Синатру. Не буду вдаваться в подробности и рассказывать, что предствляет из себя RSpec, просто покажу, как выглядят спеки.
Никаких хитростей — те же describe/it/should, что и в Rails, к примеру. Главное, не забывать подключать sinatra/test/rspec.
Сначала — бенчмарк главной страницы (список файлов).
ab -n 1000 -c 1 -A admin: secret http://127.0.0.1:3000/
ab -n 1000 -c 10 -A admin: secret http://127.0.0.1:3000/
ab -n 1000 -c 100 -A admin: secret http://127.0.0.1:3000/
Upload файлов на сервер (размер файла 1.5 Kb)
ab -n 1000 -c 1 -A admin: secret -T 'application/x-www-form-urlencoded' -p post.data http://127.0.0.1:3000/
ab -n 1000 -c 10 -A admin: secret -T 'application/x-www-form-urlencoded' -p post.data http://127.0.0.1:3000/
ab -n 1000 -c 100 -A admin: secret -T 'application/x-www-form-urlencoded' -p post.data http://127.0.0.1:3000/
Заметьте — производительность практически не изменяется при увеличении количества одновременных запросов в 100 (сто!) раз. Тестирование производилось на Mac Book Core 2 Duo 2.4 Ghz, 2 GB ram при нескольких запущенных в фоне приложениях.
Пришла пора завершать мою случайно начатую эпопею. Надеюсь, вам было интересно и я смог сподвигнуть хотя бы некоторых на изучение немейнстримовых технологий (Sinatra, DataMapper, thin, haml, sass). Приложение в своей последней версии выложено на github. Спасибо всем, кто потратил время на чтение этих немаленких статей.
В этот раз мы рассмотрим:
- Проблему с одинаковыми именами файлов
- Страницу ожидания перед загрузкой
- Создание CSS с помощью SASS
- Аутентификацию
- Запуск из под thin
- Тестирование с помощью RSpec
- Бенчмарки
Одинаковые имена файлов
В предыдущий раз Kane заметил важную ошибку в приложении: ключом для загрузки файла является дайджест от его имени -, но что же случится, если мы загрузим два файла с одинаковыми именами? К сожалению, раз их дайджесты совпадают, в текущей версии мы даже не сможем сказать, какой из файлов будет отдаваться пользователю — первый или второй. Но, к счастью, эту ошибку легко исправить: мы просто добавим в ссылку на скачивание еще и id файла, который мы хотим скачать. Так что два одинаковых файла будут иметь разные ссылки вида:
/DIGEST/ID1 /DIGEST/ID2
Но пользователи всё равно не смогут определить ссылки для скачивания других файлов (единственное, что можно сделать — перебирать ID для того же дайджеста в надежде скачать одноименный файл). Для этого исправления нам придется поменять совсем немного: код в init.rb и шаблон list.haml.
init.rb:
get '/:sha/:id' do @file = StoredFile.first :sha => params[:sha], :id => params[:id] # Далее без изменений get '/:sha/:id/delete' do @file = StoredFile.first :sha => params[:sha], :id => params[:id] # Далее без изменений
list.haml:
%a{ :href => "/#{file.sha}/#{file.id}", :title => file.filename }= file.filename
Теперь файлы-тезки нам не страшны!
Страница ожидания перед загрузкой
Представим себе ситауацию: ваш знакомый просит прислать ему фотографию, сделанную вами. Вы скидываете ее в свой файлообменник и присылаете ему ссылку на файлообменник, он начинает ее скачивать. А А теперь усложним задачу: размер файла 20 мегабайт, а знакомый сидит на GPRS. Естественно, знай он размер файла заранее, он не стал бы его скачивать, чтобы сэкономить дорогущий трафик. Решение: создадим страницу, которая будет показываться перед загрузкой и разместим на ней информацию об имени и размере файла.
Начнем с init.rb:
get '/:sha/:id' do @file = StoredFile.first :sha => params[:sha], :id => params[:id] unless params[:nowait] == 'true' haml :download else @file.downloads += 1 @file.save send_file "./files/#{@file.id}.upload", :filename => @file.filename, :type => 'Application/octet-stream' end end
Итак: если в параметрах ссылки передано «nowait=true», то скачивание начинается мгновенно, в противном случае мы просто показываем шаблон download.haml.
А вот, собственно, и он:
download.haml:
%script{ :type => "text/javascript" } nowait = '?nowait=true'; var timeout = true; setTimeout('if (timeout) {window.location = window.location + nowait;}', 10000); %h1 Загрузка файла .info Вы собираетесь скачать файл %span.filename>= " #{@file.filename}" , размером %span.filesize = @file.filesize/1024 килобайт. Скачивание начнется в течение %span#seconds 10 секунд. Нажмите на %a{ :href => "/#{params[:sha]}?nowait=true", :onclick => 'timeout = false;' }эту ссылку если не хотите ждать
В начале идет простой JavaScript, который ждет 10 секунд и редиректит нас на ту же ссылку, но с параметром «nowait=true», а затем сам текст, с указанием на имя файла и его размер.
Осталось только расширить шаблон списка файлов, чтобы он содержал две ссылки — для немедленного скачивания (ее будем использовать мы сами) и для скачивания с задержкой (эту ссылку мы будем отсылать по аське). Выглядит так:
list.haml:
%td.filename %a{ :href => "/#{file.sha}/#{file.id}?nowait=true", :title => file.filename }= file.filename = "(#{file.filesize/1024} Kb)" %a{ :href => "/#{file.sha}/#{file.id}" } Для пересылки
Cтавим галочку и преходим к следующему пункту.
SASS
SASS — это часть пакета Haml, отвечающая за создание CSS файлов. С точки зрения синтаксиса SASS расположился между CSS и Haml: он использует схему с селекторами и аттрибутами (CSS), но при этом в качестве ограничителя используются отступы (Haml), а не фигурные скобки.
SASS-файл состоит из набора правил:
SELECTOR(S) :PROPERTY1 VALUE1 :PROPERTY2 VALUE2 ... :PROPERTY_N VALUE_N
Где SELECTOR (S) — один или несколько обычных CSS-селекторов (класс, id, имя тега), а PROPRETY_X/VALUE_X — названия и значения CSS-свойств. Весьма похоже на CSS, но есть и некоторые отличия:
- Вместо фигурных скобок используется отступ в 2 пробела
- Можно использовать вложенные правила (чертовски удобно в случае сложного набора CSS-правил):
#main p :color #00ff00 :width 97% .redbox :background-color #ff0000 :color #000000
компилируется в#main p { color: #00ff00; width: 97%; } #main p .redbox { background-color: #ff0000; color: #000000; }
- Вложенные «пространства имен»:
.funky :font :family fantasy :size 30em :weight bold
Скомпилируется в.funky { font-family: fantasy; font-size: 30em; font-weight: bold; }
- Использование констант и арифметических операций:
!main_width = 10 !unit1 = em !unit2 = px !bg_color = #a5f39e #main :background-color = !bg_color p :background-color = !bg_color + #202020 :width = !main_width + !unit1 img.thumb :width = (!main_width + 15) + !unit2
- SASS-комментарии — они присутствуют в SASS-шаблоне, но их нету в итоговом CSS
- Несколько вариантов форматирования итогового CSS (начиная от максимально читаемого expanded и заканчивая минималистичным compressed)
Но вернемся к нашим баранам: SASS файл можно использовать двояко — можно получить из него CSS файл и подключить его к приложению, а можно использовать встроенный в Синатру SASS-шаблонизатор для генерации CSS «на лету». Мы воспользуемся вторым методом не смотря на его бессмысленность:)
init.rb:
get '/style.css' do response['Content-Type'] = 'text/css; charset=utf-8' # Устанавливаем header ответа sass :style end
layout.haml:
%link{ :href => "/style.css", :media => "screen", :rel => "stylesheet", :type => "text/css"}
Ну, а мой файл style.sass вы можете посмотреть по этой ссылке.
Теперь у нашего приложения появилось какое-никакое оформление.
Аутентификация
Пришла пора приделать к нашему приложению нормальный механизм аутентификации. Мы хотим, чтобы все желающие могли скачивать файлы по прямым ссылкам, но upload и удаление файлов, а также просмотр общего списка должны быть доступны только после ввода пароля (для простоты установим один, жестко закодированный пароль).
Я решил задачу следующим образом: взял модуль HTTP-аутентификации (код по ссылке), положил его в папку lib и внес следующие изменения в init.rb:
require 'lib/authorization' helpers do include Sinatra::Authorization end get '/' do require_administrative_privileges # Дальше get без изменений end post '/' do require_administrative_privileges # Дальше post без изменений end get '/:sha/:id/delete' do require_administrative_privileges # Дальше delete без изменений end
Если вкратце, то блок «helpers do … end» выполняется в контексте всех наших блоков — обработчиков URL’ов, то есть мы делаем модуль Sinatra: Authorization доступным внутри приложения. В этом же блоке можно определять методы, которые можно будет использовать в шаблонах и основном приложении (так называемые хелперы — вспомогательные методы, которые позволяют избежать повторений одного и того же кода в шаблонах).
Запуск из под thin
Итак, наше приложение достигло промышленных высот и готово к развертыванию на рабочем сервере. Напомню, что сейчас мы запускаем его командой «ruby init.rb» и оно работает пока открыта консоль с ruby — естественно, это несерьезно — веб-приложение должно запускаться веб-сервером. В качестве веб-сервера я выбираю thin — компактный и чрезвычайно быстрый сервер для Ruby-приложений. Установка проста:
sudo gem install thin
Теперь пришла пора создать несколько папок в каталоге нашего приложения.
mkdir config mkdir tmp mkdir log
В папку config перенесем файл config.rb из папки lib (одновременно поправив к нему путь в init.rb). Для настройки thin нам понадобится файл, который мы назовем thin.yml — создадим его в папке config и запишем следующее:
--- environment: production chdir: КАТАЛОГ_ПРИЛОЖЕНИЯ pid: КАТАЛОГ_ПРИЛОЖЕНИЯ/tmp/thin.pid rackup: КАТАЛОГ_ПРИЛОЖЕНИЯ/config/config.ru log: КАТАЛОГ_ПРИЛОЖЕНИЯ/log/thin.log max_conns: 1024 timeout: 30 max_persistent_conns: 512 daemonize: true
Мы говорим thin’у, что надо работать в production окружении, сделав chdir в корневой каталог приложения, разместить PID файл в папке tmp, взять Rackup-файл (о нём ниже) в папке config, лог вести в log/thin.log, поддерживать до 1024 одновременных соединений с таймаутом в 30 секунд, держать до 512 постоянных соединений и работать в качестве демона (то есть независимо от наличия залогиненого пользователя в системе).
Теперь о rackup-файле: по сути, это конфигурационный файл для Rack — интерфейса между Ruby и веб-сервером (в нашем случае thin). Этот файл содержит всего две строки:
require 'init' Rack::Handler::Thin.run Sinatra::Application, :Port => 3000, :Host => "0.0.0.0"
Первая строка подключает init.rb (то есть, наше приложение), вторая говорит Rack, что надо запустить thin на 3000-м порту и передать ему синатровское приложение.
Дело сделано! Теперь приложение запускается вот такой командой
thin start -C config/thin.yml
Мы просто передаем thin’у конфигурационный файл.
Остановка происходит командой
thin stop -C config/thin.yml
Тестирование с помощью RSpec
Этот раздел я специально оставил напоследок так как понимаю, что мало кто будет тестировать приложения под Синатру. Не буду вдаваться в подробности и рассказывать, что предствляет из себя RSpec, просто покажу, как выглядят спеки.
require 'sinatra' require 'sinatra/test/rspec' require 'init' describe 'TrashFiles app' do it 'should render template with delay' do @file = StoredFile.first get "/#{@file.sha}/#{@file.id}" @response['Content-Type'].should == "text/html" end it 'should give file if ?nowait=true is supplied' do @file = StoredFile.first get "/#{@file.sha}/#{@file.id}?nowait=true" @response['Content-Type'].should == "Application/octet-stream" @response['Content-Disposition'].should == "attachment; filename=\"#{@file.filename}\"" end end
Никаких хитростей — те же describe/it/should, что и в Rails, к примеру. Главное, не забывать подключать sinatra/test/rspec.
Бенчмарки
В одном из комментариев меня попросили замерить производительность получившегося приложения — без проблем.Сначала — бенчмарк главной страницы (список файлов).
ab -n 1000 -c 1 -A admin: secret http://127.0.0.1:3000/
Concurrency Level: 1 Time taken for tests: 24.109 seconds Total transferred: 3739000 bytes HTML transferred: 3604000 bytes Requests per second: 41.48 [#/sec] (mean) Time per request: 24.109 [ms] (mean) Time per request: 24.109 [ms] (mean, across all concurrent requests) Transfer rate: 151.45 [Kbytes/sec] received
ab -n 1000 -c 10 -A admin: secret http://127.0.0.1:3000/
Concurrency Level: 10 Time taken for tests: 24.381 seconds Total transferred: 3739000 bytes HTML transferred: 3604000 bytes Requests per second: 41.02 [#/sec] (mean) Time per request: 243.811 [ms] (mean) Time per request: 24.381 [ms] (mean, across all concurrent requests) Transfer rate: 149.76 [Kbytes/sec] received
ab -n 1000 -c 100 -A admin: secret http://127.0.0.1:3000/
Concurrency Level: 100 Time taken for tests: 23.798 seconds Total transferred: 3739000 bytes HTML transferred: 3604000 bytes Requests per second: 42.02 [#/sec] (mean) Time per request: 2379.816 [ms] (mean) Time per request: 23.798 [ms] (mean, across all concurrent requests) Transfer rate: 153.43 [Kbytes/sec] received
Upload файлов на сервер (размер файла 1.5 Kb)
ab -n 1000 -c 1 -A admin: secret -T 'application/x-www-form-urlencoded' -p post.data http://127.0.0.1:3000/
Concurrency Level: 1 Time taken for tests: 16.305 seconds Total transferred: 160000 bytes Total POSTed: 242000 Requests per second: 61.33 [#/sec] (mean) Time per request: 16.305 [ms] (mean) Time per request: 16.305 [ms] (mean, across all concurrent requests) Transfer rate: 9.58 [Kbytes/sec] received 14.49 kb/s sent 24.08 kb/s total
ab -n 1000 -c 10 -A admin: secret -T 'application/x-www-form-urlencoded' -p post.data http://127.0.0.1:3000/
Concurrency Level: 10 Time taken for tests: 18.463 seconds Total transferred: 161280 bytes Total POSTed: 243936 HTML transferred: 0 bytes Requests per second: 54.16 [#/sec] (mean) Time per request: 184.631 [ms] (mean) Time per request: 18.463 [ms] (mean, across all concurrent requests) Transfer rate: 8.53 [Kbytes/sec] received 12.90 kb/s sent 21.43 kb/s total
ab -n 1000 -c 100 -A admin: secret -T 'application/x-www-form-urlencoded' -p post.data http://127.0.0.1:3000/
Concurrency Level: 100 Time taken for tests: 16.029 seconds Total transferred: 160160 bytes Total POSTed: 242242 HTML transferred: 0 bytes Requests per second: 62.39 [#/sec] (mean) Time per request: 1602.899 [ms] (mean) Time per request: 16.029 [ms] (mean, across all concurrent requests) Transfer rate: 9.76 [Kbytes/sec] received 14.76 kb/s sent 24.52 kb/s total
Заметьте — производительность практически не изменяется при увеличении количества одновременных запросов в 100 (сто!) раз. Тестирование производилось на Mac Book Core 2 Duo 2.4 Ghz, 2 GB ram при нескольких запущенных в фоне приложениях.
The End
Пришла пора завершать мою случайно начатую эпопею. Надеюсь, вам было интересно и я смог сподвигнуть хотя бы некоторых на изучение немейнстримовых технологий (Sinatra, DataMapper, thin, haml, sass). Приложение в своей последней версии выложено на github. Спасибо всем, кто потратил время на чтение этих немаленких статей.