Обновление сайта до Ruby on Rails 3 - отчет

05 апреля 2011

За последний месяц я обновил два сайта на Ruby on Rails 3. Первый — RentFeed — обновить было несложно, потому что функционала там немного. На обновление CarGid ушло около 40 человеко-часов; теперь и он работает на Ruby on Rails 3.

До обновления существующего сайта советую ознакомиться с Rails 3 на новом проекте. Нет, не так — считаю, что нельзя обновлять проект, не работав никогда с Rails 3.

Эта статья — не руководство по обновлению, а мои заметки о нем. Полноты раскрытия темы не обещаю.

Мотивация

Зачем тратить 40 часов на то, что не принесет ни новой функциональности, ни новых пользователей, ни существенного прироста производительности? Причины такие:

  • Возможность использовать свежие библиотеки. Все больше библиотек перестают поддерживать Rails 2.3, и скоро тебе придется обновляться.
  • Исправленные баги и недочеты в безопасности.
  • В некоторых местах — намного более красивый и наглядный код. Красивые запросы на Arel в моделях. Лаконичные роуты. Понятные мейлеры.

Мое такое мнение, что обновляться уже пора. После знакомства с третьими рельсами недостатки вторых начинают бросаться в глаза.

Подготовка

Создаем ветку rails3 в репозитории.

Устанавливаем плагин rails_upgrade. Следуя инструкциям плагина, создаем резервные копии ценных файлов, а также создаем новый routes.rb и application.rb, и, если bundler еще не задействован, новый Gemfile. Помимо это, делаем:

rake rails:upgrade:check > upgrade_log.txt

Пригодится.

Коммитим изменения, дабы ничего не потерять.

Подбор гемов

Удаляем плагин rails_upgrade, ибо он больше не пригодится.

В Gemfile нужно принудительно указать gem 'rails', '~> 3.0.0', а кроме этого, если используется MySQL — то и gem 'mysql2'. Остальные гемы оставляем без изменений.

Возносим молитвы.

Производим bundle update. Эта команда попробует подобрать такие версии гемов, которые заработают с Rails 3.

Скорее всего, у нее это не получится. Проблемы с совместимостью решаются гуглением — большая часть гемов уже работает с Rails 3. Заодно пройдись по плагинам — возможно, некоторые из них также уже доступны в виде гемов.

Результатом этого этапа будет успешно собранный Gemfile.lock; коммитимся.

Установка Rails 3

Выполняем в каталоге проекта

rails .

Да, просто так. Эта команда предложит затереть ряд файлов — разрешаем, ведь все под контролем (версий, в смысле).

Коммитимся. Теоретически проект уже работоспособен (но вряд ли).

Зачистка

Убираем все файлы в script, кроме script/rails – они больше не используются.

Меняем mysql на mysql2 в database.yml, если используется MySQL.

Заменяем базовый config/application.rb на тот, который сгенерировали.

Подчищаем роуты

Подставляем тот routes.rb, который сгенерировали из старых роутов. Увы, скорее всего он сломанный и содержит синтаксические ошибки. Устраняем их.

Вот теперь проект точно должен запуститься.

Модели

В моделях надо будет поменять named_scope на scope и переформулировать запросы в Arel. Это пока необязательно.

Контроллеры

В контроллерах ничего особенного не поменялось

Виды

Большая часть времени уйдет именно на обновление видов.

Изменения в Javascript

Разнообразные рельсовые обработчики событий теперь вынесены в отдельный Javascript-файл, что прекрасно. Его нужно подключить. Для jQuery этот файл берется из репозитория rails/jquery-ujs.

Этому файлу нужен authenticity_token, поэтому в заголовок дописываем

<head>
  ...
  <%=csrf_meta_tag %>
</head>

- style block helpers are deprecated. Please use =

Это такая шутка разработчиков, по-видимому. Везде, где используются блочные хелперы, придется дефис поменять на знак равенства:

<!-- было -->
<%-form_for @foo do %>

<!-- надо -->
<%=form_for @foo do %>

Слава богу, делать это не обязательно. Но ошибки в лог будут сыпаться.

Введение обязательного html_safe

В Rails 3 все строки, выводимые в шаблон, неизбежно проходят через эксейпинг. Это прекрасно, поскольку в 98% случаев ты выводишь текст, требующий оного.

<%=@blog_post.title_needs_to_be_escaped%>
&lt;script&gt;alert('U NO ESCAPE JAVASCRIPT');&lt;/script&gt;

Хелперы h можно не убирать, двойного эскейпинга не произойдет:

<%=h @blog_post.title_needs_to_be_escaped%>
&lt;script&gt;alert('U NO ESCAPE JAVASCRIPT');&lt;/script&gt;

Как же выводить текст с тегами? Применять к нему метод String#html_safe:

<%=@blog_post.nice_text_with_markup%>
&lt;FONT COLOR="#FF00FF"&gt;ЛОВИ СИМПАФКУ!&lt;/FONT&;gt

<%=@blog_post.nice_text_with_markup.html_safe%>
<FONT COLOR="#FF00FF">ЛОВИ СИМПАФКУ!</FONT>

Получается, во местах, где ты выводишь строку с тегами, придется дописывать html_safe. Это сложный и кропотливый кусок работы, который нужно сделать в обмен на чистый и безопасный код.

ActionMailer

Синтаксис ActionMailer поменялся. Мейлеры придется переписать, а скорее, переформатировать. Это довольно прозрачный процесс.

Тесты

Разумеется, процесс обновления будет проще, если проект покрыт тестами. Если не покрыт, то, вполне возможно, ты (как и я) решишь заняться тестами перед обновлением. Однако советую начинать сразу после обновления: Rspec 2 работает только с Rails 3.

Обновляем

Скорее всего, ничего кроме исходников обновлять на сервере не придется, поэтому обновление ничем не будет отличаться от обычного деплоя. Возможно, придется сначала обновить на сервере Rubygems и Bundler.



Четырнадцать комментариев. Напиши еще один
  1. 8cbd31fea81477c4b353eb32209df69f # 05 апреля 2011 fxposter (blog.fxposter.org) написал:

    Очень советую перед переводом на третьи рельсы поставить себе rails_xss плагин, тем самым избавившись от необходимости постфактум чинить вьюхи.

    1. 777894ea5153122bfa6b83f5bbf23622 # 05 апреля 2011 Леонид Шевцов (автор) написал:

      По-моему это тот случай, когда от перестановки слагаемых сумма не меняется. Объем работы тот же самый.

      Другое дело что тем, кто уже пользуется rails_xss'ом, крупно повезло с апгрейдом. :)

      1. 8cbd31fea81477c4b353eb32209df69f # 05 апреля 2011 fxposter (blog.fxposter.org) написал:

        Мы переехали на rails_xss. Скоро будем переезжать на третьи рельсы. Обьем работы тот же, но показать готовый результат ты сможешь быстрее. Дробя задачу на подзадачи ты сможешь сделать больше мелких работающих релизов. Принципы agile ;)

  2. Ava_normal # 22 мая 2011 rakoth (@rakoth3d) написал:

    Вместо
    <%=@blog_post.nice_text_with_markup.html_safe%>
    можно
    <%=raw @blog_post.nice_text_with_markup %>
    или
    <%== @blog_post.nice_text_with_markup %>

    1. 777894ea5153122bfa6b83f5bbf23622 # 22 мая 2011 Леонид Шевцов (автор) написал:

      О, спасибо. По-моему так нагляднее. Хотя raw идентичен .html_safe.

      def raw(stringish)
        stringish.to_s.html_safe
      end
      
  3. Ec7fb72f4e4b01091e1943f58c9231e5 # 24 ноября 2011 Сергей Луконин (linux-easy.ru) написал:

    Хотя raw идентичен .html_safe

    Не идентичен. html_safe на nil выдаст ошибку.

    Вопрос… А как позволить проходить тегам script, заключенным в pre, нетронутыми? Никак не могу допедрить…
    То есть я хочу с помощью sanitize чистить текст от «плохих» тегов, но не чистить их в pre, чтобы можно было выкладывать примеры скриптов.

    1. 777894ea5153122bfa6b83f5bbf23622 # 25 ноября 2011 Леонид Шевцов (автор) написал:

      Так скрипты внутри <pre> не менее опасны. Я бы сначала парсил документ нокогирей, эспейпил содержимое тегов <pre>, сохранял результат, а потом уже делал sanitize.

      1. Ec7fb72f4e4b01091e1943f58c9231e5 # 25 ноября 2011 Сергей Луконин (linux-easy.ru) написал:

        А чем они опасны, если они будут выводиться заэскейпенными? По-моему, это то же самое, что не применять метод html_safe, а в этом случае скрипты не опасны. Я не прав?

        UPD: а, я, видимо, в первый раз выразился не совсем верно… Я как раз и имел в виду, что sanitize не должен чистить скрипты, находящиеся в pre, но не подумал, что для этого их нужно просто заэскейпить.
        С nokogiri не работал, но попробую разобраться, как заэскейпить содержимое pre, спасибо за совет.

  4. Ec7fb72f4e4b01091e1943f58c9231e5 # 25 ноября 2011 Сергей Луконин (linux-easy.ru) написал:

    Весь stackoverflow облазил, так и не смог разобраться с nokogiri. Можете привести пример, как пропарсить @post.content (и @post.title, наверное) на наличие pre и заэскейпить содержимое? Только, наверное, нужно какое-то исключение, чтобы не эскейпить тег code внутри pre.
    Может проще через регулярку пропустить?
    И правильно ли я понимаю, что пропускать через nokogiri придется все поля, содержащие текст (title, content и т.д.)?

    1. 777894ea5153122bfa6b83f5bbf23622 # 25 ноября 2011 Леонид Шевцов (автор) написал:

      Не надо HTML через регулярку, он этого не любит :)

      https://gist.github.com/1394229 – где-то так.

      Если я правильно понял задачу, то нужно сначала сделать это, а потом прогнать по результату sanitize, который поубирает лишние теги.

      1. Ec7fb72f4e4b01091e1943f58c9231e5 # 25 ноября 2011 Сергей Луконин (linux-easy.ru) написал:

        Спасибо, Леонид! Попробую так.
        Так я правильно понял, что метод escape_pres с последующим sanitize применять перед сохранением поста ко всему, что имеет текст? Или sanitize (насколько я понял по тому, что именно так и реализовал) только к самому @post? То есть так:
        escape_pres(@post.title)
        escape_pres(@post.content)
        @post = Sanitize.clean(@post, …)

        1. 777894ea5153122bfa6b83f5bbf23622 # 25 ноября 2011 Леонид Шевцов (автор) написал:

          Только к тем полям, где по дизайну может быть <pre>. По-моему к названию можно и не применять.

          1. Ec7fb72f4e4b01091e1943f58c9231e5 # 26 ноября 2011 Сергей Луконин (linux-easy.ru) написал:

            Да, как-то не подумал… К title только sanitize нужно будет.

  5. Ec7fb72f4e4b01091e1943f58c9231e5 # 05 декабря 2011 Сергей Луконин (linux-easy.ru) написал:

    Игрался с этим скриптом, пытаясь определить очередность выполнения среди преобразования текста в html через redcloth, очистки с помощью sanitize и эскейпингом pre. В итоге понял, что я изначально делал не ту очередность, и теперь работает все и без скрипта, вот таким вот макаром:

    if @comment.save
        @comment.formatted_content = RedCloth.new(@comment.content).to_html
        @comment.formatted_content = Sanitize.clean(@comment.formatted_content, Sanitize::Config::BASIC)
        @comment.update_attributes(@comment[:formatted_content] => @comment.formatted_content)
        @path = posts_path + "#{@post.id}/#c#{@comment.id}"
        flash[:success] = "Ваш комментарий успешно отправлен"
        redirect_to @path
    end
    

(нужна разметка?)

  • **жирный**
  • > цитата

отменить