Ruby

индекс
129,59

Домашний файлообменник на базе Sinatra и DataMapper. Часть 3 — Very Advanced features

Предыдущие две статьи (раз и два) оказались гораздо популярнее, чем я мог ожидать. А теперь пришла пора третьей, завершающей статьи о файлообменнике на базе Sinatra и DataMapper.

В этот раз мы рассмотрим:
  • Проблему с одинаковыми именами файлов
  • Страницу ожидания перед загрузкой
  • Создание 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 при создании серьезных проектов.

Но вернемся к нашим баранам: 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. Спасибо всем, кто потратил время на чтение этих немаленких статей.
+24
27 января 2009, 07:29
32

комментарии (31)

0
keymone #
нужно
нет, НУЖНО написать обширную статью об использовании HAML, так как его аналоги есть уже и для PHP и для .NET платформы и даже для Erlang
0
indigo #
Аналоги?
0
bsdemon #
Извините, вопрос не по теме, но:
DataMapper действительно оправдывает своё название, то есть является data mapper'ом как описывается PoEAA? Если сравнивать с python: он больше похож на django orm или на sqlalchemy? Можно ли мапить класс не на одну таблицу а на селект например?
+1
valodzka #
Очень поверхностно знаком с django orm и sqlalchemy, но в рамках того что я знаю datamapper ближе к django orm, а аналогом sqlalchemy в руби будет скорее sequel.
Он соответствуем этому патерну в том, что выполнет мэпинг, но он также включает и сами модели, а также, в ближайшим планах, мапинг на yaml, xml, веб сервисы и т.д., т.е. в общем независимость от реляционной БД
0
indigo #
Не знаком с django orm и sqlalchemy вообще, но класс можно мапить только на таблицу (возможно и на представление, но сталкиваться с таким не приходилось).
НЛО прилетело и опубликовало эту надпись здесь
0
ZEGO #
На бис еще статейку можно? А лучше не одну! =) Спасибо.
+1
indigo #
А на какую тему? :)
0
keymone #
например об возможностях DataMapper. в частности об IdentityMap.
0
ZEGO #
Лично мне интересно программирование на Руби для веб «на низком уровне», то есть без фреймворков вообще, или как совсем крайность и без использования библиотек входящих в дистрибутив. «Легковесные фреймворки» пример с Синатрой был хорош, но можно еще Кампин к примеру. Из под кнута шедевр не выдашь, так что пишите на тему в первую очередь интересную вам. Если это будет не очередной блог на рельсах, и не описание синтаксиса Руби с неизменным «там даже число — объект, вах», лично для меня это будет уже однозначно интересно.
0
indigo #
Без фреймворков вообще? Это примерно то же самое, что писать веб-приложения на C++ — а кто должен заниматься взаимодействием с веб-сервером (Rack — это тоже фреймворк) и выполнять рутинные задачи (отсылка HTTP-заголовков и всё в таком духе). Можно, конечно, писать CGI-приложения в стиле классических perl-скриптов середины 90-х, но неужели это действительно практично? А Camping уже погиб, как я недавно узнал — так что о нем писать тоже бессмысленно.
0
ZEGO #
Не обязательно подробно, можно поверхностно. Но чтобы было понимание как это устроено и функционирует. К примеру тот же Rack, в чем его отличительная особенность в связи с чем он ста столь популярен. Библиотеки CGI или CGI-lib которые входят в стандартную дистрибьюцию языка. О том как в самом Ruby устроен механизм взаимодействия с переменными окружения. И на счет без фреймворков, я писал что как крайность (намеренно утрировал, дабы более нагляден был смысл). Вобщем хочется в том же стиле, побольше маргинального и пауз на разъяснение. Касательно кампина раз погиб, значит того не стоил.
0
mikhailian #
Что значит погиб? Когда вобще может погибнуть проект с открытым исходным кодом? Только тогда, когда им никто не пользуется. Но я например пользуюсь, и некоторые другие разработчики тоже. Потихоньку исправления туда попадают, см.

github.com/why/camping/network
0
indigo #
Я за что купил, за то и продаю. Open-source проект погибает, когда нет коммитерров (если его при этом нельзя назвать законченым). Посмотрел историю коммитов — не густо их для живого развивающегося проекта. Хотя в коммиты не глядел — может, там горы переворачивают каждый раз :)
+1
trybeee #
Спасибо, было интересно читать этот цикл :)
0
zak2k #
Для решения проблемы с одноименными файлами, я бы предложил подмешивать в дайджест размер файла и проверять по базе, возможно данный файл уже выкладывался, тогда просто отдавать на него линк. Конечно бывают ситуации когда имена и размеры совпадают, это можно избежать делая дайджест по самому файлу.
Отличный цикл статей!
0
indigo #
Вариантом, действительно свободным от коллизий, будет именно дайджест по всему файлу, но полагаю, что это довольно неэффективно с точки зрения ресурсов сервера.
0
zak2k #
если взять в расчет то что нахождение дайджеста происходит при добавлении нового файла, а это не самая частая операция, то можно пожертвовать ресурсами и добавить новую фишку, показ дайджеста для проверки на целостность скаченного файла
+1
ZEGO #
Банально добавлять к названию, сразу после дайджеста, время создания файла типо ГодМесяцДеньМинутаСекунда. Нагрузка минимальна и задачу выполняет на все 99,9.
0
indigo #
Да, хороший вариант, спасибо. Я, скорее всего, у себя оставлю, как есть. Но все желающие приглашаются на github для развития проекта (если вдруг есть такое желание) =)
0
Kane #
Алексей, не подскажите, как с помощью RSpec тестировать post/delete/put запросы?
0
indigo #
Боюсь, что не подскажу, к сожалению — не сталкивался. Гугл пишет что-то более-менее вменяемое…
0
Kane #
Никогда не сталкивался с тестирование до этого, поэтому быть может глупый вопрос…

Когда я запускаю тест как-то так «spec test/data_spec.rb», то шаблоны вида ищутся не в views а в test/views. Подозреваю что я как-тоне так запускаю тесты. Не подскажите, в чём дело?
0
indigo #
Ммм… А зачем вам шаблоны в тестах? RSpec — это для бизнес-логики и моделей, а не представлений.
0
Kane #
Я хочу протестировать валидацию данных, то есть я отправляю невалидные данные а шаблон просто заворачивает ошибку в xml
0
indigo #
Проверяте, что контроллер делает то, что надо. Например, @object.valid?.should_be false
Для тестирования шаблонов есть Selenium — он в стопицот раз лучше.
0
Kane #
Спасибо, посмотрю
+1
akzhan #
0
indigo #
Спасибо, мне уже не пригодится, а вот остальным — наверняка.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.