2017-11-06 9 views
1

일부 파일을 스트리밍하고 즉시 압축하여 사용자가 로컬 디스크에 아무 것도 쓰지 않고 여러 개의 파일을 하나의 압축 파일로 다운로드 할 수 있습니다. 그러나, 현재 구현은 메모리에있는 모든 것을 보유하고 있으며 대용량 파일에서는 작동하지 않습니다. 그것을 고칠 방법이 있습니까?Play Framework 2.5를 통해 압축 된 파일을 스칼라로 스트리밍하는 방법은 무엇입니까?

이 구현을보고 : https://gist.github.com/kirked/03c7f111de0e9a1f74377bf95d3f0f60,하지만 그것을 사용하는 방법을 알아낼 수 없습니다. 대신 당신이 자바의 배관 메커니즘을 사용할 수있는 ByteArrayInputStream에 넣어 다음 배열의 내용을 버퍼에 ByteArrayOutputStream를 사용

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) 
    } 
} 

답변

1

.

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이 전송 스레드와 다른 스레드에서 실행되기를 바랍니다. - 오류 처리가 정상입니다. 예 : 서버 (예 : 파일을 찾을 수 없음) 또는 클라이언트 측 (조기 연결 해제)에 오류가있는 경우 모든 스트림이 올바르게 닫힘

+1

고마워요! "Source.asOutputStream()."에서 "Source"를 "StreamConverts"로 변경하면 mapMaterializedValue (fileByteData)가 컴파일됩니다. 그러나이 구현에서는 IllegalStateException이 발생합니다. 바이트 배열 버퍼를 사용하여 파일을 읽은 후에는 illegalStateException이 사라졌지 만 파일은 다운로드되지 않습니다. – rileyss

+1

확인 코드가 업데이트되도록 스 니펫을 업데이트했습니다. 'IllegalStateException'을 던지는 것은 무엇입니까? –

+1

"zos.write (FileUtils.readFileToByteArray (file))"입니다. "val 버퍼 : Array [Byte] = 새 배열 [Byte] (2048) val bis = new BufferedInputStream (Files.newInputStream (file.toPath)) var bytesRead = bis.read (버퍼) while (bytesRead! = -1) { zos.write (버퍼, 0, bytesRead) bytesRead = bis.read (버퍼) } "예외가 사라졌습니다. 그러나 zip 폴더는 다운로드되지 않습니다. – rileyss