• Время чтения ~2 мин
  • 01.03.2023

Сегодня мы загружаем файл кусками с помощью Livewire. Загружайте свои файлы рядом с вашими пользователями с помощью приложения Fly.io, вы можете запустить свой Laravel за считанные минуты!

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

Что происходит, когда пользователь запрашивает загрузку файла, превышающего установленные ограничения? Вероятно, загрузка завершится ошибкой либо с пользовательским сообщением, которое мы пишем, либо с ошибкой кода состояния 413, возвращенной нашим сервером.

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

Проблема

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

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

Разве нет способа избежать этой вечной перенастройки и увеличения времени обработки запросов?

Решение

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

Это правильно! Сегодня мы не будем менять нашу конфигурацию под давлением меняющихся ограничений. Вместо этого мы решим эту проблему без изменения нашей конфигурации.

Сегодня мы будем нарезать, нарезать кубиками и объединять куски файлов — с помощью Livewire!

План

У нас есть трехэтапный план нарезки и слияния:

  1. сначала мы сообщим Livewire ожидаемую общую $fileSize получить от всех объединенных кусков.
  2. Затем мы начинаем нарезать, загружать и объединять куски в «окончательный файл» на нашем сервере один за другим с помощью Livewire.
  3. Как только размер нашего окончательного файла достигает заданного $fileSize, это означает, что все куски были объединены. Поэтому мы передаем конечный файл в класс Livewire TemporaryUploadedFile, чтобы использовать загруженный файл Livewire features.

Чтобы собрать воедино все здесь, вы можете посетить наш файл readme repository и проверить соответствующие файлы.

Представление

Начнем с создание компонента Livewire путем выполнения команды php artisan make:livewire chunked-file-upload. После этого обновите наше представление Livewire, включив в него тег формы, содержащий как элемент ввода файла, так и кнопку для отправки.

<form wire:submit.prevent="submit">
  <input type="file" id="myFile"/>
  <button type="button" onClick="uploadChunks()">Submit</button>
</form>

Каждый раз, когда пользователь нажимает на кнопку отправки, наша пользовательская функция JavaScript uploadChunks() нарезает выбранный файл на куски и запрашивает Livewire для загрузки каждого блока.

Чтобы загрузить большой файл, мы будем разрезать его на более мелкие куски, которые находятся в пределах ограничений размера запроса нашего сервера. Мы будем загружать каждый кусок один за другим, чтобы мы могли немедленно объединить загруженный кусок в «окончательный файл».

Но как именно сервер узнает, что все куски были объединены в наш окончательный файл? Конечно, он должен знать ожидаемый окончательный размер нашего окончательного файла!

Свойства Livewire идеально подходят для обмена информацией с клиента на сервер, поэтому давайте включим информацию о нашем файле, такую как его $fileName и $fileSize в качестве общедоступных атрибутов, в наш компонент Livewire . Сегодня мы разделим наш файл на блоки размером 1 МБ, поэтому давайте объявим отдельный атрибут для блока, загруженного $fileChunk, и ожидаемый максимальный размер блока $chunkSize:

// app/Http/Livewire/ChunkedFileUpload.php
public $chunkSize = 1000000; // 1 MB
public $fileChunk; 

public $fileName;
public $fileSize; 

Давайте вернемся к нашему виду Livewire и изменим uploadChunks() функция, запускаемая нашей кнопкой отправки. Каждый раз, когда пользователь отправляет файл для загрузки, мы устанавливаем значения для $fileName и $fileSize, которые будут позже отправлены в наш компонент Livewire:

// resources/views/livewire/chunked-file-upload.blade.php
function uploadChunks()
{
    const file = document.querySelector('#myFile').files[0];

    // Send the following later at the next available call to component
    @this.set('fileName', file.name, true);
    @this.set('fileSize', file.size, true);

Обратите внимание, что мы используем метод set Livewire здесь. Это позволяет нам установить атрибут public в нашем клиенте, но не делать немедленный вызов на сервер. Изменения в $fileName и $fileSize будут отправлены в Livewire в следующем немедленном запросе компонента.

Теперь, когда наши окончательные сведения о файле готовы к совместному использованию с нашим компонентом Livewire, мы можем взять фрагмент в нашем первом фрагменте, начиная с 0-го байта файла:

    livewireUploadChunk( file, 0 );
}

Slicing A Chunk

Как мы вырезаем кусок из нашего файла?

Ну, нам нужно знать, где начинается кусок и где он заканчивается. Для первого куска нашего файла отправной точкой является данность: 0. Но как насчет того, где заканчивается кусок?

Конец блока всегда будет равен 1 МБ (наш $chunkSize) от начальной точки блока или размера файла — в зависимости от того, что меньше между ними:

// resources/views/livewire/chunked-file-upload.blade.php
function livewireUploadChunk( file, start ){
    const chunkEnd  = Math.min( start + @js($chunkSize), file.size );
    const chunk     = file.slice( start, chunkEnd ); 

теперь, когда у нас есть наш кусок, нам придется отправить его на наш сервер. Мы можем использовать функцию Livewire upload JavaScript для загрузки и связывания блока с нашим атрибутом $fileChunk, объявленным выше:

    @this.upload('fileChunk', chunk);

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

-    @this.upload('fileChunk', chunk);
+    @this.upload('fileChunk', chunk,(uName)=>{}, ()=>{}, (event)=>{
+        if( event.detail.progress == 100 ){
+          // We recursively call livewireUploadChunk from within itself
+          start = chunkEnd;
+          if( start < file.size ){
+            livewireUploadChunk( file, start );
+          }
+        }
+    });
}

функции загрузки: загрузка завершается, когда значение event.detail.progress достигает 100. Как только это произойдет, мы рекурсивно вызываем текущую функцию livewireUploadChunk(), чтобы загрузить наш следующий кусок.

Диапазон метода file.sliceисключительный для chunkEnd. Например, диапазон среза (0,10) на самом деле означает от 0 до 9, но не 10! Это означает, что нашей следующей отправной точкой будет chunkEnd.

Сохранение и слияние

Теперь, когда наш JavaScript представления Livewire настроен для нарезки и загрузки фрагментов, мы подошли к заключительной части нашего путешествия по фрагментам и кубикам: сохранение и слияние блоков в наша компонент Livewire!

Мы будем использовать функцию Livewire WithFileUploads, чтобы сделать загрузку файла легкой. Эта черта позволяет нам объявить атрибут, который можно загрузить— $fileChunk в нашем случае!

// app/Http/Livewire/ChunkedFileUpload.php

+ use WithFileUploads;

// Chunks info
public $chunkSize = 1000000; // 1M
public $fileChunk;

// Final file 
public $fileName;
public $fileSize;

+ public $finalFile;

После загрузки фрагмента Livewire должен объединить его в «окончательный файл». Чтобы сделать это, нам придется перехватить поток Livewire после того, как наш кусок будет загружен.

К счастью для нас, Livewire предоставляет "hooks", которые мы можем использовать для перехвата потока жизненного цикла Livewire для наших общедоступных атрибутов. В нашем конкретном случае мы можем подключиться к обновленному хуку для нашего атрибута $fileChunk.

Из нашего обновленногоfileChunk hook мы получим имя файла, сгенерированное Livewire для текущего блока, используя метод getFileName():

public function updatedFileChunk()
{
    $chunkFileName = $this->fileChunk->getFileName();

Затем мы объединим этот кусок в наш окончательный файл и удалим кусок после объединения:

      $finalPath = Storage::path('/livewire-tmp/'.$this->fileName);
      $tmpPath   = Storage::path('/livewire-tmp/'.$chunkFileName);
      $file = fopen($tmpPath, 'rb');
      $buff = fread($file, $this->chunkSize);
      fclose($file);

      $final = fopen($finalPath, 'ab');
      fwrite($final, $buff);
      fclose($final);
      unlink($tmpPath);

В конце концов все куски прибудут один за другим и будут объединены в наш окончательный файл. Чтобы определить, все ли куски были объединены, мы просто сравниваем окончательный размер файла с ожидаемым $fileSize.

Конечно, этот недавно сгенерированный файл является нашим пользовательским файлом. Нам нужно будет заключить его в класс Livewire TemporaryUploadedFile, чтобы использовать загруженный файл Livewire features.

      $curSize = Storage::size('/livewire-tmp/'.$this->fileName);
      if( $curSize == $this->fileSize ){
          $this->finalFile = 
          TemporaryUploadedFile::createFromLivewire('/'.$this->fileName);
      }
}

Не забудьте импортировать класс TemporaryUploadedFile и объявить новый открытый атрибут $finalFile!

Скажем, например, предварительный просмотр нового, временного изображения, на наш взгляд, например:

@if ($finalFile)
    Photo Preview:
    <img src="{{ $finalFile->temporaryUrl() }}">
@endif

Реализации, как правило, намного более плавные с Livewire, загрузка фрагментов файлов ничем не отличается!

Последнее обновление 5 дней назад.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297