Skip to content

Commit 2c22f68

Browse files
committed
Enable to download in parallel
1 parent baca793 commit 2c22f68

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package downloader
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"path"
10+
11+
"github.com/gopherdojo/dojo3/kadai3-2/hioki-daichi/bytesranger"
12+
)
13+
14+
var errExist = errors.New("downloader: file already exists")
15+
16+
// Downloader has URL.
17+
type Downloader struct {
18+
// Usually, stdout is specified, and at the time of testing, buffer is specified.
19+
OutStream io.Writer
20+
21+
// URL represents the download URL.
22+
URL string
23+
24+
// Parallelism represents how many parallel downloads.
25+
Parallelism int
26+
}
27+
28+
// NewDownloader returns new Downloader.
29+
func NewDownloader(w io.Writer, url string) *Downloader {
30+
return &Downloader{OutStream: w, URL: url, Parallelism: 4} // TODO: Use flags instead of hard-coded 4
31+
}
32+
33+
// Download downloads the file with the specified URL.
34+
func (d *Downloader) Download() error {
35+
_, filename := path.Split(d.URL)
36+
if isFileExist(filename) {
37+
return errExist
38+
}
39+
40+
bytesrangeStrings, err := d.getBytesrangeStrings()
41+
if err != nil {
42+
return err
43+
}
44+
45+
ch := make(chan map[int]*http.Response)
46+
47+
// send to channels...
48+
for i, bytesrangeString := range bytesrangeStrings {
49+
i := i
50+
bytesrangeString := bytesrangeString
51+
go func() {
52+
resp, err := d.getHTTPResponseWithinRange(bytesrangeString)
53+
if err != nil {
54+
panic(err) // TODO: error handling
55+
}
56+
57+
fmt.Fprintf(d.OutStream, "ch snd [i: %d, ContentLength: %d, Range: %s]\n", i, resp.ContentLength, bytesrangeString)
58+
59+
ch <- map[int]*http.Response{i: resp}
60+
}()
61+
}
62+
63+
// receive channels...
64+
responses := make(map[int]*http.Response, 0)
65+
for i := 0; i < len(bytesrangeStrings); i++ {
66+
m := <-ch
67+
68+
for i, resp := range m {
69+
fmt.Fprintf(d.OutStream, "ch rcv [i: %d, ContentLength: %d]\n", i, resp.ContentLength)
70+
71+
responses[i] = resp
72+
}
73+
}
74+
75+
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
76+
if err != nil {
77+
return err
78+
}
79+
80+
// concat responses...
81+
for i := 0; i < len(responses); i++ {
82+
resp := responses[i]
83+
_, err := io.Copy(fp, resp.Body)
84+
if err != nil {
85+
os.Remove(filename)
86+
return err
87+
}
88+
}
89+
90+
fmt.Fprintf(d.OutStream, "Downloaded: %q\n", d.URL)
91+
92+
return nil
93+
}
94+
95+
func (d *Downloader) getBytesrangeStrings() ([]string, error) {
96+
resp, err := http.Head(d.URL)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
return bytesranger.Split(int(resp.ContentLength), d.Parallelism)
102+
}
103+
104+
func (d *Downloader) getHTTPResponseWithinRange(rangeString string) (*http.Response, error) {
105+
client := &http.Client{Timeout: 0}
106+
107+
req, err := http.NewRequest("GET", d.URL, nil)
108+
if err != nil {
109+
return &http.Response{}, err
110+
}
111+
112+
req.Header.Set("Range", rangeString)
113+
114+
return client.Do(req)
115+
}
116+
117+
func isFileExist(filename string) bool {
118+
_, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL, 0644)
119+
if os.IsExist(err) {
120+
return true
121+
}
122+
123+
// If the file does not exist, delete the garbage file.
124+
os.Remove(filename)
125+
126+
// If any other unexpected error occurs, then panic.
127+
if err != nil {
128+
panic(err)
129+
}
130+
131+
return false
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package downloader
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
"testing"
7+
)
8+
9+
func TestDownloader_Download_IsFileExist(t *testing.T) {
10+
t.Parallel()
11+
12+
_, err := os.Create("foo.txt")
13+
if err != nil {
14+
t.Fatalf("err %s", err)
15+
}
16+
defer os.Remove("foo.txt")
17+
18+
d := NewDownloader(ioutil.Discard, "https://example.com/foo.txt")
19+
actual := d.Download()
20+
expected := errExist
21+
if actual != expected {
22+
t.Errorf(`expected="%s" actual="%s"`, expected, actual)
23+
}
24+
}

kadai3-2/hioki-daichi/main.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"log"
6+
"os"
7+
8+
"github.com/gopherdojo/dojo3/kadai3-2/hioki-daichi/downloader"
9+
)
10+
11+
func main() {
12+
url := os.Args[1]
13+
err := execute(os.Stdout, url)
14+
if err != nil {
15+
log.Fatal(err)
16+
}
17+
}
18+
19+
func execute(w io.Writer, url string) error {
20+
d := downloader.NewDownloader(w, url)
21+
return d.Download()
22+
}

kadai3-2/hioki-daichi/main_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package main

0 commit comments

Comments
 (0)