From ee9ad8d27d7dbe97ce556bea3dfe340fed6f78c0 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 9 Apr 2021 08:41:35 +0900 Subject: [PATCH 01/38] Initialize Repository --- kadai3-2/misonog/.gitignore | 15 +++++++++++++++ kadai3-2/misonog/README.md | 8 ++++++++ kadai3-2/misonog/go.mod | 3 +++ 3 files changed, 26 insertions(+) create mode 100644 kadai3-2/misonog/.gitignore create mode 100644 kadai3-2/misonog/README.md create mode 100644 kadai3-2/misonog/go.mod diff --git a/kadai3-2/misonog/.gitignore b/kadai3-2/misonog/.gitignore new file mode 100644 index 00000000..f7b34922 --- /dev/null +++ b/kadai3-2/misonog/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ \ No newline at end of file diff --git a/kadai3-2/misonog/README.md b/kadai3-2/misonog/README.md new file mode 100644 index 00000000..9d8c8c92 --- /dev/null +++ b/kadai3-2/misonog/README.md @@ -0,0 +1,8 @@ +# 課題 3-2 【TRY】分割ダウンローダを作ろう + +- 分割ダウンロードを行う + - Range アクセスを用いる + - いくつかのゴルーチンでダウンロードしてマージする + - エラー処理を工夫する + - golang.org/x/sync/errgourp パッケージなどを使ってみる + - キャンセルが発生した場合の実装を行う diff --git a/kadai3-2/misonog/go.mod b/kadai3-2/misonog/go.mod new file mode 100644 index 00000000..bb84a805 --- /dev/null +++ b/kadai3-2/misonog/go.mod @@ -0,0 +1,3 @@ +module github.com/misonog/gopherdojo-studyroom/kadai3-2/misonog + +go 1.16 From cbddf50292582debf0b7524dee8e987021a429de Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Tue, 13 Apr 2021 09:03:45 +0900 Subject: [PATCH 02/38] Create Makefile --- kadai3-2/misonog/.gitignore | 6 +++++- kadai3-2/misonog/Makefile | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 kadai3-2/misonog/Makefile diff --git a/kadai3-2/misonog/.gitignore b/kadai3-2/misonog/.gitignore index f7b34922..b4b63240 100644 --- a/kadai3-2/misonog/.gitignore +++ b/kadai3-2/misonog/.gitignore @@ -4,6 +4,7 @@ *.dll *.so *.dylib +/pdownload # Test binary, built with `go test -c` *.test @@ -12,4 +13,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ \ No newline at end of file +# vendor/ + +# Test Data +testdata/ diff --git a/kadai3-2/misonog/Makefile b/kadai3-2/misonog/Makefile new file mode 100644 index 00000000..d5a36442 --- /dev/null +++ b/kadai3-2/misonog/Makefile @@ -0,0 +1,9 @@ +BINARY_NAME=pdownload + +all: build + +build: + go build -o $(BINARY_NAME) + +test: + go test -v ./ \ No newline at end of file From 88b2d43092f121e95732663a524006b712ed4873 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Tue, 13 Apr 2021 21:45:14 +0900 Subject: [PATCH 03/38] Add download func --- kadai3-2/misonog/main.go | 10 ++++++++++ kadai3-2/misonog/pdownload.go | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 kadai3-2/misonog/main.go create mode 100644 kadai3-2/misonog/pdownload.go diff --git a/kadai3-2/misonog/main.go b/kadai3-2/misonog/main.go new file mode 100644 index 00000000..c91792e5 --- /dev/null +++ b/kadai3-2/misonog/main.go @@ -0,0 +1,10 @@ +package main + +import "log" + +func main() { + url := "https://blog.golang.org/gopher/header.jpg" + if err := download("header.jpg", url); err != nil { + log.Fatal(err) + } +} diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go new file mode 100644 index 00000000..b8177dbe --- /dev/null +++ b/kadai3-2/misonog/pdownload.go @@ -0,0 +1,24 @@ +package main + +import ( + "io" + "net/http" + "os" +) + +func download(filename string, url string) error { + res, err := http.Get(url) + if err != nil { + return err + } + defer res.Body.Close() + + out, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return err + } + defer out.Close() + + io.Copy(out, res.Body) + return nil +} From 3af52b8fe7e49418702cb6e1f58d6d07f9a206b5 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Tue, 13 Apr 2021 22:29:47 +0900 Subject: [PATCH 04/38] Add target directory option --- kadai3-2/misonog/main.go | 19 ++++++++++++++++--- kadai3-2/misonog/pdownload.go | 7 +++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/kadai3-2/misonog/main.go b/kadai3-2/misonog/main.go index c91792e5..78923f98 100644 --- a/kadai3-2/misonog/main.go +++ b/kadai3-2/misonog/main.go @@ -1,10 +1,23 @@ package main -import "log" +import ( + "flag" + "log" + "os" +) func main() { - url := "https://blog.golang.org/gopher/header.jpg" - if err := download("header.jpg", url); err != nil { + pwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + var targetDir string + + flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") + flag.Parse() + + if err := download("header.jpg", targetDir, flag.Arg(0)); err != nil { log.Fatal(err) } } diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index b8177dbe..02348c07 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -1,19 +1,22 @@ package main import ( + "fmt" "io" "net/http" "os" ) -func download(filename string, url string) error { +func download(filename string, dirname string, url string) error { res, err := http.Get(url) if err != nil { return err } defer res.Body.Close() - out, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + filepath := fmt.Sprintf("%s/%s", dirname, filename) + + out, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return err } From 38f84cb7f558c1a5ac4d476201f10db5b9cfd1d0 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 18 Apr 2021 11:18:32 +0900 Subject: [PATCH 05/38] Add Data struct and func --- kadai3-2/misonog/util.go | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 kadai3-2/misonog/util.go diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go new file mode 100644 index 00000000..f7a47c75 --- /dev/null +++ b/kadai3-2/misonog/util.go @@ -0,0 +1,52 @@ +package main + +import "fmt" + +// Data struct has file of relational data +type Data struct { + filename string + filesize uint + dirname string + fullfilename string +} + +// FileName get from Data structs member +func (d Data) FileName() string { + return d.filename +} + +// FileSize get from Data structs member +func (d Data) FileSize() uint { + return d.filesize +} + +// DirName get from Data structs member +func (d Data) DirName() string { + return d.dirname +} + +// FullFileName get from Data structs member +func (d Data) FullFileName() string { + return d.fullfilename +} + +// SetFileName set to Data structs member +func (d *Data) SetFileName(filename string) { + d.filename = filename +} + +// SetFileSize set to Data structs member +func (d *Data) SetFileSize(size uint) { + d.filesize = size +} + +// SetFullFileName set to Data structs member +func (d *Data) SetFullFileName(dir, filename string) { + if dir == "" { + d.fullfilename = filename + } else { + d.fullfilename = fmt.Sprintf("%s/%s", dir, filename) + } +} + +// TODO: URLFileName()とSetDirName()の実装が必要か検討 From 588096f8e58f5a4c4f086d8499f6d039c9341244 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 18 Apr 2021 16:47:34 +0900 Subject: [PATCH 06/38] Add Utils interface --- kadai3-2/misonog/util.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index f7a47c75..f17bc7b9 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -1,6 +1,8 @@ package main -import "fmt" +import ( + "fmt" +) // Data struct has file of relational data type Data struct { @@ -10,6 +12,20 @@ type Data struct { fullfilename string } +// Utils interface indicate function +type Utils interface { + // like setter + SetFileName(string) + SetFileSize(uint) + SetFullFileName(string, string) + + // like getter + FileName() string + FileSize() uint + DirName() string + FullFileName() string +} + // FileName get from Data structs member func (d Data) FileName() string { return d.filename @@ -48,5 +64,3 @@ func (d *Data) SetFullFileName(dir, filename string) { d.fullfilename = fmt.Sprintf("%s/%s", dir, filename) } } - -// TODO: URLFileName()とSetDirName()の実装が必要か検討 From 8e744fc487cf336980d465261441268d7e88150d Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 18 Apr 2021 16:59:21 +0900 Subject: [PATCH 07/38] Add Check func --- kadai3-2/misonog/requests.go | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 kadai3-2/misonog/requests.go diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go new file mode 100644 index 00000000..27b006d6 --- /dev/null +++ b/kadai3-2/misonog/requests.go @@ -0,0 +1,56 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "path" +) + +// Check method check be able to range access. +func (p *Pdownload) Check() error { + res, err := http.Head(p.URL) + if err != nil { + return err + } + + if res.Header.Get("Accept-Ranges") != "bytes" { + return fmt.Errorf("not supported range access: %s", p.URL) + } + + if res.ContentLength <= 0 { + return errors.New("invalid content length") + } + + filename := p.Utils.FileName() + if filename == "" { + filename = path.Base(p.URL) + } + p.SetFileName(filename) + p.SetFullFileName(p.TargetDir, filename) + + p.SetFileSize(uint(res.ContentLength)) + + return nil +} + +func download(filename string, dirname string, url string) error { + res, err := http.Get(url) + if err != nil { + return err + } + defer res.Body.Close() + + filepath := fmt.Sprintf("%s/%s", dirname, filename) + + out, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return err + } + defer out.Close() + + io.Copy(out, res.Body) + return nil +} From e4ba981c99c24dd66c03cb837c9ce8650a36203d Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 18 Apr 2021 17:01:33 +0900 Subject: [PATCH 08/38] add pdownload constructor --- kadai3-2/misonog/pdownload.go | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 02348c07..9acc1a04 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -1,27 +1,14 @@ package main -import ( - "fmt" - "io" - "net/http" - "os" -) - -func download(filename string, dirname string, url string) error { - res, err := http.Get(url) - if err != nil { - return err - } - defer res.Body.Close() - - filepath := fmt.Sprintf("%s/%s", dirname, filename) +// Pdownload structs +type Pdownload struct { + Utils + URL string + TargetDir string +} - out, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - return err +func New() *Pdownload { + return &Pdownload{ + Utils: &Data{}, } - defer out.Close() - - io.Copy(out, res.Body) - return nil } From 84fe8b81c9947d114c8ed2d69a1c520dc65707e5 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Wed, 21 Apr 2021 08:06:58 +0900 Subject: [PATCH 09/38] Add pdownload test --- kadai3-2/misonog/pdownload_test.go | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 kadai3-2/misonog/pdownload_test.go diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go new file mode 100644 index 00000000..a1a0e932 --- /dev/null +++ b/kadai3-2/misonog/pdownload_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +var ts *httptest.Server + +func TestMain(m *testing.M) { + setUp() + code := m.Run() + tearDown() + os.Exit(code) +} + +func setUp() { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/header.jpg", http.StatusFound) + }) + + mux.HandleFunc("/header.jpg", func(w http.ResponseWriter, r *http.Request) { + fp := "testdata/header.jpg" + data, err := os.ReadFile(fp) + if err != nil { + panic(err) + } + http.ServeContent(w, r, fp, time.Now(), bytes.NewReader(data)) + }) + + ts = httptest.NewServer(mux) + // defer ts.Close() +} + +func tearDown() { + ts.Close() +} + +func TestCheck(t *testing.T) { + p := New() + p.URL = ts.URL + + if err := p.Check(); err != nil { + t.Errorf("failed to check header: %s", err) + } +} From 09d012a51a15b988b40d4b2f548668c4997f879c Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Wed, 21 Apr 2021 08:11:23 +0900 Subject: [PATCH 10/38] Change test file name --- kadai3-2/misonog/{pdownload_test.go => requests_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename kadai3-2/misonog/{pdownload_test.go => requests_test.go} (100%) diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/requests_test.go similarity index 100% rename from kadai3-2/misonog/pdownload_test.go rename to kadai3-2/misonog/requests_test.go From 3ff17b765c4725d0907db7b0100ed6c393cada51 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Wed, 21 Apr 2021 09:37:01 +0900 Subject: [PATCH 11/38] Add Go sync --- kadai3-2/misonog/go.mod | 2 ++ kadai3-2/misonog/go.sum | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 kadai3-2/misonog/go.sum diff --git a/kadai3-2/misonog/go.mod b/kadai3-2/misonog/go.mod index bb84a805..f9b531a6 100644 --- a/kadai3-2/misonog/go.mod +++ b/kadai3-2/misonog/go.mod @@ -1,3 +1,5 @@ module github.com/misonog/gopherdojo-studyroom/kadai3-2/misonog go 1.16 + +require golang.org/x/sync v0.0.0-20210220032951-036812b2e83c diff --git a/kadai3-2/misonog/go.sum b/kadai3-2/misonog/go.sum new file mode 100644 index 00000000..5c00efd3 --- /dev/null +++ b/kadai3-2/misonog/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 72d57bdb37a241c79e800c27058838095b74d427 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Wed, 21 Apr 2021 09:37:24 +0900 Subject: [PATCH 12/38] Add assignment func --- kadai3-2/misonog/pdownload.go | 6 ++ kadai3-2/misonog/requests.go | 100 ++++++++++++++++++++++++++++++++++ kadai3-2/misonog/util.go | 17 ++++++ 3 files changed, 123 insertions(+) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 9acc1a04..b0b75437 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -1,14 +1,20 @@ package main +import "runtime" + // Pdownload structs type Pdownload struct { Utils URL string TargetDir string + Procs int + useragent string + referer string } func New() *Pdownload { return &Pdownload{ Utils: &Data{}, + Procs: runtime.NumCPU(), // default } } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index 27b006d6..dde80369 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -7,8 +7,21 @@ import ( "net/http" "os" "path" + + "golang.org/x/sync/errgroup" ) +// Range struct for range access +type Range struct { + low uint + high uint + woker uint +} + +func isLastProc(i, procs uint) bool { + return i == procs-1 +} + // Check method check be able to range access. func (p *Pdownload) Check() error { res, err := http.Head(p.URL) @@ -36,6 +49,93 @@ func (p *Pdownload) Check() error { return nil } +func (p *Pdownload) Download() error { + // procs := uint(p.Procs) + // filesize := p.FileSize() + + // split := filesize / procs + // grp, ctx := errgroup.WithContext(context.Background()) + + return nil +} + +func (p Pdownload) Assignment(grp *errgroup.Group, procs, split uint) { + filename := p.FileName() + dirname := p.DirName() + + for i := uint(0); i < procs; i++ { + partName := fmt.Sprintf("%s/%s.%d.%d", dirname, filename, procs, i) + + // make range + r := p.Utils.MakeRange(i, split, procs) + if info, err := os.Stat(partName); err == nil { + infosize := uint(info.Size()) + // check if the part is fully downloaded + if isLastProc(i, procs) { + if infosize == r.high-r.low { + continue + } + } else if infosize == split { + continue + } + + // make low range from this next byte + r.low += infosize + } + + // execute get request + grp.Go(func() error { + return p.Requests(r, filename, dirname, p.URL) + }) + } + +} + +// Requests method will download the file +func (p Pdownload) Requests(r Range, filename, dirname, url string) error { + res, err := p.MakeResponse(r, url) + if err != nil { + return fmt.Errorf("failed to split get requests: %d", r.woker) + } + defer res.Body.Close() + + partName := fmt.Sprintf("%s/%s.%d.%d", dirname, filename, p.Procs, r.woker) + + output, err := os.OpenFile(partName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 06666) + if err != nil { + return fmt.Errorf("failed to create %s in %s", filename, dirname) + } + defer output.Close() + + io.Copy(output, res.Body) + + return nil +} + +// MakeResponse return *http.Respnse include context and range header +func (p Pdownload) MakeResponse(r Range, url string) (*http.Response, error) { + // create get request + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to split NewRequest for get: %d", r.woker) + } + + // set download ranges + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", r.low, r.high)) + + // set useragent + if p.useragent != "" { + req.Header.Set("User-Agent", p.useragent) + } + + // set referer + if p.referer != "" { + req.Header.Set("Referer", p.referer) + } + + return http.DefaultClient.Do(req) +} + func download(filename string, dirname string, url string) error { res, err := http.Get(url) if err != nil { diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index f17bc7b9..13166750 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -14,6 +14,8 @@ type Data struct { // Utils interface indicate function type Utils interface { + MakeRange(uint, uint, uint) Range + // like setter SetFileName(string) SetFileSize(uint) @@ -64,3 +66,18 @@ func (d *Data) SetFullFileName(dir, filename string) { d.fullfilename = fmt.Sprintf("%s/%s", dir, filename) } } + +// MakeRange will return Range struct to download function +func (d *Data) MakeRange(i, split, procs uint) Range { + low := split * i + high := low + split - 1 + if i == procs-1 { + high = d.FileSize() + } + + return Range{ + low: low, + high: high, + woker: i, + } +} From ad5b30e31ce2429af45fe24cf60cf90fa4bb0b86 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 23 Apr 2021 08:42:19 +0900 Subject: [PATCH 13/38] Add Download func --- kadai3-2/misonog/main.go | 6 ++--- kadai3-2/misonog/requests.go | 38 ++++++++++++------------------- kadai3-2/misonog/requests_test.go | 27 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/kadai3-2/misonog/main.go b/kadai3-2/misonog/main.go index 78923f98..b6ba43f2 100644 --- a/kadai3-2/misonog/main.go +++ b/kadai3-2/misonog/main.go @@ -17,7 +17,7 @@ func main() { flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") flag.Parse() - if err := download("header.jpg", targetDir, flag.Arg(0)); err != nil { - log.Fatal(err) - } + // if err := download("header.jpg", targetDir, flag.Arg(0)); err != nil { + // log.Fatal(err) + // } } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index dde80369..6e3172b9 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "io" @@ -49,12 +50,22 @@ func (p *Pdownload) Check() error { return nil } +// Download method distributes the task to each goroutine func (p *Pdownload) Download() error { - // procs := uint(p.Procs) - // filesize := p.FileSize() + procs := uint(p.Procs) + filesize := p.FileSize() - // split := filesize / procs - // grp, ctx := errgroup.WithContext(context.Background()) + // calculate split file size + split := filesize / procs + + grp, _ := errgroup.WithContext(context.Background()) + + p.Assignment(grp, procs, split) + + // wait for Assignment method + if err := grp.Wait(); err != nil { + return err + } return nil } @@ -135,22 +146,3 @@ func (p Pdownload) MakeResponse(r Range, url string) (*http.Response, error) { return http.DefaultClient.Do(req) } - -func download(filename string, dirname string, url string) error { - res, err := http.Get(url) - if err != nil { - return err - } - defer res.Body.Close() - - filepath := fmt.Sprintf("%s/%s", dirname, filename) - - out, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - return err - } - defer out.Close() - - io.Copy(out, res.Body) - return nil -} diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index a1a0e932..16ecd6e3 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "fmt" "net/http" "net/http/httptest" "os" @@ -34,7 +35,6 @@ func setUp() { }) ts = httptest.NewServer(mux) - // defer ts.Close() } func tearDown() { @@ -49,3 +49,28 @@ func TestCheck(t *testing.T) { t.Errorf("failed to check header: %s", err) } } + +func TestDownload(t *testing.T) { + p := New() + p.URL = ts.URL + p.Utils = &Data{ + filename: "header.jpg", + dirname: "testdata/test_download", + } + + if err := p.Check(); err != nil { + t.Errorf("failed to check header: %s", err) + } + + if err := p.Download(); err != nil { + t.Errorf("failed to download: %s", err) + } + + for i := 0; i < p.Procs; i++ { + filename := fmt.Sprintf("testdata/test_download/header.jpg.%d.%d", p.Procs, i) + _, err := os.Stat(filename) + if err != nil { + t.Errorf("file not exist: %s", err) + } + } +} From 54f2a64f8458eace9b1c08486930781d6a5a8bbb Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 23 Apr 2021 20:44:47 +0900 Subject: [PATCH 14/38] Add MergeFiles method --- kadai3-2/misonog/requests_test.go | 15 ++++++++++++++ kadai3-2/misonog/util.go | 33 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index 16ecd6e3..62355bf4 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -74,3 +74,18 @@ func TestDownload(t *testing.T) { } } } + +// utils.goにあるメソッドをテストするのは違和感があるがこのファイルの中でテストを行う +func TestMergeFiles(t *testing.T) { + p := New() + p.URL = ts.URL + p.Utils = &Data{ + filename: "header.jpg", + dirname: "testdata/test_download", + fullfilename: "testdata/test_download/header.jpg", + } + + if err := p.MergeFiles(p.Procs); err != nil { + t.Errorf("failed to MergeFiles: %s", err) + } +} diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 13166750..02ad98a7 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "io" + "os" ) // Data struct has file of relational data @@ -15,6 +17,7 @@ type Data struct { // Utils interface indicate function type Utils interface { MakeRange(uint, uint, uint) Range + MergeFiles(int) error // like setter SetFileName(string) @@ -81,3 +84,33 @@ func (d *Data) MakeRange(i, split, procs uint) Range { woker: i, } } + +// MergeFiles function merege file after split download +func (d *Data) MergeFiles(procs int) error { + filename := d.filename + dirname := d.dirname + + mergefile, err := os.Create(d.fullfilename) + if err != nil { + return err + } + defer mergefile.Close() + + var f string + for i := 0; i < procs; i++ { + f = fmt.Sprintf("%s/%s.%d.%d", dirname, filename, procs, i) + subfp, err := os.Open(f) + if err != nil { + return err + } + + io.Copy(mergefile, subfp) + subfp.Close() + + if err := os.Remove(f); err != nil { + return err + } + } + + return nil +} From 06646062566f8f91c067171bd22bb61d64266236 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Tue, 27 Apr 2021 21:43:15 +0900 Subject: [PATCH 15/38] Add parseURL func --- kadai3-2/misonog/pdownload.go | 41 +++++++++++++++++++++++++++++- kadai3-2/misonog/pdownload_test.go | 30 ++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 kadai3-2/misonog/pdownload_test.go diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index b0b75437..69ce8b37 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -1,6 +1,10 @@ package main -import "runtime" +import ( + "errors" + "net/url" + "runtime" +) // Pdownload structs type Pdownload struct { @@ -18,3 +22,38 @@ func New() *Pdownload { Procs: runtime.NumCPU(), // default } } + +func (pdownload *Pdownload) Run() error { + if err := pdownload.Check(); err != nil { + return err + } + + if err := pdownload.Download(); err != nil { + return err + } + + if err := pdownload.Utils.MergeFiles(pdownload.Procs); err != nil { + return err + } + + return nil +} + +func (pdownload *Pdownload) parseURL(args []string) error { + if len(args) > 1 { + return errors.New("URL must be a single") + } + if len(args) < 1 { + return errors.New("urls not found in the arguments passed") + } + + for _, arg := range args { + _, err := url.ParseRequestURI(arg) + if err != nil { + return err + } + pdownload.URL = arg + } + + return nil +} diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go new file mode 100644 index 00000000..59e32ad0 --- /dev/null +++ b/kadai3-2/misonog/pdownload_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "testing" +) + +func TestParseURL(t *testing.T) { + + cases := []struct { + name string + input []string + expected string + }{ + {name: "an URL", input: []string{"https://www.google.com/"}, expected: "https://www.google.com/"}, + {name: "URLs", input: []string{"https://www.google.com/", "https://golang.org/"}, expected: ""}, + {name: "invalid URL", input: []string{"invalid_url"}, expected: ""}, + } + + for _, c := range cases { + c := c + p := New() + t.Run(c.name, func(t *testing.T) { + _ = p.parseURL(c.input) + if actual := p.URL; c.expected != actual { + t.Errorf("want p.URL = %v, got %v", + c.expected, actual) + } + }) + } +} From 7005d937ab6b5fc139a891d69a290fe437368802 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Wed, 28 Apr 2021 16:11:05 +0900 Subject: [PATCH 16/38] CLI setup --- kadai3-2/misonog/Makefile | 2 +- kadai3-2/misonog/main.go | 19 +++---------------- kadai3-2/misonog/pdownload.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/kadai3-2/misonog/Makefile b/kadai3-2/misonog/Makefile index d5a36442..79777ea9 100644 --- a/kadai3-2/misonog/Makefile +++ b/kadai3-2/misonog/Makefile @@ -1,6 +1,6 @@ BINARY_NAME=pdownload -all: build +all: test build build: go build -o $(BINARY_NAME) diff --git a/kadai3-2/misonog/main.go b/kadai3-2/misonog/main.go index b6ba43f2..208b46a7 100644 --- a/kadai3-2/misonog/main.go +++ b/kadai3-2/misonog/main.go @@ -1,23 +1,10 @@ package main -import ( - "flag" - "log" - "os" -) +import "log" func main() { - pwd, err := os.Getwd() - if err != nil { + cli := New() + if err := cli.Run(); err != nil { log.Fatal(err) } - - var targetDir string - - flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") - flag.Parse() - - // if err := download("header.jpg", targetDir, flag.Arg(0)); err != nil { - // log.Fatal(err) - // } } diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 69ce8b37..dbac75f9 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -2,7 +2,10 @@ package main import ( "errors" + "flag" + "fmt" "net/url" + "os" "runtime" ) @@ -24,6 +27,10 @@ func New() *Pdownload { } func (pdownload *Pdownload) Run() error { + if err := pdownload.Ready(); err != nil { + return err + } + if err := pdownload.Check(); err != nil { return err } @@ -39,6 +46,29 @@ func (pdownload *Pdownload) Run() error { return nil } +func (pdownload *Pdownload) Ready() error { + var targetDir string + + pwd, err := os.Getwd() + if err != nil { + return err + } + + flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") + flag.Parse() + + if err := pdownload.parseURL(flag.Args()); err != nil { + return err + } + + if _, err := os.Stat(targetDir); os.IsNotExist(err) { + return fmt.Errorf("target directory is not exist: %v", err) + } + pdownload.TargetDir = targetDir + + return nil +} + func (pdownload *Pdownload) parseURL(args []string) error { if len(args) > 1 { return errors.New("URL must be a single") From ba7f9716a6db255f12625e0e2723e6228e513d4d Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 30 Apr 2021 10:45:45 +0900 Subject: [PATCH 17/38] Add SetDirName func --- kadai3-2/misonog/requests.go | 1 + kadai3-2/misonog/requests_test.go | 2 +- kadai3-2/misonog/util.go | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index 6e3172b9..7f81c0cc 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -44,6 +44,7 @@ func (p *Pdownload) Check() error { } p.SetFileName(filename) p.SetFullFileName(p.TargetDir, filename) + p.Utils.SetDirName(p.TargetDir) p.SetFileSize(uint(res.ContentLength)) diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index 62355bf4..8029d3cf 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -53,9 +53,9 @@ func TestCheck(t *testing.T) { func TestDownload(t *testing.T) { p := New() p.URL = ts.URL + p.TargetDir = "testdata/test_download" p.Utils = &Data{ filename: "header.jpg", - dirname: "testdata/test_download", } if err := p.Check(); err != nil { diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 02ad98a7..438865df 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -22,6 +22,7 @@ type Utils interface { // like setter SetFileName(string) SetFileSize(uint) + SetDirName(string) SetFullFileName(string, string) // like getter @@ -61,6 +62,11 @@ func (d *Data) SetFileSize(size uint) { d.filesize = size } +// SetDirName set to Data structs member +func (d *Data) SetDirName(dir string) { + d.dirname = dir +} + // SetFullFileName set to Data structs member func (d *Data) SetFullFileName(dir, filename string) { if dir == "" { From 6f18cc2cc1f8faa0727303cee1c9ef948800046d Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 30 Apr 2021 11:58:39 +0900 Subject: [PATCH 18/38] Add run test --- kadai3-2/misonog/pdownload_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go index 59e32ad0..8a1cf9a8 100644 --- a/kadai3-2/misonog/pdownload_test.go +++ b/kadai3-2/misonog/pdownload_test.go @@ -1,9 +1,32 @@ package main import ( + "fmt" + "os" "testing" ) +// requests_test.goで作成しているテストサーバを利用してテストを行う +func TestRun(t *testing.T) { + url := ts.URL + + os.Args = []string{ + "pdownload", + "-d", + "testdata/test_download", + fmt.Sprintf("%s/%s", url, "header.jpg"), + } + + p := New() + if err := p.Run(); err != nil { + t.Errorf("failed to Run: %s", err) + } + + if err := os.Remove(p.FullFileName()); err != nil { + t.Errorf("failed to remove of result file: %s", err) + } +} + func TestParseURL(t *testing.T) { cases := []struct { From 4ccd9be2e71bf9d25476f98133dd04019e8c7003 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 30 Apr 2021 15:11:10 +0900 Subject: [PATCH 19/38] Add tempdir for download --- kadai3-2/misonog/pdownload.go | 8 +++++++- kadai3-2/misonog/requests.go | 4 ++-- kadai3-2/misonog/requests_test.go | 33 +++++++++++++++++++++++++------ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index dbac75f9..79c35f3e 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -31,7 +31,13 @@ func (pdownload *Pdownload) Run() error { return err } - if err := pdownload.Check(); err != nil { + dir, err := os.MkdirTemp(pdownload.TargetDir, "") + if err != nil { + return err + } + defer os.RemoveAll(dir) + + if err := pdownload.Check(dir); err != nil { return err } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index 7f81c0cc..a9970e23 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -24,7 +24,7 @@ func isLastProc(i, procs uint) bool { } // Check method check be able to range access. -func (p *Pdownload) Check() error { +func (p *Pdownload) Check(dir string) error { res, err := http.Head(p.URL) if err != nil { return err @@ -44,7 +44,7 @@ func (p *Pdownload) Check() error { } p.SetFileName(filename) p.SetFullFileName(p.TargetDir, filename) - p.Utils.SetDirName(p.TargetDir) + p.Utils.SetDirName(dir) p.SetFileSize(uint(res.ContentLength)) diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index 8029d3cf..ad1a60d8 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -10,7 +10,13 @@ import ( "time" ) -var ts *httptest.Server +const TESTDIR = "testdata/test_download" + +var ( + dir string + mkdirErr error + ts *httptest.Server +) func TestMain(m *testing.M) { setUp() @@ -35,17 +41,23 @@ func setUp() { }) ts = httptest.NewServer(mux) + + dir, mkdirErr = os.MkdirTemp(TESTDIR, "") + if mkdirErr != nil { + panic(mkdirErr) + } } func tearDown() { ts.Close() + os.RemoveAll(dir) } func TestCheck(t *testing.T) { p := New() p.URL = ts.URL - if err := p.Check(); err != nil { + if err := p.Check(dir); err != nil { t.Errorf("failed to check header: %s", err) } } @@ -53,12 +65,12 @@ func TestCheck(t *testing.T) { func TestDownload(t *testing.T) { p := New() p.URL = ts.URL - p.TargetDir = "testdata/test_download" + p.TargetDir = TESTDIR p.Utils = &Data{ filename: "header.jpg", } - if err := p.Check(); err != nil { + if err := p.Check(dir); err != nil { t.Errorf("failed to check header: %s", err) } @@ -67,7 +79,8 @@ func TestDownload(t *testing.T) { } for i := 0; i < p.Procs; i++ { - filename := fmt.Sprintf("testdata/test_download/header.jpg.%d.%d", p.Procs, i) + // filename := fmt.Sprintf("testdata/test_download/header.jpg.%d.%d", p.Procs, i) + filename := fmt.Sprintf(p.DirName()+"/header.jpg.%d.%d", p.Procs, i) _, err := os.Stat(filename) if err != nil { t.Errorf("file not exist: %s", err) @@ -79,12 +92,20 @@ func TestDownload(t *testing.T) { func TestMergeFiles(t *testing.T) { p := New() p.URL = ts.URL + p.TargetDir = TESTDIR p.Utils = &Data{ filename: "header.jpg", - dirname: "testdata/test_download", fullfilename: "testdata/test_download/header.jpg", } + if err := p.Check(dir); err != nil { + t.Errorf("failed to check header: %s", err) + } + + if err := p.Download(); err != nil { + t.Errorf("failed to download: %s", err) + } + if err := p.MergeFiles(p.Procs); err != nil { t.Errorf("failed to MergeFiles: %s", err) } From 01395e945a22e6e52e39dd93843edec3111327ed Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 30 Apr 2021 15:36:51 +0900 Subject: [PATCH 20/38] Add termination package for cancel process --- kadai3-2/misonog/Makefile | 2 +- kadai3-2/misonog/termination/termination.go | 37 +++++++++++++++++++ .../misonog/termination/termination_test.go | 36 ++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 kadai3-2/misonog/termination/termination.go create mode 100644 kadai3-2/misonog/termination/termination_test.go diff --git a/kadai3-2/misonog/Makefile b/kadai3-2/misonog/Makefile index 79777ea9..28cb0153 100644 --- a/kadai3-2/misonog/Makefile +++ b/kadai3-2/misonog/Makefile @@ -6,4 +6,4 @@ build: go build -o $(BINARY_NAME) test: - go test -v ./ \ No newline at end of file + go test -v ./... diff --git a/kadai3-2/misonog/termination/termination.go b/kadai3-2/misonog/termination/termination.go new file mode 100644 index 00000000..7ca037a8 --- /dev/null +++ b/kadai3-2/misonog/termination/termination.go @@ -0,0 +1,37 @@ +package termination + +import ( + "context" + "fmt" + "io" + "os" + "os/signal" + "syscall" +) + +var cleanFns []func() +var osExit = os.Exit + +// Listen listens signal +func Listen(ctx context.Context, w io.Writer) (context.Context, func()) { + ctx, cancel := context.WithCancel(ctx) + + ch := make(chan os.Signal, 2) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + go func() { + <-ch + fmt.Fprintln(w, "Ctrl+C pressed in Terminal") + cancel() + for _, f := range cleanFns { + f() + } + osExit(0) + }() + + return ctx, cancel +} + +// CleanFunc registers clean function +func CleanFunc(f func()) { + cleanFns = append(cleanFns, f) +} diff --git a/kadai3-2/misonog/termination/termination_test.go b/kadai3-2/misonog/termination/termination_test.go new file mode 100644 index 00000000..c9eeeadb --- /dev/null +++ b/kadai3-2/misonog/termination/termination_test.go @@ -0,0 +1,36 @@ +package termination + +import ( + "context" + "io" + "os" + "testing" + "time" +) + +func TestListen(t *testing.T) { + CleanFunc(func() {}) + + doneCh := make(chan struct{}) + osExit = func(code int) { doneCh <- struct{}{} } + + _, clean := Listen(context.Background(), io.Discard) + defer clean() + + proc, err := os.FindProcess(os.Getpid()) + if err != nil { + t.Fatal(err) + } + + err = proc.Signal(os.Interrupt) + if err != nil { + t.Fatal(err) + } + + select { + case <-doneCh: + return + case <-time.After(100 * time.Millisecond): + t.Fatal("timeout") + } +} From 004ee1e759eb8fba80ac73ff7893e055d40de45b Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 30 Apr 2021 16:17:38 +0900 Subject: [PATCH 21/38] Add context for Ctrl + c --- kadai3-2/misonog/pdownload.go | 17 ++++++++++++++--- kadai3-2/misonog/requests.go | 17 ++++++++++------- kadai3-2/misonog/requests_test.go | 13 ++++++++----- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 79c35f3e..6c5281af 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -1,12 +1,15 @@ package main import ( + "context" "errors" "flag" "fmt" "net/url" "os" "runtime" + + "github.com/misonog/gopherdojo-studyroom/kadai3-2/misonog/termination" ) // Pdownload structs @@ -27,6 +30,11 @@ func New() *Pdownload { } func (pdownload *Pdownload) Run() error { + ctx := context.Background() + + ctx, clean := termination.Listen(ctx, os.Stdout) + defer clean() + if err := pdownload.Ready(); err != nil { return err } @@ -35,13 +43,16 @@ func (pdownload *Pdownload) Run() error { if err != nil { return err } - defer os.RemoveAll(dir) + clean = func() { os.RemoveAll(dir) } + defer clean() + termination.CleanFunc(clean) - if err := pdownload.Check(dir); err != nil { + ctx, err = pdownload.Check(ctx, dir) + if err != nil { return err } - if err := pdownload.Download(); err != nil { + if err := pdownload.Download(ctx); err != nil { return err } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index a9970e23..0badd2b4 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -24,18 +24,21 @@ func isLastProc(i, procs uint) bool { } // Check method check be able to range access. -func (p *Pdownload) Check(dir string) error { +func (p *Pdownload) Check(ctx context.Context, dir string) (context.Context, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + res, err := http.Head(p.URL) if err != nil { - return err + return nil, err } if res.Header.Get("Accept-Ranges") != "bytes" { - return fmt.Errorf("not supported range access: %s", p.URL) + return nil, fmt.Errorf("not supported range access: %s", p.URL) } if res.ContentLength <= 0 { - return errors.New("invalid content length") + return nil, errors.New("invalid content length") } filename := p.Utils.FileName() @@ -48,18 +51,18 @@ func (p *Pdownload) Check(dir string) error { p.SetFileSize(uint(res.ContentLength)) - return nil + return ctx, nil } // Download method distributes the task to each goroutine -func (p *Pdownload) Download() error { +func (p *Pdownload) Download(ctx context.Context) error { procs := uint(p.Procs) filesize := p.FileSize() // calculate split file size split := filesize / procs - grp, _ := errgroup.WithContext(context.Background()) + grp, _ := errgroup.WithContext(ctx) p.Assignment(grp, procs, split) diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index ad1a60d8..3e325582 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "fmt" "net/http" "net/http/httptest" @@ -57,7 +58,7 @@ func TestCheck(t *testing.T) { p := New() p.URL = ts.URL - if err := p.Check(dir); err != nil { + if _, err := p.Check(context.Background(), dir); err != nil { t.Errorf("failed to check header: %s", err) } } @@ -70,11 +71,12 @@ func TestDownload(t *testing.T) { filename: "header.jpg", } - if err := p.Check(dir); err != nil { + ctx, err := p.Check(context.Background(), dir) + if err != nil { t.Errorf("failed to check header: %s", err) } - if err := p.Download(); err != nil { + if err := p.Download(ctx); err != nil { t.Errorf("failed to download: %s", err) } @@ -98,11 +100,12 @@ func TestMergeFiles(t *testing.T) { fullfilename: "testdata/test_download/header.jpg", } - if err := p.Check(dir); err != nil { + ctx, err := p.Check(context.Background(), dir) + if err != nil { t.Errorf("failed to check header: %s", err) } - if err := p.Download(); err != nil { + if err := p.Download(ctx); err != nil { t.Errorf("failed to download: %s", err) } From bdada631ed7e4501dd2ebd69dc8940b6e99db6ed Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 30 Apr 2021 16:40:12 +0900 Subject: [PATCH 22/38] Add timeout setting --- kadai3-2/misonog/pdownload.go | 11 +++++++++-- kadai3-2/misonog/pdownload_test.go | 2 ++ kadai3-2/misonog/requests.go | 3 ++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 6c5281af..cdc14c77 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -12,20 +12,24 @@ import ( "github.com/misonog/gopherdojo-studyroom/kadai3-2/misonog/termination" ) +const TIMEOUT = 10 + // Pdownload structs type Pdownload struct { Utils URL string TargetDir string Procs int + timeout int useragent string referer string } func New() *Pdownload { return &Pdownload{ - Utils: &Data{}, - Procs: runtime.NumCPU(), // default + Utils: &Data{}, + Procs: runtime.NumCPU(), // default + timeout: TIMEOUT, } } @@ -65,6 +69,7 @@ func (pdownload *Pdownload) Run() error { func (pdownload *Pdownload) Ready() error { var targetDir string + var timeout int pwd, err := os.Getwd() if err != nil { @@ -72,6 +77,7 @@ func (pdownload *Pdownload) Ready() error { } flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") + flag.IntVar(&timeout, "t", TIMEOUT, "timeout of checking request in seconds") flag.Parse() if err := pdownload.parseURL(flag.Args()); err != nil { @@ -82,6 +88,7 @@ func (pdownload *Pdownload) Ready() error { return fmt.Errorf("target directory is not exist: %v", err) } pdownload.TargetDir = targetDir + pdownload.timeout = timeout return nil } diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go index 8a1cf9a8..1d3c174d 100644 --- a/kadai3-2/misonog/pdownload_test.go +++ b/kadai3-2/misonog/pdownload_test.go @@ -14,6 +14,8 @@ func TestRun(t *testing.T) { "pdownload", "-d", "testdata/test_download", + "-t", + "30", fmt.Sprintf("%s/%s", url, "header.jpg"), } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index 0badd2b4..efefd34f 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path" + "time" "golang.org/x/sync/errgroup" ) @@ -25,7 +26,7 @@ func isLastProc(i, procs uint) bool { // Check method check be able to range access. func (p *Pdownload) Check(ctx context.Context, dir string) (context.Context, error) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithTimeout(ctx, time.Duration(p.timeout)*time.Second) defer cancel() res, err := http.Head(p.URL) From c0d57138ad3f2bd06e3369c9131a5cf007a01503 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Fri, 30 Apr 2021 16:53:47 +0900 Subject: [PATCH 23/38] Add testdata and Update README --- kadai3-2/misonog/.gitignore | 3 - kadai3-2/misonog/README.md | 55 +++++++++++++++++- kadai3-2/misonog/testdata/header.jpg | Bin 0 -> 35704 bytes .../misonog/testdata/test_download/header.jpg | Bin 0 -> 35704 bytes 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 kadai3-2/misonog/testdata/header.jpg create mode 100644 kadai3-2/misonog/testdata/test_download/header.jpg diff --git a/kadai3-2/misonog/.gitignore b/kadai3-2/misonog/.gitignore index b4b63240..9e03bbfa 100644 --- a/kadai3-2/misonog/.gitignore +++ b/kadai3-2/misonog/.gitignore @@ -14,6 +14,3 @@ # Dependency directories (remove the comment below to include it) # vendor/ - -# Test Data -testdata/ diff --git a/kadai3-2/misonog/README.md b/kadai3-2/misonog/README.md index 9d8c8c92..80e18aa8 100644 --- a/kadai3-2/misonog/README.md +++ b/kadai3-2/misonog/README.md @@ -1,4 +1,6 @@ -# 課題 3-2 【TRY】分割ダウンローダを作ろう +# 分割ダウンローダ + +## 仕様 - 分割ダウンロードを行う - Range アクセスを用いる @@ -6,3 +8,54 @@ - エラー処理を工夫する - golang.org/x/sync/errgourp パッケージなどを使ってみる - キャンセルが発生した場合の実装を行う + +## オプション + +| オプション | 内容 | デフォルト | +| ---------- | -------------------------------------- | ---------- | +| -d | ファイルをダウンロードするディレクトリ | $PWD | +| -t | タイムアウトするまでの時間(秒) | 10 | + +## 利用方法 + +### setup + +```shell +$ make # テスト & ビルド +``` + +### ダウンロードコマンドの例 + +```shell +$ ./pdownload https://blog.golang.org/gopher/header.jpg +$ # ディレクトリとタイムアウトまでの時間の指定 +$ ./pdownload -d testdata/ -t 30 https://blog.golang.org/gopher/header.jpg +``` + +## ディレクトリ構造 + +``` +. +├── Makefile +├── README.md +├── go.mod +├── go.sum +├── main.go +├── pdownload +├── pdownload.go +├── pdownload_test.go +├── requests.go +├── requests_test.go +├── termination +│ ├── termination.go +│ └── termination_test.go +├── testdata +│ ├── header.jpg +│ └── test_download +│ └── header.jpg +└── util.go +``` + +## 参考 + +[Code-Hex/pget](https://github.com/Code-Hex/pget)と[gopherdojo/dojo3#50](https://github.com/gopherdojo/dojo3/pull/50)を参考にさせていただきました。 diff --git a/kadai3-2/misonog/testdata/header.jpg b/kadai3-2/misonog/testdata/header.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bcf63e98766b8e8a539eab0eb207b24b0df574e9 GIT binary patch literal 35704 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 literal 0 HcmV?d00001 diff --git a/kadai3-2/misonog/testdata/test_download/header.jpg b/kadai3-2/misonog/testdata/test_download/header.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bcf63e98766b8e8a539eab0eb207b24b0df574e9 GIT binary patch literal 35704 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 literal 0 HcmV?d00001 From 230579e5ef10c02298570e61b84547845e94a4f2 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 2 May 2021 13:23:51 +0900 Subject: [PATCH 24/38] Update flag package parse to main function --- kadai3-2/misonog/main.go | 20 ++++++++++++++++++-- kadai3-2/misonog/pdownload.go | 21 ++++----------------- kadai3-2/misonog/pdownload_test.go | 14 ++++---------- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/kadai3-2/misonog/main.go b/kadai3-2/misonog/main.go index 208b46a7..41c67532 100644 --- a/kadai3-2/misonog/main.go +++ b/kadai3-2/misonog/main.go @@ -1,10 +1,26 @@ package main -import "log" +import ( + "flag" + "log" + "os" +) func main() { + var targetDir string + var timeout int + + pwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") + flag.IntVar(&timeout, "t", TIMEOUT, "timeout of checking request in seconds") + flag.Parse() + cli := New() - if err := cli.Run(); err != nil { + if err := cli.Run(flag.Args(), targetDir, timeout); err != nil { log.Fatal(err) } } diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index cdc14c77..ed67eb36 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -3,7 +3,6 @@ package main import ( "context" "errors" - "flag" "fmt" "net/url" "os" @@ -33,13 +32,13 @@ func New() *Pdownload { } } -func (pdownload *Pdownload) Run() error { +func (pdownload *Pdownload) Run(args []string, targetDir string, timeout int) error { ctx := context.Background() ctx, clean := termination.Listen(ctx, os.Stdout) defer clean() - if err := pdownload.Ready(); err != nil { + if err := pdownload.Ready(args, targetDir, timeout); err != nil { return err } @@ -67,20 +66,8 @@ func (pdownload *Pdownload) Run() error { return nil } -func (pdownload *Pdownload) Ready() error { - var targetDir string - var timeout int - - pwd, err := os.Getwd() - if err != nil { - return err - } - - flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") - flag.IntVar(&timeout, "t", TIMEOUT, "timeout of checking request in seconds") - flag.Parse() - - if err := pdownload.parseURL(flag.Args()); err != nil { +func (pdownload *Pdownload) Ready(args []string, targetDir string, timeout int) error { + if err := pdownload.parseURL(args); err != nil { return err } diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go index 1d3c174d..a821dede 100644 --- a/kadai3-2/misonog/pdownload_test.go +++ b/kadai3-2/misonog/pdownload_test.go @@ -9,18 +9,12 @@ import ( // requests_test.goで作成しているテストサーバを利用してテストを行う func TestRun(t *testing.T) { url := ts.URL - - os.Args = []string{ - "pdownload", - "-d", - "testdata/test_download", - "-t", - "30", - fmt.Sprintf("%s/%s", url, "header.jpg"), - } + args := []string{fmt.Sprintf("%s/%s", url, "header.jpg")} + targetDir := "testdata/test_download" + timeout := 30 p := New() - if err := p.Run(); err != nil { + if err := p.Run(args, targetDir, timeout); err != nil { t.Errorf("failed to Run: %s", err) } From 57fee38eb73c0f09ee5d285d8cbedd057a75b174 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 2 May 2021 13:39:46 +0900 Subject: [PATCH 25/38] Update timeout from Int type to time.Duration type --- kadai3-2/misonog/main.go | 7 +++++-- kadai3-2/misonog/pdownload.go | 11 +++++------ kadai3-2/misonog/pdownload_test.go | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/kadai3-2/misonog/main.go b/kadai3-2/misonog/main.go index 41c67532..2607fee9 100644 --- a/kadai3-2/misonog/main.go +++ b/kadai3-2/misonog/main.go @@ -4,11 +4,14 @@ import ( "flag" "log" "os" + "time" ) +const timeout = 10 * time.Second + func main() { var targetDir string - var timeout int + var timeout time.Duration pwd, err := os.Getwd() if err != nil { @@ -16,7 +19,7 @@ func main() { } flag.StringVar(&targetDir, "d", pwd, "path to the directory to save the downloaded file, filename will be taken from url") - flag.IntVar(&timeout, "t", TIMEOUT, "timeout of checking request in seconds") + flag.DurationVar(&timeout, "t", timeout, "timeout of checking request in seconds") flag.Parse() cli := New() diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index ed67eb36..bb05d7b6 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -7,19 +7,18 @@ import ( "net/url" "os" "runtime" + "time" "github.com/misonog/gopherdojo-studyroom/kadai3-2/misonog/termination" ) -const TIMEOUT = 10 - // Pdownload structs type Pdownload struct { Utils URL string TargetDir string Procs int - timeout int + timeout time.Duration useragent string referer string } @@ -28,11 +27,11 @@ func New() *Pdownload { return &Pdownload{ Utils: &Data{}, Procs: runtime.NumCPU(), // default - timeout: TIMEOUT, + timeout: timeout, } } -func (pdownload *Pdownload) Run(args []string, targetDir string, timeout int) error { +func (pdownload *Pdownload) Run(args []string, targetDir string, timeout time.Duration) error { ctx := context.Background() ctx, clean := termination.Listen(ctx, os.Stdout) @@ -66,7 +65,7 @@ func (pdownload *Pdownload) Run(args []string, targetDir string, timeout int) er return nil } -func (pdownload *Pdownload) Ready(args []string, targetDir string, timeout int) error { +func (pdownload *Pdownload) Ready(args []string, targetDir string, timeout time.Duration) error { if err := pdownload.parseURL(args); err != nil { return err } diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go index a821dede..431e0d76 100644 --- a/kadai3-2/misonog/pdownload_test.go +++ b/kadai3-2/misonog/pdownload_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "testing" + "time" ) // requests_test.goで作成しているテストサーバを利用してテストを行う @@ -11,7 +12,7 @@ func TestRun(t *testing.T) { url := ts.URL args := []string{fmt.Sprintf("%s/%s", url, "header.jpg")} targetDir := "testdata/test_download" - timeout := 30 + timeout := 30 * time.Second p := New() if err := p.Run(args, targetDir, timeout); err != nil { From d34d94edf7c2a12cb83f004df71a61f9f41429eb Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 2 May 2021 13:53:54 +0900 Subject: [PATCH 26/38] Fix error handling --- kadai3-2/misonog/pdownload.go | 2 +- kadai3-2/misonog/requests.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index bb05d7b6..db5fcc46 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -71,7 +71,7 @@ func (pdownload *Pdownload) Ready(args []string, targetDir string, timeout time. } if _, err := os.Stat(targetDir); os.IsNotExist(err) { - return fmt.Errorf("target directory is not exist: %v", err) + return fmt.Errorf("target directory is not exist: %w", err) } pdownload.TargetDir = targetDir pdownload.timeout = timeout diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index efefd34f..411ab428 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -123,7 +123,9 @@ func (p Pdownload) Requests(r Range, filename, dirname, url string) error { } defer output.Close() - io.Copy(output, res.Body) + if _, err := io.Copy(output, res.Body); err != nil { + return err + } return nil } From 40aaf9338dbfee9ba9b0a43a460a7d307ff9f31b Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 2 May 2021 15:07:11 +0900 Subject: [PATCH 27/38] Update context in main function --- kadai3-2/misonog/main.go | 5 ++++- kadai3-2/misonog/pdownload.go | 4 +--- kadai3-2/misonog/pdownload_test.go | 5 ++++- kadai3-2/misonog/requests.go | 3 +-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/kadai3-2/misonog/main.go b/kadai3-2/misonog/main.go index 2607fee9..158ae0ad 100644 --- a/kadai3-2/misonog/main.go +++ b/kadai3-2/misonog/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "log" "os" @@ -10,6 +11,8 @@ import ( const timeout = 10 * time.Second func main() { + ctx := context.Background() + var targetDir string var timeout time.Duration @@ -23,7 +26,7 @@ func main() { flag.Parse() cli := New() - if err := cli.Run(flag.Args(), targetDir, timeout); err != nil { + if err := cli.Run(ctx, flag.Args(), targetDir, timeout); err != nil { log.Fatal(err) } } diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index db5fcc46..33cd3485 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -31,9 +31,7 @@ func New() *Pdownload { } } -func (pdownload *Pdownload) Run(args []string, targetDir string, timeout time.Duration) error { - ctx := context.Background() - +func (pdownload *Pdownload) Run(ctx context.Context, args []string, targetDir string, timeout time.Duration) error { ctx, clean := termination.Listen(ctx, os.Stdout) defer clean() diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go index 431e0d76..7cffbb90 100644 --- a/kadai3-2/misonog/pdownload_test.go +++ b/kadai3-2/misonog/pdownload_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" "testing" @@ -9,13 +10,15 @@ import ( // requests_test.goで作成しているテストサーバを利用してテストを行う func TestRun(t *testing.T) { + ctx := context.Background() + url := ts.URL args := []string{fmt.Sprintf("%s/%s", url, "header.jpg")} targetDir := "testdata/test_download" timeout := 30 * time.Second p := New() - if err := p.Run(args, targetDir, timeout); err != nil { + if err := p.Run(ctx, args, targetDir, timeout); err != nil { t.Errorf("failed to Run: %s", err) } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index 411ab428..a46850d8 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -8,7 +8,6 @@ import ( "net/http" "os" "path" - "time" "golang.org/x/sync/errgroup" ) @@ -26,7 +25,7 @@ func isLastProc(i, procs uint) bool { // Check method check be able to range access. func (p *Pdownload) Check(ctx context.Context, dir string) (context.Context, error) { - ctx, cancel := context.WithTimeout(ctx, time.Duration(p.timeout)*time.Second) + ctx, cancel := context.WithTimeout(ctx, p.timeout) defer cancel() res, err := http.Head(p.URL) From b2e77665d57e04ec2038421c66e92c4347352940 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 2 May 2021 16:30:55 +0900 Subject: [PATCH 28/38] Update context --- kadai3-2/misonog/requests.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index a46850d8..5a4ba43a 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -62,9 +62,9 @@ func (p *Pdownload) Download(ctx context.Context) error { // calculate split file size split := filesize / procs - grp, _ := errgroup.WithContext(ctx) + grp, ctx := errgroup.WithContext(ctx) - p.Assignment(grp, procs, split) + p.Assignment(grp, ctx, procs, split) // wait for Assignment method if err := grp.Wait(); err != nil { @@ -74,7 +74,7 @@ func (p *Pdownload) Download(ctx context.Context) error { return nil } -func (p Pdownload) Assignment(grp *errgroup.Group, procs, split uint) { +func (p Pdownload) Assignment(grp *errgroup.Group, ctx context.Context, procs, split uint) { filename := p.FileName() dirname := p.DirName() @@ -100,17 +100,18 @@ func (p Pdownload) Assignment(grp *errgroup.Group, procs, split uint) { // execute get request grp.Go(func() error { - return p.Requests(r, filename, dirname, p.URL) + return p.Requests(ctx, r, filename, dirname, p.URL) }) } } // Requests method will download the file -func (p Pdownload) Requests(r Range, filename, dirname, url string) error { - res, err := p.MakeResponse(r, url) +func (p Pdownload) Requests(ctx context.Context, r Range, filename, dirname, url string) error { + res, err := p.MakeResponse(ctx, r, url) if err != nil { - return fmt.Errorf("failed to split get requests: %d", r.woker) + // return fmt.Errorf("failed to split get requests: %d", r.woker) + return err } defer res.Body.Close() @@ -130,9 +131,10 @@ func (p Pdownload) Requests(r Range, filename, dirname, url string) error { } // MakeResponse return *http.Respnse include context and range header -func (p Pdownload) MakeResponse(r Range, url string) (*http.Response, error) { +func (p Pdownload) MakeResponse(ctx context.Context, r Range, url string) (*http.Response, error) { // create get request req, err := http.NewRequest("GET", url, nil) + // req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to split NewRequest for get: %d", r.woker) } From b82fb950833d95c9988622ea05fdfc17bbc9cb0b Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 2 May 2021 17:25:38 +0900 Subject: [PATCH 29/38] Fix context handling --- kadai3-2/misonog/go.mod | 5 ++++- kadai3-2/misonog/go.sum | 7 +++++++ kadai3-2/misonog/pdownload.go | 7 ++++++- kadai3-2/misonog/requests.go | 20 +++++++++----------- kadai3-2/misonog/requests_test.go | 10 +++++----- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/kadai3-2/misonog/go.mod b/kadai3-2/misonog/go.mod index f9b531a6..9b04e335 100644 --- a/kadai3-2/misonog/go.mod +++ b/kadai3-2/misonog/go.mod @@ -2,4 +2,7 @@ module github.com/misonog/gopherdojo-studyroom/kadai3-2/misonog go 1.16 -require golang.org/x/sync v0.0.0-20210220032951-036812b2e83c +require ( + golang.org/x/net v0.0.0-20210502030024-e5908800b52b + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c +) diff --git a/kadai3-2/misonog/go.sum b/kadai3-2/misonog/go.sum index 5c00efd3..76b3e047 100644 --- a/kadai3-2/misonog/go.sum +++ b/kadai3-2/misonog/go.sum @@ -1,2 +1,9 @@ +golang.org/x/net v0.0.0-20210502030024-e5908800b52b h1:jCRjgm6WJHzM8VQrm/es2wXYqqbq0NZ1yXFHHgzkiVQ= +golang.org/x/net v0.0.0-20210502030024-e5908800b52b/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 33cd3485..e5b738c7 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -32,6 +32,8 @@ func New() *Pdownload { } func (pdownload *Pdownload) Run(ctx context.Context, args []string, targetDir string, timeout time.Duration) error { + var cancel context.CancelFunc + ctx, clean := termination.Listen(ctx, os.Stdout) defer clean() @@ -47,7 +49,10 @@ func (pdownload *Pdownload) Run(ctx context.Context, args []string, targetDir st defer clean() termination.CleanFunc(clean) - ctx, err = pdownload.Check(ctx, dir) + ctx, cancel = context.WithTimeout(ctx, pdownload.timeout) + defer cancel() + + err = pdownload.Check(ctx, dir) if err != nil { return err } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index 5a4ba43a..b3cd0edf 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -9,6 +9,7 @@ import ( "os" "path" + "golang.org/x/net/context/ctxhttp" "golang.org/x/sync/errgroup" ) @@ -24,21 +25,18 @@ func isLastProc(i, procs uint) bool { } // Check method check be able to range access. -func (p *Pdownload) Check(ctx context.Context, dir string) (context.Context, error) { - ctx, cancel := context.WithTimeout(ctx, p.timeout) - defer cancel() - - res, err := http.Head(p.URL) +func (p *Pdownload) Check(ctx context.Context, dir string) error { + res, err := ctxhttp.Head(ctx, http.DefaultClient, p.URL) if err != nil { - return nil, err + return err } if res.Header.Get("Accept-Ranges") != "bytes" { - return nil, fmt.Errorf("not supported range access: %s", p.URL) + return fmt.Errorf("not supported range access: %s", p.URL) } if res.ContentLength <= 0 { - return nil, errors.New("invalid content length") + return errors.New("invalid content length") } filename := p.Utils.FileName() @@ -51,7 +49,7 @@ func (p *Pdownload) Check(ctx context.Context, dir string) (context.Context, err p.SetFileSize(uint(res.ContentLength)) - return ctx, nil + return nil } // Download method distributes the task to each goroutine @@ -133,8 +131,8 @@ func (p Pdownload) Requests(ctx context.Context, r Range, filename, dirname, url // MakeResponse return *http.Respnse include context and range header func (p Pdownload) MakeResponse(ctx context.Context, r Range, url string) (*http.Response, error) { // create get request - req, err := http.NewRequest("GET", url, nil) - // req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + // req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to split NewRequest for get: %d", r.woker) } diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index 3e325582..3158043d 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -58,7 +58,7 @@ func TestCheck(t *testing.T) { p := New() p.URL = ts.URL - if _, err := p.Check(context.Background(), dir); err != nil { + if err := p.Check(context.Background(), dir); err != nil { t.Errorf("failed to check header: %s", err) } } @@ -71,12 +71,12 @@ func TestDownload(t *testing.T) { filename: "header.jpg", } - ctx, err := p.Check(context.Background(), dir) + err := p.Check(context.Background(), dir) if err != nil { t.Errorf("failed to check header: %s", err) } - if err := p.Download(ctx); err != nil { + if err := p.Download(context.Background()); err != nil { t.Errorf("failed to download: %s", err) } @@ -100,12 +100,12 @@ func TestMergeFiles(t *testing.T) { fullfilename: "testdata/test_download/header.jpg", } - ctx, err := p.Check(context.Background(), dir) + err := p.Check(context.Background(), dir) if err != nil { t.Errorf("failed to check header: %s", err) } - if err := p.Download(ctx); err != nil { + if err := p.Download(context.Background()); err != nil { t.Errorf("failed to download: %s", err) } From aa2e3ae47ed7dda500e67802b8e80baaaa3555a8 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Sun, 2 May 2021 17:27:01 +0900 Subject: [PATCH 30/38] Fix const name from upper case to camel case --- kadai3-2/misonog/requests_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index 3158043d..c9d2e51a 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -11,7 +11,7 @@ import ( "time" ) -const TESTDIR = "testdata/test_download" +const testDir = "testdata/test_download" var ( dir string @@ -43,7 +43,7 @@ func setUp() { ts = httptest.NewServer(mux) - dir, mkdirErr = os.MkdirTemp(TESTDIR, "") + dir, mkdirErr = os.MkdirTemp(testDir, "") if mkdirErr != nil { panic(mkdirErr) } @@ -66,7 +66,7 @@ func TestCheck(t *testing.T) { func TestDownload(t *testing.T) { p := New() p.URL = ts.URL - p.TargetDir = TESTDIR + p.TargetDir = testDir p.Utils = &Data{ filename: "header.jpg", } @@ -94,7 +94,7 @@ func TestDownload(t *testing.T) { func TestMergeFiles(t *testing.T) { p := New() p.URL = ts.URL - p.TargetDir = TESTDIR + p.TargetDir = testDir p.Utils = &Data{ filename: "header.jpg", fullfilename: "testdata/test_download/header.jpg", From 51d9d486c5d1523cb46cc181e1e6207f7bd9d837 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:00:43 +0900 Subject: [PATCH 31/38] Reimplemented pdwonload func without Data struct --- kadai3-2/misonog/pdownload.go | 44 +++++++++++++++++++++++++----- kadai3-2/misonog/pdownload_test.go | 2 +- kadai3-2/misonog/requests.go | 24 ++++++++++------ kadai3-2/misonog/requests_test.go | 7 +++-- kadai3-2/misonog/util.go | 28 +++++++++++++++++++ 5 files changed, 87 insertions(+), 18 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index e5b738c7..3d22ca8e 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -15,12 +15,16 @@ import ( // Pdownload structs type Pdownload struct { Utils - URL string - TargetDir string - Procs int - timeout time.Duration - useragent string - referer string + URL string + TargetDir string + Procs int + timeout time.Duration + useragent string + referer string + filename string + filesize uint + dirname string + fullfilename string } func New() *Pdownload { @@ -61,7 +65,10 @@ func (pdownload *Pdownload) Run(ctx context.Context, args []string, targetDir st return err } - if err := pdownload.Utils.MergeFiles(pdownload.Procs); err != nil { + // if err := pdownload.Utils.MergeFiles(pdownload.Procs); err != nil { + // return err + // } + if err := mergeFiles(pdownload.Procs, pdownload.filename, pdownload.dirname, pdownload.fullfilename); err != nil { return err } @@ -100,3 +107,26 @@ func (pdownload *Pdownload) parseURL(args []string) error { return nil } + +func (pdownload *Pdownload) setFullFileName(dir, filename string) { + if dir == "" { + pdownload.fullfilename = filename + } else { + pdownload.fullfilename = fmt.Sprintf("%s/%s", dir, filename) + } +} + +// makeRange will return Range struct to download function +func (pdownload *Pdownload) makeRange(i, split, procs uint) Range { + low := split * i + high := low + split - 1 + if i == procs-1 { + high = pdownload.filesize + } + + return Range{ + low: low, + high: high, + woker: i, + } +} diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go index 7cffbb90..21d8c830 100644 --- a/kadai3-2/misonog/pdownload_test.go +++ b/kadai3-2/misonog/pdownload_test.go @@ -22,7 +22,7 @@ func TestRun(t *testing.T) { t.Errorf("failed to Run: %s", err) } - if err := os.Remove(p.FullFileName()); err != nil { + if err := os.Remove(p.fullfilename); err != nil { t.Errorf("failed to remove of result file: %s", err) } } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index b3cd0edf..4757fce4 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -43,11 +43,15 @@ func (p *Pdownload) Check(ctx context.Context, dir string) error { if filename == "" { filename = path.Base(p.URL) } - p.SetFileName(filename) - p.SetFullFileName(p.TargetDir, filename) - p.Utils.SetDirName(dir) + // p.SetFileName(filename) + // p.SetFullFileName(p.TargetDir, filename) + // p.Utils.SetDirName(dir) + p.filename = filename + p.setFullFileName(p.TargetDir, filename) + p.dirname = dir + p.filesize = uint(res.ContentLength) - p.SetFileSize(uint(res.ContentLength)) + // p.SetFileSize(uint(res.ContentLength)) return nil } @@ -55,7 +59,8 @@ func (p *Pdownload) Check(ctx context.Context, dir string) error { // Download method distributes the task to each goroutine func (p *Pdownload) Download(ctx context.Context) error { procs := uint(p.Procs) - filesize := p.FileSize() + // filesize := p.FileSize() + filesize := p.filesize // calculate split file size split := filesize / procs @@ -73,14 +78,17 @@ func (p *Pdownload) Download(ctx context.Context) error { } func (p Pdownload) Assignment(grp *errgroup.Group, ctx context.Context, procs, split uint) { - filename := p.FileName() - dirname := p.DirName() + // filename := p.FileName() + // dirname := p.DirName() + filename := p.filename + dirname := p.dirname for i := uint(0); i < procs; i++ { partName := fmt.Sprintf("%s/%s.%d.%d", dirname, filename, procs, i) // make range - r := p.Utils.MakeRange(i, split, procs) + // r := p.Utils.MakeRange(i, split, procs) + r := p.makeRange(i, split, procs) if info, err := os.Stat(partName); err == nil { infosize := uint(info.Size()) // check if the part is fully downloaded diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index c9d2e51a..a0f812d9 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -82,7 +82,7 @@ func TestDownload(t *testing.T) { for i := 0; i < p.Procs; i++ { // filename := fmt.Sprintf("testdata/test_download/header.jpg.%d.%d", p.Procs, i) - filename := fmt.Sprintf(p.DirName()+"/header.jpg.%d.%d", p.Procs, i) + filename := fmt.Sprintf(p.dirname+"/header.jpg.%d.%d", p.Procs, i) _, err := os.Stat(filename) if err != nil { t.Errorf("file not exist: %s", err) @@ -109,7 +109,10 @@ func TestMergeFiles(t *testing.T) { t.Errorf("failed to download: %s", err) } - if err := p.MergeFiles(p.Procs); err != nil { + // if err := p.MergeFiles(p.Procs); err != nil { + // t.Errorf("failed to MergeFiles: %s", err) + // } + if err := mergeFiles(p.Procs, p.filename, p.dirname, p.fullfilename); err != nil { t.Errorf("failed to MergeFiles: %s", err) } } diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 438865df..835a7e7b 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -120,3 +120,31 @@ func (d *Data) MergeFiles(procs int) error { return nil } + +func mergeFiles(procs int, filename, dirname, fullfilename string) error { + mergefile, err := os.Create(fullfilename) + if err != nil { + return err + } + defer mergefile.Close() + + var f string + for i := 0; i < procs; i++ { + f = fmt.Sprintf("%s/%s.%d.%d", dirname, filename, procs, i) + subfp, err := os.Open(f) + if err != nil { + return err + } + + if _, err := io.Copy(mergefile, subfp); err != nil { + return err + } + subfp.Close() + + if err := os.Remove(f); err != nil { + return err + } + } + + return nil +} From c4b316f26219f568dc77360598b8d2e528aa47be Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:12:43 +0900 Subject: [PATCH 32/38] Remove MakeRange --- kadai3-2/misonog/pdownload.go | 2 +- kadai3-2/misonog/requests.go | 2 +- kadai3-2/misonog/requests_test.go | 10 +++------- kadai3-2/misonog/util.go | 15 --------------- 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 3d22ca8e..7ede1f8e 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -29,7 +29,7 @@ type Pdownload struct { func New() *Pdownload { return &Pdownload{ - Utils: &Data{}, + // Utils: &Data{}, Procs: runtime.NumCPU(), // default timeout: timeout, } diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index 4757fce4..ca0b2e21 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -39,7 +39,7 @@ func (p *Pdownload) Check(ctx context.Context, dir string) error { return errors.New("invalid content length") } - filename := p.Utils.FileName() + filename := p.filename if filename == "" { filename = path.Base(p.URL) } diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index a0f812d9..9f01473f 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -67,9 +67,7 @@ func TestDownload(t *testing.T) { p := New() p.URL = ts.URL p.TargetDir = testDir - p.Utils = &Data{ - filename: "header.jpg", - } + p.filename = "header.jpg" err := p.Check(context.Background(), dir) if err != nil { @@ -95,10 +93,8 @@ func TestMergeFiles(t *testing.T) { p := New() p.URL = ts.URL p.TargetDir = testDir - p.Utils = &Data{ - filename: "header.jpg", - fullfilename: "testdata/test_download/header.jpg", - } + p.filename = "header.jpg" + p.fullfilename = "testdata/test_download/header.jpg" err := p.Check(context.Background(), dir) if err != nil { diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 835a7e7b..806278b4 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -76,21 +76,6 @@ func (d *Data) SetFullFileName(dir, filename string) { } } -// MakeRange will return Range struct to download function -func (d *Data) MakeRange(i, split, procs uint) Range { - low := split * i - high := low + split - 1 - if i == procs-1 { - high = d.FileSize() - } - - return Range{ - low: low, - high: high, - woker: i, - } -} - // MergeFiles function merege file after split download func (d *Data) MergeFiles(procs int) error { filename := d.filename From e5b722cbbaa281c7eebaac2e7429fcb1a0712252 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:18:29 +0900 Subject: [PATCH 33/38] Remove MergeFiles --- kadai3-2/misonog/util.go | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 806278b4..822cce53 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -76,36 +76,7 @@ func (d *Data) SetFullFileName(dir, filename string) { } } -// MergeFiles function merege file after split download -func (d *Data) MergeFiles(procs int) error { - filename := d.filename - dirname := d.dirname - - mergefile, err := os.Create(d.fullfilename) - if err != nil { - return err - } - defer mergefile.Close() - - var f string - for i := 0; i < procs; i++ { - f = fmt.Sprintf("%s/%s.%d.%d", dirname, filename, procs, i) - subfp, err := os.Open(f) - if err != nil { - return err - } - - io.Copy(mergefile, subfp) - subfp.Close() - - if err := os.Remove(f); err != nil { - return err - } - } - - return nil -} - +// mergeFiles function merege file after split download func mergeFiles(procs int, filename, dirname, fullfilename string) error { mergefile, err := os.Create(fullfilename) if err != nil { From 7af0bb5c749d714d56475643d759afa14d657fea Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:20:43 +0900 Subject: [PATCH 34/38] Remove getter --- kadai3-2/misonog/util.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 822cce53..eddb7dcb 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -24,32 +24,6 @@ type Utils interface { SetFileSize(uint) SetDirName(string) SetFullFileName(string, string) - - // like getter - FileName() string - FileSize() uint - DirName() string - FullFileName() string -} - -// FileName get from Data structs member -func (d Data) FileName() string { - return d.filename -} - -// FileSize get from Data structs member -func (d Data) FileSize() uint { - return d.filesize -} - -// DirName get from Data structs member -func (d Data) DirName() string { - return d.dirname -} - -// FullFileName get from Data structs member -func (d Data) FullFileName() string { - return d.fullfilename } // SetFileName set to Data structs member From 25034f23adde9adda3846083232600e44eebec21 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:22:33 +0900 Subject: [PATCH 35/38] Remove setter --- kadai3-2/misonog/util.go | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index eddb7dcb..25d66746 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -18,36 +18,6 @@ type Data struct { type Utils interface { MakeRange(uint, uint, uint) Range MergeFiles(int) error - - // like setter - SetFileName(string) - SetFileSize(uint) - SetDirName(string) - SetFullFileName(string, string) -} - -// SetFileName set to Data structs member -func (d *Data) SetFileName(filename string) { - d.filename = filename -} - -// SetFileSize set to Data structs member -func (d *Data) SetFileSize(size uint) { - d.filesize = size -} - -// SetDirName set to Data structs member -func (d *Data) SetDirName(dir string) { - d.dirname = dir -} - -// SetFullFileName set to Data structs member -func (d *Data) SetFullFileName(dir, filename string) { - if dir == "" { - d.fullfilename = filename - } else { - d.fullfilename = fmt.Sprintf("%s/%s", dir, filename) - } } // mergeFiles function merege file after split download From 37a3051d192bdeb1b2bbf51f647ddc74c4a66ecf Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:24:16 +0900 Subject: [PATCH 36/38] Remove Data struct --- kadai3-2/misonog/util.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 25d66746..5074c389 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -6,14 +6,6 @@ import ( "os" ) -// Data struct has file of relational data -type Data struct { - filename string - filesize uint - dirname string - fullfilename string -} - // Utils interface indicate function type Utils interface { MakeRange(uint, uint, uint) Range From 664bf2865ec10509ccb4444a504e70e91032c472 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:25:17 +0900 Subject: [PATCH 37/38] Remove Utils interface --- kadai3-2/misonog/pdownload.go | 2 -- kadai3-2/misonog/util.go | 6 ------ 2 files changed, 8 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 7ede1f8e..0f61cfd1 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -14,7 +14,6 @@ import ( // Pdownload structs type Pdownload struct { - Utils URL string TargetDir string Procs int @@ -29,7 +28,6 @@ type Pdownload struct { func New() *Pdownload { return &Pdownload{ - // Utils: &Data{}, Procs: runtime.NumCPU(), // default timeout: timeout, } diff --git a/kadai3-2/misonog/util.go b/kadai3-2/misonog/util.go index 5074c389..dbaa0e3f 100644 --- a/kadai3-2/misonog/util.go +++ b/kadai3-2/misonog/util.go @@ -6,12 +6,6 @@ import ( "os" ) -// Utils interface indicate function -type Utils interface { - MakeRange(uint, uint, uint) Range - MergeFiles(int) error -} - // mergeFiles function merege file after split download func mergeFiles(procs int, filename, dirname, fullfilename string) error { mergefile, err := os.Create(fullfilename) From 81a25e87664eace7e795871b0a83c71fa11b8c78 Mon Sep 17 00:00:00 2001 From: Gimpei Misono Date: Mon, 3 May 2021 12:32:00 +0900 Subject: [PATCH 38/38] Remove unnecessary comments --- kadai3-2/misonog/pdownload.go | 3 --- kadai3-2/misonog/pdownload_test.go | 1 - kadai3-2/misonog/requests.go | 13 +------------ kadai3-2/misonog/requests_test.go | 4 ---- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/kadai3-2/misonog/pdownload.go b/kadai3-2/misonog/pdownload.go index 0f61cfd1..ad305e07 100644 --- a/kadai3-2/misonog/pdownload.go +++ b/kadai3-2/misonog/pdownload.go @@ -63,9 +63,6 @@ func (pdownload *Pdownload) Run(ctx context.Context, args []string, targetDir st return err } - // if err := pdownload.Utils.MergeFiles(pdownload.Procs); err != nil { - // return err - // } if err := mergeFiles(pdownload.Procs, pdownload.filename, pdownload.dirname, pdownload.fullfilename); err != nil { return err } diff --git a/kadai3-2/misonog/pdownload_test.go b/kadai3-2/misonog/pdownload_test.go index 21d8c830..c32011ba 100644 --- a/kadai3-2/misonog/pdownload_test.go +++ b/kadai3-2/misonog/pdownload_test.go @@ -28,7 +28,6 @@ func TestRun(t *testing.T) { } func TestParseURL(t *testing.T) { - cases := []struct { name string input []string diff --git a/kadai3-2/misonog/requests.go b/kadai3-2/misonog/requests.go index ca0b2e21..881cdccc 100644 --- a/kadai3-2/misonog/requests.go +++ b/kadai3-2/misonog/requests.go @@ -43,23 +43,17 @@ func (p *Pdownload) Check(ctx context.Context, dir string) error { if filename == "" { filename = path.Base(p.URL) } - // p.SetFileName(filename) - // p.SetFullFileName(p.TargetDir, filename) - // p.Utils.SetDirName(dir) p.filename = filename p.setFullFileName(p.TargetDir, filename) p.dirname = dir p.filesize = uint(res.ContentLength) - // p.SetFileSize(uint(res.ContentLength)) - return nil } // Download method distributes the task to each goroutine func (p *Pdownload) Download(ctx context.Context) error { procs := uint(p.Procs) - // filesize := p.FileSize() filesize := p.filesize // calculate split file size @@ -78,8 +72,6 @@ func (p *Pdownload) Download(ctx context.Context) error { } func (p Pdownload) Assignment(grp *errgroup.Group, ctx context.Context, procs, split uint) { - // filename := p.FileName() - // dirname := p.DirName() filename := p.filename dirname := p.dirname @@ -87,7 +79,6 @@ func (p Pdownload) Assignment(grp *errgroup.Group, ctx context.Context, procs, s partName := fmt.Sprintf("%s/%s.%d.%d", dirname, filename, procs, i) // make range - // r := p.Utils.MakeRange(i, split, procs) r := p.makeRange(i, split, procs) if info, err := os.Stat(partName); err == nil { infosize := uint(info.Size()) @@ -116,8 +107,7 @@ func (p Pdownload) Assignment(grp *errgroup.Group, ctx context.Context, procs, s func (p Pdownload) Requests(ctx context.Context, r Range, filename, dirname, url string) error { res, err := p.MakeResponse(ctx, r, url) if err != nil { - // return fmt.Errorf("failed to split get requests: %d", r.woker) - return err + return fmt.Errorf("failed to split get requests: %d", r.woker) } defer res.Body.Close() @@ -139,7 +129,6 @@ func (p Pdownload) Requests(ctx context.Context, r Range, filename, dirname, url // MakeResponse return *http.Respnse include context and range header func (p Pdownload) MakeResponse(ctx context.Context, r Range, url string) (*http.Response, error) { // create get request - // req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to split NewRequest for get: %d", r.woker) diff --git a/kadai3-2/misonog/requests_test.go b/kadai3-2/misonog/requests_test.go index 9f01473f..1046757d 100644 --- a/kadai3-2/misonog/requests_test.go +++ b/kadai3-2/misonog/requests_test.go @@ -79,7 +79,6 @@ func TestDownload(t *testing.T) { } for i := 0; i < p.Procs; i++ { - // filename := fmt.Sprintf("testdata/test_download/header.jpg.%d.%d", p.Procs, i) filename := fmt.Sprintf(p.dirname+"/header.jpg.%d.%d", p.Procs, i) _, err := os.Stat(filename) if err != nil { @@ -105,9 +104,6 @@ func TestMergeFiles(t *testing.T) { t.Errorf("failed to download: %s", err) } - // if err := p.MergeFiles(p.Procs); err != nil { - // t.Errorf("failed to MergeFiles: %s", err) - // } if err := mergeFiles(p.Procs, p.filename, p.dirname, p.fullfilename); err != nil { t.Errorf("failed to MergeFiles: %s", err) }