前面的教程提供了关于tus协议的介绍,我们还创建了 DB CRUD 方法。

在本教程中,我们将创建http处理程序以支持 POSTPATCHHEAD http方法。

本教程包含以下部分

  • POST http处理程序

  • HEAD http处理程序

  • PATCH http处理程序

    • 文件验证

    • 上传完整验证

    • 上传偏移验证

    • 内容长度验证

    • 文件补丁

POST http 处理程序

在我们创建 POST 处理程序之前,我们需要一个目录来存储文件。为简单起见,我们将在 home 目录中创建一个名为 fileserver 的目录来存储文件。

  1. const dirName="fileserver"
  2. func createFileDir() (string, error) {
  3. u, err := user.Current()
  4. if err != nil {
  5. log.Println("Error while fetching user home directory", err)
  6. return "", err
  7. }
  8. home := u.HomeDir
  9. dirPath := path.Join(home, dirName)
  10. err = os.MkdirAll(dirPath, 0744)
  11. if err != nil {
  12. log.Println("Error while creating file server directory", err)
  13. return "", err
  14. }
  15. return dirPath, nil
  16. }

在上面的函数中,我们获取当前用户的名称和主目录,并附加 dirName 常量来创建目录。此函数将返回新创建的目录的路径或错误(如果有)。

将从 main 函数里调用此函数,POST 文件处理程序将使用此函数返回的 dirPath 来创建该文件。

现在我们准备好了目录,让我们转到 POST http 处理程序。我们将此处理程序命名为createFileHandlerPOST
http 处理程序用于创建新文件,并在 Location 标头中返回新创建的文件的位置。请求必须包含指示整个文件大小的 Upload-Length header头。

  1. func (fh fileHandler) createFileHandler(w http.ResponseWriter, r *http.Request) {
  2. ul, err := strconv.Atoi(r.Header.Get("Upload-Length"))
  3. if err != nil {
  4. e := "Improper upload length"
  5. log.Printf("%s %s", e, err)
  6. w.WriteHeader(http.StatusBadRequest)
  7. w.Write([]byte(e))
  8. return
  9. }
  10. log.Printf("upload length %d\n", ul)
  11. io := 0
  12. uc := false
  13. f := file{
  14. offset: &io,
  15. uploadLength: ul,
  16. uploadComplete: &uc,
  17. }
  18. fileID, err := fh.createFile(f)
  19. if err != nil {
  20. e := "Error creating file in DB"
  21. log.Printf("%s %s\n", e, err)
  22. w.WriteHeader(http.StatusInternalServerError)
  23. return
  24. }
  25. filePath := path.Join(fh.dirPath, fileID)
  26. file, err := os.Create(filePath)
  27. if err != nil {
  28. e := "Error creating file in filesystem"
  29. log.Printf("%s %s\n", e, err)
  30. w.WriteHeader(http.StatusInternalServerError)
  31. return
  32. }
  33. defer file.Close()
  34. w.Header().Set("Location", fmt.Sprintf("localhost:8080/files/%s", fileID))
  35. w.WriteHeader(http.StatusCreated)
  36. return
  37. }

第 2 行我们检查 Upload-Length 标头是否有效。 如果不是,我们返回 Bad Request响应。

如果 Upload-Length 有效,我们在 DB 中创建一个文件,其中包含提供的上传长度,初始 offset 0upload complete false。 然后我们在文件系统中创建文件,并在Location http头中返回文件的位置,并返回 201 created 的响应代码。

应将包含存储文件路径的 dirPath 字段添加到 fileHandler 结构中。 此字段将使用从 main()createFileDir() 函数稍后返回的 dirPath 进行更新。 下面提供了更新的 fileHandler 结构。

  1. type fileHandler struct {
  2. db *sql.DB
  3. dirPath string
  4. }

HEAD http处理程序

当收到 HEAD 请求时,我们应该返回文件的偏移量(如果存在)。如果文件不存在,那么我们应该返回 404 未找到的响应。我们将此处理程序命名为 fileDetailsHandler

  1. func (fh fileHandler) fileDetailsHandler(w http.ResponseWriter, r *http.Request) {
  2. vars := mux.Vars(r)
  3. fID := vars["fileID"]
  4. file, err := fh.File(fID)
  5. if err != nil {
  6. w.WriteHeader(http.StatusNotFound)
  7. return
  8. }
  9. log.Println("going to write upload offset to output")
  10. w.Header().Set("Upload-Offset", strconv.Itoa(*file.offset))
  11. w.WriteHeader(http.StatusOK)
  12. return
  13. }

我们将使用 mux 路由器来管理http请求。请运行命令 go get github.com/gorilla/mux 获取 mux 路由器。

在第 3 行中,我们使用 mux 路由器从请求 URL 获取 fileID

为了更加容易理解,我提供了将调用上述 fileDetailsHandler 的代码。我们稍后将在主函数中编写以下代码。

  1. 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 请求中很少需要进行验证。我们先来做吧。

  1. func (fh fileHandler) filePatchHandler(w http.ResponseWriter, r *http.Request) {
  2. log.Println("going to patch file")
  3. vars := mux.Vars(r)
  4. fID := vars["fileID"]
  5. file, err := fh.File(fID)
  6. if err != nil {
  7. w.WriteHeader(http.StatusNotFound)
  8. return
  9. }
  10. }

上面的代码类似于我们在 head http 处理程序中编写的代码。它验证文件是否存在。

上传完成验证

下一步是检查文件是否已经完全上传。

  1. if *file.uploadComplete == true {
  2. e := "Upload already completed" //change to string
  3. w.WriteHeader(http.StatusUnprocessableEntity)
  4. w.Write([]byte(e))
  5. return
  6. }

如果上传已经完成,则返回 StatusUnprocessableEntity 状态。

上传偏移验证

每个 patch 请求都应包含一个 Upload-Offset header头字段,指示数据的当前偏移量,并且要 patch 到文件的实际数据应存在于邮件正文中。

轻灵划

  1. off, err := strconv.Atoi(r.Header.Get("Upload-Offset"))
  2. if err != nil {
  3. log.Println("Improper upload offset", err)
  4. w.WriteHeader(http.StatusBadRequest)
  5. return
  6. }
  7. log.Printf("Upload offset %d\n", off)
  8. if *file.offset != off {
  9. e := fmt.Sprintf("Expected Offset %d got offset %d", *file.offset, off)
  10. w.WriteHeader(http.StatusConflict)
  11. w.Write([]byte(e))
  12. return
  13. }

在上面的代码中,我们首先检查请求标头中的 Upload-Offset 是否有效。 如果不是,我们返回一个 StatusBadRequest

第 8 行中,我们将表 *file.Offset 中的偏移量与 header 的 off 进行比较。 他们应该是相等的。 __我们以一个上传长度为 250 字节的文件为例 果已经上传了 100 个字节,则数据库中的上传偏移量将为 100。现在服务器将有 Upload-offset 头为 100 的请求__。如果它们不相等,则返回 StatusConflict 头。

内容长度验证

下一步是验证 content-length

  1. clh := r.Header.Get("Content-Length")
  2. cl, err := strconv.Atoi(clh)
  3. if err != nil {
  4. log.Println("unknown content length")
  5. w.WriteHeader(http.StatusInternalServerError)
  6. return
  7. }
  8. if cl != (file.uploadLength - *file.offset) {
  9. e := fmt.Sprintf("Content length doesn't not match upload length.Expected content length %d got %d", file.uploadLength-*file.offset, cl)
  10. log.Println(e)
  11. w.WriteHeader(http.StatusBadRequest)
  12. w.Write([]byte(e))
  13. return
  14. }

假设文件长度为 250 个字节,当前偏移量为 150.这表示还有 100 个字节要上传。因此,patch 请求 Content-Length 应该恰好为 100。此验证在第 9 行中完成。

文件 patch

有趣的来了。我们已完成所有验证并准备 patch 文件。

  1. body, err := ioutil.ReadAll(r.Body)
  2. if err != nil {
  3. log.Printf("Received file partially %s\n", err)
  4. log.Println("Size of received file ", len(body))
  5. }
  6. fp := fmt.Sprintf("%s/%s", fh.dirPath, fID)
  7. f, err := os.OpenFile(fp, os.O_APPEND|os.O_WRONLY, 0644)
  8. if err != nil {
  9. log.Printf("unable to open file %s\n", err)
  10. w.WriteHeader(http.StatusInternalServerError)
  11. return
  12. }
  13. defer f.Close()
  14. n, err := f.WriteAt(body, int64(off))
  15. if err != nil {
  16. log.Printf("unable to write %s", err)
  17. w.WriteHeader(http.StatusInternalServerError)
  18. return
  19. }
  20. log.Println("number of bytes written ", n)
  21. no := *file.offset + n
  22. file.offset = &no
  23. uo := strconv.Itoa(*file.offset)
  24. w.Header().Set("Upload-Offset", uo)
  25. if *file.offset == file.uploadLength {
  26. log.Println("upload completed successfully")
  27. *file.uploadComplete = true
  28. }
  29. err = fh.updateFile(file)
  30. if err != nil {
  31. log.Println("Error while updating file", err)
  32. w.WriteHeader(http.StatusInternalServerError)
  33. return
  34. }
  35. log.Println("going to send succesfully uploaded response")
  36. 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/