├── .gitignore ├── .travis.yml ├── README.md ├── main.go ├── ratelimiter.go ├── task.go ├── termutil ├── term.go ├── term_appengine.go ├── term_bsd.go ├── term_linux.go ├── term_nix.go ├── term_solaris.go ├── term_win.go └── term_x.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | 6 | script: 7 | - go install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dl 2 | 3 | [![Build Status](https://travis-ci.org/ejunjsh/dl.svg?branch=master)](https://travis-ci.org/ejunjsh/dl) 4 | 5 | a concurrent http file downloader,support rate limit, resume from break-point. 6 | 7 | # install 8 | 9 | go get github.com/ejunjsh/dl 10 | 11 | # usage 12 | 13 | # dl 14 | usage: dl [--header
[ --header
]] [[rate limit:]url...] 15 | --header: specify your http header,format is "key:value" 16 | rate limit: limit the speed,unit is KB 17 | url...: urls you want to download 18 | 19 | 20 | # example 21 | 22 | ## concurrent download 23 | 24 | ➜ dl https://download.jetbrains.com/idea/ideaIU-2018.2.1.dmg http://mirrors.neusoft.edu.cn/centos/7/isos/x86_64/CentOS-7-x86_64-Minimal-1804.iso 25 | ideaIU-2018.2.1.dmg |607.13MB[> ]26m13s|384.02KB/s 26 | CentOS-7-x86_64-Mini|906.00MB[===> ] 3m22s| 3.96MB/s 27 | 28 | ## rate limit 29 | 30 | below example shows the download speed that is limited in 200KB 31 | 32 | ➜ dl 200:https://download.jetbrains.com/idea/ideaIU-2018.2.1.dmg 33 | ideaIU-2018.2.1.dmg |607.13MB[===> ]46m14s|199.34KB/s 34 | 35 | ## resume from break-point 36 | 37 | below shows two commands,the second command resume from the first command 38 | 39 | ➜ dl https://download.jetbrains.com/idea/ideaIU-2018.2.1.dmg 40 | ideaIU-2018.2.1.dmg |607.13MB[====> ] 5m 1s| 1.73MB/s 41 | ^C 42 | 43 | ➜ dl https://download.jetbrains.com/idea/ideaIU-2018.2.1.dmg 44 | ideaIU-2018.2.1.dmg |607.13MB[=====> ] 3m17s| 2.57MB/s 45 | 46 | ## customize header 47 | 48 | dl --header aaa:bbb --header ccc:ddd https://download.jetbrains.com/idea/ideaIU-2018.2.1.dmg 49 | 50 | above download will use the "aaa:bbb;ccc:ddd" as its header 51 | 52 | ## proxy 53 | 54 | support `HTTP_PROXY` or `HTTPS_PROXY` environment parameter to setup proxy. 55 | 56 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ejunjsh/dl/termutil" 6 | "io" 7 | "os" 8 | "strings" 9 | "time" 10 | "github.com/urfave/cli" 11 | "log" 12 | ) 13 | 14 | func printUsage(){ 15 | usage := `usage: dl [--header
[ --header
]] [[rate limit:]url...] 16 | --header: specify your http header,format is "key:value" 17 | rate limit: limit the speed,unit is KB 18 | url...: urls you want to download` 19 | fmt.Println(usage) 20 | } 21 | 22 | func main() { 23 | app := cli.NewApp() 24 | app.Flags = []cli.Flag{ 25 | cli.StringSliceFlag{ 26 | Name: "header", 27 | }, 28 | } 29 | cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { 30 | printUsage() 31 | } 32 | app.Action = func(c *cli.Context) error { 33 | if c.NArg()==0{ 34 | printUsage() 35 | return nil 36 | } 37 | 38 | headers:=c.StringSlice("header") 39 | m:=parseHeaderFromStringSlice(headers) 40 | ts := make([]*task, c.NArg()) 41 | for i, url := range c.Args() { 42 | t := newTask(url,m) 43 | if t != nil { 44 | go t.start() 45 | ts[i] = t 46 | } 47 | } 48 | 49 | var isGetWidth = true 50 | 51 | width, err := termutil.TerminalWidth() 52 | 53 | if err != nil { 54 | isGetWidth = false 55 | } 56 | 57 | ticker := time.NewTicker(time.Second) 58 | isfirst := true 59 | go func() { 60 | for { 61 | select { 62 | case <-ticker.C: 63 | if !isfirst { 64 | termutil.ClearLines(int16(len(ts))) 65 | } 66 | updateTerm(isGetWidth, ts, width) 67 | isfirst = false 68 | } 69 | } 70 | }() 71 | 72 | for _, t := range ts { 73 | if t != nil { 74 | <-t.done 75 | } 76 | } 77 | 78 | time.Sleep(time.Second) 79 | 80 | fmt.Println("finished") 81 | 82 | return nil 83 | } 84 | err := app.Run(os.Args) 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | } 89 | 90 | func updateTerm(isGetWidth bool, ts []*task, width int) { 91 | for _, t := range ts { 92 | var buf string 93 | if t.err != nil && t.err != io.EOF { 94 | if t.filename==""{ 95 | buf = fmt.Sprintf("error:%s",t.err.Error()) 96 | }else { 97 | buf = fmt.Sprintf("%s:error:%s",t.filename,t.err.Error()) 98 | } 99 | } else if t.getReadNum() > 0 { 100 | var etaBuf string 101 | var fileSizeBuf string 102 | var fnbuf string 103 | 104 | fnnum:=20 105 | fnbuf=showFileName(t.filename,fnnum) 106 | 107 | if t.fileSize <= 0 { 108 | fileSizeBuf = fmt.Sprintf("|%s", formatBytes(t.getReadNum())) 109 | } else { 110 | fileSizeBuf = fmt.Sprintf("|%s",formatBytes(t.fileSize)) 111 | } 112 | 113 | etaBuf = fmt.Sprintf("%s|%s/s", t.getETA(), t.getSpeed()) 114 | 115 | if isGetWidth && t.fileSize > 0 { 116 | r := width - cellCount(fileSizeBuf+etaBuf)-fnnum 117 | if r > 4 { 118 | fileSizeBuf += "[" 119 | etaBuf = "]" + etaBuf 120 | 121 | ratio := float64(t.getReadNum()) / float64(t.fileSize) 122 | r -= 2 123 | bar := strings.Repeat(" ", r) 124 | c := int(float64(r) * ratio) 125 | progress := "" 126 | if c > 0 { 127 | progress = strings.Repeat("=", c) 128 | } 129 | if c+1", bar[c+1:]}, "") 131 | }else { 132 | bar = strings.Join([]string{progress, ">"}, "") 133 | } 134 | buf = strings.Join([]string{fnbuf,fileSizeBuf, bar, etaBuf}, "") 135 | 136 | } else if r < 0 { 137 | buf = buf[:width] 138 | } else { 139 | buf = strings.Join([]string{fnbuf,fileSizeBuf, etaBuf}, "") 140 | } 141 | } else if t.fileSize>0 { 142 | buf = strings.Join([]string{fnbuf,fileSizeBuf,fmt.Sprintf("|%.2f%%",100*float64(t.getReadNum())/float64(t.fileSize)) ,etaBuf}, "") 143 | } else { 144 | buf = strings.Join([]string{fnbuf,fmt.Sprintf("|%s",formatBytes(t.getReadNum()))}, "") 145 | } 146 | } else { 147 | buf = "waiting..." 148 | } 149 | 150 | if isGetWidth { 151 | c := cellCount(buf) 152 | if c > width { 153 | buf = buf[:width] 154 | } else if c < width { 155 | buf = buf + strings.Repeat(" ", width-c) 156 | } 157 | } 158 | 159 | fmt.Println(buf) 160 | } 161 | 162 | } 163 | 164 | func showFileName(filename string,cap int) string{ 165 | if len(filename)cap{ 178 | l-- 179 | continue 180 | }else { 181 | return strings.Join([]string{d,strings.Repeat(" ",cap-d2)},"") 182 | } 183 | } 184 | 185 | }else { 186 | return filename[:cap] 187 | } 188 | 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /ratelimiter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | type ratelimiter struct { 6 | readNum int64 7 | pasttime time.Time 8 | lim int64 9 | } 10 | 11 | func (r *ratelimiter) wait(readNum int64) { 12 | if int(time.Now().UnixNano())-int(r.pasttime.UnixNano()) <= int(time.Second) { 13 | d := readNum - r.readNum 14 | if d >= r.lim { 15 | x := time.Second.Nanoseconds() - (time.Now().UnixNano() - r.pasttime.UnixNano()) 16 | time.Sleep(time.Duration(x)) 17 | r.readNum = readNum 18 | r.pasttime = time.Now() 19 | } 20 | } else { 21 | r.readNum = readNum 22 | r.pasttime = time.Now() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | "strings" 13 | ) 14 | 15 | type task struct { 16 | done chan struct{} 17 | src io.ReadCloser 18 | dst io.WriteCloser 19 | bytePerSecond float64 20 | err error 21 | startTime time.Time 22 | endTime time.Time 23 | mutex sync.Mutex 24 | readNum int64 25 | fileSize int64 26 | filename string 27 | buffer []byte 28 | lim *ratelimiter 29 | url string 30 | isResume bool 31 | header map[string]string 32 | } 33 | 34 | func (t *task) getReadNum() int64 { 35 | if t == nil { 36 | return 0 37 | } 38 | return atomic.LoadInt64(&t.readNum) 39 | } 40 | 41 | func newTask(url string,h map[string]string) *task { 42 | lim, url := getLimitFromUrl(url) 43 | return &task{url: url, done: make(chan struct{}, 1), buffer: make([]byte, 32*1024), lim: &ratelimiter{lim: lim * 1000},header:h} 44 | } 45 | 46 | func (t *task) start() { 47 | defer func() { 48 | if err := recover(); err != nil { 49 | switch x := err.(type) { 50 | case string: 51 | t.err = errors.New(x) 52 | case error: 53 | t.err = x 54 | default: 55 | t.err = errors.New("Unknow panic") 56 | } 57 | close(t.done) 58 | t.endTime = time.Now() 59 | } 60 | }() 61 | var dst *os.File 62 | var rn, wn int 63 | var filename string 64 | var fi os.FileInfo 65 | req, _ := http.NewRequest("GET", t.url, nil) 66 | if t.header!=nil{ 67 | for k,v:=range t.header{ 68 | req.Header.Set(k,v) 69 | } 70 | } 71 | c := &http.Client{ 72 | Transport: &http.Transport{ 73 | Proxy: http.ProxyFromEnvironment, 74 | }, 75 | } 76 | rep, err := c.Do(req) 77 | if err != nil { 78 | goto done 79 | } else if rep.StatusCode != 200 &&rep.StatusCode != 206 { 80 | err = errors.New(fmt.Sprintf("wrong response %d", rep.StatusCode)) 81 | goto done 82 | } 83 | 84 | filename, err = guessFilename(rep) 85 | 86 | fi, err = os.Stat(filename) 87 | 88 | if err == nil { 89 | if !fi.IsDir() { 90 | rep.Body.Close() 91 | if fi.Size()==rep.ContentLength{ 92 | err=errors.New("File is downloaded! ") 93 | goto done 94 | } 95 | req.Header.Set("Range", fmt.Sprintf("bytes=%d-", fi.Size())) 96 | rep, err = c.Do(req) 97 | if err != nil { 98 | goto done 99 | } else if rep.StatusCode != 200&&rep.StatusCode != 206 { 100 | err = errors.New(fmt.Sprintf("wrong response %d", rep.StatusCode)) 101 | goto done 102 | } 103 | if rep.Header.Get("Accept-Ranges") == "bytes" ||rep.Header.Get("Content-Range") != ""{ 104 | dst, err = os.OpenFile(filename, os.O_RDWR, 0666) 105 | if err != nil { 106 | goto done 107 | } 108 | dst.Seek(0, os.SEEK_END) 109 | t.readNum = fi.Size() 110 | t.isResume=true 111 | } 112 | } 113 | } 114 | 115 | if dst == nil { 116 | dst, err = os.Create(filename) 117 | if err != nil { 118 | goto done 119 | } 120 | } 121 | 122 | t.dst = dst 123 | t.src = rep.Body 124 | t.filename=filename 125 | if rep.ContentLength>0 &&t.isResume && fi!=nil{ 126 | t.fileSize = rep.ContentLength+fi.Size() 127 | }else { 128 | t.fileSize = rep.ContentLength 129 | } 130 | 131 | 132 | go t.bps() 133 | 134 | t.startTime = time.Now() 135 | 136 | loop: 137 | 138 | if t.lim.lim > 0 { 139 | t.lim.wait(t.readNum) 140 | } 141 | 142 | rn, err = t.src.Read(t.buffer) 143 | 144 | if rn > 0 { 145 | 146 | wn, err = t.dst.Write(t.buffer[:rn]) 147 | 148 | if err != nil { 149 | goto done 150 | } else if rn != wn { 151 | err = io.ErrShortWrite 152 | goto done 153 | } else { 154 | atomic.AddInt64(&t.readNum, int64(rn)) 155 | goto loop 156 | } 157 | } 158 | 159 | done: 160 | t.err = err 161 | close(t.done) 162 | t.endTime = time.Now() 163 | return 164 | } 165 | 166 | func (t *task) bps() { 167 | var prev int64 168 | then := t.startTime 169 | 170 | ticker := time.NewTicker(time.Second) 171 | defer ticker.Stop() 172 | 173 | for { 174 | select { 175 | case <-t.done: 176 | return 177 | 178 | case now := <-ticker.C: 179 | d := now.Sub(then) 180 | then = now 181 | 182 | cur := t.getReadNum() 183 | bs := cur - prev 184 | prev = cur 185 | 186 | t.mutex.Lock() 187 | t.bytePerSecond = float64(bs) / d.Seconds() 188 | t.mutex.Unlock() 189 | } 190 | } 191 | } 192 | 193 | func (t *task) getSpeed() string { 194 | t.mutex.Lock() 195 | defer t.mutex.Unlock() 196 | return formatBytes(int64(t.bytePerSecond)) 197 | } 198 | 199 | func (t *task) getETA() string { 200 | t.mutex.Lock() 201 | defer t.mutex.Unlock() 202 | if t.fileSize == 0 || t.bytePerSecond == 0 { 203 | return " " 204 | } else { 205 | b:= formatTime((t.fileSize - t.getReadNum()) / int64(t.bytePerSecond)) 206 | if len(b)>6{ 207 | b=b[:6] 208 | }else if len(b)<6{ 209 | b=strings.Join([]string{strings.Repeat(" ",6-len(b)),b},"") 210 | } 211 | return b 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /termutil/term.go: -------------------------------------------------------------------------------- 1 | package termutil 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | ) 10 | 11 | var echoLocked bool 12 | var echoLockMutex sync.Mutex 13 | var errLocked = errors.New("terminal locked") 14 | 15 | // RawModeOn switches terminal to raw mode 16 | func RawModeOn() (quit chan struct{}, err error) { 17 | echoLockMutex.Lock() 18 | defer echoLockMutex.Unlock() 19 | if echoLocked { 20 | err = errLocked 21 | return 22 | } 23 | if err = lockEcho(); err != nil { 24 | return 25 | } 26 | echoLocked = true 27 | quit = make(chan struct{}, 1) 28 | go catchTerminate(quit) 29 | return 30 | } 31 | 32 | // RawModeOff restore previous terminal state 33 | func RawModeOff() (err error) { 34 | echoLockMutex.Lock() 35 | defer echoLockMutex.Unlock() 36 | if !echoLocked { 37 | return 38 | } 39 | if err = unlockEcho(); err != nil { 40 | return 41 | } 42 | echoLocked = false 43 | return 44 | } 45 | 46 | // listen exit signals and restore terminal state 47 | func catchTerminate(quit chan struct{}) { 48 | sig := make(chan os.Signal, 1) 49 | signal.Notify(sig, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL) 50 | defer signal.Stop(sig) 51 | select { 52 | case <-quit: 53 | RawModeOff() 54 | case <-sig: 55 | RawModeOff() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /termutil/term_appengine.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package termutil 4 | 5 | import "errors" 6 | 7 | // terminalWidth returns width of the terminal, which is not supported 8 | // and should always failed on appengine classic which is a sandboxed PaaS. 9 | func TerminalWidth() (int, error) { 10 | return 0, errors.New("Not supported") 11 | } 12 | -------------------------------------------------------------------------------- /termutil/term_bsd.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd netbsd openbsd dragonfly 2 | // +build !appengine 3 | 4 | package termutil 5 | 6 | import "syscall" 7 | 8 | const ioctlReadTermios = syscall.TIOCGETA 9 | const ioctlWriteTermios = syscall.TIOCSETA 10 | -------------------------------------------------------------------------------- /termutil/term_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | // +build !appengine 3 | 4 | package termutil 5 | 6 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 7 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS 8 | -------------------------------------------------------------------------------- /termutil/term_nix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin freebsd netbsd openbsd dragonfly 2 | // +build !appengine 3 | 4 | package termutil 5 | 6 | import "syscall" 7 | 8 | const sysIoctl = syscall.SYS_IOCTL 9 | -------------------------------------------------------------------------------- /termutil/term_solaris.go: -------------------------------------------------------------------------------- 1 | // +build solaris 2 | // +build !appengine 3 | 4 | package termutil 5 | 6 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 7 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS 8 | const sysIoctl = 54 9 | -------------------------------------------------------------------------------- /termutil/term_win.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package termutil 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "syscall" 11 | "unsafe" 12 | "log" 13 | ) 14 | 15 | var tty = os.Stdin 16 | 17 | var ( 18 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 19 | 20 | // GetConsoleScreenBufferInfo retrieves information about the 21 | // specified console screen buffer. 22 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx 23 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 24 | 25 | // GetConsoleMode retrieves the current input mode of a console's 26 | // input buffer or the current output mode of a console screen buffer. 27 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx 28 | getConsoleMode = kernel32.NewProc("GetConsoleMode") 29 | 30 | // SetConsoleMode sets the input mode of a console's input buffer 31 | // or the output mode of a console screen buffer. 32 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx 33 | setConsoleMode = kernel32.NewProc("SetConsoleMode") 34 | 35 | // SetConsoleCursorPosition sets the cursor position in the 36 | // specified console screen buffer. 37 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx 38 | setConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 39 | 40 | mingw = isMingw() 41 | ) 42 | 43 | type ( 44 | // Defines the coordinates of the upper left and lower right corners 45 | // of a rectangle. 46 | // See 47 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311(v=vs.85).aspx 48 | smallRect struct { 49 | Left, Top, Right, Bottom int16 50 | } 51 | 52 | // Defines the coordinates of a character cell in a console screen 53 | // buffer. The origin of the coordinate system (0,0) is at the top, left cell 54 | // of the buffer. 55 | // See 56 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx 57 | coordinates struct { 58 | X, Y int16 59 | } 60 | 61 | word int16 62 | 63 | // Contains information about a console screen buffer. 64 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx 65 | consoleScreenBufferInfo struct { 66 | dwSize coordinates 67 | dwCursorPosition coordinates 68 | wAttributes word 69 | srWindow smallRect 70 | dwMaximumWindowSize coordinates 71 | } 72 | ) 73 | 74 | // TerminalWidth returns width of the terminal. 75 | func TerminalWidth() (width int, err error) { 76 | if mingw { 77 | return termWidthTPut() 78 | } 79 | return termWidthCmd() 80 | } 81 | 82 | func termWidthCmd() (width int, err error) { 83 | var info consoleScreenBufferInfo 84 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0) 85 | if e != 0 { 86 | return 0, error(e) 87 | } 88 | return int(info.dwSize.X) - 1, nil 89 | } 90 | 91 | func isMingw() bool { 92 | return os.Getenv("MINGW_PREFIX") != "" || os.Getenv("MSYSTEM") == "MINGW64" 93 | } 94 | 95 | func termWidthTPut() (width int, err error) { 96 | // TODO: maybe anybody knows a better way to get it on mintty... 97 | var res []byte 98 | cmd := exec.Command("tput", "cols") 99 | cmd.Stdin = os.Stdin 100 | if res, err = cmd.CombinedOutput(); err != nil { 101 | return 0, fmt.Errorf("%s: %v", string(res), err) 102 | } 103 | if len(res) > 1 { 104 | res = res[:len(res)-1] 105 | } 106 | return strconv.Atoi(string(res)) 107 | } 108 | 109 | func getCursorPos() (pos coordinates, err error) { 110 | var info consoleScreenBufferInfo 111 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0) 112 | if e != 0 { 113 | return info.dwCursorPosition, error(e) 114 | } 115 | return info.dwCursorPosition, nil 116 | } 117 | 118 | func setCursorPos(pos coordinates) error { 119 | _, _, e := syscall.Syscall(setConsoleCursorPosition.Addr(), 2, uintptr(syscall.Stdout), uintptr(uint32(uint16(pos.Y))<<16|uint32(uint16(pos.X))), 0) 120 | if e != 0 { 121 | return error(e) 122 | } 123 | return nil 124 | } 125 | 126 | var oldState word 127 | 128 | func lockEcho() (err error) { 129 | if _, _, e := syscall.Syscall(getConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&oldState)), 0); e != 0 { 130 | err = fmt.Errorf("Can't get terminal settings: %v", e) 131 | return 132 | } 133 | 134 | newState := oldState 135 | const ENABLE_ECHO_INPUT = 0x0004 136 | const ENABLE_LINE_INPUT = 0x0002 137 | newState = newState & (^(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) 138 | if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(newState), 0); e != 0 { 139 | err = fmt.Errorf("Can't set terminal settings: %v", e) 140 | return 141 | } 142 | return 143 | } 144 | 145 | func unlockEcho() (err error) { 146 | if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(oldState), 0); e != 0 { 147 | err = fmt.Errorf("Can't set terminal settings") 148 | } 149 | return 150 | } 151 | 152 | func ClearLines(linecount int16){ 153 | coords, err := getCursorPos() 154 | if err != nil { 155 | log.Panic(err) 156 | } 157 | coords.Y -= linecount 158 | if coords.Y < 0 { 159 | coords.Y = 0 160 | } 161 | coords.X = 0 162 | 163 | err = setCursorPos(coords) 164 | if err != nil { 165 | log.Panic(err) 166 | } 167 | } -------------------------------------------------------------------------------- /termutil/term_x.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin freebsd netbsd openbsd solaris dragonfly 2 | // +build !appengine 3 | 4 | package termutil 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | var tty *os.File 14 | 15 | type window struct { 16 | Row uint16 17 | Col uint16 18 | Xpixel uint16 19 | Ypixel uint16 20 | } 21 | 22 | func init() { 23 | var err error 24 | tty, err = os.Open("/dev/tty") 25 | if err != nil { 26 | tty = os.Stdin 27 | } 28 | } 29 | 30 | // TerminalWidth returns width of the terminal. 31 | func TerminalWidth() (int, error) { 32 | w := new(window) 33 | res, _, err := syscall.Syscall(sysIoctl, 34 | tty.Fd(), 35 | uintptr(syscall.TIOCGWINSZ), 36 | uintptr(unsafe.Pointer(w)), 37 | ) 38 | if int(res) == -1 { 39 | return 0, err 40 | } 41 | return int(w.Col), nil 42 | } 43 | 44 | var oldState syscall.Termios 45 | 46 | func lockEcho() (err error) { 47 | fd := tty.Fd() 48 | if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 { 49 | err = fmt.Errorf("Can't get terminal settings: %v", e) 50 | return 51 | } 52 | 53 | newState := oldState 54 | newState.Lflag &^= syscall.ECHO 55 | newState.Lflag |= syscall.ICANON | syscall.ISIG 56 | newState.Iflag |= syscall.ICRNL 57 | if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); e != 0 { 58 | err = fmt.Errorf("Can't set terminal settings: %v", e) 59 | return 60 | } 61 | return 62 | } 63 | 64 | func unlockEcho() (err error) { 65 | fd := tty.Fd() 66 | if _, _, e := syscall.Syscall6(sysIoctl, fd, ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); e != 0 { 67 | err = fmt.Errorf("Can't set terminal settings") 68 | } 69 | return 70 | } 71 | 72 | func ClearLines(linecount int16){ 73 | fmt.Printf("\033[%dA",linecount) 74 | } -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "gopkg.in/mattn/go-runewidth.v0" 7 | "mime" 8 | "net/http" 9 | "path" 10 | "path/filepath" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | kib = 1024 18 | mib = 1048576 19 | gib = 1073741824 20 | tib = 1099511627776 21 | ) 22 | 23 | func formatBytes(i int64) (result string) { 24 | switch { 25 | case i >= tib: 26 | result = fmt.Sprintf("%6.2fTB", float64(i)/tib) 27 | case i >= gib: 28 | result = fmt.Sprintf("%6.2fGB", float64(i)/gib) 29 | case i >= mib: 30 | result = fmt.Sprintf("%6.2fMB", float64(i)/mib) 31 | case i >= kib: 32 | result = fmt.Sprintf("%6.2fKB", float64(i)/kib) 33 | default: 34 | result = fmt.Sprintf("%7dB", i) 35 | } 36 | 37 | if len(result)>8{ 38 | result=strings.Join([]string{result[:6],result[7:]},"") 39 | } 40 | 41 | return 42 | } 43 | 44 | func formatTime(i int64) string { 45 | if i < 60 { 46 | return fmt.Sprintf("%2ds", i) 47 | } else if i < 3600 { 48 | s := i % 60 49 | m := i / 60 50 | if s == 0 { 51 | return fmt.Sprintf("%2dm", m) 52 | } else { 53 | return fmt.Sprintf("%2dm", m) + formatTime(s) 54 | } 55 | 56 | } else { 57 | s := i % 3600 58 | h := i / 3600 59 | if s == 0 { 60 | return fmt.Sprintf("%2dh", h) 61 | } else { 62 | return fmt.Sprintf("%2dh", h) + formatTime(s) 63 | } 64 | } 65 | } 66 | 67 | var errNoFilename = errors.New("no filename could be determined") 68 | 69 | func guessFilename(resp *http.Response) (string, error) { 70 | filename := resp.Request.URL.Path 71 | if cd := resp.Header.Get("Content-Disposition"); cd != "" { 72 | if _, params, err := mime.ParseMediaType(cd); err == nil { 73 | filename = params["filename"] 74 | } 75 | } 76 | 77 | if filename == "" || strings.HasSuffix(filename, "/") || strings.Contains(filename, "\x00") { 78 | return "", errNoFilename 79 | } 80 | 81 | filename = filepath.Base(path.Clean("/" + filename)) 82 | if filename == "" || filename == "." || filename == "/" { 83 | return "", errNoFilename 84 | } 85 | 86 | return filename, nil 87 | } 88 | 89 | var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9]+\x6d") 90 | 91 | func cellCount(s string) int { 92 | n := runewidth.StringWidth(s) 93 | for _, sm := range ctrlFinder.FindAllString(s, -1) { 94 | n -= runewidth.StringWidth(sm) 95 | } 96 | return n 97 | } 98 | 99 | func getLimitFromUrl(url string) (int64, string) { 100 | s := strings.Split(url, ":") 101 | if len(s) >= 2 { 102 | i, err := strconv.ParseInt(s[0], 0, 0) 103 | if err != nil { 104 | return -1, url 105 | } else { 106 | return i, strings.Join(s[1:], ":") 107 | } 108 | } 109 | return -1, url 110 | } 111 | 112 | func parseHeaderFromStringSlice(h []string) (m map[string]string){ 113 | if h==nil { 114 | m=nil 115 | return 116 | } 117 | m=make(map[string]string) 118 | for _,ss:=range h{ 119 | if strings.Contains(ss,":"){ 120 | s:=strings.Split(ss,":") 121 | m[s[0]]=s[1] 122 | } 123 | } 124 | return 125 | } --------------------------------------------------------------------------------