Как транслировать заархивированный файл (на лету) через Play Framework 2.5 в scala?

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

Я смотрел на эту реализацию: https://gist.github.com/kirked/03c7f111de0e9f095d7437 , но не мог понять, как его использовать.

 import java.io.{BufferedOutputStream, ByteArrayInputStream, ByteArrayOutputStream}
import java.util.zip.{ZipEntry, ZipOutputStream}
import akka.stream.scaladsl.{StreamConverters}
import org.apache.commons.io.FileUtils
import play.api.mvc.{Action, Controller}

class HomeController extends Controller {
  def single() = Action {
                         Ok.sendFile(
                           content = new java.io.File("C:\\Users\\a.csv"),
                           fileName = _ => "a.csv"
                         )
                       }

  def zip() = Action {
                     Ok.chunked(StreamConverters.fromInputStream(fileByteData)).withHeaders(
                       CONTENT_TYPE -> "application/zip",
                       CONTENT_DISPOSITION -> s"attachment; filename = test.zip"
                     )
                   }

  def fileByteData(): ByteArrayInputStream = {
    val fileList = List(
      new java.io.File("C:\\Users\\a.csv"),
      new java.io.File("C:\\Users\\b.csv")
    )

    val baos = new ByteArrayOutputStream()
    val zos = new ZipOutputStream(new BufferedOutputStream(baos))

    try {
      fileList.map(file => {
        zos.putNextEntry(new ZipEntry(file.toPath.getFileName.toString))
        zos.write(FileUtils.readFileToByteArray(file))
        zos.closeEntry()
      })
    } finally {
      zos.close()
    }

    new ByteArrayInputStream(baos.toByteArray)
  }
}

person cozyss    schedule 06.11.2017    source источник


Ответы (1)


Вместо того, чтобы использовать ByteArrayOutputStream для буферизации содержимого в массиве, а затем помещать его в ByteArrayInputStream, вы можете использовать механизм конвейера Java.

Вот эскизное решение:

def zip() = Action {
  // Create Source that listens to an OutputStream
  // and pass it to `fileByteData` method.
  val zipSource: Source[ByteString, Unit] =
    StreamConverters
      .asOutputStream()
      .mapMaterializedValue(fileByteData)
  Ok.chunked(zipSource).withHeaders(
    CONTENT_TYPE -> "application/zip",
    CONTENT_DISPOSITION -> s"attachment; filename = test.zip")
}

// Send the file data, given an OutputStream to write to.
def fileByteData(os: OutputStream): Unit = {
  val fileList = List(
    new java.io.File("C:\\Users\\a.csv"),
    new java.io.File("C:\\Users\\b.csv")
  )

  val zos = new ZipOutputStream(os)
  val buffer: Array[Byte] = new Array[Byte](2048)
  try {
    for (file <- fileList) {
      zos.putNextEntry(new ZipEntry(file.toPath.getFileName.toString))
      val fis = new Files.newInputStream(file.toPath)
      try {
        @tailrec
        def zipFile(): Unit = {
          val bytesRead = fis.read(buffer)
          if (bytesRead == -1) () else {
            zos.write(buffer, 0, bytesRead)
            zipFile()
          }
        }
        zipFile()
      } finally fis.close()
      zos.closeEntry()
    }
  } finally {
    zos.close()
  }
}

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

person Rich Dougherty    schedule 06.11.2017
comment
Большое спасибо! изменение Source на StreamConverts в Source.asOutputStream (). mapMaterializedValue (fileByteData) компилируется для меня. Однако с этой реализацией я получаю исключение IllegalStateException. После использования буфера массива байтов для чтения файла исключение незаконного состояния исчезнет, ​​но файл никогда не загружается. - person cozyss; 07.11.2017
comment
Хорошо, я обновил фрагмент, чтобы он компилировался. Что кидает IllegalStateException? - person Rich Dougherty; 07.11.2017
comment
Это zos.write (FileUtils.readFileToByteArray (файл)). После того, как я заменил его буфером val: Array [Byte] = new Array [Byte] (2048) val bis = new BufferedInputStream (Files.newInputStream (file.toPath)) var bytesRead = bis.read (buffer) while (bytesRead! = -1) {zos.write (buffer, 0, bytesRead) bytesRead = bis.read (buffer)} исключение исчезло. Но папка zip не загружается. - person cozyss; 07.11.2017
comment
Упс совсем забыл про FileUtils.readFileToByteArray(file) чтение каждого файла в память с буфером. Я обновил код, частично основываясь на вашем цикле while. Вы хотите это проверить? Также какой класс бросал IllegalStateException? - person Rich Dougherty; 07.11.2017
comment
Вышеупомянутое исключение IllegalException было, когда мы использовали FileUtils.readFileToByteArray(file) - person cozyss; 08.11.2017
comment
ОК - так что исключение исчезло? Файл скачивается? - person Rich Dougherty; 08.11.2017
comment
О, исключение ушло. Но файл все равно не скачивается. Он просто там висит. - person cozyss; 08.11.2017
comment
Можете ли вы попробовать запустить тело fileByteData в новом потоке с ExecutionContext.run? - person Rich Dougherty; 08.11.2017
comment
Ах, после того, как я заменил файл на файл s3, он работает! Я думаю, это проблема с резьбой. Спасибо!!!!!!! :)))) - person cozyss; 08.11.2017
comment
Итак, теперь он работает с использованием файла s3. Вы также использовали ExecutionContext.run для устранения проблемы с потоком? Дайте мне знать - я обновлю фрагмент в ответе. - person Rich Dougherty; 09.11.2017
comment
Теперь он не работает даже с файлами s3 :( Не могли бы вы обновить ответ, чтобы я знал, как исправить это с помощью ExecutionContext.run .. Большое спасибо! - person cozyss; 09.11.2017
comment
Я просматриваю этот пост, чтобы узнать, почему он не загружает github.com/playframework/playframework/issues/ 7512 - person cozyss; 10.11.2017
comment
Правильно, я помню, как распараллеливание кода вызывало OutputStream как возможную проблему. Я свяжусь с этим SO-ответом из этой проблемы, чтобы помочь тем, кто использует Google. :) - person Rich Dougherty; 10.11.2017
comment
Я пытаюсь .mapMaterializedValue(os => Future { fileByteData(os); os.close();pringln("os is closed")} использовать неявный excecutionContext.global. Я вижу, что папка была загружена с начальным ярлыком, но дальше этого не продвигается. Я также не могу добраться до оператора печати, указывающего, что ОС закрыта должным образом. Я здесь что-то не так делаю? - person cozyss; 10.11.2017