-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
gemget.go
182 lines (162 loc) · 6.53 KB
/
gemget.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
package main
import (
"fmt"
"io/ioutil"
"math"
"os"
"time"
"github.com/dustin/go-humanize"
"github.com/makeworld-the-better-one/gemget/scanner"
"github.com/makeworld-the-better-one/go-gemini"
gemsocks5 "github.com/makeworld-the-better-one/go-gemini-socks5"
flag "github.com/spf13/pflag"
"golang.org/x/net/proxy"
)
var (
version = "v1.9.0"
commit = "unknown"
builtBy = "unknown"
insecure = flag.BoolP("insecure", "i", false, "Skip checking the cert\n")
dir = flag.StringP("directory", "d", ".", "\nThe directory where downloads go")
output = flag.StringP("output", "o", "", "Output path, for when there is only one URL.\n'-' means stdout and implies --quiet.\nIt overrides --directory.\n")
errorSkip = flag.BoolP("skip", "s", false, "Move to the next URL when one fails.")
exts = flag.BoolP("add-extension", "e", false, "Add .gmi extensions to gemini files that don't have it, like directories.\n")
numRedirs = flag.UintP("redirects", "r", 5, "How many redirects to follow before erroring out.")
header = flag.Bool("header", false, "\nPrint out (even with --quiet) the response header to stdout in the format:\nHeader: <status> <meta>\n")
verFlag = flag.BoolP("version", "v", false, "Find out what version of gemget you're running.")
maxSize = flag.StringP("max-size", "m", "", "Set the file size limit. Any download that exceeds this size will\ncause an Info output and be deleted.\nLeaving it blank or setting to zero bytes will result in no limit.\nThis flag is ignored when outputting to stdout.\nFormat: <num> <optional-byte-size>\nExamples: 423, 3.2KiB, '2.5 MB', '22 MiB', '10gib', 3M\n")
maxSecs = flag.UintP("max-time", "t", 0, "Set the downloading time limit, in seconds. Any download that\ntakes longer will cause an Info output and be deleted.\n")
inputFilePath = flag.StringP("input-file", "f", "", "Input file with a single URL on each line. Empty lines or lines starting\nwith # are ignored. URLs on the command line will be processed first.\n")
noBar = flag.Bool("no-progress-bar", false, "Disable the progress bar output.\n")
geminiProxy = flag.StringP("proxy", "p", "", "A proxy that can requests are sent to instead.\nCan be a domain or IP with port. Port 1965 is assumed otherwise.\n")
cert = flag.String("cert", "", "Path to a PEM encoded TLS client certificate to be sent with the request.\n")
key = flag.String("key", "", "Path to a PEM encoded TLS key for the provided client cert.\n")
connectTimeout = flag.Uint("connect-timeout", 15, "Max time allowed to form a connection and get the header, in seconds.\nSet to 0 for no timeout.")
socksHost = flag.String("socks", "", "Host/address for a SOCKS5 proxy")
socksUser = flag.String("socks-user", "", "Username for a SOCKS5 proxy (optional)")
socksPass = flag.String("socks-pass", "", "Password for a SOCKS5 proxy (optional)")
quiet bool // Set in main, so that it can be changed later if needed
maxBytes int64 // After maxSize is parsed this is set
inputFile *os.File // Global var so it can be closed on fatal errors
certPEM []byte
keyPEM []byte
)
func fatalIsFile(name, path string) {
fi, err := os.Stat(path)
if err == nil {
if fi.IsDir() {
fatal("%s path points to a directory: %s", name, path)
}
} else if os.IsNotExist(err) {
fatal("%s does not exist: %v", path, err)
} else {
// Some other error - permissions for example
fatal("Couldn't access %s at %s: %v", name, path, err)
}
}
func main() {
flag.BoolVarP(&quiet, "quiet", "q", false, "Neither info strings or the progress bar will be printed.\nNote that normally infos are printed to stderr, not stdout.\n")
flag.Parse()
if *verFlag {
fmt.Println("gemget", version)
fmt.Println("Commit:", commit)
fmt.Println("Built by:", builtBy)
return
}
// Validate urls
if len(flag.Args()) == 0 && *inputFilePath == "" {
flag.Usage()
fmt.Println("\nNo URLs provided, by command line or file.")
os.Exit(1)
}
// Validate flags
if *output == "-" {
quiet = true
}
if (len(flag.Args()) > 1 || *inputFilePath != "") && *output != "" && *output != "-" {
fatal("The output flag cannot be specified when there are multiple URLs, unless it is '-', meaning stdout.")
}
if *maxSize != "" {
tmpMaxBytes, err := humanize.ParseBytes(*maxSize)
if err != nil {
fatal("Max bytes string could not be parsed: %v", err)
}
if tmpMaxBytes > math.MaxInt64-1 {
fatal("Max bytes is too large: %s, %d bytes", *maxSize, tmpMaxBytes)
}
maxBytes = int64(tmpMaxBytes)
}
if (*cert == "" && *key != "") || (*cert != "" && *key == "") {
fatal("Both --cert and --key must be defined for a client certificate to be sent.")
}
// Validate cert and key files
if *cert != "" && *key != "" {
fatalIsFile("cert file", *cert)
fatalIsFile("key file", *key)
var err error
certPEM, err = ioutil.ReadFile(*cert)
if err != nil {
fatal("Cert file could not be read: %v", err)
}
keyPEM, err = ioutil.ReadFile(*key)
if err != nil {
fatal("Key file could not be read: %v", err)
}
}
// Validate output directory
if *dir == "" {
fatal("Directory path cannot be empty.")
} else if *dir != "." {
// Not the default, verify it's a real directory
di, err := os.Stat(*dir)
if err == nil {
if !di.IsDir() {
fatal("Directory path is a not a directory: %s", *dir)
}
} else if os.IsNotExist(err) {
fatal("Directory does not exist: %s", *dir)
} else {
// Some other error - permissions for example
fatal("Couldn't access directory %s: %v", *dir, err)
}
}
// Validate input file
if *inputFilePath != "" {
fatalIsFile("input file", *inputFilePath)
}
// Set Proxy func for gemini client
var Proxy gemini.ProxyFunc = nil
if *socksHost != "" {
var auth *proxy.Auth = nil
if *socksUser != "" || *socksPass != "" {
auth = &proxy.Auth{*socksUser, *socksPass}
}
Proxy = gemsocks5.ProxyFunc(*socksHost, auth)
}
client := &gemini.Client{
Insecure: *insecure,
ConnectTimeout: time.Duration(*connectTimeout) * time.Second,
Proxy: Proxy,
}
var urls *scanner.Scanner
if *inputFilePath == "" {
urls = scanner.NewScanner(nil, flag.Args()...)
} else {
var err error
inputFile, err = os.Open(*inputFilePath)
if err != nil {
fatal("Issue opening input file: %v", err)
}
defer inputFile.Close()
urls = scanner.NewScanner(inputFile, flag.Args()...)
}
// Fetch each URL
for urls.Scan() {
if urls.Err() != nil {
urlError("URL couldn't be parsed: %v", urls.Err())
} else {
info("Started %s", urls.URL().String())
fetch(0, urls.URL(), client)
}
}
}