/
gobble.go
218 lines (189 loc) · 5.14 KB
/
gobble.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// Copyright 2014 Markus Dittrich. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// gobble is a simple program for retrieving files via
// http, https, and ftp á la wget
package main
import (
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
// command line settings
var (
urlTarget = flag.String("u", "", "url to download")
outFileName = flag.String("o", "", "name of output file")
toStdout = flag.Bool("s", false, "output to stdout")
)
// general settings
var (
numBytes = 40960 // chunk site for reading and writing
version = 0.1 // gobble version
)
// progress bar
var progressBar = "-----------------------------------"
func main() {
flag.Parse()
if *urlTarget == "" {
usage()
}
url := normalizeURLTarget(*urlTarget)
// start http client
client := &http.Client{}
resp, err := client.Get(url)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// open output file; nil if stdout was requested
file := os.Stdout
if !*toStdout {
file, err = openOutfile(*outFileName, url)
if err != nil {
log.Fatal("failed to open output file: ", err)
}
defer file.Close()
printInfo(url, resp)
}
totalBytes := resp.ContentLength
bytesRead, err := copyContent(resp.Body, file, totalBytes, *toStdout)
if err != nil {
log.Fatal(err)
}
if !*toStdout {
fmt.Println(statusString(bytesRead, totalBytes, true))
}
}
// copyContent reads the body content from the http connection and then
// copies it either to the provided file or stdou
func copyContent(body io.ReadCloser, file *os.File, totalBytes int64,
wantStdout bool) (int, error) {
buffer := make([]byte, numBytes)
bytesRead := 0
n := 0
for {
// read numBytes
var err error
n, err = io.ReadFull(body, buffer)
if err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
break // this is the regular end-of-file - we are done
} else {
return 0, err
}
}
// write numBytes
nOut, err := bufWrite(buffer, file)
if err != nil {
log.Fatal(err)
} else if nOut != n {
return 0, fmt.Errorf("% bytes read but %d byte written", n, nOut)
}
bytesRead += n
if !wantStdout {
fmt.Print(statusString(bytesRead, totalBytes, false))
}
}
// write whatever is left
_, err := bufWrite(buffer[:n], file)
if err != nil {
return 0, err
}
bytesRead += n
return bytesRead, nil
}
// bufWrite writes content either to stdout or the requested output file
func bufWrite(content []byte, file *os.File) (int, error) {
n, err := file.Write(content)
if err != nil {
return n, err
}
return n, nil
}
// openOutfile opens the output file if one was requested
// Otherwise, we assume the output file is index.html
func openOutfile(outFileName, urlTarget string) (*os.File, error) {
fileName := outFileName
if fileName == "" {
// can we extract a
urlInfo, err := url.Parse(urlTarget)
if err != nil {
return nil, err
}
if fileName = filepath.Base(urlInfo.Path); fileName == "." || fileName == "/" {
fileName = "index.html"
}
}
// if fileName already exists we bail
if _, err := os.Stat(fileName); err == nil {
return nil, fmt.Errorf("%s already exists\n", fileName)
}
file, err := os.Create(fileName)
if err != nil {
return nil, err
}
return file, nil
}
// normalizeURLTarget currently only checks if an URL starts with
// http:// and if not appends it
func normalizeURLTarget(urlTarget string) string {
outString := urlTarget
if !strings.HasPrefix(urlTarget, "http://") {
outString = "http://" + urlTarget
}
return outString
}
// statusString returns the status string corresponding to the given
// number of bytes read.
// NOTE: Sites which don't provide the content length return a value of
// -1 for totalbytes. In this case we print a simpler content string
func statusString(bytesRead int, totalBytes int64, allDone bool) string {
var msg string
if allDone {
msg = "Finished: "
} else {
msg = "In progress: "
}
var formatString string
if totalBytes == -1 {
progressString := "<=>"
formatString = fmt.Sprintf("%s %10d Bytes %-30s \r", msg, bytesRead,
progressString)
} else {
percentage := float64(bytesRead) / float64(totalBytes) * 100
progressString := strings.Join(
[]string{progressBar[1 : 2+int(percentage/4)], ">"}, "")
formatString = fmt.Sprintf("%s %10d Bytes %-30s %2.1f%%\r", msg,
bytesRead, progressString, percentage)
}
return formatString
}
// printInfo prints a brief informative header about the connection
func printInfo(urlTarget string, resp *http.Response) {
fmt.Println("********* This is gobble version ", version, " ***************")
urlInfo, err := url.Parse(urlTarget)
if err != nil {
return
}
cname, _ := net.LookupCNAME(urlInfo.Host)
ips, _ := net.LookupIP(cname)
fmt.Println("Connecting to", cname, " ", ips)
fmt.Printf("Status %s Protocol %s TransferEncoding %v\n", resp.Status,
resp.Proto, resp.TransferEncoding)
fmt.Printf("Content Length: %d bytes\n", resp.ContentLength)
fmt.Println()
}
// usage prints the package usage and then exits
func usage() {
fmt.Println(os.Args[0], "[options]", "\n\noptions:")
flag.PrintDefaults()
os.Exit(1)
}