В нашей повседневной деятельности мы часто взаимодействуем с архивами. Если вы хотите отправить своему другу пачку документов, вы, вероятно, сначала заархивируете их. Когда вы загружаете книгу из Интернета, она, вероятно, будет заархивирована вместе с сопроводительными материалами. Итак, как мы можем взаимодействовать с архивами в Ruby?
Сегодня мы обсудим популярный гем под названием rubyzip, который используется для управления zip-архивами. С его помощью вы можете легко читать и создавать архивы или генерировать их на лету. В этой статье я покажу вам, как создавать записи базы данных из zip-файла, отправленного пользователем, и как отправить архив, содержащий все записи из таблицы.
Исходный код доступен на GitHub .
Прежде чем начать, я хочу напомнить вам, что различные сжатые форматы имеют разные коэффициенты сжатия. Таким образом, даже если вы архивируете файл, его размер может оставаться более или менее одинаковым:
- Текстовые файлы сжимаются очень хорошо. В зависимости от их содержания, соотношение составляет около 3: 1.
- Некоторые изображения могут выиграть от сжатия, но при использовании формата, такого как .jpg, который уже имеет собственное сжатие, он не сильно изменится.
- Бинарные файлы могут быть сжаты до 2 раз от их оригинального размера.
- Аудио и видео, как правило, плохие кандидаты на сжатие.
Начиная
Создайте новое приложение Rails:
$ rails new Zipper -T
Я использую Rails 5 beta 3 и Ruby 2.2.3 для этой демонстрации, но rubyzip работает с Ruby 1.9.2 или выше.
В нашем сценарии сегодня демо-приложение отслеживает животных. Каждое животное имеет следующие атрибуты:
-
name
(string
) -
age
(integer
) — конечно, вы можете использовать десятичную вместо -
species
(string
)
Мы хотим перечислить всех животных, добавить к ним способности и загрузить данные о них в каком-либо формате.
Создайте и примените соответствующую миграцию:
$ rails g model Animal name:string age:integer species:string $ rake db:migrate
Теперь давайте подготовим страницу по умолчанию для нашего приложения:
animals_controller.rb
class AnimalsController < ApplicationController def index @animals = Animal.order('created_at DESC') end end
просмотров / животные / index.html.erb
<h1>My animals</h1> <ul> <% @animals.each do |animal| %> <li> <strong>Name:</strong> <%= animal.name %><br> <strong>Age:</strong> <%= animal.age %><br> <strong>Species:</strong> <%= animal.species %> </li> <% end %> </ul>
конфиг / routes.rb
[...] resources :animals, only: [:index, :new, :create] root to: 'animals#index' [...]
Ницца! Перейдите к следующему разделу, и давайте сначала позаботимся о создании.
Создание животных из архива
Введите new
действие:
animals_controller.rb
[...] def new end [...]
* Вид / животные / index.html.erb
<h1>My animals</h1> <%= link_to 'Add!', new_animal_path %> [...]
Конечно, мы могли бы создать базовую форму Rails для добавления животных одного за другим, но вместо этого давайте позволим пользователям загружать архивы с файлами JSON. Каждый файл будет содержать атрибуты для конкретного животного. Структура файла выглядит следующим образом:
- animals.zip
- животных 1.json
- животных 2.json
Каждый файл JSON будет иметь следующую структуру:
{ name: 'My name', age: 5, species: 'Dog' }
Конечно, вы можете использовать другой формат, например, XML.
Наша задача — получить архив, открыть его, прочитать каждый файл и создать записи на основе входных данных. Начните с формы:
просмотров / животные / new.html.erb
<h1>Add animals</h1> <p> Upload a zip archive with JSON files in the following format:<br> <code>{name: 'name', age: 1, species: 'species'}</code> </p> <%= form_tag animals_path, method: :post, multipart: true do %> <%= label_tag 'archive', 'Select archive' %> <%= file_field_tag 'archive' %> <%= submit_tag 'Add!' %> <% end %>
Это базовая форма, позволяющая пользователю выбрать файл (не забудьте опцию multipart: true
).
Теперь действие контроллера:
animals_controller.rb
def create if params[:archive].present? # params[:archive].tempfile ... end redirect_to root_path end
Единственный интересующий нас параметр — это :archive
. Пока он содержит файл, он отвечает на метод tempfile
который возвращает путь к загруженному файлу.
Для чтения архива мы будем использовать метод Zip::File.open(file)
который принимает блок. Внутри этого блока вы можете извлечь каждый заархивированный файл и либо извлечь его куда-нибудь, используя extract
либо прочитать его в память с помощью get_input_stream.read
. Нам на самом деле не нужно никуда извлекать наш архив, поэтому давайте вместо этого сохраним содержимое в памяти.
animals_controller.rb
require 'zip' [...] def create if params[:archive].present? Zip::File.open(params[:archive].tempfile) do |zip_file| zip_file.each do |entry| Animal.create!(JSON.load(entry.get_input_stream.read)) end end end redirect_to root_path end [...]
Довольно просто, не правда ли? entry.get_input_stream.read
считывает содержимое файла, а JSON.load
анализирует его. Нас интересуют только файлы .json , поэтому давайте ограничим область видимости, используя метод glob
:
animals_controller.rb
[...] def create if params[:archive].present? Zip::File.open(params[:archive].tempfile) do |zip_file| zip_file.glob('*.json').each do |entry| Animal.create!(JSON.load(entry.get_input_stream.read)) end end end redirect_to root_path end [...]
Вы также можете извлечь часть кода в модель и ввести базовую обработку ошибок:
animals_controller.rb
[...] def create if params[:archive].present? Zip::File.open(params[:archive].tempfile) do |zip_file| zip_file.glob('*.json').each { |entry| Animal.from_json(entry) } end end redirect_to root_path end [...]
animal.rb
[...] class << self def from_json(entry) begin Animal.create!(JSON.load(entry.get_input_stream.read)) rescue => e warn e.message end end end [...]
Я также хочу добавить в белый список атрибуты, которые пользователь может назначить, чтобы он не мог переопределить поля id
или created_at
:
animal.rb
[...] WHITELIST = ['age', 'name', 'species'] class << self def from_json(entry) begin Animal.create!(JSON.load(entry.get_input_stream.read).select {|k,v| WHITELIST.include?(k)}) rescue => e warn e.message end end end [...]
Вместо этого вы можете использовать подход черного списка, заменив select
на исключением , но белый список более безопасен.
Большой! Теперь создайте zip-архив и попробуйте загрузить его!
Создание и загрузка архива
Давайте выполним противоположную операцию, позволив пользователю загрузить архив, содержащий файлы JSON, представляющие животных.
Добавить новую ссылку на корневую страницу:
просмотров / животные / index.html.erb
[...] <%= link_to 'Download archive', animals_path(format: :zip) %>
Мы будем использовать то же действие index
и оснастим его методом respond_to
:
animals_controller.rb
[...] def index @animals = Animal.order('created_at DESC') respond_to do |format| format.html format.zip do end end end [...]
Чтобы отправить архив пользователю, вы можете либо создать его где-то на диске, либо создать его на лету. Создание архива на диске включает в себя следующие этапы:
- Создайте массив файлов, которые должны быть помещены в архив:
files << File.open("path/name.ext", 'wb') { |file| file << 'content' }
- Создать архив:
Zip::File.open('path/archive.zip', Zip::File::CREATE) do |z|
- Добавьте ваши файлы в архив:
Zip::File.open('path/archive.zip', Zip::File::CREATE) do |z| files.each do |f| z.add('file_name', f.path) end end
Метод add
принимает два аргумента: имя файла в том виде, в котором оно должно отображаться в архиве, а также путь и имя исходного файла.
- Отправить архив:
send_file 'path/archive.zip', type: 'application/zip', disposition: 'attachment', filename: "my_archive.zip"
Это, однако, означает, что все эти файлы и сам архив будут сохраняться на диске. Конечно, вы можете удалить их вручную и даже попытаться создать временный zip-файл, как описано здесь, но это связано с чрезмерной сложностью.
Вместо этого я хотел бы создать наш архив на лету и использовать метод send_data для отображения ответа в виде вложения. Это немного сложнее, но мы ничего не можем поделать.
Для выполнения этой задачи нам потребуется метод с именем Zip::OutputStream.write_buffer
который принимает блок:
animals_controller.rb
[...] def index @animals = Animal.order('created_at DESC') respond_to do |format| format.html format.zip do compressed_filestream = Zip::OutputStream.write_buffer do |zos| end end end end [...]
Чтобы добавить новый файл в архив, используйте zos.put_next_entry
, zos.put_next_entry
имя файла. Вы даже можете указать каталог для zos.put_next_entry('nested_dir/my_file.txt')
вашего файла, сказав zos.put_next_entry('nested_dir/my_file.txt')
. Чтобы записать что-то в файл, используйте print
:
animals_controller.rb
compressed_filestream = Zip::OutputStream.write_buffer do |zos| @animals.each do |animal| zos.put_next_entry "#{animal.name}-#{animal.id}.json" zos.print animal.to_json(only: [:name, :age, :species]) end end
Мы не хотим, чтобы такие поля, как id
или created_at
присутствовали в файле, поэтому говорим :only
мы ограничиваем их name
, age
и species
.
Теперь перемотайте поток:
compressed_filestream.rewind
И отправь это:
send_data compressed_filestream.read, filename: "animals.zip"
Вот результирующий код:
animals_controller.rb
[...] def index @animals = Animal.order('created_at DESC') respond_to do |format| format.html format.zip do compressed_filestream = Zip::OutputStream.write_buffer do |zos| @animals.each do |animal| zos.put_next_entry "#{animal.name}-#{animal.id}.json" zos.print animal.to_json(only: [:name, :age, :species]) end end compressed_filestream.rewind send_data compressed_filestream.read, filename: "animals.zip" end end end [...]
Идите и попробуйте ссылку «Скачать архив»!
Вы даже можете защитить архив с помощью пароля . Эта функция rubyzip является экспериментальной и может измениться в будущем, но, похоже, работает в настоящее время:
animals_controller.rb
[...] compressed_filestream = Zip::OutputStream.write_buffer(::StringIO.new(''), Zip::TraditionalEncrypter.new('password')) do |zos| [...]
Настройка Rubyzip
Rubyzip предоставляет множество параметров конфигурации, которые могут быть предоставлены в блоке:
Zip.setup do |c| end
или один за другим:
Zip.option = value
Вот доступные варианты:
-
on_exists_proc
— Должны ли существующие файлы быть перезаписаны во время извлечения? По умолчанию установлено значениеfalse
. -
continue_on_exists_proc
— Должны ли существующие файлы быть перезаписаны при создании архива? По умолчанию установлено значениеfalse
. -
unicode_names
— установите этот параметр, если вы хотите хранить имена файлов, отличные от Unicode, в Windows Vista и болееunicode_names
установлено значениеfalse
. -
warn_invalid_date
— должно ли отображаться предупреждение, если архив имеет неправильный формат даты? По умолчанию этоtrue
. -
default_compression
— Используемый по умолчанию уровень сжатия. Первоначально установлено значениеZlib::DEFAULT_COMPRESSION
, другими возможными значениями являютсяZlib::BEST_COMPRESSION
иZlib::NO_COMPRESSION
. -
write_zip64_support
— следует ли отключить поддержку Zip64 для записи? По умолчанию установлено значениеfalse
.
Вывод
В этой статье мы посмотрели библиотеку rubyzip. Мы написали приложение, которое читает пользовательские архивы, создает записи на их основе и генерирует архивы на лету в ответ. Надеемся, что предоставленные фрагменты кода пригодятся в одном из ваших проектов.
Как всегда, спасибо, что остаетесь со мной и до скорой встречи!