Домашний файлообменник на базе 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. Спасибо всем, кто потратил время на чтение этих немаленких статей.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 31

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

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

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

                        Only users with full accounts can post comments. Log in, please.