Когда мы хотим скопировать данные из production окружения Ruby on Rails приложения в development или staging, обычно, нам нужно скопировать дамп базы данных и статические файлы (например, изображения загруженные пользователями). Копирование базы может не представляет проблем (например, ее можно копировать из бэкапов или резервных серверов БД). А вот копирование статических файлов занимает много времени и ресурсов сервера с которого копируют (и на который копируются) файлы.
В рассылке ror2ru Макс Лапшин предложил копировать статические файлы с production в текущее окружение по запросу, обработку производить в middleware.
Ниже, моя реализация такой middleware.
Поместите следующий код в файл соответствующего окружения
(например config/environments/development.rb
).
public_path = Rails.root.join('public')
# регулярные выражения запросов, которые будут обрабатываться
matching_paths = [/^\/uploads/, /^\/system/] # localhost:300/uploads/pic.jpg
# сервер с которого будем пытаться подгрузить файлы, если они отсутствуют
# на текущем сервере
remote_host = 'http://production-example.com'
config.middleware.insert_after ActionDispatch::Static,
StaticMissing::Middleware, public_path, matching_paths, remote_host
Наше middleware подключается после ActionDispatch::Static - это middleware, которое отдает статические файлы в Ruby on Rails. Если ActionDispatch::Static не найдет файла соответствующего запросу в текущем окружении, то оно передает обработку запроса ниже по цепи middleware. Вот тут-то мы и должны вклиниться!
В нашей StaticMissing::Middleware мы проверим, есть ли файл соответствующий запросу на production-сервере. Если есть - то загрузим, сохраним в текущем окружении и передадим обработку запроса Rack::File middleware.
Файл lib/static_missing.rb
module StaticMissing
class Middleware
def initialize(app, public_path, matching_paths, remote_host)
@app = app
@root = public_path.to_s.chomp('/')
@file_server = ::Rack::File.new(@root)
@file_handler = FileHandler.new(@root, matching_paths, remote_host)
end
def call(env)
case env['REQUEST_METHOD']
when 'GET', 'HEAD'
path = env['PATH_INFO'].chomp('/')
if @file_handler.load_if_static_path(path)
env['PATH_INFO'] = path
return @file_server.call(env)
end
end
@app.call(env)
end
end
class FileHandler
def initialize(root, matching_paths, remote_host)
@root, @matching_paths, @remote_host = root, matching_paths, remote_host
end
def load_if_static_path(path)
static_path?(path) && load_file(path) == '200'
end
protected
def static_path?(path)
@matching_paths.any? do |matching_path|
path.match matching_path
end
end
def load_file(path)
uri = URI.join(@remote_host, path)
response = Net::HTTP.get_response(uri)
if response.code == '200'
full_path = escape_glob_chars(unescape_path File.join(@root, path))
FileUtils.mkdir_p File.dirname(full_path)
open(full_path, 'wb') do |file|
file.write response.body
end
Rails.logger.info("Static file downloaded from #{uri} to #{full_path}")
end
response.code
end
def escape_glob_chars(path)
path.force_encoding('binary') if path.respond_to? :force_encoding
path.gsub(/[*?{}\[\]]/, "\\\\\\&")
end
def unescape_path(path)
URI.parser.unescape(path)
end
end
end