Загрузка больших файлов с помощью Play framework

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

def downloadLocalBackup() = Action {
  var pathOfFile = "/opt/mydir/backups/big/backup"
  val file = new java.io.File(pathOfFile)
  val path: java.nio.file.Path = file.toPath
  val source: Source[ByteString, _] = FileIO.fromPath(path)
  logger.info("from local backup set the length in header as "+file.length())
  Ok.sendEntity(HttpEntity.Streamed(source, Some(file.length()), Some("application/zip"))).withHeaders("Content-Disposition" -> s"attachment; filename=backup")
}

Я не знаю, как потоковая передача в приведенном выше случае учитывает разницу в скорости чтения с диска (которая быстрее, чем сеть). Это никогда не заканчивается памяти даже для больших файлов. Но когда я использую приведенный ниже код с потоком zipOutput, я не уверен в причине нехватки памяти. Почему-то тот же файл размером 3 ГБ, когда я пытаюсь использовать его с zip-потоком, не работает.

def downloadLocalBackup2() = Action {
  var pathOfFile = "/opt/mydir/backups/big/backup"
  val file = new java.io.File(pathOfFile)
  val path: java.nio.file.Path = file.toPath
  val enumerator = Enumerator.outputStream { os =>
    val zipStream = new ZipOutputStream(os)
    zipStream.putNextEntry(new ZipEntry("backup2"))
    val is = new BufferedInputStream(new FileInputStream(pathOfFile))
    val buf = new Array[Byte](1024)
    var len = is.read(buf)
    var totalLength = 0L;
    var logged = false;
    while (len >= 0) {
      zipStream.write(buf, 0, len)
      len = is.read(buf)
      if (!logged) {
        logged = true;
        logger.info("logging the while loop just one time")
      }
    }
    is.close

    zipStream.close()
  }
  logger.info("log right before sendEntity")
  val kk = Ok.sendEntity(HttpEntity.Streamed(Source.fromPublisher(Streams.enumeratorToPublisher(enumerator)).map(x => {
    val kk = Writeable.wByteArray.transform(x); kk
  }),
    None, Some("application/zip"))
  ).withHeaders("Content-Disposition" -> s"attachment; filename=backupfile.zip")
  kk
}

person curiousengineer    schedule 12.09.2017    source источник


Ответы (1)


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

То, как мы сегодня потребляем услуги из Интернета, включает в себя множество случаев потоковой передачи данных, как загрузки из службы, так и загрузки в нее или одноранговой передачи данных. Рассматривать данные как поток элементов, а не целиком, очень полезно, потому что это соответствует тому, как компьютеры отправляют и получают их (например, через TCP), но часто это также необходимо, потому что наборы данных часто становятся слишком большими, чтобы их можно было использовать. обрабатываться как единое целое. Мы распределяем вычисления или анализ по большим кластерам и называем это «большими данными», где весь принцип их обработки заключается в последовательной подаче этих данных — в виде потока — через несколько процессоров.

...

Цель [Akka Streams] — предложить интуитивно понятный и безопасный способ сформулировать настройки потоковой обработки таким образом, чтобы мы могли затем выполнять их эффективно и с ограниченным использованием ресурсов — больше никаких OutOfMemoryErrors. Чтобы добиться этого, наши потоки должны иметь возможность ограничивать буферизацию, которую они используют, они должны иметь возможность замедлять производителей, если потребители не могут идти в ногу. Эта функция называется обратным давлением и лежит в основе инициативы Reactive Streams которого Акка является одним из основателей.

Во втором примере вы сами обрабатываете входные/выходные потоки, используя стандартный API блокировки. Я не уверен на 100%, как здесь работает запись в ZipOutputStream, но возможно, что он не сбрасывает записи и не накапливает все до close.

Хорошо, что вам не нужно обрабатывать это вручную, поскольку Akka Streams предоставляет способ gzip для Source из ByteStrings:

import javax.inject.Inject

import akka.util.ByteString
import akka.stream.scaladsl.{Compression, FileIO, Source}

import play.api.http.HttpEntity
import play.api.mvc.{BaseController, ControllerComponents}

class FooController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {

  def download = Action {
    val pathOfFile = "/opt/mydir/backups/big/backup"
    val file = new java.io.File(pathOfFile)
    val path: java.nio.file.Path = file.toPath
    val source: Source[ByteString, _] = FileIO.fromPath(path)
    val gzipped = source.via(Compression.gzip)
    Ok.sendEntity(HttpEntity.Streamed(gzipped, Some(file.length()), Some("application/zip"))).withHeaders("Content-Disposition" -> s"attachment; filename=backup")
  }

}
person marcospereira    schedule 13.09.2017
comment
У меня есть несколько вещей, чтобы спросить об этом, но я хотел сначала попробовать ваше предложение. Что мне нужно иметь в библиотеках, чтобы иметь Compression.gzip? не могу найти/ - person curiousengineer; 13.09.2017
comment
Я пробовал, и длина загрузки всегда равна нулю. Есть ли какие-то изменения в приведенном выше отрывке, который вы предложили? - person curiousengineer; 13.09.2017
comment
этот метод не работает для меня. После сжатия длина файла должна измениться. - person cozyss; 06.11.2017