<|start_filename|>run.bat<|end_filename|>
::===========================================================
:: Curl  : Simple http download and readline lib by Golang
:: HOST  : https://github.com/kenshin/curl
:: Author: Kenshin <<EMAIL>>
::===========================================================

@ECHO off

IF "%1" == "doc" GOTO doc
IF "%1" == "install" GOTO install
IF "%1" == "test" GOTO test

:doc
@ECHO godoc http://127.0.0.1:6060
godoc -http=:6060 -server=:6060
IF "%1" == "doc" GOTO exit

:install
@ECHO go install
go install
IF "%1" == "install" GOTO exit

:test
@ECHO go test
go test
GOTO exit

:exit
@ECHO complete.

<|start_filename|>curl_windows.go<|end_filename|>
package curl

import (
	"fmt"
	"syscall"
	"unsafe"
)

type (
	SHORT int16
	WORD  uint16

	SMALL_RECT struct {
		Left   SHORT
		Top    SHORT
		Right  SHORT
		Bottom SHORT
	}

	COORD struct {
		X SHORT
		Y SHORT
	}

	CONSOLE_SCREEN_BUFFER_INFO struct {
		Size              COORD
		CursorPosition    COORD
		Attributes        WORD
		Window            SMALL_RECT
		MaximumWindowSize COORD
	}
)

var (
	kernel32DLL                    = syscall.NewLazyDLL("kernel32.dll")
	getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
	setConsoleCursorPositionProc   = kernel32DLL.NewProc("SetConsoleCursorPosition")
	stdoutHandle                   = getStdHandle(syscall.STD_OUTPUT_HANDLE)
	position                       = COORD{1, -1}
)

func getCursorPosition() {
	if position.Y == -1 {
		var info, err = getConsoleScreenBufferInfo(stdoutHandle)
		if err != nil {
			panic("could not get console screen buffer info")
		}
		position = info.CursorPosition
	}
}

func getError(r1, r2 uintptr, lastErr error) error {
	if r1 == 0 {
		if lastErr != nil {
			return lastErr
		}
		return syscall.EINVAL
	}
	return nil
}

func getStdHandle(stdhandle int) uintptr {
	handle, err := syscall.GetStdHandle(stdhandle)
	if err != nil {
		panic(fmt.Errorf("could not get standard io handle %d", stdhandle))
	}
	return uintptr(handle)
}

func getConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
	var info CONSOLE_SCREEN_BUFFER_INFO
	if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil {
		return nil, err
	}
	return &info, nil
}

func setConsoleCursorPosition(handle uintptr, position COORD) {
	if err := getError(setConsoleCursorPositionProc.Call(handle, uintptr(*(*int32)(unsafe.Pointer(&position))))); err != nil {
		panic(err)
	}
}

func curReset(i int) {
	var info, err = getConsoleScreenBufferInfo(stdoutHandle)
	if err != nil {
		panic("could not get console screen buffer info")
	}
	position = info.CursorPosition
	position.Y -= SHORT(i)
	setConsoleCursorPosition(stdoutHandle, position)
}

func curUp(i int) {
	getCursorPosition()
	position.Y -= SHORT(i)
	setConsoleCursorPosition(stdoutHandle, position)
}

func curDown(i int) {
	getCursorPosition()
	position.Y += SHORT(i)
	setConsoleCursorPosition(stdoutHandle, position)
}


<|start_filename|>curl_util.go<|end_filename|>
package curl

import (
	"os"
	"strings"
)

/*
 resolve arguments and create Download struct
*/
func parseArgs(args ...interface{}) (int, Download) {
	dl := Download{}
	if len(args) == 0 {
		panic(CurlError{"curl.New()", -6, "curl.New() parameter type error."})
	} else {
		switch args[0].(type) {
		case string:
			url, title, name, dst := safeArgs(args...)
			dl.AddTask(Task{url, title, name, dst, 0})
		case Task:
			for _, v := range args {
				dl.AddTask(v.(Task))
			}
		case Download:
			dl = args[0].(Download)
		}
	}
	return len(dl), dl
}

/*

 resolve arguments and create url, title, name, dst

 arg1: url
 arg2: title
 arg3: name
 arg4: dst

*/
func safeArgs(args ...interface{}) (url, title, name, dst string) {
	url = args[0].(string)
	switch len(args) {
	case 1:
		names := strings.Split(url, "/")
		title = names[len(names)-1:][0]
		name = title
		dst, _ = os.Getwd()
	case 2:
		title = args[1].(string)
		name = title
		dst, _ = os.Getwd()
	case 3:
		title, name = args[1].(string), args[2].(string)
		dst, _ = os.Getwd()
	case 4:
		title, name, dst = args[1].(string), args[2].(string), args[3].(string)
	}
	return
}

/*
 verify path is folder
*/
func isDirExist(path string) bool {
	_, err := os.Stat(path)
	if err != nil {
		return os.IsExist(err)
	} else {
		return true
	}
}

/*
 verify Content-Type is binary
*/
func isBodyBytes(content string) (isBytes bool) {
	if strings.Index(content, "json") != -1 {
		isBytes = false
	} else if strings.Index(content, "text") != -1 {
		isBytes = false
	} else if strings.Index(content, "application") != -1 {
		isBytes = true
	}
	return
}

/*
 get max len(title) num
*/
func maxTitleLength(titles []string) int {
	max := 0
	for _, v := range titles {
		if len(v) > max {
			max = len(v)
		}
	}
	if max > 15 {
		max = 15
	}
	return max
}

/*
 when title out range of max(15) format xxxx...
*/
func safeTitle(title string) string {
	h := ""
	if len(title) > 15 {
		title = title[:12] + "..."
	} else if len(title) <= maxNameLen {
		h = strings.Repeat(" ", maxNameLen-len(title))
	}
	return h + title + ":"
}

/*
 when dst the end of no '/', return format dst
*/
func safeDst(dst string) string {
	if !strings.HasSuffix(dst, "/") {
		dst += "/"
	}
	return dst
}


<|start_filename|>curl.go<|end_filename|>
/*
 Curl - Multiple download lib with CLI by Golang. Vesion 0.0.4

 Website https://github.com/kenshin/curl

 Copyright (c) 2014-2016 <NAME> <<EMAIL>>
*/
package curl

import (
	"bufio"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"reflect"
	"strings"
	"sync"
	"time"
)

var (
	wg         sync.WaitGroup
	curLine    int = -1
	maxNameLen int
	mutex      *sync.RWMutex = new(sync.RWMutex)
	count      int           = 0
)

type (
	CurlError struct {
		name    string      // Task struct Name
		code    int         // Task struct Code
		message interface{} // Error message
	}

	Task struct {
		Url   string
		Title string
		Name  string
		Dst   string
		Code  int
	}

	Download []Task

	// Read line use callback Process
	// Line by line to obtain content and line num
	processFunc func(content string, line int) bool
)

/*
  defined error interace, Print Error
*/
func (this CurlError) Error() string {
	name := fmt.Sprintf("Name  : %v\n", this.name)
	code := fmt.Sprintf("Code  : %v\n", this.code)
	msg := fmt.Sprintf("Error : %v", this.message)
	return "\n" + name + code + msg
}

/*
 Receive arguments and return an new task struct
*/
func (this Task) New(args ...interface{}) Task {
	if len(args) == 0 {
		panic(CurlError{"curl.New()", -6, "curl.New() parameter type error."})
	} else {
		this.Url, this.Title, this.Name, this.Dst = safeArgs(args...)
	}
	return this
}

/*
 Append task struct to downlaod struct
*/
func (this *Download) AddTask(ts Task) {
	*this = append(*this, ts)
}

/*
 Return downlaod struct values by key
*/
func (this Download) GetValues(key string) []string {
	var arr []string
	for i := 0; i < len(this); i++ {
		v := reflect.ValueOf(this[i]).FieldByName(key)
		arr = append(arr, v.String())
	}
	return arr
}

/*
   Get URL method

    url e.g. http://nodejs.org/dist/v0.10.0/node.exe

   Return code
     0: success
    -1: status code != 200

   Return res, err

   For example:
    code, res, _ := curl.Get("http:  nodejs.org/dist/")
    if code != 0 {
        return
    }
    defer res.Body.Close()
*/
func Get(url string) (code int, res *http.Response, err error) {

	// get res
	res, err = http.Get(url)

	// err
	if err != nil {
		return -5, res, CurlError{url, -5, err.Error()}
	}

	// check state code
	if res.StatusCode != 200 {
		s := fmt.Sprintf("%v an [%v] error occurred.", url, res.StatusCode)
		return -1, res, CurlError{url, -1, s}
	}

	return 0, res, err
}

/*
   Read line from io.ReadCloser

   For example:
    versionFunc := func(content string, line int) bool {
         TO DO
      return false
    }

    if err := curl.ReadLine(res.Body, versionFunc); err != nil && err != io.EOF {
        TO DO
    }
*/
func ReadLine(body io.ReadCloser, process processFunc) error {

	var content string
	var err error
	var line int = 1

	// set buff
	buff := bufio.NewReader(body)

	for {
		content, err = buff.ReadString('\n')

		if line > 1 && (err != nil || err == io.EOF) {
			break
		}

		if ok := process(content, line); ok {
			break
		}

		line++
	}

	return err
}

/*
   Download method

   Parameter:
	simple download model:
		url  : download url e.g. http://nodejs.org/dist/v0.10.0/node.exe
		title: download task label.
		name : download file name e.g. node.exe
		dst  : download path
	multi download model:
	Download struct

   Return code:
	 0: success
	-2: create file error.
	-3: download node.exe size error.
	-4: content length = -1.
	-5: panic error.
	-6: curl.New() parameter type error.
	-7: Download size error.
	-8: Write Content-Type:text error.

   Return:
	dl( []Task Download struct )
	err( []CurlError array)

   Console show:
	Start download [aaa, bbb, node, npm, ccccccc].
		aaa: 70% [==============>__________________] 925ms
		bbb: 10% [===>_____________________________] 2s
	   node: 100% [===============================>] 10s
		npm: download error.
	cccc...: 30% [=========>________________________] 2s
	End download.

   For example:

	// simple download
	newDL, err := New("http://npm.taobao.org/mirrors/node/v0.10.26/node.exe")
	fmt.Printf("curl.New return ld  is %v\n", newDL)
	fmt.Printf("curl.New return err is %v\n", err)

	// multi download
	ts := Task{}
	ts1 := ts.New("http://7x2xql.com1.z0.glb.clouddn.com/visualhunt.json")
	ts2 := ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/02073.jpg")
	ts3 := ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/0207.jpg")
	newDL, err = New(ts1, ts2, ts3)
	fmt.Printf("curl.New return ld  is %v\n", newDL)
	fmt.Printf("curl.New return err is %v\n", err)

	dl := Download{
		ts.New("http://7x2xql.com1.z0.glb.clouddn.com/visualhunt.json"),
		ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/02073.jpg"),
		ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/0207.jpg"),
	}
	dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/latest/node.exe", "nodeeeeeeeeeeeeeeeeeeeeeeee.exe"))
	dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v5.7.0/win-x64/node.exe", "node4.exe"))
	dl.AddTask(ts.New("https://www.google.com/intl/zh-CN/chrome/browser/?standalone=1&extra=devchannel&platform=win64", "Chrome 49"))
	newDL, err = New(dl)

	fmt.Printf("curl.New return ld  is %v\n", newDL)
	fmt.Printf("curl.New return err is %v\n", err)
*/
func New(args ...interface{}) (dl Download, errStack []CurlError) {
	curLine = -1
	count, dl = parseArgs(args...)
	defer func() {
		if err := recover(); err != nil {
			if v, ok := err.(CurlError); ok {
				errStack = append(errStack, v)
			} else {
				errStack = append(errStack, CurlError{"curl.New()", -5, err})
			}
		}
	}()

	maxNameLen = maxTitleLength(dl.GetValues("Title"))
	header(&dl)

	wg.Add(count)
	for i := 0; i < count; i++ {
		progressbar(dl[i].Title, time.Now(), 0, "\n")
		go func(dl Download, num int) {
			download(&dl[num], num, count, &errStack)
			wg.Done()
		}(dl, i)
	}
	wg.Wait()

	curDown(count - curLine)
	footer()

	return
}

/*
 Download ( text/binary ) and save it.
*/
func download(ts *Task, line, max int, errStack *[]CurlError) {
	url, title, name, dst := ts.Url, ts.Title, ts.Name, safeDst(ts.Dst)
	defer func() {
		if err := recover(); err != nil {
			if v, ok := err.(CurlError); ok {
				*errStack = append(*errStack, v)
				ts.Code = v.code
			} else {
				*errStack = append(*errStack, CurlError{url, -5, err})
				ts.Code = -5
			}
			curMove(line, max)
			msg := fmt.Sprintf("%v download error.", safeTitle(title))
			empty := strings.Repeat(" ", 80-len(msg))
			fmt.Printf("\r%v%v", msg, empty)
		}
	}()

	// get url
	code, res, err := Get(url)
	if code == -1 {
		panic(err)
	}
	defer res.Body.Close()

	// create dst
	if !isDirExist(dst) {
		if err := os.Mkdir(dst, 0777); err != nil {
			panic(CurlError{url, -2, "Create folder error, Error: " + err.Error()})
		}
	}

	// create file
	file, createErr := os.Create(dst + name)
	if createErr != nil {
		panic(CurlError{url, -2, "Create file error, Error: " + createErr.Error()})
	}
	defer file.Close()

	// verify content length
	if res.ContentLength == -1 && isBodyBytes(res.Header.Get("Content-Type")) {
		panic(CurlError{url, -4, "Download content length is -1."})
	}

	start := time.Now()
	if isBodyBytes(res.Header.Get("Content-Type")) {
		buf := make([]byte, res.ContentLength)
		var m float32
		for {
			n, err := res.Body.Read(buf)
			if n == 0 && err.Error() == "EOF" {
				break
			}
			if err != nil && err.Error() != "EOF" {
				panic(CurlError{url, -7, "Download size error, Error: ." + err.Error()})
			}
			m = m + float32(n)
			i := int(m / float32(res.ContentLength) * 50)
			file.WriteString(string(buf[:n]))

			func(title string, start time.Time, i, line, max int) {
				curMove(line, max)
				progressbar(title, start, i, "")
			}(title, start, i, line, max)
		}

		// valid download exe
		fi, err := file.Stat()
		if err == nil {
			if fi.Size() != res.ContentLength {
				panic(CurlError{url, -3, "Downlaod size verify error, please check your network."})
			}
		}
	} else {
		if bytes, err := ioutil.ReadAll(bufio.NewReader(res.Body)); err != nil {
			panic(CurlError{url, -8, err.Error()})
		} else {
			file.Write(bytes)
			curMove(line, max)
			progressbar(title, start, 50, "")
		}
	}
}

/*
  Cursor move( up and down )
*/
func curMove(line, max int) {
	mutex.Lock()
	switch {
	case curLine == -1:
		curReset(max - line)
	case line < curLine:
		curUp(curLine - line)
	case line > curLine:
		curDown(line - curLine)
	}
	if curLine != line {
		curLine = line
	}
	mutex.Unlock()
}


<|start_filename|>curl_others.go<|end_filename|>
// +build !windows

package curl

import "fmt"

func curReset(i int) {
	fmt.Printf("\r\033[%dA", i)
}

func curUp(i int) {
	fmt.Printf("\r\033[%dA", i)
}

func curDown(i int) {
	fmt.Printf("\r\033[%dB", i)
}


<|start_filename|>curl_test.go<|end_filename|>
package curl

import (
	"fmt"
	"io"
	"os"
	"testing"
	"time"
)

func TestCurl(t *testing.T) {
	testPrint()
	testGet()
	testReadLine()
	testSimpleNew()
	testMultiNewMode1()
	testMultiNewMode2()
}

func testGet() {
	code, res, _ := Get("http://npm.taobao.org/mirrors/node/latest/SHASUMS256.txt")
	if code != 0 {
		return
	}
	defer res.Body.Close()
}

func testReadLine() {
	// curl.Get
	code, res, _ := Get("http://npm.taobao.org/mirrors/node/latest/SHASUMS256.txt")
	if code != 0 {
		return
	}
	// close
	defer res.Body.Close()

	// parse callback
	processFunc := func(content string, line int) bool {
		fmt.Printf("line is %v, content is %v", line, content)
		return false
	}
	// relad line
	if err := ReadLine(res.Body, processFunc); err != nil && err != io.EOF {
		fmt.Println(err)
	}
}

func testSimpleNew() {
	newDL, err := New("http://npm.taobao.org/mirrors/node/v0.10.26/node.exe")
	fmt.Printf("curl.New return ld  is %v\n", newDL)
	fmt.Printf("curl.New return err is %v\n", err)
}

func testMultiNewMode1() {
	ts := Task{}
	ts1 := ts.New("http://7x2xql.com1.z0.glb.clouddn.com/visualhunt.json")
	ts2 := ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/02073.jpg")
	ts3 := ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/0207.jpg")
	newDL, err := New(ts1, ts2, ts3)
	fmt.Printf("curl.New return ld  is %v\n", newDL)
	fmt.Printf("curl.New return err is %v\n", err)
}

func testMultiNewMode2() {
	ts := Task{}
	dl := Download{
		ts.New("http://7x2xql.com1.z0.glb.clouddn.com/visualhunt.json"),
		ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/02073.jpg", "1111.jpg"),
		ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/0207.jpg", "2222.jpg"),
	}
	/*
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/latest/node.exe", "node.exe", os.TempDir()))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v5.7.0/win-x64/node.exe", "node4.exe", os.TempDir()))
	   dl.AddTask(ts.New("https://www.google.com/intl/zh-CN/chrome/browser/?standalone=1&extra=devchannel&platform=win64", "ChromeSetup.zip", os.TempDir()))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/latest/node.exe", "nodeeeeeeeeeeeeeeeeeeeeeeee.exe", os.TempDir()))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v5.7.0/win-x64/node.exe", "node4.exe", os.TempDir()))
	   dl.AddTask(ts.New("https://www.google.com/intl/zh-CN/chrome/browser/?standalone=1&extra=devchannel&platform=win64", "ChromeSetup.zip", os.TempDir()))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v5.7.0/win-x86/node.exe", "node40.exe"))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v4.1.1/win-x86/node.exe", "node41.exe"))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v4.4.0/win-x86/node.exe", "node42.exe"))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v4.3.0/win-x86/node.exe", "node43.exe"))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v4.2.0/win-x86/node.exe", "node44.exe"))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v4.1.0/win-x86/node.exe", "node45.exe"))
	   dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v4.0.0/win-x86/node.exe", "node46.exe"))
	   dl.AddTask(ts.New("http://7x2xql.com1.z0.glb.clouddn.com/visualhunt.json"))
	   dl.AddTask(ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/02073.jpg"))
	   dl.AddTask(ts.New("http://7x2xql.com1.z0.glb.clouddn.com/holiday/0207.jpg"))
	*/
	dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/latest/node.exe", "latest", "nodeeeeeeeeeeeeeeeeeeeeeeee.exe", os.TempDir()))
	dl.AddTask(ts.New("http://npm.taobao.org/mirrors/node/v5.7.0/win-x64/node.exe", "5.7.0", "node4.exe"))
	dl.AddTask(ts.New("https://www.google.com/intl/zh-CN/chrome/browser/?standalone=1&extra=devchannel&platform=win64", "chrome 49.01", "ChromeSetup.zip"))
	newDL, err := New(dl)
	fmt.Printf("curl.New return ld  is %v\n", newDL)
	fmt.Printf("curl.New return err is %v\n", err)
}

func testPrint() {
	Options = PrintOps{true, true, "", "", "█", "", " "}
	start := time.Now()
	for i := 1; i <= 50; i++ {
		time.Sleep(time.Millisecond * 200)
		progressbar("Test Process", start, i, "")
	}
}


<|start_filename|>curl_print.go<|end_filename|>
package curl

import (
	"fmt"
	"strings"
	"time"
	"unicode/utf8"
)

/*
 curl.Print Options
*/
type PrintOps struct {
	Header   bool
	Footer   bool
	LeftEnd  string
	RightEnd string
	Fill     string
	Arrow    string
	Empty    string
}

/*
 Set PrintOps default values
*/
var Options = PrintOps{true, true, "[", "]", "=", ">", "_"}

/*
 Print Header
*/
func header(dl *Download) {
	if Options.Header {
		fmt.Printf("Start download [%v].\n", strings.Join((*dl).GetValues("Title"), ", "))
	}
}

/*
 Print Footer
*/

func footer() {
	if Options.Footer {
		fmt.Println("\r--------\nEnd download.")
	}
}

/*
 title: 70% [==============>__________________] 925ms
*/
func progressbar(title string, start time.Time, i int, suffix string) {
	h := Options.LeftEnd + strings.Repeat(Options.Fill, i) + Options.Arrow + strings.Repeat(Options.Empty, 50-i) + Options.RightEnd
	d := time.Now().Sub(start)
	s := fmt.Sprintf("%v %.0f%% %s %v", safeTitle(title), float32(i)/50*100, h, time.Duration(d.Seconds())*time.Second)
	l := utf8.RuneCountInString(s)
	if l > 80 {
		l = l - 80
	} else {
		l = 80 - l
	}
	e := strings.Repeat(" ", l)
	fmt.Printf("\r%v%v%v", s, e, suffix)
}
