Этот пост посвящен постам 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]экземпляр класса типов, что позволяет нам избавиться от Strings в 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)
}
}
}
}
}
}
Теперь это лучше. Там нет Strings, и мы прекрасно справляемся с загрузкой файлов. К сожалению, все еще слишком много заметок . Разобрав 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. В моем случае, список ~/SitesIS
~/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 ; не стесняйтесь сообщать о проблемах или присылать свои материалы.

