Rewrite as library

This commit is contained in:
Trevor Slocum 2019-05-09 02:41:50 -07:00
parent 845655a1ac
commit 1e1f83ea18
12 changed files with 542 additions and 411 deletions

View file

@ -18,10 +18,10 @@ tasks:
go mod download
- test: |
cd $PROJECT_DIR/$PROJECT_NAME
go test
go test -v -cover ./...
- build: |
cd $PROJECT_DIR/$PROJECT_NAME
cd $PROJECT_DIR/$PROJECT_NAME/cmd/$PROJECT_NAME
go build
- run: |
cd $PROJECT_DIR/$PROJECT_NAME
cd $PROJECT_DIR/$PROJECT_NAME/cmd/$PROJECT_NAME
./$PROJECT_NAME --help

View file

@ -18,10 +18,10 @@ tasks:
go mod download
- test: |
cd $PROJECT_DIR/$PROJECT_NAME
go test
go test -v -cover ./...
- build: |
cd $PROJECT_DIR/$PROJECT_NAME
cd $PROJECT_DIR/$PROJECT_NAME/cmd/$PROJECT_NAME
go build
- run: |
cd $PROJECT_DIR/$PROJECT_NAME
cd $PROJECT_DIR/$PROJECT_NAME/cmd/$PROJECT_NAME
./$PROJECT_NAME --help

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
.idea/
dist/
*.sh
gophast
cmd/gophast/gophast
vendor/

101
cmd/gophast/main.go Normal file
View file

@ -0,0 +1,101 @@
package main
import (
"errors"
"log"
"os"
"sort"
"git.sr.ht/~tslocum/gophast/pkg/gophast"
"git.sr.ht/~tslocum/gophast/pkg/progress"
isatty "github.com/mattn/go-isatty"
"github.com/urfave/cli"
)
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
app := cli.NewApp()
app.HideVersion = true
app.Usage = "Download accelerator"
cli.AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.HelpName}} [OPTIONS] URL
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}
`
app.Flags = []cli.Flag{
cli.StringFlag{Name: "dir, d", Usage: "directory to save in (default: current directory)", EnvVar: "GOPHAST_DIR"},
cli.StringFlag{Name: "out, o", Usage: "file name to save as (default: from URL)", EnvVar: "GOPHAST_OUT"},
cli.Int64Flag{Name: "min-split-size, k", Usage: "minimum byte range size", EnvVar: "GOPHAST_SPLIT_SIZE", Value: gophast.DefaultMinSplitSize},
cli.Int64Flag{Name: "split-servers, s", Usage: "split download using multiple URLs", EnvVar: "GOPHAST_SPLIT_SERVERS", Value: 1},
cli.Int64Flag{Name: "split-conns, x", Usage: "split download using multiple connections", EnvVar: "GOPHAST_SPLIT_CONNECTIONS", Value: gophast.DefaultMaxConnections},
cli.Int64Flag{Name: "downloads, j", Usage: "number of files to download concurrently", EnvVar: "GOPHAST_DOWNLOADS", Value: 5},
cli.BoolFlag{Name: "no-progress, p", Usage: "disable progress bars", EnvVar: "GOPHAST_NO_PROGRESS"},
cli.BoolFlag{Name: "no-resume, r", Usage: "discard existing data before downloading", EnvVar: "GOPHAST_NO_RESUME"},
cli.BoolFlag{Name: "quiet, q", Usage: "print error messages only", EnvVar: "GOPHAST_QUIET"},
cli.BoolFlag{Name: "verbose, v", Usage: "print verbose messages (disables progress bars)", EnvVar: "GOPHAST_VERBOSE"},
}
sort.Sort(cli.FlagsByName(app.Flags))
app.Action = GoPhast
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
func GoPhast(c *cli.Context) error {
if !c.Bool("no-progress") && (isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())) {
gophast.C.ProgressLevel = progress.Dynamic
}
gophast.C.MaxLogLevel = gophast.LogStandard
if c.Bool("quiet") && c.Bool("verbose") {
panic("--quiet and --verbose options are mutually exclusive")
} else if c.Bool("quiet") {
gophast.C.MaxLogLevel = gophast.LogNone
} else if c.Bool("verbose") {
gophast.C.MaxLogLevel = gophast.LogVerbose
gophast.C.ProgressLevel = progress.Static
}
gophast.C.MinSplitSize = c.Int64("min-split-size")
gophast.C.MaxConnections = c.Int64("split-conns")
downloadDir := c.String("dir")
if downloadDir != "" {
gophast.C.DownloadDir = downloadDir
}
gophast.C.DownloadName = c.String("out")
gophast.C.NoResume = c.Bool("no-resume")
var postData []byte
if gophast.C.MinSplitSize < 0 {
return errors.New("invalid minimum byte range request size (--min-split-size)")
}
downloadURL := c.Args().First()
if downloadURL == "" {
return c.App.Command("help").Run(c)
}
gophast.LogMessage(gophast.LogStandard, "Fetching metadata...")
downloadName, downloadSize := gophast.Metadata(downloadURL, postData)
if !gophast.Download(downloadURL, postData, gophast.DownloadPath(downloadName), downloadSize) {
log.Fatal("failed to download file")
}
return nil
}

3
go.mod
View file

@ -6,7 +6,4 @@ require (
github.com/mattn/go-isatty v0.0.7
github.com/urfave/cli v1.20.0
github.com/vbauerster/mpb/v4 v4.7.0
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 // indirect
golang.org/x/text v0.3.2 // indirect
golang.org/x/tools v0.0.0-20190501045030-23463209683d // indirect
)

6
go.sum
View file

@ -9,16 +9,10 @@ github.com/vbauerster/mpb/v4 v4.7.0/go.mod h1:ugxYn2kSUrY10WK5CWDUZvQxjdwKFN9K3J
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 h1:cGjJzUd8RgBw428LXP65YXni0aiGNA4Bl+ls8SmLOm8=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190501045030-23463209683d/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=

395
main.go
View file

@ -1,395 +0,0 @@
package main
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
isatty "github.com/mattn/go-isatty"
"github.com/urfave/cli"
mpb "github.com/vbauerster/mpb/v4"
"github.com/vbauerster/mpb/v4/decor"
)
const DefaultMinimumSplitSize = 10 * 1024 * 1024 // 10 megabytes
type LogLevel int
const (
LogNone = LogLevel(0)
LogStandard = LogLevel(1)
LogVerbose = LogLevel(2)
)
type ProgressLevel int
const (
ProgressStatic = ProgressLevel(0)
ProgressDynamic = ProgressLevel(1)
)
type Config struct {
MaxLogLevel LogLevel
ProgressLevel ProgressLevel
}
type ProgressReport struct {
wrote int
duration time.Duration
}
var (
config = new(Config)
wg = new(sync.WaitGroup)
progress *mpb.Progress
progressBars []*mpb.Bar
progressTaskWidth int
progressTaskFormat string
progressWrote = make(chan *ProgressReport, 10)
)
type OffsetWriter struct {
io.WriterAt
offset int64
bar *mpb.Bar
lastUpdate time.Time
}
func (dst *OffsetWriter) Write(b []byte) (n int, err error) {
n, err = dst.WriteAt(b, dst.offset)
dst.offset += int64(n)
if dst.bar != nil {
dst.bar.IncrBy(n, time.Since(dst.lastUpdate))
progressWrote <- &ProgressReport{n, time.Since(dst.lastUpdate)}
dst.lastUpdate = time.Now()
}
return
}
func CheckError(err error) {
if err != nil {
panic(err)
}
}
func formatBytes(bytes int64) string {
if bytes == 0 {
return "0 B"
}
k := float64(1024)
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
i := math.Floor(math.Log(float64(bytes)) / math.Log(k))
format := "%.1f"
if i == 0 {
format = "%f"
}
return fmt.Sprintf(format+" %s", float64(bytes)/math.Pow(k, i), sizes[int(i)])
}
func request(method string, url string, postData []byte, rangeStart int64, rangeEnd int64) (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewReader(postData))
CheckError(err)
req.Header.Add("User-Agent", "gophast https://git.sr.ht/~tslocum/gophast")
if rangeEnd > 0 {
req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd))
}
return client.Do(req)
}
func metadata(url string, postData []byte) (string, int64) {
name := path.Base(url)
resp, err := request("HEAD", url, postData, 0, 0)
CheckError(err)
defer resp.Body.Close()
if resp.StatusCode == 404 {
log.Fatal("404: not found")
} else if resp.StatusCode != 200 {
log.Fatalf("server responded with status %d (expected 200 OK)", resp.StatusCode)
}
if resp.ContentLength < 0 {
panic("server did not send Content-Length")
}
if !strings.Contains(resp.Header.Get("Accept-Ranges"), "bytes") {
panic("server does not support Range")
}
// TODO: Handle redirect, update name
return name, resp.ContentLength
}
func download(url string, postData []byte, rangeStart int64, rangeEnd int64, file *os.File, id int64, maxid int64, bar *mpb.Bar) {
var (
wrote int64
)
defer wg.Done()
start := time.Now()
rangeSize := rangeEnd - rangeStart + 1
resp, err := request("GET", url, postData, rangeStart, rangeEnd)
CheckError(err)
if resp.StatusCode != http.StatusPartialContent {
panic(fmt.Sprintf("invalid response code: %d (expected: 206 Partial Content)", resp.StatusCode))
}
logMessagef(LogVerbose, "Starting range %d/%d (%d-%d)", id, maxid, rangeStart, rangeEnd)
ow := &OffsetWriter{WriterAt: file, offset: rangeStart, bar: bar, lastUpdate: time.Now()}
wrote, err = io.Copy(ow, resp.Body)
CheckError(err)
if wrote != rangeSize {
_ = resp.Body.Close()
panic(fmt.Sprintf("received %d bytes of expected %d", wrote, rangeSize))
}
err = resp.Body.Close()
CheckError(err)
logMessagef(LogVerbose, "Finished range %d/%d in %s (%s @ %s/s)", id, maxid, time.Since(start).Truncate(time.Second), formatBytes(rangeSize), formatBytes(int64(float64(float64(rangeSize))/time.Since(start).Seconds())))
}
func logMessage(level LogLevel, message string) {
if level > config.MaxLogLevel {
return
}
log.Println(message)
}
func logMessagef(level LogLevel, format string, a ...interface{}) {
logMessage(level, fmt.Sprintf(format, a...))
}
func handleProgress(bar *mpb.Bar) {
var report *ProgressReport
for {
report = <-progressWrote
bar.IncrBy(report.wrote, report.duration)
}
}
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
app := cli.NewApp()
app.HideVersion = true
app.Usage = "Download accelerator"
cli.AppHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.HelpName}} [OPTIONS] URL
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}
`
app.Flags = []cli.Flag{
cli.StringFlag{Name: "dir, d", Usage: "directory to save in (default: current directory)", EnvVar: "GOPHAST_DIR"},
cli.StringFlag{Name: "out, o", Usage: "file name to save as (default: from URL)", EnvVar: "GOPHAST_OUT"},
cli.Int64Flag{Name: "split-size, k", Usage: "minimum byte range size", EnvVar: "GOPHAST_SPLIT_SIZE", Value: DefaultMinimumSplitSize},
cli.Int64Flag{Name: "split-servers, s", Usage: "split download using multiple URLs", EnvVar: "GOPHAST_SPLIT_SERVERS", Value: 1},
cli.Int64Flag{Name: "split-conns, x", Usage: "split download using multiple connections", EnvVar: "GOPHAST_SPLIT_CONNECTIONS", Value: 3},
cli.Int64Flag{Name: "downloads, j", Usage: "number of files to download concurrently", EnvVar: "GOPHAST_DOWNLOADS", Value: 5},
cli.BoolFlag{Name: "no-progress, n", Usage: "disable progress bars", EnvVar: "GOPHAST_VERBOSE"},
cli.BoolFlag{Name: "quiet, q", Usage: "print error messages only", EnvVar: "GOPHAST_QUIET"},
cli.BoolFlag{Name: "verbose, v", Usage: "print verbose messages (disables progress bars)", EnvVar: "GOPHAST_VERBOSE"},
}
sort.Sort(cli.FlagsByName(app.Flags))
app.Action = GoPhast
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
func GoPhast(c *cli.Context) error {
if !c.Bool("no-progress") && (isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())) {
config.ProgressLevel = ProgressDynamic
}
config.MaxLogLevel = LogStandard
if c.Bool("quiet") && c.Bool("verbose") {
panic("--quiet and --verbose options are mutually exclusive")
} else if c.Bool("quiet") {
config.MaxLogLevel = LogNone
} else if c.Bool("verbose") {
config.MaxLogLevel = LogVerbose
config.ProgressLevel = ProgressStatic
}
if config.ProgressLevel == ProgressDynamic {
progress = mpb.New(mpb.WithWidth(40), mpb.WithRefreshRate(120*time.Millisecond), mpb.WithWaitGroup(wg))
}
var (
postData []byte
downloadName string
downloadSize int64 = -1
downloadDir = c.String("dir")
downloadFileName = c.String("out")
downloadFilePath string
downloadFile *os.File
downloadSplitSize = c.Int64("split-size")
downloadURL = c.Args().First()
err error
)
if downloadSplitSize < 0 {
return errors.New("invalid minimum byte range request size (--min-split-size)")
} else if downloadSplitSize == 0 {
downloadSplitSize = DefaultMinimumSplitSize
}
if downloadURL == "" {
return c.App.Command("help").Run(c)
}
if downloadDir == "" {
downloadDir = "./"
}
downloadDir, err = filepath.Abs(downloadDir)
CheckError(err)
logMessage(LogStandard, "Fetching metadata...")
downloadName, downloadSize = metadata(downloadURL, postData)
if downloadFileName == "" {
downloadFileName = downloadName
}
if filepath.IsAbs(downloadFileName) {
downloadFilePath = downloadFileName
} else {
downloadFilePath = filepath.Join(downloadDir, downloadFileName)
}
downloadFileName = filepath.Base(downloadFileName)
downloadFile, err = os.OpenFile(downloadFilePath, os.O_CREATE|os.O_WRONLY, 0644)
CheckError(err)
defer downloadFile.Close()
if downloadSize == 0 {
return nil
}
logMessage(LogStandard, "Allocating disk space...")
err = downloadFile.Truncate(downloadSize)
CheckError(err)
splitCount := int64(1)
if downloadSize >= (downloadSplitSize * 2) {
splitCount = int64(math.Floor(float64(downloadSize) / float64(downloadSplitSize)))
}
maxConnections := c.Int64("split-conns")
if splitCount > maxConnections {
splitCount = maxConnections
}
if config.ProgressLevel == ProgressStatic {
logMessage(LogStandard, "Downloading...")
} else {
log.Println() // Divider
}
progressTaskFormat = "Range %" + strconv.Itoa(len(fmt.Sprintf("%d", splitCount))) + "d"
progressTaskWidth = int(math.Max(math.Max(float64(len(downloadFileName)), float64(len(fmt.Sprintf(progressTaskFormat, 1)))), 11)) + 1
downloadSplitSize = downloadSize / splitCount
downloadStart := time.Now()
var bar *mpb.Bar
rangeStart := int64(0)
rangeEnd := int64(0)
for i := int64(1); i <= splitCount; i++ {
if i < splitCount {
rangeEnd += downloadSplitSize
} else {
rangeEnd = downloadSize - 1
}
wg.Add(1)
if config.ProgressLevel == ProgressDynamic {
task := fmt.Sprintf(progressTaskFormat, i)
bar = progress.AddBar(rangeEnd-rangeStart+1,
mpb.BarClearOnComplete(),
mpb.PrependDecorators(
decor.Name(task, decor.WC{W: progressTaskWidth, C: decor.DidentRight}),
decor.OnComplete(decor.Name("downloading", decor.WCSyncSpaceR), "downloaded"),
decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_HHMMSS, 60, decor.WCSyncWidth), "")),
mpb.AppendDecorators(
decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
),
)
}
go download(downloadURL, postData, rangeStart, rangeEnd, downloadFile, i, splitCount, bar)
rangeStart = rangeEnd + 1
}
if config.ProgressLevel == ProgressDynamic {
progress.AddBar(0, mpb.BarStyle(" "), mpb.BarNoBrackets()).SetTotal(0, true) // Divider
b := progress.AddBar(downloadSize,
mpb.BarRemoveOnComplete(),
mpb.PrependDecorators(
decor.Name(downloadFileName, decor.WC{W: progressTaskWidth, C: decor.DidentRight}),
decor.OnComplete(decor.Name("downloading", decor.WCSyncSpaceR), "downloaded"),
decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_HHMMSS, 60, decor.WCSyncWidth), "")),
mpb.AppendDecorators(
decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
),
)
go handleProgress(b)
progress.Wait()
} else {
wg.Wait()
}
logMessagef(LogStandard, "%s downloaded in %s (%s @ %s/s)", downloadFileName, time.Since(downloadStart).Truncate(time.Second), formatBytes(downloadSize), formatBytes(int64(float64(float64(downloadSize))/time.Since(downloadStart).Seconds())))
return nil
}

26
pkg/gophast/config.go Normal file
View file

@ -0,0 +1,26 @@
package gophast
import "git.sr.ht/~tslocum/gophast/pkg/progress"
type Config struct {
MaxLogLevel LogLevel
ProgressLevel progress.DisplayLevel
NoResume bool
DownloadDir string
DownloadName string
MinSplitSize int64
MaxConnections int64
}
var C = &Config{
MaxLogLevel: LogNone,
ProgressLevel: progress.None,
NoResume: false,
DownloadDir: "./",
DownloadName: "",
MinSplitSize: DefaultMinSplitSize,
MaxConnections: DefaultMaxConnections}

235
pkg/gophast/gophast.go Normal file
View file

@ -0,0 +1,235 @@
package gophast
import (
"bytes"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"git.sr.ht/~tslocum/gophast/pkg/progress"
)
const (
DefaultMinSplitSize = 10 * 1024 * 1024 // 10 megabytes
DefaultMaxConnections = 3
)
var (
wg = new(sync.WaitGroup)
)
func request(method string, url string, postData []byte, rangeStart int64, rangeEnd int64) (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewReader(postData))
CheckError(err)
req.Header.Add("User-Agent", "gophast https://git.sr.ht/~tslocum/gophast")
if rangeEnd > 0 {
req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd))
}
return client.Do(req)
}
func DownloadRange(url string, postData []byte, rangeStart int64, rangeEnd int64, file *os.File, id int64, maxid int64, localProgress chan<- *progress.BytesWrote) {
var (
wrote int64
)
defer wg.Done()
start := time.Now()
rangeSize := rangeEnd - rangeStart + 1
resp, err := request("GET", url, postData, rangeStart, rangeEnd)
CheckError(err)
if resp.StatusCode != http.StatusPartialContent {
panic(fmt.Sprintf("invalid response code: %d (expected: 206 Partial Content)", resp.StatusCode))
}
ow := NewOffsetWriter(file, rangeStart, localProgress)
wrote, err = io.Copy(ow, resp.Body)
CheckError(err)
if wrote != rangeSize {
_ = resp.Body.Close()
panic(fmt.Sprintf("received %d bytes of expected %d", wrote, rangeSize))
}
err = resp.Body.Close()
CheckError(err)
LogMessagef(LogVerbose, "Finished range %d/%d (%s in %s at %s/s)", id, maxid, formatBytes(rangeSize), formatDuration(time.Since(start)), formatBytes(int64(float64(float64(rangeSize))/time.Since(start).Seconds())))
}
func Metadata(url string, postData []byte) (string, int64) {
name := path.Base(url)
resp, err := request("HEAD", url, postData, 0, 0)
CheckError(err)
defer resp.Body.Close()
if resp.StatusCode == 404 {
log.Fatal("404: not found")
} else if resp.StatusCode != 200 {
log.Fatalf("server responded with status %d (expected 200 OK)", resp.StatusCode)
}
if resp.ContentLength < 0 {
panic("server did not send Content-Length")
}
if !strings.Contains(resp.Header.Get("Accept-Ranges"), "bytes") {
panic("server does not support Range")
}
// TODO: Handle redirect, update name
return name, resp.ContentLength
}
func Download(url string, postData []byte, filePath string, size int64) bool {
var localProgress chan<- *progress.BytesWrote
if C.ProgressLevel == progress.Dynamic {
progress.Initialize(40, wg)
}
fileName := filepath.Base(filePath)
if !C.NoResume {
if ex, err := os.Stat(filePath); err == nil {
if ex.Size() == size {
LogMessagef(LogStandard, "%s already downloaded", fileName)
return true
}
} else if !os.IsNotExist(err) {
return false
}
}
downloadFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0644)
CheckError(err)
defer downloadFile.Close()
if size == 0 {
return true
}
LogMessage(LogStandard, "Allocating disk space...")
err = downloadFile.Truncate(size)
CheckError(err)
splitCount := int64(1)
if C.MinSplitSize == 0 {
if C.MaxConnections > 0 {
splitCount = C.MaxConnections
}
} else if size >= (C.MinSplitSize * 2) {
splitCount = int64(math.Floor(float64(size) / float64(C.MinSplitSize)))
if splitCount > C.MaxConnections {
splitCount = C.MaxConnections
}
}
if splitCount > size {
splitCount = size
}
if C.ProgressLevel == progress.Static {
LogMessage(LogStandard, "Starting download...")
} else {
log.Println() // Divider
}
progress.TaskFormat = "Range %" + strconv.Itoa(len(fmt.Sprintf("%d", splitCount))) + "d"
progress.TaskWidth = int(math.Max(math.Max(float64(len(fileName)), float64(len(fmt.Sprintf(progress.TaskFormat, 1)))), 11)) + 1
downloadSplitSize := size / splitCount
downloadStart := time.Now()
rangeStart := int64(0)
rangeEnd := int64(0)
for i := int64(1); i <= splitCount; i++ {
if i < splitCount {
rangeEnd += downloadSplitSize
} else {
rangeEnd = size - 1
}
LogMessagef(LogVerbose, "Starting range %d/%d (%d-%d)", i, splitCount, rangeStart, rangeEnd)
wg.Add(1)
if C.ProgressLevel == progress.Dynamic {
localProgress = progress.AddLocalBar(i, rangeEnd-rangeStart+1)
}
go DownloadRange(url, postData, rangeStart, rangeEnd, downloadFile, i, splitCount, localProgress)
rangeStart = rangeEnd + 1
}
if C.ProgressLevel == progress.Dynamic {
progress.AddGlobalBar(fileName, size)
progress.Wait()
} else {
wg.Wait()
}
LogMessagef(LogStandard, "%s downloaded (%s in %s at %s/s)", fileName, formatBytes(size), formatDuration(time.Since(downloadStart)), formatBytes(int64(float64(float64(size))/time.Since(downloadStart).Seconds())))
return true
}
func DownloadPath(defaultFileName string) string {
if C.DownloadName == "" {
C.DownloadName = defaultFileName
}
if filepath.IsAbs(C.DownloadName) {
return C.DownloadName
}
downloadDir, err := filepath.Abs(C.DownloadDir)
if err != nil {
return ""
}
return filepath.Join(downloadDir, C.DownloadName)
}
func formatDuration(duration time.Duration) string {
return fmt.Sprintf("%02d:%02d:%02d", int64((duration/time.Hour)%60), int64((duration/time.Minute)%60), int64((duration/time.Second)%60))
}
func formatBytes(bytes int64) string {
if bytes == 0 {
return "0 B"
}
k := float64(1024)
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
i := math.Floor(math.Log(float64(bytes)) / math.Log(k))
format := "%.1f"
if i == 0 {
format = "%f"
}
return fmt.Sprintf(format+" %s", float64(bytes)/math.Pow(k, i), sizes[int(i)])
}
func CheckError(err error) {
if err != nil {
panic(err)
}
}

26
pkg/gophast/log.go Normal file
View file

@ -0,0 +1,26 @@
package gophast
import (
"fmt"
"log"
)
type LogLevel int
const (
LogNone LogLevel = iota
LogStandard
LogVerbose
)
func LogMessage(level LogLevel, message string) {
if level > C.MaxLogLevel {
return
}
log.Println(message)
}
func LogMessagef(level LogLevel, format string, a ...interface{}) {
LogMessage(level, fmt.Sprintf(format, a...))
}

View file

@ -0,0 +1,39 @@
package gophast
import (
"io"
"log"
"time"
"git.sr.ht/~tslocum/gophast/pkg/progress"
)
type OffsetWriter struct {
io.WriterAt
offset int64
updateProgress chan<- *progress.BytesWrote
lastUpdate time.Time
}
func NewOffsetWriter(w io.WriterAt, offset int64, updateProgress chan<- *progress.BytesWrote) *OffsetWriter {
return &OffsetWriter{WriterAt: w, offset: offset, updateProgress: updateProgress, lastUpdate: time.Now()}
}
func (dst *OffsetWriter) Write(b []byte) (n int, err error) {
n, err = dst.WriteAt(b, dst.offset)
dst.offset += int64(n)
if dst.updateProgress == nil {
return
}
select {
case dst.updateProgress <- &progress.BytesWrote{Wrote: n, Duration: time.Since(dst.lastUpdate)}:
default:
log.Fatal("starved writer")
}
dst.lastUpdate = time.Now()
return
}

108
pkg/progress/progress.go Normal file
View file

@ -0,0 +1,108 @@
package progress
import (
"fmt"
"sync"
"time"
"github.com/vbauerster/mpb/v4/decor"
"github.com/vbauerster/mpb/v4"
)
const BufferSize = 10
type DisplayLevel int
const (
None DisplayLevel = iota
Static
Dynamic
)
var (
p *mpb.Progress
globalProgress chan *BytesWrote
TaskWidth int
TaskFormat string
)
type BytesWrote struct {
Wrote int
Duration time.Duration
}
func Initialize(width int, wg *sync.WaitGroup) <-chan *BytesWrote {
if p != nil {
return globalProgress
}
p = mpb.New(mpb.WithWidth(width), mpb.WithRefreshRate(120*time.Millisecond), mpb.WithWaitGroup(wg))
globalProgress = make(chan *BytesWrote, BufferSize)
return globalProgress
}
func AddLocalBar(index int64, total int64) chan<- *BytesWrote {
task := fmt.Sprintf(TaskFormat, index)
bar := p.AddBar(total,
mpb.BarClearOnComplete(),
mpb.PrependDecorators(
decor.Name(task, decor.WC{W: TaskWidth, C: decor.DidentRight}),
decor.OnComplete(decor.Name("downloading", decor.WCSyncSpaceR), "downloaded"),
decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_HHMMSS, 60, decor.WCSyncWidth), "")),
mpb.AppendDecorators(
decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
),
)
localProgress := make(chan *BytesWrote, BufferSize)
go HandleLocalBar(bar, localProgress)
return localProgress
}
func AddGlobalBar(fileName string, total int64) {
// Divider
p.AddBar(0, mpb.BarStyle(" "), mpb.BarNoBrackets()).SetTotal(0, true)
globalBar := p.AddBar(total,
mpb.BarRemoveOnComplete(),
mpb.PrependDecorators(
decor.Name(fileName, decor.WC{W: TaskWidth, C: decor.DidentRight}),
decor.OnComplete(decor.Name("downloading", decor.WCSyncSpaceR), "downloaded"),
decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_HHMMSS, 60, decor.WCSyncWidth), "")),
mpb.AppendDecorators(
decor.OnComplete(decor.Percentage(decor.WC{W: 5}), ""),
),
)
go HandleGlobalBar(globalBar)
}
func HandleLocalBar(bar *mpb.Bar, localProgress <-chan *BytesWrote) {
var bw *BytesWrote
for {
bw = <-localProgress
bar.IncrBy(bw.Wrote, bw.Duration)
globalProgress <- bw
}
}
func HandleGlobalBar(bar *mpb.Bar) {
var bw *BytesWrote
for {
bw = <-globalProgress
bar.IncrBy(bw.Wrote, bw.Duration)
}
}
func Wait() {
if p == nil {
return
}
p.Wait()
}