Fast jede Serveranwendung erfordert früher oder später die Verarbeitung von Dateien, die der Benutzer hochgeladen hat. Solange es sich um kleine Dateien in geringen Mengen handelt, reicht es aus, sie zunächst im RAM des Servers zu speichern. Das Problem beginnt, wenn die Dateien größer werden als der RAM-Speicher. Dies kann zu Abstürzen und Ausfällen führen. Die Lösung für dieses Problem besteht darin, die Dateien direkt in den Langzeitspeicher zu schreiben.

Für die Zwecke dieses Artikels reicht es aus, die Dateien auf dem lokalen Speicher eines Hostservers zu speichern, aber die vorgestellte Lösung kann leicht modifiziert werden, um Dateien in der Cloud zu speichern.

PlayFramework wird mit einer großartigen Bibliothek namens Akka geliefert. Akka Streams ist ein Untermodul von Akka, das bei der Verarbeitung von Datenströmen – wie hochgeladenen Dateien – hilft. Der unten vorgestellte Code speichert Byteströme – die hochgeladene Datei – direkt auf dem Laufwerk, ohne sie zuerst als ganze Datei im RAM zu speichern.

Sink

Um Dateien direkt auf dem Laufwerk zu speichern, werden wir Akka Streams verwenden. Das erste, was wir brauchen, ist Sink.

val basePath = "/path/to/files/dir"
def localFileSink(filename: String): Sink[ByteString, Future[java.io.File]] = {
    val timestamp: Long = System.currentTimeMillis
    new java.io.File(s"$basePath/$timestamp/").mkdirs() // Creates directory with timestamp as name
    val path = s"$basePath/$timestamp/$filename" // Path string to file
    val file = new java.io.File(path) // Creates new file

    FileIO.toPath(file.toPath).mapMaterializedValue { t => // Creates Sink from file
        Future.successful(file)
    }
}

Die oben vorgestellte Methode erzeugt ein Sink-Objekt und gibt es zurück. Sink speichert alle Bytes in einer Datei mit einem bestimmten Namen. Dies ermöglicht es uns, den ursprünglichen Dateinamen beizubehalten. Um Namenskonflikte mit anderen hochgeladenen Dateien zu vermeiden, wird die hochgeladene Datei in einem Verzeichnis mit dem Zeitstempel als Namen abgelegt.

Accumulator

Der nächste Schritt besteht darin, einen BodyParser zu erstellen, der alle in der Anfrage gesendeten Dateibytes in dem zuvor erstellten Sink sammelt.

 def multipartFormDataToLocalFile: BodyParser[MultipartFormData[java.io.File]] = parse.using { request =>
    controllerComponents.parsers.multipartFormData { // Retrieves file data from request
        case FileInfo(partName, filename, contentType, dispositionType) =>
        val sink = fileService.localFileSink(filename).mapMaterializedValue(_.map { result => // Creates file sink for uploaded file
            FilePart(partName, filename, contentType, result)
        })

        Accumulator(sink) // Creates Accumulator which will transform Sing into Future object.
    }
}

 

Body parser

Der nächste Schritt besteht darin, einen Endpunkt zu erstellen, der alle Datei-Uploads abwickeln wird. Zu diesem Zweck erstellen wir eine Aktion, die den zuvor erstellten BodyParser verwendet.

 def upload = Action(multipartFormDataToLocalFile).async { implicit request =>
    request.body.file("file") match { // Check body parser returned any file
        case Some(filePart) => // User uploaded some file
        fileService.doStuff(filePart).map { result =>
            Ok(Json.toJson(result))
        }
        case None =>
        Future.successful(NotFound) // No file was found in request.
    }
}

Um auf diesen Endpunkt zuzugreifen, müssen wir ihn mit der POST-Methode in die Routendatei aufnehmen.

 POST /file/upload controllers.FileController.upload 

 

Form

Der letzte Schritt besteht darin, ein mehrteiliges HTML-Formular zu erstellen.

 @helper.form(action = routes.FileController.upload, Symbol("enctype") -> "multipart/form-data") {
    <input type="file" name="file">
    <p>
        <input type="submit">
    </p>
}

 

Weitere Informationen über Akka Streams finden Sie unter Akka Documentation