Дані POST за допомогою багаточастинних / форм-даних Content-Type


77

Я намагаюся завантажити зображення зі свого комп’ютера на веб-сайт за допомогою go. Зазвичай я використовую скрипт bash, який надсилає файл і ключ на сервер:

curl -F "image"=@"IMAGEFILE" -F "key"="KEY" URL

це працює нормально, але я намагаюся перетворити цей запит у свою програму golang.

http://matt.aimonetti.net/posts/2013/07/01/golang-multipart-file-upload-example/

Я спробував це посилання та багато інших, але на кожен код, який я намагаюся, відповідь сервера - "зображення не надіслано", і я не знаю, чому. Якщо хтось знає, що відбувається з прикладом вище.


1
Чи можете ви розповісти дещо про свій сервер?
nvcnvn

6
Опублікуйте код, який не працює.
четвер,

Відповіді:


145

Ось зразок коду.

Коротше кажучи, вам потрібно буде використовувати mime/multipartпакет для побудови форми.

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/httptest"
    "net/http/httputil"
    "os"
    "strings"
)

func main() {

    var client *http.Client
    var remoteURL string
    {
        //setup a mocked http client.
        ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            b, err := httputil.DumpRequest(r, true)
            if err != nil {
                panic(err)
            }
            fmt.Printf("%s", b)
        }))
        defer ts.Close()
        client = ts.Client()
        remoteURL = ts.URL
    }

    //prepare the reader instances to encode
    values := map[string]io.Reader{
        "file":  mustOpen("main.go"), // lets assume its this file
        "other": strings.NewReader("hello world!"),
    }
    err := Upload(client, remoteURL, values)
    if err != nil {
        panic(err)
    }
}

func Upload(client *http.Client, url string, values map[string]io.Reader) (err error) {
    // Prepare a form that you will submit to that URL.
    var b bytes.Buffer
    w := multipart.NewWriter(&b)
    for key, r := range values {
        var fw io.Writer
        if x, ok := r.(io.Closer); ok {
            defer x.Close()
        }
        // Add an image file
        if x, ok := r.(*os.File); ok {
            if fw, err = w.CreateFormFile(key, x.Name()); err != nil {
                return
            }
        } else {
            // Add other fields
            if fw, err = w.CreateFormField(key); err != nil {
                return
            }
        }
        if _, err = io.Copy(fw, r); err != nil {
            return err
        }

    }
    // Don't forget to close the multipart writer.
    // If you don't close it, your request will be missing the terminating boundary.
    w.Close()

    // Now that you have a form, you can submit it to your handler.
    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return
    }
    // Don't forget to set the content type, this will contain the boundary.
    req.Header.Set("Content-Type", w.FormDataContentType())

    // Submit the request
    res, err := client.Do(req)
    if err != nil {
        return
    }

    // Check the response
    if res.StatusCode != http.StatusOK {
        err = fmt.Errorf("bad status: %s", res.Status)
    }
    return
}

func mustOpen(f string) *os.File {
    r, err := os.Open(f)
    if err != nil {
        panic(err)
    }
    return r
}

1
Хороший зразок коду. Не вистачає одного: деякі веб-сервери, такі як Django, перевіряють заголовок "Content-Type" частини. Ось як встановити цей заголовок: <pre> partHeader: = textproto.MIMEHeader {} disp: = fmt.Sprintf ( form-data; name="data"; filename="%s", fn) partHeader.Add ("Content-Disposition", disp) partHeader.Add ("Content-Type", "image / jpeg") частина, помилка: = Writer.CreatePart (partHeader) </pre>
Zhichang Yu

Працює добре, крім того file.Name(), що не працює для мене. Здається, він повертає шлях до файлу, переданий os.Open (), а не ім’я ... (string) (len=37) "./internal/file_example_JPG_500kB.jpg" // Name returns the name of the file as presented to Open. func (f *File) Name() string { return f.name }Якщо я жорстко кодую ім’я файлу, w.CreateFormFile()воно працює нормально. Дякую Аттіла
Лукас Лукач

На жаль, а якщо наші значення схожі на рядок map [string] []?
Аліхан

4

Ось функція, яку я використовував, яка використовує, io.Pipe()щоб уникнути читання всього файлу в пам’ять або необхідності керувати будь-якими буферами. Він обробляє лише один файл, але його можна легко розширити, щоб обробити більше, додавши більше частин до програми. Щасливий шлях працює добре. Шляхи помилок не проводять багато перевірок.

import (
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func UploadMultipartFile(client *http.Client, uri, key, path string) (*http.Response, error) {
    body, writer := io.Pipe()

    req, err := http.NewRequest(http.MethodPost, uri, body)
    if err != nil {
        return nil, err
    }

    mwriter := multipart.NewWriter(writer)
    req.Header.Add("Content-Type", mwriter.FormDataContentType())

    errchan := make(chan error)

    go func() {
        defer close(errchan)
        defer writer.Close()
        defer mwriter.Close()

        w, err := mwriter.CreateFormFile(key, path)
        if err != nil {
            errchan <- err
            return
        }

        in, err := os.Open(path)
        if err != nil {
            errchan <- err
            return
        }
        defer in.Close()

        if written, err := io.Copy(w, in); err != nil {
            errchan <- fmt.Errorf("error copying %s (%d bytes written): %v", path, written, err)
            return
        }

        if err := mwriter.Close(); err != nil {
            errchan <- err
            return
        }
    }()

    resp, err := client.Do(req)
    merr := <-errchan

    if err != nil || merr != nil {
        return resp, fmt.Errorf("http error: %v, multipart error: %v", err, merr)
    }

    return resp, nil
}

2

Після того, як мені довелося розшифрувати прийняту відповідь на це питання для використання в моєму модульному тестуванні, я нарешті отримав наступний реконструйований код:

func createMultipartFormData(t *testing.T, fieldName, fileName string) (bytes.Buffer, *multipart.Writer) {
    var b bytes.Buffer
    var err error
    w := multipart.NewWriter(&b)
    var fw io.Writer
    file := mustOpen(fileName)
    if fw, err = w.CreateFormFile(fieldName, file.Name()); err != nil {
        t.Errorf("Error creating writer: %v", err)
    }
    if _, err = io.Copy(fw, file); err != nil {
        t.Errorf("Error with io.Copy: %v", err)
    }
    w.Close()
    return b, w
}

func mustOpen(f string) *os.File {
    r, err := os.Open(f)
    if err != nil {
        pwd, _ := os.Getwd()
        fmt.Println("PWD: ", pwd)
        panic(err)
    }
    return r
}

Тепер це повинно бути досить простим у використанні:

    b, w := createMultipartFormData(t, "image","../luke.png")

    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return
    }
    // Don't forget to set the content type, this will contain the boundary.
    req.Header.Set("Content-Type", w.FormDataContentType())

1

Надіслати файл з однієї служби на іншу:

func UploadFile(network, uri string, f multipart.File, h *multipart.FileHeader) error {

    buf := new(bytes.Buffer)
    writer := multipart.NewWriter(buf)

    part, err := writer.CreateFormFile("file", h.Filename)

    if err != nil {
        log.Println(err)
        return err
    }

    b, err := ioutil.ReadAll(f)

    if err != nil {
        log.Println(err)
        return err
    }

    part.Write(b)
    writer.Close()

    req, _ := http.NewRequest("POST", uri, buf)

    req.Header.Add("Content-Type", writer.FormDataContentType())
    client := &http.Client{}
    resp, err := client.Do(req)

    if err != nil {
        return err
    }
    defer resp.Body.Close()

    b, _ = ioutil.ReadAll(resp.Body)
    if resp.StatusCode >= 400 {
        return errors.New(string(b))
    }
    return nil
}

Можливо, ви могли б трохи детальніше розказати про те, що робить ваш код (включаючи коментарі), оскільки це зробило б його корисним як ресурс, а не просто як відповідь, адаптовану до конкретного питання.
chabad360

0

Щоб продовжити відповідь @ attila-o, ось код, з яким я пішов для виконання запиту POST HTTP в Go with:

  • 1 файл
  • налаштоване ім'я файлу (f.Name () не працювало)
  • додаткові поля форми.

Представлення завитків:

curl -X POST \
  http://localhost:9091/storage/add \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
  -F owner=0xc916Cfe5c83dD4FC3c3B0Bf2ec2d4e401782875e \
  -F password=$PWD \
  -F file=@./internal/file_example_JPG_500kB.jpg

Перейти шлях:

client := &http.Client{
        Timeout: time.Second * 10,
    }
req, err := createStoragePostReq(cfg)
res, err := executeStoragePostReq(client, req)


func createStoragePostReq(cfg Config) (*http.Request, error) {
    extraFields := map[string]string{
        "owner": "0xc916cfe5c83dd4fc3c3b0bf2ec2d4e401782875e",
        "password": "pwd",
    }

    url := fmt.Sprintf("http://localhost:%d%s", cfg.HttpServerConfig().Port(), lethstorage.AddRoute)
    b, w, err := createMultipartFormData("file","./internal/file_example_JPG_500kB.jpg", "file_example_JPG_500kB.jpg", extraFields)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest("POST", url, &b)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", w.FormDataContentType())

    return req, nil
}

func executeStoragePostReq(client *http.Client, req *http.Request) (lethstorage.AddRes, error) {
    var addRes lethstorage.AddRes

    res, err := client.Do(req)
    if err != nil {
        return addRes, err
    }
    defer res.Body.Close()

    data, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return addRes, err
    }

    err = json.Unmarshal(data, &addRes)
    if err != nil {
        return addRes, err
    }

    return addRes, nil
}

func createMultipartFormData(fileFieldName, filePath string, fileName string, extraFormFields map[string]string) (b bytes.Buffer, w *multipart.Writer, err error) {
    w = multipart.NewWriter(&b)
    var fw io.Writer
    file, err := os.Open(filePath)

    if fw, err = w.CreateFormFile(fileFieldName, fileName); err != nil {
        return
    }
    if _, err = io.Copy(fw, file); err != nil {
        return
    }

    for k, v := range extraFormFields {
        w.WriteField(k, v)
    }

    w.Close()

    return
}

0

Була та сама проблема в модульному тесті, і якщо вам просто потрібно надіслати дані, щоб переконатися, що публікація це (нижче) для мене стала трохи простішою. Сподіваюся, це допоможе заощадити комусь ще трохи часу.

fileReader := strings.NewReader("log file contents go here")
b := bytes.Buffer{} // buffer to write the request payload into
fw := multipart.NewWriter(&b)
fFile, _ := fw.CreateFormFile("file", "./logfile.log")
io.Copy(fFile, fileReader)
fw.Close()

req, _ := http.NewRequest(http.MethodPost, url, &b)
req.Header.Set("Content-Type", fw.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

Для мене fileReader - це просто зчитувач рядків, оскільки я публікую файл журналу. У випадку із зображенням ви надішлете відповідного зчитувача.


-2

Я знайшов цей підручник дуже корисним для роз'яснення моїх сумнівів щодо завантаження файлів у Go.

В основному ви завантажуєте файл через ajax за form-dataдопомогою клієнта і використовуєте такий невеликий фрагмент коду Go на сервері:

file, handler, err := r.FormFile("img") // img is the key of the form-data
if err != nil {
    fmt.Println(err)
    return
}
defer file.Close()

fmt.Println("File is good")
fmt.Println(handler.Filename)
fmt.Println()
fmt.Println(handler.Header)


f, err := os.OpenFile(handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
    fmt.Println(err)
    return
}
defer f.Close()
io.Copy(f, file)

Ось rце *http.Request. PS це просто зберігає файл в тій самій папці і не виконує жодних перевірок безпеки.


8
OP запитував, як розмістити файл за допомогою Go (клієнт HTTP), не приймати та обробляти файл, розміщений з веб-сторінки в Go (сервер HTTP).
Mike Atlas
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.