Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kadai3 2 kimuson13 #50

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions kadai3-2/kimuson13/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
17 changes: 17 additions & 0 deletions kadai3-2/kimuson13/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 分割ダウンローダ
Rangeアクセスを用いて、ダウンロードする。
## 使用方法
まずはkimuson13のディレクトリに移動する。
その後、
```go run main.go``
もしくは、
```go build main.go```
```./main```
で実行可能。
## オプション
```-p <number>```
分割数を指定できる。
```-t <number of second>```
タイムアウトが起きる時間を指定できる。
```-f <file name>```
ダウンロード後のファイル名を指定できる。
20 changes: 20 additions & 0 deletions kadai3-2/kimuson13/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
TODO
===
- [x] GET requestを送ってみる
- [x] ダウンロードしたものをファイルにする
- [x] HEADでコンテンツのサイズを確認する
- [x] range requestの実装
- [x] 分割ダウンロードの実装
- [x] timeoutの実装

- [x] エラー処理を工夫する。(道場の資料39を参考にする。)
- [x] キャンセルが発生した場合の実装(道場の資料48を参考にする。)
- [x] 型とメソッドにまとめる
- [x] 入力された情報が正しいか判断する(govalidateのIsURLが使えそう。)

- [x] オプションのstructを作る(分割数、拡張子、タイムアウト、ダウンロードしたファイルの名前)
- [x] オプションに入力された引数が正しいか判断する。

- [x] 各関数の説明を書く
- [] レビューで指摘してもらった箇所の修正
- [] テスト書く
Empty file.
Binary file added kadai3-2/kimuson13/download/_testdata/foo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
297 changes: 297 additions & 0 deletions kadai3-2/kimuson13/download/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
package download

import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"time"

"golang.org/x/sync/errgroup"
)

var (
ErrValidateParallelism = errors.New("the parallel number needs to be bigger than 1")
ErrValidateTimeout = errors.New("the timeout needs to be bigeer than 0")
ErrNotIncludeRangeAccess = errors.New("the response does not include Accept-Range header")
ErrNotContent = errors.New("it is not content")
ErrNoContentLength = errors.New("it does not have Content-Length")
)

// Donwnloader struct
type Downloader struct {
parallel int
timeout int
filename string
url string
}

// Rnage struct
type Range struct {
low int
high int
number int
}

// New for download package
func New(opts *Options) *Downloader {
return &Downloader{
parallel: opts.Parallel,
timeout: opts.Timeout,
filename: opts.Filename,
url: opts.URL,
}
}

func CreateTempdir() error {
if err := os.Mkdir("tempdir", 0755); err != nil {
return err
}

return nil
}

func DeleteTempdir() {
err := os.RemoveAll("tempdir")
if err != nil {
log.Println("can't remove the tempdir", err)
}
}

// Run excecute method in download package
func (d *Downloader) Run(ctx context.Context) error {
if err := d.Validation(); err != nil {
return err
}

if err := CreateTempdir(); err != nil {
return err
}
defer DeleteTempdir()

contentLength, err := d.CheckContentLength(ctx)
if err != nil {
return err
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downloadでtempdirの作成、MergeFileでtempdirの削除を行なっていますが、エラーになったときにtmpdirが消されないケースがあるので、ここで以下のように作成の関数と削除の関数を作り削除の関数をdeferを使って呼ぶと良いです

if err := createTmpDir(); err != nil {
    return err
}
defer deleteTmpDir()

if err := d.Download(contentLength, ctx); err != nil {
return err
}

if err := d.MergeFile(d.parallel, contentLength); err != nil {
return err
}

return nil
}

//Preparate method define the variables to Donwload
func (d *Downloader) Validation() error {
if d.parallel <= 1 {
return ErrValidateParallelism
}

if d.timeout < 1 {
return ErrValidateTimeout
}

return nil
}

// CheckContentLength method gets the Content-Length want to download
func (d *Downloader) CheckContentLength(ctx context.Context) (int, error) {
if _, err := fmt.Fprintf(os.Stdout, "Start HEAD request to check Content-Length\n"); err != nil {
return 0, err
}

req, err := http.NewRequest("HEAD", d.url, nil)
if err != nil {
return 0, err
}

req = req.WithContext(ctx)

res, err := http.DefaultClient.Do(req)
if err != nil {
return 0, err
}

acceptRange := res.Header.Get("Accept-Ranges")
if _, err := fmt.Fprintf(os.Stdout, "got: Accept-Ranges: %s\n", acceptRange); err != nil {
return 0, err
}

if acceptRange == "" {
return 0, ErrNotIncludeRangeAccess
}

if acceptRange != "bytes" {
return 0, ErrNotContent
}

contentLength := int(res.ContentLength)
if _, err := fmt.Fprintf(os.Stdout, "got: Content-Length: %v\n", contentLength); err != nil {
return 0, err
}

if contentLength < 1 {
return 0, ErrNoContentLength
}

return contentLength, nil
}

// Download method does split-download with goroutine
func (d *Downloader) Download(contentLength int, ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, time.Duration(d.timeout)*time.Second)
defer cancel()

parallel := d.parallel
split := contentLength / parallel
grp, ctx := errgroup.WithContext(ctx)
for i := 0; i < parallel; i++ {
r := MakeRange(i, split, parallel, contentLength)
tempfile := fmt.Sprintf("tempdir/tempfile.%d.%d", parallel, r.number)
filename, err := CreateTempfile(tempfile)
if err != nil {
return err
}

grp.Go(func() error {
err := Requests(r, d.url, filename)
return err
})
}

if err := grp.Wait(); err != nil {
return err
}

return nil
}

func CreateTempfile(name string) (string, error) {
file, err := os.Create(name)
if err != nil {
return "", err
}

defer func() {
err := file.Close()
if err != nil {
log.Println("can't close the "+name, err)
}
}()

return file.Name(), nil
}

// MakeRange function distributes Content-Length for split-download
func MakeRange(i, split, parallel, contentLength int) Range {
low := split * i
high := low + split - 1
if i == parallel-1 {
high = contentLength
}

return Range{
low: low,
high: high,
number: i,
}
}

// Requests function sends GET request
func Requests(r Range, url, filename string) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", r.low, r.high))
if _, err := fmt.Fprintf(os.Stdout, "start GET request: bytes=%d-%d\n", r.low, r.high); err != nil {
return err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}

defer func() {
err := res.Body.Close()
if err != nil {
log.Println("the response body can't close", err)
}
}()

if res.StatusCode != http.StatusPartialContent {
return fmt.Errorf("unexpected status code: %d", res.StatusCode)
}

output, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return err
}
defer func() {
err := output.Close()
if err != nil {
log.Println("can't close the tempfile", err)
}
}()

_, err = io.Copy(output, res.Body)
if err != nil {
return err
}
return nil
}

// MergeFile method merges tempfiles to new file
func (d *Downloader) MergeFile(parallel, contentLength int) error {
fmt.Println("\nmerging files...")
filename := d.filename
fh, err := os.Create(filename)
if err != nil {
return err
}

defer func() {
err := fh.Close()
if err != nil {
log.Println("can't close the download file!", err)
}
}()

for i := 0; i < parallel; i++ {
if err := Merger(parallel, i, fh); err != nil {
return err
}
}

fmt.Println("complete parallel donwload")
return nil
}

func Merger(parallel, i int, fh *os.File) error {
f := fmt.Sprintf("tempdir/tempfile.%d.%d", parallel, i)
sub, err := os.Open(f)
if err != nil {
return err
}

_, err = io.Copy(fh, sub)
if err != nil {
return err
}

defer func() {
err := sub.Close()
if err != nil {
log.Println("can't close the "+f, err)
}
}()
return nil
}
Loading