Go Up or Go Down

Go Up or Go Down

An Efficient Guide To Downloading and Uploading Files in Golang

ยท

6 min read

With Golang, we have a straightforward and efficient way of creating and managing web servers. The Golang standard library has an inbuilt support for handling HTTP requests and sending back responses. Also, Its efficient memory management and rich concurrency support makes it simple to manage web services in Golang.

This article will show you how to handle files in your server with Golang and at the end of the article, you would be able to upload files to a server and download the files. It's almost like building a mini cloud-storage application.

With a POST /upload request, you will be able to upload a file to our server and with a GET /download request you will download a file from the server.

Prerequisite

Before proceeding, it is expected that you have a basic understanding of the Golang programming language and you have Golang installed on your machine.

In addition to this, the article will also be using the Gorilla Mux package, a powerful URL router and dispatcher for Go web servers. Therefore, it is recommended that the reader has Gorilla Mux installed and a basic understanding of how it works. If you do not have Gorilla Mux installed, you can install it by running the following command in your terminal: go get -u github.com/gorilla/mux

Set Up Server

We would create a server that would be served on port 8000, then listen for incoming requests and process them as they come in.

  1. Create a main.go file and a main function.

  2. In the main function create a server.http.Server{} type which will allow you to define parameters for running an HTTP server

  3. Call the ListenAndServe() method which fires up the server.

You would also need to provide a Handler to enable proper routing whenever a request hits the server, for that Gorilla Mux would be used.

    r := mux.NewRouter()
    port := "8000"

    server := http.Server{
        Addr:        fmt.Sprintf(":%s", port),
        Handler:     r,
        IdleTimeout: 120 * time.Second,
    }
    go func() {
        log.Printf("---- SERVER STARTING ON PORT: %s ----\n", port)
        err := server.ListenAndServe()
        if err != nil {
            log.Printf("ERROR STARTING SERVER: %v", err)
            os.Exit(1)
        }

    }()
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, os.Kill)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    sig := <-sigChan
    log.Printf("Closing now, We've gotten signal: %v", sig)

    ctx := context.Background()
    server.Shutdown(ctx)

The server is run on a different go-routine so the main go-routine can listen for signals, once a signal has been sent to the channel, we can then proceed to a graceful shutdown.

Handle Upload

Now that the server is up and running, a function to upload files to the server is the next thing. For this, a POST request and a form-data as the body will be used, you will configure the server to accept a name and a file, so when uploading the file, it saves with that particular name on the server.

To get started, create a cmd/routes.go file and in the file create a HandleUpload function which takes in a responseWriter and Request Parameters.

  1. Set the content type to handle form data using ("Content-Type", "application/x-www-form-urlencoded")

  2. Parse the form and set the maximum size.

// set content type
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")

    // set file size to 10MB max
    err := r.ParseMultipartForm(10 << 20)
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("Error parsing form")
        return
    }
  1. Get the file name from the form

  2. Get the file from the form

    // get name of file
    name := r.Form.Get("name")
    if name == "" {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("specify a file name please")
        return
    }

    // get file
    f, handler, err := r.FormFile("file")
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("something went wrong")
        return
    }
    defer f.Close()
  1. Get the file extension

  2. Create a filepath

  3. Copy the file to the server

// get file extension
    fileExtension := strings.ToLower(filepath.Ext(handler.Filename))

    // create folders
    path := filepath.Join(".", "files")
    _ = os.MkdirAll(path, os.ModePerm)
    fullPath := path + "/" + name + fileExtension

    // open and copy files
    file, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE, os.ModePerm)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("something went wrong")
        return
    }
    defer file.Close()

    _, err = io.Copy(file, f)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("something went wrong")
        return
    }

The full function should look like this

func HandleUpload(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/x-www-form-urlencoded")

    // set file size to 10MB max
    err := r.ParseMultipartForm(10 << 20)
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("Error parsing form")
        return
    }

    // get name of file
    name := r.Form.Get("name")
    if name == "" {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("specify a file name please")
        return
    }

    // get file
    f, handler, err := r.FormFile("file")
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("something went wrong")
        return
    }
    defer f.Close()

    // get file extension
    fileExtension := strings.ToLower(filepath.Ext(handler.Filename))

    // create folders
    path := filepath.Join(".", "files")
    _ = os.MkdirAll(path, os.ModePerm)
    fullPath := path + "/" + name + fileExtension

    // open and copy files
    file, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE, os.ModePerm)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("something went wrong")
        return
    }
    defer file.Close()

    _, err = io.Copy(file, f)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("something went wrong")
        return
    }

    // send response
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode("File uploaded successfully")

}

A sample curl request would look like

curl --location --request POST 'localhost:8000/upload' \
--form 'name="testImg1"' \
--form 'file=@"/location.jpg"'

Handle Download

Downloading a file, you have to know the name of the file, the user can input this through diffrent ways but for this particular article you will get the file name from the URL.

  1. Once you have the file name, create a filepath

  2. Check if the file exists on the server

  3. Set the file extension and set the content-disposition to force a download.

  4. Use the http.ServeFile Method is used to serve the file to the end user.

Setting the content-disposition to attachment; filename="+filepath.Base(directory) means the response will be treated as a file for download and also suggesting a name for the downloaded file.

func HandleDownload(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    // get the file name to download from url
    name := r.URL.Query().Get("name")

    // join to get the full file path
    directory := filepath.Join("files", name)

    // open file (check if exists)
    ff, err := os.Open(directory)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        json.NewEncoder(w).Encode("Unable to open file ")
        return
    }

    // force a download with the content- disposition field
    w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(directory))

    // serve file out.
    http.ServeFile(w, r, directory)
}

A sample curl request to hit this end point would be

curl --location --request GET 'localhost:8000/download?name=test.jpg' \
--header 'Content-Type: application/json'
 '

Summary

At the end of this article you should be able to:

  1. Start up a server in Golang

  2. Upload a file to a server

  3. Download a file from a server

That concludes it! Thank you for taking the time to read this, and I hope you found it enjoyable. If you have any questions or suggestions, please feel free to leave them in the comment section below or reach out to me.

Wishing you a pleasant day! ๐Ÿ˜Š

Resources

Let's Connect

ย