第26章:添加个人资料图片¶
在前几章中,你学会了如何用POST
请求向你的Vapor
应用程序发送数据。你使用JSON体和表单来传输数据,但数据总是简单的文本。在本章中,你将学习如何在请求中发送文件,并在你的Vapor
应用程序中处理这些文件。你将使用这些知识来允许用户在Web应用程序中上传个人资料图片。
Note
本章教你如何向运行Vapor
应用程序的服务器上传文件。对于真正的应用,你应该考虑将文件转发到存储服务,如AWS S3
。许多主机供应商,如Heroku
,不提供持久性存储。这意味着在重新部署应用程序时,你会丢失你上传的文件。如果托管提供商重新启动你的应用程序,你也会丢失文件。此外,将文件上传到同一台服务器意味着你不能将你的应用程序扩展到一个以上的实例,因为这些文件不会存在于所有的应用程序实例。
将图片添加到模型中¶
和前几章一样,你需要改变模型,这样你就可以把图片和User
联系起来。在Xcode
中打开Vapor TIL
应用程序,打开User.swift
。在下面添加以下内容 var email: 字符串
:
@OptionalField(key: "profilePicture")
var profilePicture: String?
这为图片存储了一个可选的String
。它将包含用户在磁盘上的个人资料图片的文件名。文件名是可选的,因为你并不强制要求用户有一张个人资料图片--当他们注册时,他们也不会有一张。将初始化器替换为以下内容,以说明新的属性:
init(
name: String,
username: String,
password: String,
siwaIdentifier: String? = nil,
email: String,
profilePicture: String? = nil
) {
self.name = name
self.username = username
self.password = password
self.siwaIdentifier = siwaIdentifier
self.email = email
self.profilePicture = profilePicture
}
为profilePicture
提供一个默认值nil
,可以让你的应用程序继续编译和运行,而不需要进一步修改源代码。
Note
你可以使用Google
和GitHub
的用户API
来获取用户的个人资料图片的URL
。这将允许你下载图片并将其与普通用户的图片一起存储,或保存链接。然而,这只是留给读者的一个练习。
你可以把上传个人资料图片作为注册经验的一部分,但本章将其作为一个单独的步骤。注意到UsersController
中的createHandler(_:)
不需要为新属性做任何改变。这是因为路由处理程序使用了Codable
,如果POST请求中不存在数据,则将该属性设置为`nil'。
接下来,打开CreateUser.swift
和找到下文:
.field("email", .string, .required)`:
然后添加下面的内容:
.field("profilePicture", .string)
这将在数据库中为个人资料图片添加一个新的列。注意,你没有添加.required
约束,因为这个属性是可选的。
重置数据库¶
和过去一样,由于你已经为User
添加了一个属性,你必须重置数据库。在终端,运行:
docker rm -f postgres
docker rm -f postgres-test
docker run --name postgres -e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
docker run --name postgres-test -e POSTGRES_DB=vapor-test \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5433:5432 -d postgres
像以前一样,这将删除现有的名为postgres
的容器并重新创建它。它也重置了用于测试的数据库。确保两个容器都在运行。在终端,键入:
docker ps -a
你应该看到你的主数据库容器postgres
,和测试数据库容器postgres-test
。两者都应该有一个类似于Up about a minute
的状态:
验证测试¶
在Xcode
中,输入Command+U
来运行所有的测试。他们应该全部通过。
Note
Xcode
使用来自.env
的现有环境变量。如果你使用本章的启动项目而不是现有的项目,你应该确保你正确地设置这些变量。你还需要设置自定义工作目录,以便Vapor
知道在哪里找到文件。关于这些设置的细节,请参见第22-25章。这四章中的每一章都贡献了必要的环境变量。
创建表单¶
随着模型的改变,你现在可以创建一个页面,让用户提交图片。在Xcode
中,打开WebsiteController.swift
。接下来,在下面添加resetPasswordPostHandler(_:data:)
:
func addProfilePictureHandler(_ req: Request)
-> EventLoopFuture<View> {
User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound)).flatMap { user in
req.view.render(
"addProfilePicture",
[
"title": "Add Profile Picture",
"username": user.name
]
)
}
}
这定义了一个新的路由处理程序,渲染addProfilePicture.leaf
。这个路由处理程序还将标题和用户的名字作为一个字典传递给模板。接下来,在boot(routes:)
的末尾添加以下内容,以注册新的路由处理程序:
protectedRoutes.get(
"users",
":userID",
"addProfilePicture",
use: addProfilePictureHandler)
这将一个GET
请求连接到/users/<user_ID>/addProfilePicture
到addProfilePictureHandler(_:)
。请注意,该路由也是一个受保护的路由--用户必须登录才能给用户添加配置文件图片。
TIL
应用程序还允许用户为任何用户上传个人资料图片,而不仅仅是他们自己的。
在Resources/Views
中,创建新的模板,addProfilePicture.leaf
。在任何文本编辑器中打开这个新文件,并插入以下内容:
<!-- 1 -->
#extend("base"):
<!-- 2 -->
#export("content"):
<!-- 3 -->
<h1>#(title)</h1>
<!-- 4 -->
<form method="post" enctype="multipart/form-data">
<!-- 5 -->
<div class="form-group">
<label for="picture">
Select Picture for #(username)
</label>
<input type="file" name="picture"
class="form-control-file" id="picture"/>
</div>
<!-- 6 -->
<button type="submit" class="btn btn-primary">
Upload
</button>
</form>
#endexport
#endextend
以下是新模板的作用:
- 扩展
base.leaf
以包括主模板。 - 按照
base.leaf
的要求导出content
。 - 使用传递给模板的标题作为页面的标题。
- 创建一个表单,并将方法设置为
POST
。当你提交表单时,浏览器会将表单作为一个POST
请求发送到同一个URL
。注意multipart/form-data
的编码类型。这允许你从浏览器中向服务器发送文件。 - 创建一个输入类型为
file
的表单组。这在你的网络浏览器中呈现一个文件浏览器。Bootstrap
使用form-control-file
来帮助输入的样式。 - 添加一个提交按钮,允许用户提交表单。
接下来,你需要一个链接,让用户能够访问新的表单。打开WebsiteController.swift
,在UserContext
的底部添加一个新属性:
let authenticatedUser: User?
这将存储该请求的认证用户,如果存在的话。在userHandler(_:)
中,将let context = ...
替换为以下内容:
// 1
let loggedInUser = req.auth.get(User.self)
// 2
let context = UserContext(
title: user.name,
user: user,
acronyms: acronyms,
authenticatedUser: loggedInUser)
以下是你改变的内容:
- 从
Request
的认证缓存中获取认证的用户。这将返回User?
,因为可能没有认证的用户。 - 将可选的、已认证的用户传递给上下文。
最后,打开user.leaf
。在#extend("acronymsTable")
前添加以下内容:
#if(authenticatedUser):
<a href="/users/#(user.id)/addProfilePicture">
#if(user.profilePicture):
Update
#else:
Add
#endif
Profile Picture
</a>
#endif
如果用户已经登录,这将添加一个链接到新的添加个人资料图片页面。如果用户已经有了个人资料图片,该链接将显示Update Profile Picture
,否则链接将显示Add Profile Picture
。
在Xcode中,构建并运行该应用程序。在浏览器中,访问http://localhost:8080/login,并作为管理员用户登录。一旦登录,点击All Users
并选择admin
用户。
有一个新的链接到添加个人资料图片页面。点击Add Profile Picture
,你会看到新的表格来添加个人资料图片:
接受文件上传¶
接下来,实现必要的代码来处理表单中的POST
请求。在终端,在TILApp
目录下输入以下内容:
# 1
mkdir ProfilePictures
# 2
touch ProfilePictures/.keep
以下是这些命令的作用:
- 创建目录来存储用户的个人资料图片。
- 添加一个空文件,以便将该目录添加到源控制中。这有助于部署应用程序,确保目录的存在。
接下来,在Xcode
中,打开WebsiteController.swift
。在文件的底部,添加以下内容:
struct ImageUploadData: Content {
var picture: Data
}
这个新类型代表表单发送的数据。picture
与HTML
表单中指定的输入名称相匹配。
由于表单上传了一个文件,你将把图片解码成Data
。
接下来,在WebsiteController
的顶部添加一个新的属性,在boot(rouse:)
的上方:
let imageFolder = "ProfilePictures/"
这定义了你将存储图片的文件夹。接下来,在addProfilePictureHandler(_:)
下面,为POST
请求添加一个请求处理器:
func addProfilePicturePostHandler(_ req: Request)
throws -> EventLoopFuture<Response> {
// 1
let data = try req.content.decode(ImageUploadData.self)
// 2
return User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { user in
// 3
let userID: UUID
do {
userID = try user.requireID()
} catch {
return req.eventLoop.future(error: error)
}
// 4
let name = "\(userID)-\(UUID()).jpg"
// 5
let path =
req.application.directory.workingDirectory +
imageFolder + name
// 6
return req.fileio
.writeFile(.init(data: data.picture), at: path)
.flatMap {
// 7
user.profilePicture = name
// 8
let redirect = req.redirect(to: "/users/\(userID)")
return user.save(on: req.db).transform(to: redirect)
}
}
}
以下是新的请求处理程序的作用:
- 将请求主体解码为
ImageUploadData
。 - 从参数中获取用户。
- 获取用户
ID
并捕捉任何抛出的错误。 - 为个人资料图片创建一个独特的名称。
- 使用应用程序的工作目录、图片文件夹和名称来设置文件的保存路径。
- 使用路径和图片数据在磁盘上保存文件。这使用了
NIO
的文件功能,以避免在等待写入完成时阻塞任何线程。 - 用个人资料图片的文件名更新用户。
- 保存更新后的用户,并返回一个重定向到用户的页面。
最后,在boot(route:)
的底部注册路由:
protectedRoutes.on(
.POST,
"users",
":userID",
"addProfilePicture",
body: .collect(maxSize: "10mb"),
use: addProfilePicturePostHandler)
这与所有其他的路由注册有一点不同。这仍然是将/users/<USER_ID>/addProfilePicture
的POST
请求连接到addProfilePicturePostHandler(_:)
。然而,在默认情况下,Vapor
将流媒体主体收集限制在16KB
,以节省内存消耗。你可以在全局或每条路由的基础上改变这一点。这个路由注册只对这个路由改变允许的最大尺寸为10MB
。
显示图片¶
现在,用户可以上传个人资料图片,你需要能够将图片送回给浏览器。通常情况下,你会使用FileMiddleware
。然而,由于你将图片存储在不同的目录中,本章将教你如何手动提供图片。
在WebsiteController.swift
中,在addProfilePicturePostHandler(_:)
下面添加一个新的路由处理器:
func getUsersProfilePictureHandler(_ req: Request)
-> EventLoopFuture<Response> {
// 1
User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMapThrowing { user in
// 2
guard let filename = user.profilePicture else {
throw Abort(.notFound)
}
// 3
let path = req.application.directory
.workingDirectory + imageFolder + filename
// 4
return req.fileio.streamFile(at: path)
}
}
以下是新的路径处理程序的作用:
- 从请求的参数中获取用户。
- 确保用户有一张保存的个人资料图片,否则会出现
404 Not Found
错误。 - 构建用户的个人资料图片的路径。
- 使用
Vapor
的FileIO
方法,将文件作为Response
返回。这将处理读取文件并将正确的信息返回给浏览器。
接下来,在boot(routes:)
下面的authSessionsRoutes.get("categories", ":categoryID", use: categoryHandler)
注册新路由:
authSessionsRoutes.get(
"users",
":userID",
"profilePicture",
use: getUsersProfilePictureHandler)
这就把对/users/<user_ID>/profilePicture
的GET
请求连接到getUsersProfilePictureHandler(_:)
。最后,打开user.leaf
。在<h1>#(user.name)</h1>
前加入以下内容:
#if(user.profilePicture):
<img src="/users/#(user.id)/profilePicture"
alt="#(user.name)">
#endif
这将检查传递到模板上下文的用户是否有一张个人资料图片。如果有,Leaf
会将图片添加到页面中。
建立并运行应用程序,在浏览器中进入http://localhost:8080/login。以默认的管理员用户身份登录,然后导航到管理员用户的个人资料页面。点击Add Profile Picture
,在表格中点击Choose File
。选择一个要上传的图片,然后点击Upload
。
该网站将把你重定向到用户的个人资料页面,在那里你会看到上传的图片:
接下来去哪?¶
在本章中,你学到了如何在Vapor
中处理文件。你看到了如何处理文件的上传和保存到磁盘。你还学习了如何在路由处理程序中从磁盘上提供文件。
现在你已经建立了一个功能齐全的API
,展示了Vapor
的许多功能。你已经建立了一个iOS
应用程序来消费API
,以及一个使用Leaf
的前端网站。你还学会了如何测试你的应用程序。
这些部分已经给了你所有的知识,你需要为你自己的应用程序建立后端和网站! 接下来的章节涵盖了你可能需要的更多高级主题,如数据库迁移和缓存。你还将学习如何将你的应用程序部署到互联网上。