Этот пост посвящен постам Akka и Spray и расширяет его, добавляя обработчик загрузки файлов. Это позволяет нам загружать изображения в аккаунт пользователя. (Обратите внимание, что, как обычно, код Akka не делает ничего особенного с загруженными файлами; он просто демонстрирует, что мы можем принять multipart/form-data
кодированный POST.)
Это наше приложение AngularJS в браузере. Мы нажимаем кнопку « Добавить файлы…» , чтобы добавить (изображения), которые будут загружены. Когда пользователи нажимают кнопку « Начать загрузку» , мы начинаем загрузку параллельно, но каждый POST содержит один файл. Кнопка Отмена загрузки очищает форму.
Спрей код
Запросы собираются register/image
, поэтому нам нужно добавить соответствующий «обработчик» RegistrationService
для обработки POST по этому пути, чтобы демонтировать запрос как MultipartFormData
. Затем мы complete
запрос с соответствующим ответом.
class RegistrationService(registration: ActorRef) (implicit executionContext: ExecutionContext) extends Directives with DefaultJsonFormats { import akka.pattern.ask import scala.concurrent.duration._ implicit val timeout = Timeout(2.seconds) implicit val userFormat = jsonFormat4(User) implicit val registerFormat = jsonFormat1(Register) implicit val registeredFormat = jsonObjectFormat[Registered.type] implicit val notRegisteredFormat = jsonObjectFormat[NotRegistered.type] implicit object EitherErrorSelector extends ErrorSelector[NotRegistered.type] { def apply(v: NotRegistered.type): StatusCode = StatusCodes.BadRequest } val route = path("register") { post { handleWith { ru: Register => (registration ? ru). mapTo[Either[NotRegistered.type, Registered.type]] } } } ~ path("register" / "image") { post { entity(as[MultipartFormData]) { data => complete { data.fields.get("files[]") match { case Some(imageEntity) => val size = imageEntity.entity.buffer.length println(s"Uploaded $size") "OK" case None => println("No files") "Not OK" } } } } } }
Это весь исходный код, RegistrationService
чтобы показать вам, куда вписывается новый код. Новый бит — это использование entity(as[A]) { a => ... }
, которое принимает опубликованное HttpEntity
, находит экземпляр класса Unmarshaller
типов для типа A
. Когда демаршаллинг завершается успешно, он применяет данную функцию { a => ... }
для завершения запроса. В нашем случае имеем:
path("register" / "image") { post { entity(as[MultipartFormData]) { data => complete { data.fields.get("files[]") match { case Some(imageEntity) => val size = imageEntity.entity.buffer.length println(s"Uploaded $size") "OK" case None => println("No files") "Not OK" } } } } }
Это означает, что в POST для register/image
мы демонтируем сущность как MultipartFormData
и применяем функцию:
{ data => complete { data.fields.get("files[]") match { case Some(imageEntity) => val size = imageEntity.entity.buffer.length println(s"Uploaded $size") s"""{"size":$size}""" case None => println("No files") """{"size":0}""" } } }
К успешно раскрытой ценности. В этой функции мы complete
просьбу, в конечном счете , возвращающих либо s"""{"size":$size}"""
или """{"size":0}"""
.
Stringly-типизированных
Ручное конструирование JSON, XML, чего угодно, ужасная идея . Давайте избавиться от тех , s"""{"size":$size}"""
и """{"size":0}"""
строки и заменить их с удобным классом случае.
case class ImageUploaded(size: Int)
Мы можем легко определить JsonWriter
экземпляр класса типов для ImageUploaded
типа, добавив неявное значение типа JsonWriter[ImageUploaded]
; и это именно то, что jsonFormat
делают функции.
implicit val imageUploadedFormat = jsonFormat1(ImageUploaded)
Out со строками
Итак, мы добавляем класс case и JsonWriter[ImageUpload]
экземпляр класса типов, что позволяет нам избавиться от String
s в complete
функции.
class RegistrationService(registration: ActorRef) (implicit executionContext: ExecutionContext) extends Directives with DefaultJsonFormats { case class ImageUploaded(size: Int) implicit val imageUploadedFormat = jsonFormat1(ImageUploaded) ... val route = ... path("register" / "image") { post { entity(as[MultipartFormData]) { data => complete { data.fields.get("files[]") match { case Some(imageEntity) => val size = imageEntity.entity.buffer.length println(s"Uploaded $size") ImageUploaded(size) case None => println("No files") ImageUploaded(0) } } } } } }
Теперь это лучше. Там нет String
s, и мы прекрасно справляемся с загрузкой файлов. К сожалению, все еще слишком много заметок . Разобрав post
обработчик, имеем:
entity(as[MultipartFormData]) { data => complete { ImageUploaded(...) } }
entity(as[A]) { a: A => complete { ... } }
Может быть заменен handleWith
: тот же код , который мы используем в register
должности. Итак, окончательный код просто:
path("register" / "image") { post { handleWith { data: MultipartFormData => data.fields.get("files[]") match { case Some(imageEntity) => val size = imageEntity.entity.buffer.length println(s"Uploaded $size") ImageUploaded(size) case None => println("No files") ImageUploaded(0) } } } }
Запуск приложения AngularJS
Теперь, когда у нас есть приложение Spray, давайте разберемся с приложением JavaScript. Его источник в src/main/angular
. Основными составляющими являются index.html
и js/app.js
. Нельзя просто открыть index.html
файл в браузере. Другие ресурсы (скрипты Java, таблицы стилей) не будут загружаться должным образом, но даже если они это сделают, браузер откажется вызывать наш сервер Spray, поскольку местоположения не совпадают. Наш сервер Spray работает http://localhost:8080
, но местоположение приложения AngularJS будет file:///.../index.html
.
Чтобы мы могли его протестировать, нам нужно (легко) обслуживать приложение AngularJS, а также наше приложение Spray в одном месте. В этом посте я расскажу только о настройке разработки, и у меня останется место, чтобы рассказать о правильной настройке AWS в будущем.
Настройка разработки
Давайте использовать Apache для обслуживания приложения AngularJS в, http://localhost/~USER/angular
и давайте разместим приложение Spray (или, фактически, все, что прослушивает порт 8080
) в http://localhost/~USER/app
.
Чтобы это произошло, мы включим домашние страницы для каждого пользователя Apache и мы зайдем ProxyPass
и ProxyPassReverse
директиву. На моей машине конфигурация для $USER
жизни в /etc/apache2/users/$USER.conf
.
<Directory "/Users/$USER/Sites/"> Options Indexes Multiviews +FollowSymLinks AllowOverride AuthConfig Limit Order allow,deny Allow from all </Directory> ProxyPass /~$USER/app/ http://localhost:8080/ ProxyPassReverse /~$USER/app/ http://localhost:8080/
Конечно, вы не можете использовать $USER
; Вы должны заменить его своим реальным именем пользователя. Я не хочу , чтобы скопировать в src/main/angular
каталог в ~/Sites
каталог, поэтому я добавил +FollowSymLinks
директиву и создал символическую связь ~/Sites
с точкой туда , где у меня есть src/main/angular
. В моем случае, список ~/Sites
IS
~/Sites$ ls -la total 16 lrwxr-xr-x ... angular -> /.../akka-spray/src/main/angular -rw-r--r-- ... index.html
Теперь, после добавления этого файла и запуска (или перезапуска) Apache sudo apachectl start
(или sudo apachectl restart
), вы готовы увидеть приложение, перейдя по ссылке http://localhost/~USER/angular
.
Итак, теперь вы готовы к работе. При открытии http://localhost/~USER/angular
отображается приложение AngularJS, куда вы можете добавить столько файлов, сколько захотите, и нажатие кнопки « Начать загрузку» отправляет файлы в приложение Spray. Приложение Spray бесцеремонно печатает размер файла. Я оставлю некоторые интересные логические обработки в качестве упражнения для читателей.
Как обычно, исходный код находится по адресу https:// github.com / eigengo / activator-akka-spray ; не стесняйтесь сообщать о проблемах или присылать свои материалы.