Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Download zipped file in browser from golang server

I am trying to download a zipped file from a Go webserver. I have successfully zipped the file and can unzip it from my server's directory. The problem I have run into is serving the file and downloading it with Javascript.

Here is an overview of my code:

1) Make a request to server which retrieves data from another endpoint

2) Structure the returned data based on the file type the user wants(CSV(setupCSVRows function) or JSON)

3) Write bytes from buffer to file and return the file's address

4) When user clicks a link, make an http get request with file address and open the contents in a new window for download

Every time I try to unzip the file I get the error: archive file is incomplete (with The Unarchiver program) and the default Archive Utility on Mac shows a brief loading screen then closes.

Go Code:

func ExportData(writer http.ResponseWriter, req *http.Request, session sessions.Session) (int, string) {

headers := HeaderCreation{
    OriginalRequest: req,
    Session:         session,
}

qs := req.URL.Query()

if len(qs["collectionID"]) != 1 {
    return 400, "ERROR: Must submit one collectionID in query string"
}
if len(qs["fileType"]) != 1 {
    return 400, "ERROR: Must submit one fileType in query string"
}

collID := qs["collectionID"][0]
fileType := qs["fileType"][0]

url := "http://" + config.Data.Address + "/api/" + collID
response, err := httpClient.DoSystemRequest("GET", url, nil, headers)

if err != nil {
    return 500, "ERROR: Could not resolve DataURL/api" + err.Error()
} else {
    contents, err := ioutil.ReadAll(response.Body)
    response.Body.Close()

    if err != nil {
        return 400, "ERROR: Response from Platform unreadable"
    }

    buf := new(bytes.Buffer)

    w := zip.NewWriter(buf)

    file, err := w.Create(collID + "." + fileType)
    if err != nil {
        return 400, "ERROR: Unable to create zip file with name of: " + collID + " and type of: " + fileType + "; " + err.Error()
    }

    switch fileType {
    case "csv":

        rows, err := setupCSVRows(contents)

        if err != nil {
            return 400, err.Error()
        }

        _, err = file.Write(rows)
        if err != nil {
            return 400, "Unable to write CSV to zip file; " + err.Error()
        }
    case "json":
        _, err := file.Write(contents)
        if err != nil {
            return 400, err.Error()
        }
    } // end switch

    err = w.Close()
    if err != nil {
        return 400, "ERROR: Unable to close zip file writer; " + err.Error()
    }

    //create fileName based on collectionID and current time
    fileAddress := collID + strconv.FormatInt(time.Now().Unix(), 10)

    //write the zipped file to the disk
    ioutil.WriteFile(fileAddress + ".zip", buf.Bytes(), 0777)

    return 200, fileAddress
} //end else
}

func ReturnFile(writer http.ResponseWriter, req *http.Request) {
queries := req.URL.Query()
fullFileName := queries["fullFileName"][0]
http.ServeFile(writer, req, fullFileName)
//delete file from server once it has been served
//defer os.Remove(fullFileName)
}

func setupCSVRows(contents []byte) ([]byte, error) {
//unmarshal into interface because we don't know json structure in advance
var collArr interface{}
jsonErr := json.Unmarshal(contents, &collArr)

if jsonErr != nil {
    return nil, errors.New("ERROR: Unable to parse JSON")
}

//had to do some weird stuff here, not sure if it's the best method
s := reflect.ValueOf(collArr)
var rows bytes.Buffer
var headers []string

for i := 0; i < s.Len(); i++ {
    var row []string
    m := s.Index(i).Interface()

    m2 := m.(map[string]interface{})

    for k, v := range m2 {

        if i == 0 {
            if k != "item_id" {
                headers = append(headers, k)
            }
        }
        if k != "item_id" {
            row = append(row, v.(string))
        }
    }

    if i == 0 {
        headersString := strings.Join(headers, ",")
        rows.WriteString(headersString + "\n")
    }
    rowsString := strings.Join(row, ",")
    rows.WriteString(rowsString + "\n")
}

return rows.Bytes(), nil
}

Javascript Code:

$scope.exportCollection = function(fileType) {
    $scope.exporting = true;
    $scope.complete = false;

    $http.get('/api/batch/export?collectionID=' + $scope.currentCollection.collectionID + '&fileType=' + fileType.toLowerCase()).success(function(data){
    $scope.fileAddress = data;

    }).error(function(err) {
  console.log(err);
    });

};

$scope.downloadFile = function() {
    $http.get('/api/batch/export/files?fullFileName=' + $scope.fileAddress + ".zip")
      .success(function(data) {
        console.log(data);

    //window.open("data:application/zip;base64," + content);
    //var content = "data:text/plain;charset=x-user-defined," + data;
    var content = "data:application/zip;charset=utf-8," + data;
    //var content = "data:application/octet-stream;charset=utf-8" + data;
    //var content = "data:application/x-zip-compressed;base64," + data;
    //var content = "data:application/x-zip;charset=utf-8," + data;
    // var content = "data:application/x-zip-compressed;base64," + data;
    window.open(content);
  })
  .error(function(err) {
    console.log(err);
  })
}

As you can see, I have tried many different URI schemes for downloading the file but nothing has worked.

Do I need to set the MIME type on the server side?

Any help would be greatly appreciated. Please let me know if I need to include any more details.

like image 606
sh3nan1gans Avatar asked Dec 20 '22 18:12

sh3nan1gans


2 Answers

I can't comment (new user) - but in regards to naming the file, simply set the headers before serving (ServeContent used, but should be interchangable for your usage here):

 func serveFile(w http.ResponseWriter, r *http.Request){
    data, err := ioutil.ReadFile("path/to/file/and/file+ext")
    if(err != nil){
        log.Fatal(err)
    }
    w.Header().Set("Content-Type", "application/octet-stream")
    w.Header().Set("Content-Disposition", "attachment; filename=" + "fileName.here")
    w.Header().Set("Content-Transfer-Encoding", "binary")
    w.Header().Set("Expires", "0")
    http.ServeContent(w, r, "path/to/file/and/file+ext", time.Now(), bytes.NewReader(data))

}
like image 195
John Campbell Avatar answered Feb 02 '23 02:02

John Campbell


I ended up going down a slightly different route. Now I set the MIME type on the response header and create a link that points to the file.

Go code:

func ReturnFile(writer http.ResponseWriter, req *http.Request) {
queries := req.URL.Query()
fullFileName := queries["fullFileName"][0]

writer.Header().Set("Content-type", "application/zip")
http.ServeFile(writer, req, fullFileName)
//delete file from server once it has been served
defer os.Remove(fullFileName)
}

Angular UI code:

<a ng-show="complete" href="/api/batch/export/files?fullFileName={{fileAddress}}">Download {{currentCollection.name}}</a>

This automatically triggers the download and the zip file is no longer corrupted.

like image 38
sh3nan1gans Avatar answered Feb 02 '23 01:02

sh3nan1gans