可恢复的文件上传器-创建http处理程序

52 阅读7分钟

在本教程中,我们将创建http处理程序,以支持POSTPATCHHEAD 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处理程序。我们将这个处理程序命名为createFileHandlerPOST 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 0upload 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命令测试这个上传器,同时讨论可能的改进。