使用Play框架下载大文件

时间:2017-09-12 05:10:03

标签: scala playframework akka-stream http-streaming

我有一个示例下载代码,如果文件没有压缩,工作正常,因为我知道长度,当我提供时,我认为流媒体播放不必将整个文件带入内存并且它可以工作。以下代码可以使用

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流时,我不确定内存不足的原因。当我尝试使用zip流时,不知何故相同的3GB文件无效。

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
}

1 个答案:

答案 0 :(得分:3)

在第一个示例中,Akka Streams会为您处理所有细节。它知道如何在不将完整文件加载到内存中的情况下读取输入流。这就是使用Akka Streams作为explained in the docs的优势:

  

我们今天从互联网上消费服务的方式包括许多流数据实例,包括从服务下载以及上传到服务或点对点数据传输。关于数据作为元素流而不是整体是非常有用的,因为它匹配计算机发送和接收它们的方式(例如通过TCP),但它通常也是必需的,因为数据集经常变得太大而不能作为一个整体处理。我们在大型集群上进行计算或分析并将其称为“大数据”,其中处理它们的整个原则是通过顺序提供这些数据 - 作为流通过某些CPU。

     

...

     

[Akka Streams]的目的是提供一种直观而安全的方式来制定流处理设置,以便我们可以有效地执行它们并且使用有限的资源 - 不再使用OutOfMemoryErrors。为了实现这一目标,我们的流需要能够限制他们使用的缓冲,如果消费者无法跟上,他们需要能够减慢生产者的速度。此功能称为back-pressure,是Akka作为创始成员的Reactive Streams计划的核心。

在第二个示例中,您使用标准阻塞API自行处理输入/输出流。我不是100%确定写ZipOutputStream如何在这里工作,但它有可能不会刷新写入并在close之前累积所有内容。

好的一点是,您不需要手动处理此问题,因为Akka Streams提供了一种方法来Source ByteStringimport 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") } }

AuthenticatesUsers.php