GridFS — это спецификация MongoDB для хранения больших файлов. В этой статье я расскажу как можно легко загружать файлы в GridFS, а затем извлекать их из базы данных и отображать в браузере.

Но перед стартом, вот краткое объяснение того, как работает GridFS от Kristina Chodorow:
GridFS разбивает большие файлы на маленькие кусочки. Кусочки сохраняются в одну коллекцию (fs.chunks), а метаданные о файле в другую коллекцию (fs.files). Когда вы делаете запрос к файлу, GridFS делает запрос в коллекцию с кусочками и возвращает файл целиком.

Конечно же драйвер MongoDB для PHP поставляется с парочкой классов, которые можно использовать для хранения и извлечения файлов из GridFS.

Несколько преимуществ GridFS, описанных в этой статье:

  • Если вы используете репликацию или сегментирование (шардинг), GridFS сделает все за вас.
  • MongoDB дробит файлы на куски по 2Гб, так что у вашей ОС точно не будет проблем с манипулированием файлами.
  • Вам не нужно беспокоиться об ограничениях ОС на имена файлов или количество файлов в одной директории.
  • MongoDB автоматически генерирует и хранит MD5 хеш вашего файла. Это удобно для сравнения загруженных файлов по MD5 хешу и обнаружения дубликатов или валидации успешной загрузки.

Создание документа GridFS


Начнем с простого документа Upload:

namespace Dennis\UploadBundle\Document;

use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;

/**
 * @MongoDB\Document
 */
class Upload
{
    /** @MongoDB\Id */
    private $id;

    /** @MongoDB\File */
    private $file;

    /** @MongoDB\String */
    private $filename;

    /** @MongoDB\String */
    private $mimeType;

    /** @MongoDB\Date */
    private $uploadDate;

    /** @MongoDB\Int */
    private $length;

    /** @MongoDB\Int */
    private $chunkSize;

    /** @MongoDB\String */
    private $md5;

    public function getFile()
    {
        return $this->file;
    }

    public function setFile($file)
    {
        $this->file = $file;
    }

    public function getFilename()
    {
        return $this->filename;
    }

    public function setFilename($filename)
    {
        $this->filename = $filename;
    }

    public function getMimeType()
    {
        return $this->mimeType;
    }

    public function setMimeType($mimeType)
    {
        $this->mimeType = $mimeType;
    }

    public function getChunkSize()
    {
        return $this->chunkSize;
    }

    public function getLength()
    {
        return $this->length;
    }

    public function getMd5()
    {
        return $this->md5;
    }

    public function getUploadDate()
    {
        return $this->uploadDate;
    }
}

Важная часть в этом листинге — аннотация @MongoDB\File. Она говорит Doctrine MongoDB ODM, что документ должен быть сохранен с использованием GridFS, и экземпляр класса MongoGridFSFile содержится в свойстве $file.

Свойства $chunkSize, $length, $md5 и $uploadDate не нуждаются в сеттерах, потому что они будут заполнены автоматически драйвером MongoDB.

Обработка загрузки файла


В качестве примера я буду использовать простой контроллер, который использует form builder для создания формы с полем типа file:

namespace Dennis\UploadBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class UploadController extends Controller
{
    public function newAction(Request $request)
    {
        $form = $this->createFormBuilder(array())
            ->add('upload', 'file')
            ->getForm();

        if ($request->isMethod('POST')) {
            $form->bind($request);

            // ...
        }

        return array('form' => $form->createView());
    }
}

Моя цель — сохранить файл в базу данных прямо из папки /tmp, куда он помещается после загрузки, чтобы избежать многократного перемещения файла по файловой системе. Для этого я извлеку отправленные данные из формы с помощью $form->getData(), чтобы получить объект UploadedFile. При использовании сущностей, объект UploadedFile можно получить из значения свойства вашей сущности, которое в form builder'e указано как поле типа file.

Объект UploadedFile содержит всю необходимую нам информацию, чтобы добавить файл в базу данных прямо из временной папки, потому что она основана на данных глобальной PHP переменной $_FILES.

use Dennis\UploadBundle\Document\Upload;

public function newAction(Request $request)
{
    // ...

    $data = $form->getData();

    /** @var $upload \Symfony\Component\HttpFoundation\File\UploadedFile */
    $upload = $data['upload'];

    $document = new Upload();
    $document->setFile($upload->getPathname());
    $document->setFilename($upload->getClientOriginalName());
    $document->setMimeType($upload->getClientMimeType());

    $dm = $this->get('doctrine.odm.mongodb.document_manager');
    $dm->persist($document);
    $dm->flush();
}

Теперь, когда вся необходимая информация у нас на руках, мы можем создать объект Upload, в который мы можем передать путь к временному файлу в свойство $file. Объект UploadedFile так же предоставляет нам дополнительную информацию о файле, часть из которой мы можем добавить к документу Upload, например MIME-тип и имя файла. На данном этапе документ готов к сохранению в базу данных и, как и ожидается от ODM, делается это с помощью persist() и flush().

Извлечение загруженных файлов из GridFS


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

В контроллер, который я описывал выше, добавим еще один метод:

/** 
 * @Route("/{id}", name="upload_show") 
 */
public function showAction($id)
{
    $upload = $this->get('doctrine.odm.mongodb.document_manager')
        ->getRepository('DennisUploadBundle:Upload')
        ->find($id);

    if (null === $upload) {
        throw $this->createNotFoundException(sprintf('Upload with id "%s" could not be found', $id));
    }

    $response = new Response();
    $response->headers->set('Content-Type', $upload->getMimeType());

    $response->setContent($upload->getFile()->getBytes());

    return $response;
}

Довольно прямолинейно, как видите. Параметр id, сгенерированный MongoDB должен указываться в URL и будет использоваться для извлечения документа Upload из базы. Для вывода файла создадим объект класса Response с указанием Content-Type, который мы возьмем из свойства $mimeType документа Upload. А контент для вывода берем из свойства $file, с помощью метода getBytes().

Потоковый ресурс и StreamedResponse


Начиная с версии 1.3.0-beta1 драйвер MongoDB поддерживает метод getResource(), который возвращает потоковый ресурс файла. Это позволяет вам использовать объект StreamedResponse вместо обычного Response. StreamedResponse позволяет стримить контент клиенту (браузеру — прим. пер.) с помощью callback. Выглядит это следующим образом:

public function showAction($id)
{
    // ...

    $response = new StreamedResponse();
    $response->headers->set('Content-Type', $upload->getMimeType());

    $stream = $upload->getFile()->getResource();

    $response->setCallback(function () use ($stream) {
        fpassthru($stream);
    });

    return $response;
}

Пока все. В следующей статье я напишу о том, как скомбинировать документ Upload c сущностью (Entity).

— Это был вольный перевод статьи Uploading files to MongoDB GridFS. Планируется перевод второй части.
Поделиться с друзьями
-->

Комментарии (4)


  1. andrew_tch
    09.11.2016 22:44

    Извините, велосипед.

    https://github.com/KnpLabs/Gaufrette
    https://github.com/KnpLabs/Gaufrette/blob/master/src/Gaufrette/Adapter/GridFS.php


  1. artem90
    10.11.2016 07:20
    +1

    А насколько вообще хорошо хранить файлы в базе? Какие есть плюсы и минусы?


    1. AlexxStY
      10.11.2016 13:50

      Все зависит от нагрузки на отдачу|получение файлов.
      Вот тут неплохая статья эту тему: https://habrahabr.ru/company/oleg-bunin/blog/313364/


    1. Reshat
      10.11.2016 14:13

      Помимо того, что указано в статье:
      — так как файлы хранятся кусками (chunks), это можно использовать для быстрого чтения нужного кусочка файла.
      — GridFS подходит для хранения статических файлов, потому что обновление файла в этой системе — это удаление и создание нового файла. Но это можно использовать для версионирования файлов.
      — если вы захотите перейти на хранение в другой файловой системе — придется писать какую-нибудь миграцию.