Files
build-web-application-with-…/zh-tw/04.5.md
2019-02-26 01:40:54 +08:00

162 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 4.5 處理檔案上傳
你想處理一個由使用者上傳的檔案比如你正在建設一個類似Instagram的網站你需要儲存使用者拍攝的照片。這種需求該如何實現呢
要使表單能夠上傳檔案首先第一步就是要新增form的`enctype`屬性,`enctype`屬性有如下三種情況:
```
application/x-www-form-urlencoded 表示在傳送前編碼所有字元(預設)
multipart/form-data 不對字元編碼。在使用包含檔案上傳控制元件的表單時,必須使用該值。
text/plain 空格轉換為 "+" 加號,但不對特殊字元編碼。
```
所以建立新的表單html檔案, 命名為upload.gtpl, html程式碼應該類似於:
```html
<html>
<head>
<title>上傳檔案</title>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="uploadfile" />
<input type="hidden" name="token" value="{{.}}"/>
<input type="submit" value="upload" />
</form>
</body>
</html>
```
在伺服器端我們增加一個handlerFunc:
```Go
http.HandleFunc("/upload", upload)
// 處理/upload 邏輯
func upload(w http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method) //取得請求的方法
if r.Method == "GET" {
crutime := time.Now().Unix()
h := md5.New()
io.WriteString(h, strconv.FormatInt(crutime, 10))
token := fmt.Sprintf("%x", h.Sum(nil))
t, _ := template.ParseFiles("upload.gtpl")
t.Execute(w, token)
} else {
r.ParseMultipartForm(32 << 20)
file, handler, err := r.FormFile("uploadfile")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
fmt.Fprintf(w, "%v", handler.Header)
f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) // 此處假設當前目錄下已存在test目錄
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
io.Copy(f, file)
}
}
```
透過上面的程式碼可以看到,處理檔案上傳我們需要呼叫`r.ParseMultipartForm`,裡面的引數表示`maxMemory`,呼叫`ParseMultipartForm`之後,上傳的檔案儲存在`maxMemory`大小的記憶體裡面,如果檔案大小超過了`maxMemory`,那麼剩下的部分將儲存在系統的臨時檔案中。我們可以透過`r.FormFile`取得上面的檔案控制代碼,然後例項中使用了`io.Copy`來儲存檔案。
>取得其他非檔案欄位資訊的時候就不需要呼叫`r.ParseForm`因為在需要的時候Go自動會去呼叫。而且`ParseMultipartForm`呼叫一次之後,後面再次呼叫不會再有效果。
透過上面的例項我們可以看到我們上傳檔案主要三步處理:
1. 表單中增加enctype="multipart/form-data"
2. 伺服器端呼叫`r.ParseMultipartForm`,把上傳的檔案儲存在記憶體和臨時檔案中
3. 使用`r.FormFile`取得檔案控制代碼,然後對檔案進行儲存等處理。
檔案handler是multipart.FileHeader,裡面儲存瞭如下結構資訊
```Go
type FileHeader struct {
Filename string
Header textproto.MIMEHeader
// contains filtered or unexported fields
}
```
我們透過上面的例項程式碼打印出來上傳檔案的資訊如下
![](images/4.5.upload2.png?raw=true)
圖4.5 列印檔案上傳後伺服器端接受的資訊
## 客戶端上傳檔案
我們上面的例子示範瞭如何透過表單上傳檔案然後在伺服器端處理檔案其實Go支援模擬客戶端表單功能支援檔案上傳詳細用法請看如下示例
```Go
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
)
func postFile(filename string, targetUrl string) error {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
//關鍵的一步操作
fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)
if err != nil {
fmt.Println("error writing to buffer")
return err
}
//開啟檔案控制代碼操作
fh, err := os.Open(filename)
if err != nil {
fmt.Println("error opening file")
return err
}
defer fh.Close()
//iocopy
_, err = io.Copy(fileWriter, fh)
if err != nil {
return err
}
contentType := bodyWriter.FormDataContentType()
bodyWriter.Close()
resp, err := http.Post(targetUrl, contentType, bodyBuf)
if err != nil {
return err
}
defer resp.Body.Close()
resp_body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Println(resp.Status)
fmt.Println(string(resp_body))
return nil
}
// sample usage
func main() {
target_url := "http://localhost:9090/upload"
filename := "./astaxie.pdf"
postFile(filename, target_url)
}
```
上面的例子詳細展示了客戶端如何向伺服器上傳一個檔案的例子客戶端透過multipart.Write把檔案的文字流寫入一個快取中然後呼叫http的Post方法把快取傳到伺服器。
>如果你還有其他普通欄位例如username之類別的需要同時寫入那麼可以呼叫multipart的WriteField方法寫很多其他類似的欄位。
## links
* [目錄](<preface.md>)
* 上一節: [防止多次遞交表單](<04.4.md>)
* 下一節: [小結](<04.6.md>)