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 sminamot #43

Open
wants to merge 2 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
12 changes: 12 additions & 0 deletions kadai3-2/sminamot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 分割ダウンローダーの作成

## Usage
```
$ go build -o main
$ ./main TARGET_URL
```

* `./tmp`ディレクトリを作り4分割のダウンロードを行う
* ファイル名は`<対象ファイル名>_{0..3}`
* 分割ダウンロードが完了後に4つのファイルをマージする
* HTTPリクエストや(分割ダウンロードの)ファイル書き込みに失敗した場合、他で実行中のgoroutine処理を終了する
173 changes: 173 additions & 0 deletions kadai3-2/sminamot/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package main

import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"

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

const processNum = 4
const tmpDir = "./tmp"

type RangeClients []RangeClient

type RangeClient struct {
Index int
FilePath string
StartBytes int64
EndBytes int64
}

func NewRangeClients(fileBytes int64, fileName string) RangeClients {
rc := make(RangeClients, 0, processNum)
b := int64(fileBytes / processNum)
sb := int64(0)
for i := 0; i < processNum; i++ {
r := RangeClient{
Index: i,
FilePath: fmt.Sprintf("%s/%s_%d", tmpDir, fileName, i),
StartBytes: sb,
EndBytes: sb + b,
}
if i == processNum-1 {
r.EndBytes = fileBytes
}
rc = append(rc, r)
sb += b + 1
}
return rc
}

func main() {
os.Exit(Run())
}

func Run() int {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: ./main TARGET_URL")
return 1
}
targetUrl := os.Args[1]

res, err := http.Head(targetUrl)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if res.Header.Get("Accept-Ranges") != "bytes" {
fmt.Fprintf(os.Stderr, "not support range header, url:%s", targetUrl)
return 1
}
rcs := NewRangeClients(res.ContentLength, filepath.Base(targetUrl))

// create tmp directory
if err := os.MkdirAll(tmpDir, 0755); err != nil {
fmt.Fprintln(os.Stderr, "could not create tmp directory")
}
// remove tmp directory with defer
defer os.RemoveAll(tmpDir)

eg, ctx := errgroup.WithContext(context.Background())
ctx, cancel := context.WithCancel(ctx)
defer cancel()

for _, rc := range rcs {
rc := rc
eg.Go(func() error {
res, err := rc.download(ctx, targetUrl)
if err != nil {
return err
}
defer res.Body.Close()

return rc.outputFile(ctx, res)
})
}

if err := eg.Wait(); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}

fh, err := os.Create(filepath.Base(targetUrl))
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
defer fh.Close()

// bind files
for _, rc := range rcs {
fp, err := os.Open(rc.FilePath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
defer fp.Close()
Copy link

Choose a reason for hiding this comment

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

deferは実行された時点の構造体などの情報を積んでしまうので、forの中で利用するのは良くない場合があります
deferはfuncのスコープでしか実行されないので、即時関数でforの中身を囲うなどで対処可能です

io.Copy(fh, fp)
Copy link

Choose a reason for hiding this comment

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

io.Copy はエラーを返す可能性があるので、チェックしたほうが良いです

}

return 0
}

func (rc *RangeClient) download(ctx context.Context, t string) (*http.Response, error) {
type requestResult struct {
res *http.Response
err error
}

req, err := http.NewRequest("GET", t, nil)
tr := &http.Transport{}
client := &http.Client{Transport: tr}
Copy link

Choose a reason for hiding this comment

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

http.Clientはsafe for concurrentですので、どこかで一つつくっておくだけでも十分です
また、Transportはnilであった場合、DefaultTransportが利用されるので、 client := new(http.Client) だけでも問題なく利用できます

https://golang.org/pkg/net/http/#Client

   // Transport specifies the mechanism by which individual
   // HTTP requests are made.
   // If nil, DefaultTransport is used.
   Transport RoundTripper

https://github.com/gopherdojo/dojo4/pull/43/files#diff-721da0c5f369336fd01ab4ac84891ea0R144
上記でCancelRequestを呼ぶために生成しているのかもしれませんが、すでにdeprecatedですので、WithContextを利用したほうが良いです
https://golang.org/pkg/net/http/#Transport.CancelRequest

Deprecated: Use Request.WithContext to create a request with a cancelable context instead.

if err != nil {
return nil, err
}

req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", rc.StartBytes, rc.EndBytes))

resCh := make(chan requestResult)
go func() {
time.Sleep(5 * time.Second)
res, err := client.Do(req)
resCh <- requestResult{
res: res,
err: err,
}
}()

select {
case <-ctx.Done():
tr.CancelRequest(req)
return nil, ctx.Err()
case result := <-resCh:
return result.res, result.err
}
}

func (rc *RangeClient) outputFile(ctx context.Context, res *http.Response) error {
fp, err := os.OpenFile(rc.FilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return err
}
defer fp.Close()

errCh := make(chan error)
go func() {
_, err := io.Copy(fp, res.Body)
errCh <- err
}()

select {
case <-ctx.Done():
return ctx.Err()
case err := <-errCh:
if err != nil {
return err
}
}
return nil
}