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