I'm writing an API client against Mapbox, uploading a batch of svg images to a custom map. The api they provide for this is documented with an example cUrl call that works fine:
curl -F images=#include/mapbox/sprites_dark/aubergine_selected.svg "https://api.mapbox.com/styles/v1/<my_company>/<my_style_id>/sprite?access_token=$MAPBOX_API_KEY" --trace-ascii /dev/stdout
When attemting to do the same from golang I quickly came across that the multiform library is very limited, and wrote some code to make the request as similar to the cUrl request mentioned above.
func createMultipartFormData(fileMap map[string]string) (bytes.Buffer, *multipart.Writer) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
var fw io.Writer
for fileName, filePath := range fileMap {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "images", fileName))
h.Set("Content-Type", "image/svg+xml")
if fw, err = w.CreatePart(h); err != nil {
fmt.Printf("Error creating form File %v, %v", fileName, err)
continue
}
fileContents, err := ioutil.ReadFile(filePath)
fileContents = bytes.ReplaceAll(fileContents, []byte("\n"), []byte("."))
blockSize := 64
remainder := len(fileContents) % blockSize
iterations := (len(fileContents) - remainder) / blockSize
newBytes := []byte{}
for i := 0; i < iterations; i++ {
start := i * blockSize
end := i*blockSize + blockSize
newBytes = append(newBytes, fileContents[start:end]...)
newBytes = append(newBytes, []byte("\n")...)
}
if remainder > 0 {
newBytes = append(newBytes, fileContents[iterations*blockSize:]...)
newBytes = append(newBytes, []byte("\n")...)
}
if err != nil {
fmt.Printf("Error reading svg file: %v: %v", filePath, err)
continue
}
_, err = fw.Write(newBytes)
if err != nil {
log.Debugf("Could not write file to multipart: %v, %v", fileName, err)
continue
}
}
w.Close()
return b, w
}
Along with setting the headers in the actual request:
bytes, formWriter := createMultipartFormData(filesMap)
req, err := http.NewRequest("Post", fmt.Sprintf("https://api.mapbox.com/styles/v1/%v/%v/sprite?access_token=%v", "my_company", styleID, os.Getenv("MAPBOX_API_KEY")), &bytes)
if err != nil {
return err
}
req.Header.Set("User-Agent", "curl/7.64.1")
req.Header.Set("Accept", "*/*")
req.Header.Set("Content-Length", fmt.Sprintf("%v", len(bytes.Bytes())))
req.Header.Set("Content-Type", formWriter.FormDataContentType())
byts, _ := httputil.DumpRequest(req, true)
fmt.Println(string(byts))
res, err := http.DefaultClient.Do(req)
Even want as far to limit the line length and replicate the encoding used by cUrl but so far no luck. Does anyone with experience know why this works from cUrl but not golang?
Well, I admit that all the parts of the "puzzle" to solve your task can be found on the 'net in abundance, there are two problems with this:
They quite often miss certain interesting details.
Sometimes, they give outright incorrect advice.
So, here's a working solution.
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
)
func main() {
const (
dst = "https://api.mapbox.com/styles/v1/AcmeInc/Style_001/sprite"
fname = "path/to/a/sprite/image.svg"
token = "an_invalid_token"
)
err := post(dst, fname, token)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func post(dst, fname, token string) error {
u, err := url.Parse(dst)
if err != nil {
return fmt.Errorf("failed to parse destination url: %w", err)
}
form, err := makeRequestBody(fname)
if err != nil {
return fmt.Errorf("failed to prepare request body: %w", err)
}
q := u.Query()
q.Set("access_token", token)
u.RawQuery = q.Encode()
hdr := make(http.Header)
hdr.Set("Content-Type", form.contentType)
req := http.Request{
Method: "POST",
URL: u,
Header: hdr,
Body: ioutil.NopCloser(form.body),
ContentLength: int64(form.contentLen),
}
resp, err := http.DefaultClient.Do(&req)
if err != nil {
return fmt.Errorf("failed to perform http request: %w", err)
}
defer resp.Body.Close()
_, _ = io.Copy(os.Stdout, resp.Body)
return nil
}
type form struct {
body *bytes.Buffer
contentType string
contentLen int
}
func makeRequestBody(fname string) (form, error) {
ct, err := getImageContentType(fname)
if err != nil {
return form{}, fmt.Errorf(
`failed to get content type for image file "%s": %w`,
fname, err)
}
fd, err := os.Open(fname)
if err != nil {
return form{}, fmt.Errorf("failed to open file to upload: %w", err)
}
defer fd.Close()
stat, err := fd.Stat()
if err != nil {
return form{}, fmt.Errorf("failed to query file info: %w", err)
}
hdr := make(textproto.MIMEHeader)
cd := mime.FormatMediaType("form-data", map[string]string{
"name": "images",
"filename": fname,
})
hdr.Set("Content-Disposition", cd)
hdr.Set("Contnt-Type", ct)
hdr.Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
part, err := mw.CreatePart(hdr)
if err != nil {
return form{}, fmt.Errorf("failed to create new form part: %w", err)
}
n, err := io.Copy(part, fd)
if err != nil {
return form{}, fmt.Errorf("failed to write form part: %w", err)
}
if int64(n) != stat.Size() {
return form{}, fmt.Errorf("file size changed while writing: %s", fd.Name())
}
err = mw.Close()
if err != nil {
return form{}, fmt.Errorf("failed to prepare form: %w", err)
}
return form{
body: &buf,
contentType: mw.FormDataContentType(),
contentLen: buf.Len(),
}, nil
}
var imageContentTypes = map[string]string{
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"svg": "image/svg+xml",
}
func getImageContentType(fname string) (string, error) {
ext := filepath.Ext(fname)
if ext == "" {
return "", fmt.Errorf("file name has no extension: %s", fname)
}
ext = strings.ToLower(ext[1:])
ct, found := imageContentTypes[ext]
if !found {
return "", fmt.Errorf("unknown file name extension: %s", ext)
}
return ct, nil
}
Some random notes on implementation to help you understands the concepts:
To construct the request's payload (body), we use a bytes.Buffer instance.
It has a nice property in that a pointer to it (*bytes.Buffer) implements both io.Writer and io.Reader and hence can be easily composed with other parts of the Go stdlib which deal with I/O.
When preparing a multipart form for sending, we do not slurp the whole file's contents into memory but instead "pipe" them right into the "mutipart form writer".
We have a lookup table which maps the extension of a file name to submit to its MIME type; I have no idea whether this is needed by the API or not; if it's not really required, the part of the code which prepares a form's field containing a file could be simplified a lot, but cURL send it, so do we.
Just to be curious, What is this for?
fileContents = bytes.ReplaceAll(fileContents, []byte("\n"), []byte("."))
blockSize := 64
remainder := len(fileContents) % blockSize
iterations := (len(fileContents) - remainder) / blockSize
newBytes := []byte{}
for i := 0; i < iterations; i++ {
start := i * blockSize
end := i*blockSize + blockSize
newBytes = append(newBytes, fileContents[start:end]...)
newBytes = append(newBytes, []byte("\n")...)
}
if remainder > 0 {
newBytes = append(newBytes, fileContents[iterations*blockSize:]...)
newBytes = append(newBytes, []byte("\n")...)
}
if err != nil {
fmt.Printf("Error reading svg file: %v: %v", filePath, err)
continue
}
Reading a entire file into memory is rarely a good idea (ioutil.ReadFile).
As #muffin-top says, how about those three lines of code?
for fileName, filePath := range fileMap {
// h := ...
fw, _ := w.CreatePart(h) // TODO: handle error
f, _ := os.Open(filePath) // TODO: handle error
io.Copy(fw, f) // TODO: handle error
f.Close() // TODO: handle error
}
Related
I am trying to send a zip file in POST request body as form-data in GO. So, it is supposed to be in a key-value format. I am able to achieve the same while sending an unzipped file. Here is the snippet,
buf := new(bytes.Buffer)
multiPartWriter := multipart.NewWriter(buf)
part, err := multiPartWriter.CreateFormFile("File", fileName)
However, when it comes to zipping it, I am able to find this,
zipWriter := zip.NewWriter(buf)
zipFile, err := zipWriter.Create(fileName)
Unlike multipart.Writer, in case of zip.Writer I can't find any option to create form file in a key-value fashion.
How can I achieve this for zip as well?
Code snippet[updated],
func SendPostRequest(url string, content []byte, fileName string,
doZip bool)(*http.Response, error) {
buf := new(bytes.Buffer)
multiPartWriter := multipart.NewWriter(buf)
part, _ := multiPartWriter.CreateFormFile("File", fileName)
if doZip {
zipWriter := zip.NewWriter(part)
zipFile, _ := zipWriter.Create(fileName)
zipFile.Write(content)
zipWriter.Close()
} else {
part.Write(content)
multiPartWriter.Close()
}
request, _ := http.NewRequest(http.MethodPost, url, buf)
request.Header.Add(constants.Header_content_type_key, multiPartWriter.FormDataContentType())
client := &http.Client{}
response, err := client.Do(request)
return client.Do(request)
}
Ordinarily, I think you'd want to use multiPartWriter.CreatePart(header), where header is of type MIMEHeader. CreatePart() will return an io.Writer - pass that to zip.NewWriter():
// Assume you've created your header already
w, err := multiPartWriter.CreatePart(header)
if err != nil {
// do whatever
}
zipWriter := zip.NewWriter(w)
// Add stuff to zipWriter
You might need to play around, YMMV.
As a solution to this one - I am doing a zip of the content byte slice only, when it comes to zipping. Rest are common for both zipped/non-zipped approach. Here are the code snippets,
func sendPostRequest(url string, content []byte, projectId string, fileName string, fileType string, doZip bool) (*http.Response, error) {
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
part, err := writer.CreateFormFile("File", fileName)
if err != nil {
return nil, errors.New("Invocation of *multipart.Writer.CreateFormFile() failed: " + err.Error())
}
content, err = ZipBytes(fileName, content, doZip)
if err != nil {
return nil, err
}
_, err = part.Write(content)
if err != nil {
return nil, errors.New("Invocation of io.Writer.Write() failed: " + err.Error())
}
if err = writer.Close(); err != nil {
return nil, errors.New("Failed to close *multipart.Writer: " + err.Error())
}
request, err := http.NewRequest(http.MethodPost, url, buf)
if err != nil {
return nil, errors.New("Failed to create request for Validation API multipart file uploading: " + err.Error())
}
request.Header.Add(constants.Header_content_type_key, writer.FormDataContentType())
request.Header.Add(constants.Project_Id_header_str, projectId)
request.Header.Add(constants.File_type_header_str, fileType)
if doZip {
request.Header.Add(constants.IsZipped_header_str, "true")
} else {
request.Header.Add(constants.IsZipped_header_str, "false")
}
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return nil, errors.New("Failed to invoke Validation API: " + err.Error())
} else if !strings.Contains(strconv.Itoa(response.StatusCode), "20") {
respMap, _ := processValidationResponse(response)
message := fmt.Sprintf("Failed to invoke Validation API with HTTP %s : %s", strconv.Itoa(response.StatusCode), respMap["message"])
return nil, errors.New(message)
}
return response, nil
}
func ZipBytes(fileName string, content []byte, doZip bool) ([]byte, error) {
if doZip {
log.Println("Zipped file will be sent for validation")
buf := new(bytes.Buffer)
zipWriter := zip.NewWriter(buf)
zipFile, err := zipWriter.Create(fileName)
if err != nil {
return nil, errors.New("Zip file creation failed: " + err.Error())
}
_, err = zipFile.Write(content)
if err != nil {
return nil, errors.New("Writing to zip file failed: " + err.Error())
}
if err = zipWriter.Close(); err != nil {
return nil, errors.New("Failed to close zip writer: " + err.Error())
}
return buf.Bytes(), nil
} else {
log.Println("Normal file will be sent for validation")
return content, nil
}
}
I'm having issues with requests being sent randomly empty. It doesn't always happen, but sometimes out of the blue, it will not send any of the multipart fields. I thought it might have to do with the upload server, so I created a local upload server to print out the request that's being sent, and it comes out empty.
I have added error checks everywhere, but no errors are being returned.
I tried to run the code with -race, but no race condition has been reported.
Edit: Update the code to use CloseWithError()
package main
import (
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
)
var (
upload_url string = "https://upload.imagekit.io/api/v1/files/upload"
file_name string = "favicon-516140983.ico"
api_secret_key string = "PRIVATE_KEY"
)
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 {
log.Println(err)
return nil, err
}
mwriter := multipart.NewWriter(writer)
req.Header.Add("Content-Type", mwriter.FormDataContentType())
req.SetBasicAuth(api_secret_key, "")
go func() {
var err error
defer func() {
if err != nil {
writer.CloseWithError(err)
} else {
writer.Close()
}
}()
var file *os.File
file, err = os.Open(path)
if err != nil {
return
}
defer file.Close()
if err = mwriter.WriteField("fileName", file_name); err != nil {
return
}
var w io.Writer
w, err = mwriter.CreateFormFile("file", path)
if err != nil {
return
}
var written int64
if written, err = io.Copy(w, file); err != nil {
err = fmt.Errorf("error copying %s (%d bytes written): %v", path, written, err)
return
}
if err = mwriter.Close(); err != nil {
return
}
}()
resp, err := client.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
func main() {
path, _ := os.Getwd()
path += "/" + file_name
client := &http.Client{}
resp, err := UploadMultipartFile(client, upload_url, "file", path)
if err != nil {
log.Println(err)
} else {
fmt.Println(resp.StatusCode)
fmt.Println(resp.Header)
_, err := io.Copy(os.Stdout, resp.Body)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
}
}
I am trying to build a binary-to-text encoder with the ability to replace every byte into a predefined unique string and then use the same string set to decode back into binary.
I am able to make the encoder and decoder for simple .txt files but I want to make this workable for .zip files too.
Encoder :
package main
import (
"archive/zip"
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
)
func main() {
// keys.json is a 256 words dictionary for every byte
keysFile, err := os.Open("keys.json")
if err != nil {
log.Printf("unable to read keys.json file , error : %v", err)
return
}
var keys []string
byteValue, _ := ioutil.ReadAll(keysFile)
if err := json.Unmarshal(byteValue, &keys); err != nil {
log.Printf("unable to unmarshal array , error : %v", err)
return
}
Encoder(keys)
}
func listFiles(file *zip.File) ([]byte, error) {
fileToRead, err := file.Open()
if err != nil {
msg := "Failed to open zip %s for reading: %s"
return nil, fmt.Errorf(msg, file.Name, err)
}
b := make([]byte, file.FileInfo().Size())
fileToRead.Read(b)
defer fileToRead.Close()
if err != nil {
return nil, fmt.Errorf("failed to read zip file : %s for reading , error : %s", file.Name, err)
}
return b, nil
}
func Encoder(keys []string) {
read, err := zip.OpenReader("some.zip")
if err != nil {
msg := "Failed to open: %s"
log.Fatalf(msg, err)
}
defer read.Close()
var encodedBytes []byte
f, err := os.Create("result.txt")
w := bufio.NewWriter(f)
defer f.Close()
for _, file := range read.File {
readBytes, err := listFiles(file)
if err != nil {
log.Fatalf("Failed to read %s from zip: %s", file.Name, err)
continue
}
for i, b := range readBytes {
for _, eb := range []byte(keys[b] + " ") {
encodedBytes = append(encodedBytes, eb)
}
}
}
_, err = w.Write(encodedBytes)
if err != nil {
log.Printf("error :%v", err)
return
}
}
Decoder :
func Decoder(keys []string) {
inputFile, err := os.Open("result.txt")
if err != nil {
log.Printf("unable to read file , error : %v", err)
return
}
inputBytes, _ := ioutil.ReadAll(inputFile)
var (
current []byte
decoded []byte
)
for _, c := range inputBytes {
if c != 32 {
current = append(current, c)
} else {
for i, key := range keys {
if string(current) == key {
decoded = append(decoded, byte(i))
break
}
}
current = []byte{}
}
}
// here i want the decoded back into zip file
}
here is a similar one in nodejs.
Two things:
You are dealing with spaces correctly, but not with newlines.
Your decoder loop is wrong. As far as I can tell, it should look like the following:
for i, key := range keys {
if string(current)==key {
decoded=append(decoded,i)
break
}
}
Also, your decoded is an int-array, not a byte-array.
I want to test a httpRequest with a json body and a test file.
I don't know how to add the created test file to the request beside body json.
body := strings.NewReader(URLTest.RequestBody)
request, err := http.NewRequest(URLTest.MethodType, "localhost:"+string(listeningPort)+URLTest.URL, body)
if err != nil {
t.Fatalf("HTTP NOT WORKING")
}
fileBuffer := new(bytes.Buffer)
mpWriter := multipart.NewWriter(fileBuffer)
fileWriter, err := mpWriter.CreateFormFile("file", "testfile.pdf")
if err != nil {
t.Fatalf(err.Error())
}
file, err := os.Open("testfile.pdf")
if err != nil {
t.Fatalf(err.Error())
}
defer file.Close()
_, err = io.Copy(fileWriter, file)
if err != nil {
t.Fatalf(err.Error())
}
rec := httptest.NewRecorder()
UploadFiles(rec, request, nil)
response := rec.Result()
if response.StatusCode != URLTest.ExpectedStatusCode {
t.Errorf(URLTest.URL + " status mismatch")
}
responseBody, err := ioutil.ReadAll(response.Body)
defer response.Body.Close()
if err != nil {
t.Errorf(URLTest.URL + " cant read response")
} else {
if strings.TrimSpace(string(responseBody)) != URLTest.ExpectedResponseBody {
t.Errorf(URLTest.URL + " response mismatch - have: " + string(responseBody) + " want: " + URLTest.ExpectedResponseBody)
}
}
}
Can I add file as a value like request.FormFile.Add(...) or something?
Regarding your question about how to send a file in an HTTP request with Go, here's some sample code.
And you will need the mime/multipart package to build the form.
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
}
Hope you can use this in your unit test
I'm new to golang and I'm trying to write a function that uploads a file with a post request to telegram for a bot I'm writing.
I've tried with this code but the error I'm getting from telegram is
Bad Request: there is no photo in the request.
I've searched on the net for how to do that, but none of what I found helped me through the problem.
func SendPostRequest (url string, filename string) []byte {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
response, err := http.Post(url, "binary/octet-stream", file)
if err != nil {
log.Fatal(err)
}
defer response.Body.Close()
content, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
return content
}
The function I'm calling the SendPostRequest from is
func (e Engine) SendPhoto (filename string, chatId int64) APIResponse {
var url = fmt.Sprintf("%ssendPhoto?chat_id=%d", e.baseUrl, chatId)
var content []byte = SendPostRequest(url, filename)
var response APIResponse
json.Unmarshal(content, &response)
return response
}
EDIT:
The link to the Telegram bot api I'm using in the code is https://core.telegram.org/bots/api
And the api method is https://core.telegram.org/bots/api#sendphoto
After some digging I figured it out with this
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"path/filepath"
)
// content is a struct which contains a file's name, its type and its data.
type content struct {
fname string
ftype string
fdata []byte
}
func sendPostRequest(url string, files ...content) ([]byte, error) {
var (
buf = new(bytes.Buffer)
w = multipart.NewWriter(buf)
)
for _, f := range files {
part, err := w.CreateFormFile(f.ftype, filepath.Base(f.fname))
if err != nil {
return []byte{}, err
}
_, err = part.Write(f.fdata)
if err != nil {
return []byte{}, err
}
}
err := w.Close()
if err != nil {
return []byte{}, err
}
req, err := http.NewRequest("POST", url, buf)
if err != nil {
return []byte{}, err
}
req.Header.Add("Content-Type", w.FormDataContentType())
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return []byte{}, err
}
defer res.Body.Close()
cnt, err := io.ReadAll(res.Body)
if err != nil {
return []byte{}, err
}
return cnt, nil
}