前面的教程提供了关于tus协议的介绍,我们还创建了 DB CRUD 方法。
在本教程中,我们将创建http处理程序以支持 POST
,PATCH
和HEAD
http方法。
本教程包含以下部分
POST http处理程序
HEAD http处理程序
PATCH http处理程序
文件验证
上传完整验证
上传偏移验证
内容长度验证
文件补丁
POST http 处理程序
在我们创建 POST 处理程序之前,我们需要一个目录来存储文件。为简单起见,我们将在 home
目录中创建一个名为 fileserver
的目录来存储文件。
const dirName="fileserver"
func createFileDir() (string, error) {
u, err := user.Current()
if err != nil {
log.Println("Error while fetching user home directory", err)
return "", err
}
home := u.HomeDir
dirPath := path.Join(home, dirName)
err = os.MkdirAll(dirPath, 0744)
if err != nil {
log.Println("Error while creating file server directory", err)
return "", err
}
return dirPath, nil
}
在上面的函数中,我们获取当前用户的名称和主目录,并附加 dirName
常量来创建目录。此函数将返回新创建的目录的路径或错误(如果有)。
将从 main 函数里调用此函数,POST 文件处理程序将使用此函数返回的 dirPath
来创建该文件。
现在我们准备好了目录,让我们转到 POST http 处理程序。我们将此处理程序命名为createFileHandler
。 POST
http 处理程序用于创建新文件,并在 Location
标头中返回新创建的文件的位置。请求必须包含指示整个文件大小的 Upload-Length
header头。
func (fh fileHandler) createFileHandler(w http.ResponseWriter, r *http.Request) {
ul, err := strconv.Atoi(r.Header.Get("Upload-Length"))
if err != nil {
e := "Improper upload length"
log.Printf("%s %s", e, err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(e))
return
}
log.Printf("upload length %d\n", ul)
io := 0
uc := false
f := file{
offset: &io,
uploadLength: ul,
uploadComplete: &uc,
}
fileID, err := fh.createFile(f)
if err != nil {
e := "Error creating file in DB"
log.Printf("%s %s\n", e, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
filePath := path.Join(fh.dirPath, fileID)
file, err := os.Create(filePath)
if err != nil {
e := "Error creating file in filesystem"
log.Printf("%s %s\n", e, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer file.Close()
w.Header().Set("Location", fmt.Sprintf("localhost:8080/files/%s", fileID))
w.WriteHeader(http.StatusCreated)
return
}
第 2 行我们检查 Upload-Length
标头是否有效。 如果不是,我们返回 Bad Request
响应。
如果 Upload-Length
有效,我们在 DB 中创建一个文件,其中包含提供的上传长度,初始 offset 0
,upload complete false
。 然后我们在文件系统中创建文件,并在Location
http头中返回文件的位置,并返回 201 created
的响应代码。
应将包含存储文件路径的 dirPath
字段添加到 fileHandler
结构中。 此字段将使用从 main()
里 createFileDir()
函数稍后返回的 dirPath 进行更新。 下面提供了更新的 fileHandler
结构。
type fileHandler struct {
db *sql.DB
dirPath string
}
HEAD http处理程序
当收到 HEAD 请求时,我们应该返回文件的偏移量(如果存在)。如果文件不存在,那么我们应该返回 404 未找到的响应。我们将此处理程序命名为 fileDetailsHandler
。
func (fh fileHandler) fileDetailsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fID := vars["fileID"]
file, err := fh.File(fID)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
log.Println("going to write upload offset to output")
w.Header().Set("Upload-Offset", strconv.Itoa(*file.offset))
w.WriteHeader(http.StatusOK)
return
}
我们将使用 mux 路由器来管理http请求。请运行命令 go get github.com/gorilla/mux
获取 mux 路由器。
在第 3 行中,我们使用 mux 路由器从请求 URL 获取 fileID
。
为了更加容易理解,我提供了将调用上述 fileDetailsHandler
的代码。我们稍后将在主函数中编写以下代码。
r.HandleFunc("/files/{fileID:[0-9]+}", fh.fileDetailsHandler).Methods("HEAD")
当 URL 具有有效的整数 fileID
时,将调用此处理程序。[0-9]+
是一个正则表达式,它匹配一个或多个数字。如果 fileID
有效,它将与键 fileID
一起存储在map[string]string
类型的字典中。此字典可以通过调用 mux 路由器的 Vars
函数来检索。第 3 行就是我们如何获取 fileID
的。
获得 fileID
后,我们通过调用第 4 行中的 File
方法检查文件是否存在。记住,我们在上个教程中编写了这个文件方法。如果文件是有效的,我们将使用 Upload-Offset
头返回响应。如果没有,则返回 http.StatusNotFound
响应。
PATCH http处理程序
剩下的处理程序就是 PATCH http 处理程序。在转到实际的文件PATCH 之前,PATCH
请求中很少需要进行验证。我们先来做吧。
func (fh fileHandler) filePatchHandler(w http.ResponseWriter, r *http.Request) {
log.Println("going to patch file")
vars := mux.Vars(r)
fID := vars["fileID"]
file, err := fh.File(fID)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
}
上面的代码类似于我们在 head http 处理程序中编写的代码。它验证文件是否存在。
上传完成验证
下一步是检查文件是否已经完全上传。
if *file.uploadComplete == true {
e := "Upload already completed" //change to string
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write([]byte(e))
return
}
如果上传已经完成,则返回 StatusUnprocessableEntity
状态。
上传偏移验证
每个 patch 请求都应包含一个 Upload-Offset
header头字段,指示数据的当前偏移量,并且要 patch 到文件的实际数据应存在于邮件正文中。
轻灵划
off, err := strconv.Atoi(r.Header.Get("Upload-Offset"))
if err != nil {
log.Println("Improper upload offset", err)
w.WriteHeader(http.StatusBadRequest)
return
}
log.Printf("Upload offset %d\n", off)
if *file.offset != off {
e := fmt.Sprintf("Expected Offset %d got offset %d", *file.offset, off)
w.WriteHeader(http.StatusConflict)
w.Write([]byte(e))
return
}
在上面的代码中,我们首先检查请求标头中的 Upload-Offset
是否有效。 如果不是,我们返回一个 StatusBadRequest
。
第 8 行中,我们将表 *file.Offset
中的偏移量与 header 的 off
进行比较。 他们应该是相等的。 __我们以一个上传长度为 250 字节的文件为例。 如果已经上传了 100 个字节,则数据库中的上传偏移量将为 100。现在服务器将有 Upload-offset
头为 100 的请求__。如果它们不相等,则返回 StatusConflict
头。
内容长度验证
下一步是验证 content-length
。
clh := r.Header.Get("Content-Length")
cl, err := strconv.Atoi(clh)
if err != nil {
log.Println("unknown content length")
w.WriteHeader(http.StatusInternalServerError)
return
}
if cl != (file.uploadLength - *file.offset) {
e := fmt.Sprintf("Content length doesn't not match upload length.Expected content length %d got %d", file.uploadLength-*file.offset, cl)
log.Println(e)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(e))
return
}
假设文件长度为 250 个字节,当前偏移量为 150.这表示还有 100 个字节要上传。因此,patch 请求 Content-Length
应该恰好为 100。此验证在第 9 行中完成。
文件 patch
有趣的来了。我们已完成所有验证并准备 patch 文件。
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Received file partially %s\n", err)
log.Println("Size of received file ", len(body))
}
fp := fmt.Sprintf("%s/%s", fh.dirPath, fID)
f, err := os.OpenFile(fp, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Printf("unable to open file %s\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer f.Close()
n, err := f.WriteAt(body, int64(off))
if err != nil {
log.Printf("unable to write %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Println("number of bytes written ", n)
no := *file.offset + n
file.offset = &no
uo := strconv.Itoa(*file.offset)
w.Header().Set("Upload-Offset", uo)
if *file.offset == file.uploadLength {
log.Println("upload completed successfully")
*file.uploadComplete = true
}
err = fh.updateFile(file)
if err != nil {
log.Println("Error while updating file", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Println("going to send succesfully uploaded response")
w.WriteHeader(http.StatusNoContent)
我们开始看上面代码第 1 行中的消息体。 ReadAll
函数返回它已读取的数据,直到 EOF
或出现错误。 EOF
不被视为错误,因为预期 ReadAll
将从源文件读取直到EOF。
假设 patch 请求在完成之前断开连接。发生这种情况时,ReadAll
将返回 unexpected EOF
错误。通常,如果请求不完整,一般的Web服务器将丢弃该请求。但我们正在创建一个可恢复的文件上传器,我们不应该这样做。我们应该用我们收到的数据patch文件到现在为止。
收到的数据长度在第 4 行打印。
第 7 行如果文件已存在,我们将以附加模式打开文件,如果文件不存在则创建新文件。
第 15 行我们将请求主体写入请求头中提供的偏移量的文件。第23行我们通过添加写入的字节数来更新文件的偏移量。第26行我们将更新的偏移量写入响应头。
第 27 行我们检查当前偏移量是否等于上传长度。如果是这种情况,则上传已完成。我们将 uploadComplete
标志设置为 true
。
第 32 行终于我们将更新的文件详细信息写入数据库并返回 StatusNoContent
标头,指示请求成功。
整个代码以及github中提供的main函数,网址为 https://github.com/golangbot/tusserver。我们需要 Postgres driver 程序来运行代码。在运行程序之前,请在终端中运行命令 go get github.com/lib/pq
来获取 postgres driver。
就是这样。我们有一个可工作的可恢复文件上传器。在下一个教程中,我们将使用 curl 和 dd 命令测试此上传器,并讨论可以增强的功能。
原文链接
https://golangbot.com/resumable-file-uploader-creating-http-handlers/