Индикатор загрузки файлов в веб-приложениях на Ruby on Rails

    Задача: показать как реализовать индикатор загрузки файла при различных конфигурациях Ruby on Rails:
    Я разделю цикл статей на три части.
    mongrel
    mongrel(s)+nginx
    mod_rails

    Нижеописанное проверено с:
    Ruby on Rails 2.2.2
    ruby 1.8.6 (2008-03-03 patchlevel 114) [universal-darwin9.0]
    mongrel 1.1.5
    Mac OS X 10.5.6


    Часть I. Как это сделать с mongrel'ом


    В основе легла статья, линк на которую есть в самом конце. Но повторив ее у меня на новых рельсах и многрелах ничего не получилось. Я немножно ее изменил.

    Создание каркаса:
    rails upload
    cd upload
    rm public/index.html
    script/generate controller home index upload progress
    touch app/views/layouts/application.html.erb


    Добавление роутов:
    Copy Source | Copy HTML
    1. ActionController::Routing::Routes.draw do |map|
    2.   map.root :controller => 'home'
    3.   map.upload '/upload', :controller => 'home', :action => 'upload'
    4.   map.progress '/progress', :controller => 'home', :action => 'progress'
    5. end
    6.  


    Этап подготовки завершен. Теперь надо немного пояснить каким образом фунционирует индикатор загрузки файлов. Сабмит формы должен идти в iframe, а с самой страницы помощью ajax запросов можно с нужной частотой обращаться к определенному контроллеру, который будет возвращать текущее состояние (можно в json, можно html, здесь сделано javascript-ом), и в зависимости от них на странице двигать индикатор.

    Итак, контроллер один, а в нем три метода: index для вывода формы для загрузки, upload — обработка загруженного файла, progress — для получения данных о статусе загрузки. Контроллер выглядит следующим образом:

    Copy Source | Copy HTML
    1. class HomeController < ApplicationController
    2.   skip_before_filter :verify_authenticity_token, :only => [:progress]
    3.  
    4.   def index
    5.     @upid = Time.now.tv_sec.to_s
    6.   end
    7.  
    8.   def upload
    9.     render :text => 'done'
    10.   end
    11.  
    12.   def progress
    13.     @status = Mongrel::Uploads.check(params[:upload_id])
    14.     respond_to do |format|
    15.       format.js
    16.     end
    17.   end
    18. end


    skip_before_filter необходим так как запросы будет идти без токена. @upid — некий уникальный идентификатор.
    Теперь необходимо написать некий плагин для монгрела, который будет перехватывать аплоад файлов (а точнее обращение к конкретному url) и хранить данные о состоянии загрузки, кроме того к этим данным нужно как-то обращаться. Хорошо, что это уже реализовано и код можно взять из svn:

    svn co svn://rubyforge.org/var/svn/mongrel/trunk/projects/mongrel_upload_progress

    Я предлагаю сделать следующим образом: создать файл lib/progress_plugin.rb со следующим содержимым:
    Copy Source | Copy HTML
    1. class Upload < GemPlugin::Plugin "/handlers"
    2.   include Mongrel::HttpHandlerPlugin
    3.  
    4.   def initialize(options = {})
    5.     @path_info = Array(options[:path_info])
    6.     @frequency = options[:frequency] || 3
    7.     @request_notify = true
    8.     if options[:drb]
    9.       require 'drb'
    10.       DRb.start_service
    11.       Mongrel.const_set :Uploads, DRbObject.new(nil, options[:drb])
    12.     else
    13.       Mongrel.const_set :Uploads, Mongrel::UploadProgress.new
    14.     end
    15.     Mongrel::Uploads.debug = true if options[:debug]
    16.   end
    17.  
    18.   def request_begins(params)
    19.     upload_notify(:add, params, params[Mongrel::Const::CONTENT_LENGTH].to_i)
    20.   end
    21.  
    22.   def request_progress(params, clen, total)
    23.     upload_notify(:mark, params, clen)
    24.   end
    25.  
    26.   def process(request, response)
    27.     upload_notify(:finish, request.params)
    28.   end
    29.  
    30.   private
    31.     def upload_notify(action, params, *args)
    32.       return unless @path_info.include?(params['PATH_INFO']) &&
    33.         params[Mongrel::Const::REQUEST_METHOD] == 'POST' &&
    34.         upload_id = Mongrel::HttpRequest.query_parse(params['QUERY_STRING'])['upload_id']
    35.       if action == :mark
    36.         last_checked_time = Mongrel::Uploads.last_checked(upload_id)
    37.         return unless last_checked_time && Time.now - last_checked_time > @frequency
    38.       end
    39.       Mongrel::Uploads.send(action, upload_id, *args)
    40.       Mongrel::Uploads.update_checked_time(upload_id) unless action == :finish
    41.     end
    42. end
    43.  
    44. # Keeps track of the status of all currently processing uploads
    45. class Mongrel::UploadProgress
    46.   attr_accessor :debug
    47.   def initialize
    48.     @guard = Mutex.new
    49.     @counters = {}
    50.   end
    51.  
    52.   def check(upid)
    53.     @counters[upid].last rescue nil
    54.   end
    55.  
    56.   def last_checked(upid)
    57.     @counters[upid].first rescue nil
    58.   end
    59.  
    60.   def update_checked_time(upid)
    61.     @guard.synchronize { @counters[upid][0] = Time.now }
    62.   end
    63.  
    64.   def add(upid, size)
    65.     @guard.synchronize do
    66.       @counters[upid] = [Time.now, {:size => size, :received => 0}]
    67.       puts "#{upid}: Added" if @debug
    68.     end
    69.   end
    70.  
    71.   def mark(upid, len)
    72.     return unless status = check(upid)
    73.     puts "#{upid}: Marking" if @debug
    74.     @guard.synchronize { status[:received] = status[:size] - len }
    75.   end
    76.  
    77.   def finish(upid)
    78.     @guard.synchronize do
    79.       puts "#{upid}: Finished" if @debug
    80.       @counters.delete(upid)
    81.     end
    82.   end
    83.  
    84.   def list
    85.     @counters.keys.sort
    86.   end
    87. end
    88.  


    После этого создать конфиг для монгрела (по сути обычный ruby-файл), который будет подключать этот плагин к монгрелу:
    touch config/mongrel_upload_progress.conf


    Поместить в mongrel_upload_progress.conf нужно следующее:
    Copy Source | Copy HTML
    1. require 'progress_plugin'
    2. uri "/", :handler => plugin('/handlers/upload', :path_info => '/upload'), :in_front => true
    3.  


    path_info это роут который будет перехватываться плагином для отслеживания состояния загрузки.
    Форму загрузки и все нужные js-ники можно взять тут.

    Осталось просто запустить монгрел с конфигурационным файлом:
    mongrel_rails start -S config/mongrel_upload_progress.conf


    Индикатор в браузере будет выглядить примерно так:

    Если монгрелов несколько можно использовать drb, детали описаны в ссылке ниже.

    Никаких косметических изменений в рамках этой статьи я не делал. В более красивом виде индикатор можно посмотреть на rghost.ru

    Конечно жаль признать, что все это написано скорей всего зря, так как простым монгрелом сейчас мало кто обходится. Обычно перед монгрелом ставят nginx или вообще заменяют его apache-ем с mod_passenger. Статьи о них я планирую написать попозже.

    Список использованной литературы


    mongrel upload progress
    Поделиться публикацией

    Комментарии 15

      0
      Оформите код, пожалуйста.
      (http://s-c.me/)
        0
        Ссылку не ту дал, извиняюсь. Сейчас поищу правильную )
          +1
          нет, ссылка все таки была правильная )
        0
        это самое свежое решение?
          0
          Для одного монгрела других решений я не встречал
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Про экзотичность я в последнем абзаце написал. Как будет время напишу про индикатор при связки с nginx.
                +1
                Кстати, было бы интересно посмотреть, как это реализовано. Насколько мне известно, nginx полностью загружает тело пост-запроса прежде, чем отдавать его бэкенду.
                  0
                  Угу.
                  Чтобы получать информацию из nginx, к нему надо прицепить специальный модуль, например, wiki.codemongers.com/NginxHttpUploadProgressModule
                    0
                    Вас не смущает что сами разработчики не рекомендуют юзать этот модуль в продакшене?
                      0
                      Смущает.
                      Хуже того, что стуация не меняется в течение более чем года.
                      ПОдозреваю, что они на него забили и и перешли на swfupload
                    0
                    Это абсолютно нормальное поведение веб-сервера. POST-запрос завершен тогда, когда пришло количество байт указанное в Content-Length количество байт, если оно еще не пришло, то передавать бекенду как бы и нечего.

                    Мое мнение, лучший способ (если есть такая возможноть), эти POST-запросы отправлять напрямую на Apache, например, забиндев последний на 8080. Эстеты без сложностей могут запретить из мира все запросы кроме нужных POST.
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    На самом деле, SWFUpload самое корректное решение. Посудите сами: не тупо ли писать плагин для веб/аппсервера, чтобы пользователю показать, сколько байтиков он загрузил?

                    Но есть доля правды (помимо того, что существуют жалкие людишки без флэша на компе :) ): swfupload надо допиливать солидно, чтобы все работало как надо (я подразумеваю сессии, токен аутентификации и UI)
              0
              можно воспользоваться бэкраунд сервером с очень хорошей поддержкой прогресс бар. Как вам такое?

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

              Самое читаемое