Skip to content

Commit

Permalink
Merge #21
Browse files Browse the repository at this point in the history
v0.2.2
  • Loading branch information
melbahja authored Sep 6, 2020
2 parents 1e7fabc + 87a7baf commit cfc5989
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
on: [push, pull_request]
on: [push]
name: Test
jobs:
test:
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<a href="#license">License</a>
</p>

![Tests](https://github.com/melbahja/got/workflows/Test/badge.svg)

## Comparison

Comparison in cloud server:
Expand Down Expand Up @@ -61,7 +63,7 @@ go get github.com/melbahja/got/cmd/got
```

#### Or from the AUR
Install `got` for the latest release version or `got-git` for the latest development version.
Install [`got`](https://aur.archlinux.org/packages/got/) for the latest release version or `got-git` for the latest development version.

> **Note:** these packages are not maintained by melbahja
Expand Down Expand Up @@ -120,11 +122,11 @@ func main() {

```
For more see [GoDocs](https://pkg.go.dev/github.com/melbahja/got).
For more see [PkgDocs](https://pkg.go.dev/github.com/melbahja/got).
## How It Works?
Got takes advantage of the HTTP range requests support in servers [RFC 7233](https://tools.ietf.org/html/rfc7233), if the requested URL server supports partial content Got split the file into chunks, then starts downloading and merging them into the destinaton file concurrently.
Got takes advantage of the HTTP range requests support in servers [RFC 7233](https://tools.ietf.org/html/rfc7233), if the server supports partial content Got split the file into chunks, then starts downloading and merging the chunks into the destinaton file concurrently.
## License
Expand Down
40 changes: 26 additions & 14 deletions cmd/got/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/url"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"

Expand All @@ -17,6 +16,7 @@ import (
"github.com/urfave/cli/v2"
"gitlab.com/poldi1405/go-ansi"
"gitlab.com/poldi1405/go-indicators/progress"
"golang.org/x/crypto/ssh/terminal"
)

var version string
Expand Down Expand Up @@ -87,19 +87,27 @@ func main() {

func run(ctx context.Context, c *cli.Context) error {

// New *Got.
g := got.NewWithContext(ctx)
var p progress.Progress
p.SetStyle(simpleProgressStyle)
p.Width = 30
var (
g *got.Got = got.NewWithContext(ctx)
p *progress.Progress = new(progress.Progress)
)

// Progress.
// Set progress style.
p.SetStyle(progressStyle)

// Progress func.
g.ProgressFunc = func(d *got.Download) {

// 55 is just an estimation of the text showed with the progress.
// it's working fine with $COLUMNS >= 47
// TODO: hide progress bar on terminal size of $COLUMNS <= 46
p.Width = getWidth() - 55

perc, err := progress.GetPercentage(float64(d.Size()), float64(d.TotalSize()))
if err != nil {
perc = 100
}

fmt.Printf(
" %6.2f%% %s%s%s %s/%s @ %s/s%s\r",
perc,
Expand Down Expand Up @@ -163,6 +171,15 @@ func run(ctx context.Context, c *cli.Context) error {
return nil
}

func getWidth() int {

if width, _, err := terminal.GetSize(0); err == nil && width > 0 {
return width
}

return 80
}

func multiDownload(ctx context.Context, c *cli.Context, g *got.Got, scanner *bufio.Scanner) error {

for scanner.Scan() {
Expand Down Expand Up @@ -190,15 +207,10 @@ func download(ctx context.Context, c *cli.Context, g *got.Got, url string) (err
return err
}

fname := c.String("output")

if fname == "" {
fname = got.GetFilename(url)
}

return g.Do(&got.Download{
URL: url,
Dest: filepath.Join(c.String("dir"), fname),
Dir: c.String("dir"),
Name: c.String("output"),
Interval: 100,
ChunkSize: c.Uint64("size"),
Concurrency: c.Uint("concurrency"),
Expand Down
5 changes: 3 additions & 2 deletions cmd/got/variables_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
)

var (
simpleProgressStyle = "block"
r, l = "|", "|"
progressStyle = "block"
r, l = "|", "|"
)

func color(content ...interface{}) string {
return ansi.Blue(fmt.Sprint(content...))
}

5 changes: 3 additions & 2 deletions cmd/got/variables_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import "fmt"

// Windows doesn't handle the block-style very well
var (
simpleProgressStyle = "double-"
r, l = "[", "]"
progressStyle = "double-"
r, l = "[", "]"
)

func color(content ...interface{}) string {
return fmt.Sprint(content...)
}

69 changes: 47 additions & 22 deletions download.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import (
)

type (

// Info holds downloadable file info.
Info struct {
Size uint64
Name string
Rangeable bool
}

// ProgressFunc to show progress state, called by RunProgress based on interval.
ProgressFunc func(d *Download)

Expand All @@ -24,50 +32,54 @@ type (

Concurrency uint

URL, Dest string
URL, Dir, Name, Dest string

Interval, ChunkSize, MinChunkSize, MaxChunkSize uint64

StopProgress bool

ctx context.Context

size, totalSize, lastSize uint64
size, lastSize uint64

chunks []Chunk
info *Info

rangeable bool
chunks []Chunk

startedAt time.Time
}
)

// GetInfo returns URL file size and rangeable state, and error if any.
func (d Download) GetInfo() (size uint64, rangeable bool, err error) {
// GetInfo returns URL info, and error if any.
func (d Download) GetInfo() (*Info, error) {

req, err := NewRequest(d.ctx, "HEAD", d.URL)

if err != nil {
return 0, false, err
return nil, err
}

res, err := d.Client.Do(req)

if err != nil {
return 0, false, err
return nil, err
}

if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {

// On 4xx HEAD request (work around for #3).
if res.StatusCode != 404 && res.StatusCode >= 400 && res.StatusCode < 500 {
return 0, false, nil
return &Info{}, nil
}

return 0, false, fmt.Errorf("Response status code is not ok: %d", res.StatusCode)
return nil, fmt.Errorf("Response status code is not ok: %d", res.StatusCode)
}

return uint64(res.ContentLength), res.Header.Get("accept-ranges") == "bytes", nil
return &Info{
Size: uint64(res.ContentLength),
Name: getNameFromHeader(res.Header.Get("content-disposition")),
Rangeable: res.Header.Get("accept-ranges") == "bytes",
}, nil
}

// Init set defaults and split file into chunks and gets Info,
Expand All @@ -88,12 +100,25 @@ func (d *Download) Init() (err error) {
}

// Get and set URL size and partial content support state.
if d.totalSize, d.rangeable, err = d.GetInfo(); err != nil {
if d.info, err = d.GetInfo(); err != nil {
return err
}

// Set default dest path.
if d.Dest == "" {

fname := d.info.Name

// if info name invalid get name from url.
if fname == "" {
fname = GetFilename(d.URL)
}

d.Dest = filepath.Join(d.Dir, fname)
}

// Partial content not supported 😢!
if d.rangeable == false || d.totalSize == 0 {
if d.info.Rangeable == false || d.info.Size == 0 {
return nil
}

Expand All @@ -116,7 +141,7 @@ func (d *Download) Init() (err error) {
// Set default chunk size
if d.ChunkSize == 0 {

d.ChunkSize = d.totalSize / uint64(d.Concurrency)
d.ChunkSize = d.info.Size / uint64(d.Concurrency)

// if chunk size >= 102400000 bytes set default to (ChunkSize / 2)
if d.ChunkSize >= 102400000 {
Expand All @@ -128,8 +153,8 @@ func (d *Download) Init() (err error) {

d.MinChunkSize = 2000000

if d.MinChunkSize >= d.totalSize {
d.MinChunkSize = d.totalSize / 2
if d.MinChunkSize >= d.info.Size {
d.MinChunkSize = d.info.Size / 2
}
}

Expand All @@ -143,14 +168,14 @@ func (d *Download) Init() (err error) {
d.ChunkSize = d.MaxChunkSize
}

} else if d.ChunkSize >= d.totalSize {
} else if d.ChunkSize >= d.info.Size {

d.ChunkSize = d.totalSize / 2
d.ChunkSize = d.info.Size / 2
}

var i, startRange, endRange, chunksLen uint64

chunksLen = d.totalSize / d.ChunkSize
chunksLen = d.info.Size / d.ChunkSize

d.chunks = make([]Chunk, 0, chunksLen)

Expand All @@ -164,7 +189,7 @@ func (d *Download) Init() (err error) {
startRange = 0
}

if endRange > d.totalSize || i == (chunksLen-1) {
if endRange > d.info.Size || i == (chunksLen-1) {
endRange = 0
}

Expand Down Expand Up @@ -297,7 +322,7 @@ func (d Download) Context() context.Context {

// TotalSize returns file total size (0 if unknown).
func (d Download) TotalSize() uint64 {
return d.totalSize
return d.info.Size
}

// Size returns downloaded size.
Expand Down Expand Up @@ -334,7 +359,7 @@ func (d *Download) Write(b []byte) (int, error) {

// IsRangeable returns file server partial content support state.
func (d Download) IsRangeable() bool {
return d.rangeable
return d.info.Rangeable
}

// Download chunks
Expand Down
32 changes: 28 additions & 4 deletions download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestDownloading(t *testing.T) {
t.Run("downloadOkFileContentTest", downloadOkFileContentTest)
t.Run("downloadHeadNotSupported", downloadHeadNotSupported)
t.Run("downloadPartialContentNotSupportedTest", downloadPartialContentNotSupportedTest)
t.Run("getFilenameTest", getFilenameTest)
}

func getInfoTest(t *testing.T) {
Expand All @@ -51,22 +52,45 @@ func getInfoTest(t *testing.T) {

dl.Client = got.GetDefaultClient()

size, rangeable, err := dl.GetInfo()
info, err := dl.GetInfo()

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

if rangeable == false {
if info.Rangeable == false {
t.Error("rangeable should be true")
}

if size != uint64(okFileStat.Size()) {
t.Errorf("Invalid file size, wants %d but got %d", okFileStat.Size(), size)
if info.Size != uint64(okFileStat.Size()) {
t.Errorf("Invalid file size, wants %d but got %d", okFileStat.Size(), info.Size)
}
}

func getFilenameTest(t *testing.T) {

tmpFile := createTemp()
defer clean(tmpFile)

dl := got.NewDownload(context.Background(), httpt.URL+"/file_name", tmpFile)

dl.Client = got.GetDefaultClient()

info, err := dl.GetInfo()

if err != nil {

t.Errorf("Unexpected error: " + err.Error())
}

if info.Name != "go.mod" {

t.Errorf("Expecting file name to be: go.mod but got: "+ info.Name)
}

}

func okInitTest(t *testing.T) {

tmpFile := createTemp()
Expand Down
Loading

0 comments on commit cfc5989

Please sign in to comment.