DEV Community

Cover image for Attempting to Learn Go - Building a Downloader Part 04
Steve Layton
Steve Layton

Posted on • Originally published at shindakun.glitch.me on

Attempting to Learn Go - Building a Downloader Part 04

Welcome back! If you recall, last time we added the ability to actually download a file. This time, we're going to build on that. You can see right off the bat we have only added a single import, strings. You'll see this in use a bit further down as the majority of the base code stayed the same in this iteration.


Pass Five

package main

import (  
  "encoding/json"
  "fmt"
  "io"
  "io/ioutil"
  "log"
  "net/http"
  "net/url"
  "os"
  "path/filepath"
  "strings"
)

type download struct {  
  Title string `json:"title"`
  Location string `json:"location"`
}

func status(response http.ResponseWriter, request *http.Request) {  
  fmt.Fprintf(response, "Hello!")
}

func handleDownloadRequest(response http.ResponseWriter, request *http.Request) {  
  var downloadRequest download
  r, err := ioutil.ReadAll(request.Body)
  if err != nil {
    http.Error(response, "bad request", 400)
    log.Println(err)
    return
  }
  defer request.Body.Close()

  err = json.Unmarshal(r, &downloadRequest)
  if err != nil {
    http.Error(response, "bad request: "+err.Error(), 400)
    return
  }
  log.Printf("%#v", downloadRequest)

  err = getFile(downloadRequest)
  if err != nil {
    http.Error(response, "internal server error: "+err.Error(), 500)
    return
  }

  fmt.Fprintf(response, "Download!")
}

Here we come to our first new function createSaveDirectory. There is quite a bit going on so I'll try to break it down. The function itself takes in a string, in this case, the title field defined in our struct (that we built from the incoming JSON), and return a string or an error.

Our first step, toward saving the file, is to get the absolute path of the directory in which the program is running in currently. During this phase of development, on my local machine, that happens to be /e/Projects/Go/src/downloader.

func createSaveDirectory(title string) (string, error) {  
  dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
  if err != nil {
    log.Fatal(err)
  }

If any of that fails we aren't just noting an error and returning, instead, we're throwing a fatal error and exiting completely. Now that we have the directory stored in variable dir we're going to use filepath.Join (as shown below) to add the title we passed in as the name of the directory we want to save the file in. For this example using our previous JSON it would be /e/Projects/Go/src/downloader/Attempting Go. Now, this is another case where we are inviting issues down the road - we're are assuming that our input is well-formed and won't cause issues on the file system. Do not do this in production. I'm thinking we'll revisit these issues soon. After all, we don't want to risk writing to the wrong spot on the server.

Anyhow, once we have the file path we call os.Stat to see if the path actually exists already, if not we attempt to create it. If it does already exist we're just going to return the full path to the caller.

path := filepath.Join(dir, title)
  _, err = os.Stat(path)

  // directory does not exist we should create it.
  if err != nil {
    err = os.Mkdir(path, 0755)
    if err != nil {
      log.Fatal(err)
    }
  }

  return path, nil
}

func getFile(downloadRequest download) error {  
  u, err := url.Parse(downloadRequest.Location)
  if err != nil {
    log.Println(err)
    return err
  }

I had to modify the http.Get call slightly as it was having issues with improperly encoded URLs, which is to be expected. More specifically, URLs that contain spaces, instead of + or %20. I tried a couple different methods of encoding the URL using the built-in URL packages but, it never seemed to encode just the way it needed too. In the end, to keep things moving forward I decided just to use strings.Replace to simply swap the spaces for the proper %20 encoding. In this case, I've done it right within the GET call, in a future revision, we'll address our input validation concerns in a completely separate function.

response, err := http.Get(strings.Replace(downloadRequest.Location, " ", "%20", -1))
  if err != nil {
    log.Println(err)
    return err
  }
  defer response.Body.Close()

Finally, we have the response body so we're going to make sure we have somewhere to save it. So, we call the createSaveDirectory function described above. I may move this few lines up to above the http.Get, after all, if we have an issue locally getting ready to save it's not worth actually performing the download.

save, err := createSaveDirectory(downloadRequest.Title)
  if err != nil {
    log.Println(err)
    return err
  }

  out, err := os.Create(filepath.Join(save, filepath.Base(u.Path)))
  defer out.Close()
  _, err = io.Copy(out, response.Body)
  if err != nil {
    log.Println(err)
    return err
  }
  return nil
}

func main() {  
  log.Println("Downloader")

  http.HandleFunc("/", status)
  http.HandleFunc("/download", handleDownloadRequest)
  http.ListenAndServe(":3000", nil)
}

That brings us to the end of this revision. I'm going to go through tomorrow and try to clean up some of our encoding concerns and put together some input validation. Then we'll determine how to run this code in "the cloud" and maybe touch on how we'd actually use it.

Until next time...


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.



Oldest comments (0)