Download as ZIP

Сегодня в нашем блоге гостевой пост — о своем опыте использования нашего облачного хранилища расскажет Андрей Суржиков, разработчик в одной из медицинских компаний.

Мы храним в облаке Selectel огромное количество файлов. В среднем, каждый месяц пользователи загружают к нам около 1 миллиона файлов (200 Гб данных), которые мы обязуемся хранить в течение 5 лет. Помимо хранения, мы должны дать пользователям возможность скачивать нужные наборы файлов в виде архивов.

Для этого у нас есть два варианта:

  • Стандартный — запросом в API скомандовать облаку отдать определенный контейнер или папку в виде архива (этот вариант предлагается в официальной документации Selectel по облачному хранилищу).
  • Самодельный — забирать файлы из облака, а непосредственно архивацию выполнять у себя на сервере.

Рассмотрим преимущества и недостатки обоих вариантов.

Стандартный вариант

Облако дает возможность скачать любой контейнер или папку внутри контейнера в виде ZIP-архива. Эта возможность описана в официальной документации.

Плюсы:

  • не надо писать код: просто сгенерировал ссылку и отдал пользователю;
  • не требует ресурсов вашего сервера;
  • меньше запросов к API — экономит копеечку с баланса.

Минусы:

  • облако отдает архив без сжатия;
  • неподходящая для нас структура папок в архиве с директориями от корня контейнера.

В случае с публичным контейнером, достаточно включить в настройках контейнера возможность скачивания в виде архива (либо с помощью заголовка X-Container-Meta-Allow-ZipDownload: True) и сформировать ссылку вида:

https://api.selcdn.ru/v1/SEL_*****/container/?download-all-as-zip=container.zip

После этого отдать ее пользователю. Как только он откроет ссылку — начнется загрузка container.zip содержащего в себе все объекты контейнера.

Приватный контейнер

Скачать архив из приватного контейнера тоже можно, только нужно передать авторизационный HTTP-заголовок X-Auth-Token (не забывайте, Облачное хранилище построено на базе OpenStack, если не нашли чего-то в документации Selectel — попробуйте поискать в документации OpenStack).

Сценарий действий такой:

  1. Пользователь хочет скачать заархивированную папку из приватного контейнера.
  2. Мы даем ему ссылку примерно такого вида:

    https://наш-сервер.ru/download/directory-in-private-container

  3. Пользователь переходит по ссылке.
  4. Наш сервер выполняет запрос к API Облака Selectel с целью авторизации.

    Передаем X-Auth-User и X-Auth-Key (логин и пароль пользователя) и получаем в ответ X-Auth-Token и X-Storage-Url (токен и базовый url хранилища).

    Подробнее об авторизации в документации.

  5. Далее наш сервер формирует запрос вида:

    ${STORAGE_URL}/container/object_prefix?download-all-as-zip

  6. Затем стримит его пользователю в браузер (в пакете Guzzle-HTTP для PHP эта опция называется stream (подробнее в документации Guzzle).
  7. Все довольны: мы своими средствами проверили что этот пользователь имеет право скачивать эти файлы, нигде не «засветили» логин и пароль от хранилища, без нагрузки на свой сервер отдали пользователю zip-архив нужной директории.

Самодельный вариант

Этот вариант упрощенно выглядит так:

  1. Пользователь захотел скачать архив.
  2. Наш сервер забирает нужные файлы из облака.
  3. Из файлов сервер формирует архив.
  4. Отдает архив пользователю.

Плюсы:

  • можно использовать сжатие – пользователю меньше скачивать;
  • можно сделать нужную структуру папок в архиве и добавить дополнительные файлы.

Минусы:

  • надо писать код;
  • нагрузка на сервер больше, чем с первым вариантом;
  • много запросов к API Облака.

Есть два варианта действий:

  • скачать все файлы к себе на сервер, заархивировать и потом отдать пользователю архив;

    На мой взгляд это плохой вариант, особенно если у вас такие же большие (~1Гб) архивы как у нас.

  • «на лету» собирать zip-архив и стримить пользователю.

    Классный вариант, которым мы пользуемся сейчас.

Для реализации второго варианта мы воспользовались двумя библиотеками (PHP):

  1. argentcrusade/selectel-cloud-storage — работа с API Облака Selectel.
  2. maennchen/zipstream-php — архивация файлов «на лету» (обязательно используйте v1.0.0-alpha.1 ветку).

Ниже приведен фрагмент кода, который реализует скачивание файлов, архивацию «на лету» и стриминг результата пользователю:

<?php
 
/**
 * Библиотека для работы с Облаком
 */
use ArgentCrusade\Selectel\CloudStorage\Api\ApiClient;
use ArgentCrusade\Selectel\CloudStorage\CloudStorage;
 
/**
 * Библиотека для архивации файлов
 */
use ZipStream\ZipStream;
use ZipStream\Option\Archive as ArchiveOptions;
 
 
....
 
 
public function StreamArchive()
{
 
	/**
	 * Название контейнера в Облаке
	 */
	$container_name = "private1";
 
	/**
	 * Определяем объекты в контейнере Облака Selectel,
	 * которые нужно заархивировать
	 */
	$objects = [
		'/dir1/file1.jpg',
		'/dir1/file2.jpg',
		'/dir1/file3.jpg',
		'/dir1/file4.jpg',
		'/dir1/file5.jpg',
		'/dir1/file5.jpg',
		'/dir2/video1.mov',
		'/dir2/video2.mov',
		'/dir2/video3.mov',
		'/dir2/video4.mov'
	];
 
	/**
	 * Подключаемся к облаку (укажите свой логин/пароль)
	 * Выбираем контейнер
	 */
	try {
		$apiClient = new ApiClient("12345_cloud_username", "123456789");
		$storage = new CloudStorage($apiClient);
		$container = $storage->getContainer($container_name);
	} catch (\Throwable $e) {
		throw new \Exception("Ошибка подключения к облаку. Причина: " . $e->getMessage());
	}
 
	/**
	 * Опции архиватора – смотрите в официальном репозитории
	 * https://github.com/maennchen/ZipStream-PHP/tree/v1.0.0-alpha.1
	 */
	$opt = new ArchiveOptions();
	$opt->setDeflateLevel(2);
	$opt->setZeroHeader(true);
	$opt->setContentDisposition('attachment');
	$opt->setContentType('application/octet-stream');
	$opt->setHttpHeaderCallback('header');
	$opt->setSendHttpHeaders(true);
	
        /**
	 * Выходной поток
	 */
	$fd = fopen('php://output', 'w');
	$opt->setOutputStream($fd);
 
	$ZipStream = new ZipStream('archive.zip', $opt);
	
	/**
	 * Включаем буферизацию вывода (
	 */
        ob_start();
 
	/**
	 * Поочередно добавляем файлы в архив
	 */
	foreach ($objects as $i => $object) {
		try {
			$cloud_file_raw = $container->files()->find($object)->read();
			$ZipStream->addFile($container_name . $object, $cloud_file_raw);
		} catch (\Throwable $e) {
			// Обработка ошибок
		}
		/**
	 	 * Опустошаем буфер
		 */
		ob_flush();
		flush();
		ob_clean();
 
	}
 
	$ZipStream->finish();
}

Не забудьте понаблюдать за расходом памяти: воспользуйтесь профайлером, либо просто добавьте нечто подобное в тело цикла.

file_put_contents('/tmp/mem.txt', $i . ' : ' . strval(memory_get_usage() / 1048576) . 'mb');

Если вы все правильно сделали, потребление памяти не должно увеличиваться. В нашем случае, при выкачке 2Gb архива расход памяти около 12Mb за все время работы скрипта.

Заключение

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

Расскажите нам о своем опыте использования Облачного хранилища. Ждем вас в комментариях.