POST-Daten unter Verwendung der mehrteiligen / Formulardaten des Inhaltstyps

75

Ich versuche, Bilder von meinem Computer mit go auf eine Website hochzuladen. Normalerweise verwende ich ein Bash-Skript, das eine Datei und einen Schlüssel an den Server sendet:

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

es funktioniert gut, aber ich versuche, diese Anfrage in mein Golang-Programm zu konvertieren.

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

Ich habe diesen und viele andere Links ausprobiert, aber für jeden Code, den ich versuche, lautet die Antwort vom Server "kein Bild gesendet", und ich habe keine Ahnung, warum. Wenn jemand weiß, was mit dem obigen Beispiel passiert.

Epitouille
quelle
1
Können Sie einige Details zu Ihrem Server angeben?
nvcnvn
6
Veröffentlichen Sie den Code, der nicht funktioniert.
Uhr

Antworten:

142

Hier ist ein Beispielcode.

Kurz gesagt, Sie müssen das mime/multipartPaket verwenden , um das Formular zu erstellen.

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
}
Attila O.
quelle
Guter Beispielcode. Eines fehlt: Einige Webserver wie Django überprüfen den "Content-Type" -Header des Teils. So setzen Sie diesen Header: <pre> partHeader: = textproto.MIMEHeader {} disp: = fmt.Sprintf ( form-data; name="data"; filename="%s", fn) partHeader.Add ("Inhaltsdisposition", disp) partHeader.Add ("Inhaltstyp", "image / jpeg") part, err: = writer.CreatePart (partHeader) </ pre>
Zhichang Yu
Funktioniert gut, außer dass das file.Name()bei mir nicht funktioniert. Scheint, als würde der an os.Open () übergebene Dateipfad kein Name zurückgegeben ... (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 }Wenn ich den Dateinamen w.CreateFormFile()darin fest codiere, funktioniert er einwandfrei . Danke Attila
Lukas Lukac
Entschuldigung, was ist, wenn unsere Werte wie map [string] [] string sind?
Alikhan
3

Hier ist eine Funktion, die ich verwendet habe, um io.Pipe()zu vermeiden, dass die gesamte Datei in den Speicher eingelesen wird oder Puffer verwaltet werden müssen. Es verarbeitet nur eine einzelne Datei, kann jedoch leicht erweitert werden, um mehr zu verarbeiten, indem mehr Teile innerhalb der Goroutine hinzugefügt werden. Der glückliche Weg funktioniert gut. Die Fehlerpfade haben nicht viel getestet.

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
}
Patrick
quelle
2

Nachdem ich die akzeptierte Antwort auf diese Frage zur Verwendung in meinen Unit-Tests dekodieren musste, erhielt ich schließlich den folgenden überarbeiteten Code:

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
}

Jetzt sollte es ziemlich einfach zu bedienen sein:

    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())
utx0
quelle
0

Um die @ attila-o-Antwort zu erweitern, ist hier der Code, mit dem ich eine POST-HTTP-Anforderung in Go with ausgeführt habe:

  • 1 Datei
  • konfigurierbarer Dateiname (f.Name () hat nicht funktioniert)
  • zusätzliche Formularfelder.

Curl-Darstellung:

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

Geh weg:

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
}
Lukas Lukac
quelle
0

Hatte das gleiche Problem bei einem Unit-Test, und wenn Sie nur Daten senden müssen, um zu überprüfen, ob der Beitrag (unten) für mich etwas einfacher war. Hoffe, es hilft jemand anderem Zeit zu sparen.

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)

Für mich ist fileReader nur ein String-Reader, da ich eine Protokolldatei veröffentliche. Im Falle eines Bildes würden Sie den entsprechenden Leser senden.

Atifm
quelle
0

Datei von einem Dienst an einen anderen senden:

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
}
Esaldino Fonseca
quelle
Vielleicht könnten Sie etwas näher darauf eingehen, was Ihr Code tut (einschließlich Kommentare), da dies ihn als Ressource und nicht nur als Antwort auf eine bestimmte Frage nützlich machen würde.
chabad360
-2

Ich fand dieses Tutorial sehr hilfreich, um meine Verwirrungen über das Hochladen von Dateien in Go zu klären.

Grundsätzlich laden Sie die Datei über Ajax form-dataauf einem Client hoch und verwenden den folgenden kleinen Ausschnitt aus Go-Code auf dem Server:

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)

Hier rist *http.Request. PS: Dadurch wird die Datei nur im selben Ordner gespeichert und es werden keine Sicherheitsüberprüfungen durchgeführt.

Salvador Dali
quelle
8
OP fragte, wie eine Datei mit Go (HTTP-Client) POSTEN soll, ohne eine von einer Webseite in Go (HTTP-Server) gepostete Datei zu akzeptieren und zu verarbeiten.
Mike Atlas