Pull to refresh

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

Reading time 10 min
Views 4K
Предыдущие две статьи (раз и два) оказались гораздо популярнее, чем я мог ожидать. А теперь пришла пора третьей, завершающей статьи о файлообменнике на базе 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. Спасибо всем, кто потратил время на чтение этих немаленких статей.
Tags:
Hubs:
+24
Comments 31
Comments Comments 31

Articles