Golang USES http Client to download files

  • 2020-07-21 08:30:38
  • OfStack

When I used beego's http library, I sometimes needed to download files. beego is possible, but it has a few problems: it doesn't support callbacks and doesn't show download speeds, which is intolerable in everyday development.

The implementation of beego mainly USES the io.copy function, so I took a deep look at the implementation principle, found it is quite simple, so I implemented a simple downloader according to the principle of ES9en.copy


// Define the file to download 
var durl = "https://dl.google.com/go/go1.10.3.darwin-amd64.pkg";
// parsing url
uri, err := url.ParseRequestURI(durl)
if err != nil {
 panic(" Site error ")
}

For a normal flow, check for errors in Url


filename := path.Base(uri.Path)
log.Println("[*] Filename " + filename)

Use path. Base to retrieve the file name of url. Here is a place where bug may appear: if the url is 302, you cannot retrieve the file name after the jump.

Get the file name step can send http request when client.CheckRedirect definition 1 function to fetch the file name.


client := http.DefaultClient;
client.Timeout = time.Second * 60 // Set the timeout 
resp, err := client.Get(durl)

One httpClient is created, and the Timeout client sets the timeout for reading data.

Here I prefer to use Do method to pass 1 Reqeust in the past, because some url need to verify the http header, you ask me why not use 1 word lazy.


raw := resp.Body
defer raw.Close()
reader := bufio.NewReaderSize(raw, 1024*32);

To be honest, it's not clear that bufio can actually speed up htpp's reading speed. No comparison has been made with bufio. But it was added for psychological reassurance


file, err := os.Create(filename)
if err != nil {
 panic(err)
}
writer := bufio.NewWriter(file)

It is also not clear how much speed bufio can add to file writes


buff := make([]byte, 32*1024)
written := 0
go func() {
 for {
  nr, er := reader.Read(buff)
  if nr > 0 {
   nw, ew := writer.Write(buff[0:nr])
   if nw > 0 {
    written += nw
   }
   if ew != nil {
    err = ew
    break
   }
   if nr != nw {
    err = io.ErrShortWrite
    break
   }
  }
  if er != nil {
   if er != io.EOF {
    err = er
   }
   break
  }
 }
 if err != nil {
  panic(err)
 }
}()

This source code is I directly copy the ES51en. copyBuffer function just made a few simple changes to understand the general meaning of the line


// Time interval between 
spaceTime := time.Second * 1
// The timer 
ticker := time.NewTicker(spaceTime)
// Last read data size 
lastWtn := 0
stop := false

for {
 select {
 case <-ticker.C:
  // The file size of this read - The size of the last read = speed 
  speed := written - lastWtn
  log.Printf("[*] Speed %s / %s \n", bytesToSize(speed), spaceTime.String())
  if written-lastWtn == 0 {
   ticker.Stop()
   stop = true
   break
  }
  lastWtn = written
 }
 if stop {
  break
 }
}

The code blocks the program, and the timer calculates the speed at intervals. The place where bug might appear here is if it jumps out of the loop at intervals when no data is read. Now this time is very sleepy, write blog is already sleepy to die when you have time to solve this bug


func bytesToSize(length int) string {
 var k = 1024 // or 1024
 var sizes = []string{"Bytes", "KB", "MB", "GB", "TB"}
 if length == 0 {
  return "0 Bytes"
 }
 i := math.Floor(math.Log(float64(length)) / math.Log(float64(k)))
 r := float64(length) / math.Pow(float64(k), i)
 return strconv.FormatFloat(r, 'f', 3, 64) + " " + sizes[int(i)]
}

This function is a conversion from my personal php project


2018/08/17 00:24:50 [*] Filename go1.10.3.darwin-amd64.pkg
2018/08/17 00:24:51 [*] Speed 9.000 MB / 1s 
2018/08/17 00:24:52 [*] Speed 11.125 MB / 1s 
2018/08/17 00:24:53 [*] Speed 11.125 MB / 1s 
2018/08/17 00:24:54 [*] Speed 10.562 MB / 1s 
2018/08/17 00:24:55 [*] Speed 11.187 MB / 1s 
2018/08/17 00:24:56 [*] Speed 11.109 MB / 1s 
2018/08/17 00:24:57 [*] Speed 11.109 MB / 1s 
2018/08/17 00:24:58 [*] Speed 11.141 MB / 1s 
2018/08/17 00:24:59 [*] Speed 11.172 MB / 1s 
2018/08/17 00:25:00 [*] Speed 11.141 MB / 1s 
2018/08/17 00:25:01 [*] Speed 8.453 MB / 1s 
2018/08/17 00:25:02 [*] Speed 6.385 MB / 1s 
2018/08/17 00:25:03 [*] Speed 0 Bytes / 1s 

This is the final result of the run, and Then I put all the source code in the bottom and go to sleep.


package main

import (
 "net/http"
 "log"
 "time"
 "net/url"
 "path"
 "os"
 "io"
 "bufio"
 "math"
 "strconv"
)

var durl = "https://dl.google.com/go/go1.10.3.darwin-amd64.pkg";

func main() {
 uri, err := url.ParseRequestURI(durl)
 if err != nil {
  panic(" Site error ")
 }

 filename := path.Base(uri.Path)
 log.Println("[*] Filename " + filename)

 client := http.DefaultClient;
 client.Timeout = time.Second * 60 // Set the timeout 
 resp, err := client.Get(durl)
 if err != nil {
  panic(err)
 }
 if resp.ContentLength <= 0 {
  log.Println("[*] Destination server does not support breakpoint download.")
 }
 raw := resp.Body
 defer raw.Close()
 reader := bufio.NewReaderSize(raw, 1024*32);


 file, err := os.Create(filename)
 if err != nil {
  panic(err)
 }
 writer := bufio.NewWriter(file)

 buff := make([]byte, 32*1024)
 written := 0
 go func() {
  for {
   nr, er := reader.Read(buff)
   if nr > 0 {
    nw, ew := writer.Write(buff[0:nr])
    if nw > 0 {
     written += nw
    }
    if ew != nil {
     err = ew
     break
    }
    if nr != nw {
     err = io.ErrShortWrite
     break
    }
   }
   if er != nil {
    if er != io.EOF {
     err = er
    }
    break
   }
  }
  if err != nil {
   panic(err)
  }
 }()

 spaceTime := time.Second * 1
 ticker := time.NewTicker(spaceTime)
 lastWtn := 0
 stop := false

 for {
  select {
  case <-ticker.C:
   speed := written - lastWtn
   log.Printf("[*] Speed %s / %s \n", bytesToSize(speed), spaceTime.String())
   if written-lastWtn == 0 {
    ticker.Stop()
    stop = true
    break
   }
   lastWtn = written
  }
  if stop {
   break
  }
 }
}

func bytesToSize(length int) string {
 var k = 1024 // or 1024
 var sizes = []string{"Bytes", "KB", "MB", "GB", "TB"}
 if length == 0 {
  return "0 Bytes"
 }
 i := math.Floor(math.Log(float64(length)) / math.Log(float64(k)))
 r := float64(length) / math.Pow(float64(k), i)
 return strconv.FormatFloat(r, 'f', 3, 64) + " " + sizes[int(i)]
}

Related articles: