在本教程中,我们将创建http处理程序,以支持POST 、PATCH 和HEAD http方法。
本教程有以下几个部分
- POST http处理程序
- HEAD http处理程序
- PATCH http handler
- 文件验证
- 上传完整验证
- 上传偏移验证
- 内容长度验证
- 文件补丁
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调用,从这个函数返回的dirPath ,将被POST文件处理程序用来创建文件。
现在我们已经准备好了目录,让我们转到POST http处理程序。我们将这个处理程序命名为createFileHandler 。POST http处理程序用于创建一个新的文件,并在Location 头部返回新创建文件的位置。请求中必须包含一个表明整个文件大小的Upload-Length 标头。
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 not found的响应。我们将这个处理程序命名为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
}
我们将使用muxrouter来路由http请求。请运行命令go get github.com/gorilla/mux ,从github获取mux router。
在第3行,我们使用muxrouter从请求的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 。3.
在得到fileID 之后,我们通过调用第3行的File 方法检查文件是否存在。4.记得我们在上一个教程中写了这个File 方法。如果文件是有效的,我们返回带有Upload-Offset 头的响应。如果不是,我们返回一个http.StatusNotFound 响应。
PATCH http处理程序
唯一剩下的处理程序是PATCH http处理程序。在我们进行实际的文件修补之前,在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 状态。
上传偏移验证
每个补丁请求都应该包含一个Upload-Offset 头字段,表明数据的当前偏移量,实际要修补到文件的数据应该出现在消息体中。
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行,我们比较了请求头中的偏移量。在第8行,我们比较了表*file.Offset 中的偏移量和头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个字节要上传。因此,补丁请求的Content-Length 应该正好是100。这个验证是在上述代码的第9行完成的。上述代码的第9行进行了验证。
文件补丁
现在是有趣的部分。我们已经完成了所有的验证,并准备对文件进行修补。
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。
假设补丁请求在完成之前就断开了连接。当这种情况发生时,ReadAll 将返回一个unexpected EOF 错误。通常情况下,如果请求不完整,通用的网络服务器会丢弃该请求。但我们正在创建一个可恢复的文件上传器,我们不应该这样做。我们应该用我们到现在为止收到的数据来修补文件。
收到的数据的长度被打印在第4行。4.
在第4行中,我们在第7行中打开文件。第7行,如果文件已经存在,我们以附加模式打开文件,如果不存在,则创建一个新文件。
在第7行,如果文件已经存在,我们将以附加模式打开文件;如果不存在,则创建新的文件。第15行,我们在请求头中提供的偏移处将请求正文写入文件。在第23行,我们通过增加写入的字节数来更新文件的偏移量。在第26行,我们将更新的偏移量写到响应头。
在第27行,我们检查当前偏移量是否等于上传长度。如果是这样的话,上传已经完成。我们将uploadComplete 标志设置为true 。
最后在第32行中,我们将更新后的文件细节写到32我们将更新的文件细节写入数据库,并返回一个StatusNoContent 头,表示请求成功。
整个代码和主函数可在github上找到:github.com/golangbot/t…我们将需要Postgres驱动来运行该代码。在运行程序之前,请在终端中运行go get github.com/lib/pq ,获取postgres驱动。
就这样了。我们有了一个可以工作的可恢复的文件上传器。在下一篇教程中,我们将使用curl和dd命令测试这个上传器,同时讨论可能的改进。