1. 程式人生 > 其它 >使用 Go 語言完成 HTTP 檔案上傳與下載

使用 Go 語言完成 HTTP 檔案上傳與下載

最近我使用 Go 語言完成了一個正式的 web 應用,有一些方面的問題在使用 Go 開發 web 應用過程中比較重要。過去,我將 web 開發作為一項職業並且把使用不同的語言和正規化開發 web 應用作為一項愛好,因此對於 web 開發領域有一些心得體會。

總的來說,我喜歡使用 Go 語言進行 web 開發,儘管開始一段時間需要去適應它。Go 語言有一些坑,但是正如本篇文章中所要討論的檔案上傳與下載,Go 語言的標準庫與內建函式,使得開發是種愉快的體驗。

在接下來的幾篇文章中,我將重點討論我在 Go 中編寫生產級 Web 應用程式時遇到的一些問題,特別是關於身份驗證/授權的問題。

這篇文章將展示HTTP檔案上傳和下載的基本示例。我們將一個有 type 文字框和一個 uploadFile 上傳框的 HTML 表單作為客戶端。

讓我們來看下 Go 語言中是如何解決這種在 web 開發中隨處可見的問題的。

程式碼示例

首先,我們在伺服器端設定兩個路由,/upload 用於檔案上傳, /files/* 用於檔案下載。

const maxUploadSize = 2 * 1024 * 2014 // 2 MB 

const uploadPath = "./tmp"



func main() {

    http.HandleFunc("/upload", uploadFileHandler())



    fs := http.FileServer(http.Dir(uploadPath))

    http.Handle("/files/", http.StripPrefix("/files", fs))



    log.Print("Server started on localhost:8080, use /upload for uploading files and /files/{fileName} for downloading files.")

    log.Fatal(http.ListenAndServe(":8080", nil))

}

我們還將要上傳的目標目錄,以及我們接受的最大檔案大小定義為常量。注意這裡,整個檔案服務的概念是如此的簡單 —— 我們僅使用標準庫中的工具,使用 http.FileServe 建立一個 HTTP 處理程式,它將使用 http.Dir(uploadPath) 提供的目錄來上傳檔案。

現在我們只需要實現 uploadFileHandler。

這個處理程式將包含以下功能:

  • 驗證檔案最大值
  • 從請求驗證檔案和 POST 引數
  • 檢查所提供的檔案型別(我們只接受影象和 PDF)
  • 建立一個隨機檔名
  • 將檔案寫入硬碟
  • 處理所有錯誤,如果一切順利返回成功訊息

第一步,我們定義處理程式:

func uploadFileHandler() http.HandlerFunc {

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

然後,我們使用 http.MaxBytesReader 驗證檔案大小,當檔案大小大於設定值時它將返回一個錯誤。錯誤將被一個助手程式 renderError 進行處理,它返回錯誤資訊及對應的 HTTP 狀態碼。

r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)

    if err := r.ParseMultipartForm(maxUploadSize); err != nil {

        renderError(w, "FILE_TOO_BIG", http.StatusBadRequest)

        return

    }

如果檔案大小驗證通過,我們將檢查並解析表單引數型別和上傳的檔案,並讀取檔案。在本例中,為了清晰起見,我們不使用花哨的 io.Reader 和 io.Writer 介面,我們只是簡單的將檔案讀取到一個位元組陣列中,這點我們後面會寫到。

fileType := r.PostFormValue("type")

    file, _, err := r.FormFile("uploadFile")

    if err != nil {

        renderError(w, "INVALID_FILE", http.StatusBadRequest)

        return

    }

    defer file.Close()

    fileBytes, err := ioutil.ReadAll(file)

    if err != nil {

        renderError(w, "INVALID_FILE", http.StatusBadRequest)

        return

    }

現在我們成功的驗證了檔案的大小,並且讀取了檔案,接下來我們該檢驗檔案的型別了。一種廉價但是並不安全的方式,只檢查副檔名,並相信使用者沒有改變它,但是對於一個正式的專案來講不應該這麼做。

幸運的是,Go 標準庫提供給我們一個 http.DetectContentType 函式,這個函式基於 mimesniff 演算法,只需要讀取檔案的前 512 個位元組就能夠判定檔案型別。

filetype := http.DetectContentType(fileBytes)

    if filetype != "image/jpeg" && filetype != "image/jpg" &&

        filetype != "image/gif" && filetype != "image/png" &&

        filetype != "application/pdf" {

        renderError(w, "INVALID_FILE_TYPE", http.StatusBadRequest)

        return

    }

在實際應用程式中,我們可能會使用檔案元資料做一些事情,例如將其儲存到資料庫或將其推送到外部服務——以任何方式,我們將解析和操作元資料。這裡我們建立一個隨機的新名字(這在實踐中可能是一個UUID)並將新檔名記錄下來。

  1. fileName := randToken(12)
  2. fileEndings, err := mime.ExtensionsByType(fileType)
  3. if err != nil {
  4. renderError(w, "CANT_READ_FILE_TYPE", http.StatusInternalServerError)
  5. return
  6. }
  7. newPath := filepath.Join(uploadPath, fileName+fileEndings[0])
  8. fmt.Printf("FileType: %s, File: %sn", fileType, newPath)

馬上就大功告成了,只剩下一個關鍵步驟-寫檔案。如上文所提到的,我們只需要複製讀取的二進位制檔案到一個新建立的名為 newFile的檔案處理程式裡。

如果所有部分都沒問題,我們給使用者返回一個 SUCCESS 資訊。

  1. newFile, err := os.Create(newPath)
  2. if err != nil {
  3. renderError(w, "CANT_WRITE_FILE", http.StatusInternalServerError)
  4. return
  5. }
  6. defer newFile.Close()
  7. if _, err := newFile.Write(fileBytes); err != nil {
  8. renderError(w, "CANT_WRITE_FILE", http.StatusInternalServerError)
  9. return
  10. }
  11. w.Write([]byte("SUCCESS"))

這樣可以了. 你可以對這個簡單的例子進行測試,使用虛擬的檔案上傳 HTML 頁面,cURL 或者工具例如 postman。

結論

這是又一個證明了 Go 如何允許使用者為 web 編寫簡單而強大的軟體,而不必像處理其他語言和生態系統中固有的無數抽象層。

在接下來的篇幅中,我將展示一些在我第一次使用 Go 語言編寫正式的 web 應用中其他細節,敬請期待。;)