Prawie każda aplikacja serwerowa prędzej czy później wymaga jakiejś formy przetwarzania plików wgrywanych przez użytkownika. O ile pliki są małe i w niewielkich ilościach, można uciec od zapisywania ich najpierw w pamięci RAM serwera. Problem zaczyna się, gdy stają się one większe niż rozmiar pamięci RAM. Może to spowodować awarie i przerwy w działaniu. Rozwiązaniem tego problemu jest zapisywanie plików bezpośrednio do pamięci długoterminowej.
Na potrzeby tego artykułu wystarczy zapisywanie plików na lokalnym magazynie serwera hostującego maszyny, ale przedstawione rozwiązanie można łatwo zmodyfikować do zapisywania plików w chmurze.
PlayFramework jest dostarczany ze świetną biblioteką o nazwie Akka. Akka Streams jest podmodułem Akki, który pomaga w przetwarzaniu strumieni danych – takich jak przesłany plik. Przedstawiony poniżej kod zapisze strumień bajtów – czyli uploadowany plik – bezpośrednio na dysku, bez zapisywania go najpierw jako całego pliku w pamięci RAM.
Sink
Aby zapisać pliki bezpośrednio na dysku wykorzystamy Akka Streams. Pierwszą rzeczą, której będziemy potrzebować jest 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) } }
Przedstawiona powyżej metoda utworzy i zwróci obiekt Sink. Sink zapisze wszystkie bajty do pliku o podanej nazwie. Pozwoli nam to na zachowanie oryginalnej nazwy pliku. Aby zapobiec kolizji nazw z innymi wgrywanymi plikami, umieszcza wgrywany plik w katalogu, którego nazwą jest timestamp.
Accumulator
Kolejnym krokiem jest stworzenie BodyParser’a, który będzie gromadził wszystkie bajty plików wysyłanych w żądaniu do wcześniej utworzonego Sink’a.
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
Następnym krokiem jest stworzenie punktu końcowego, który będzie obsługiwał wszystkie pliki przesyłane do serwera. Aby to osiągnąć stworzymy Action, który wykorzysta wcześniej stworzony BodyParser.
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. } }
Aby uzyskać dostęp do tego endpointu musimy dodać go do pliku routes z metodą POST.
POST /file/upload controllers.FileController.upload
Form
Ostatnim krokiem jest stworzenie wieloczęściowego formularza HTML.
@helper.form(action = routes.FileController.upload, Symbol("enctype") -> "multipart/form-data") { <input type="file" name="file"> <p> <input type="submit"> </p> }
Aby uzyskać więcej informacji na temat Akka Streams sprawdź Akka Documentation