diff --git a/kadai3-2/README.md b/kadai3-2/README.md new file mode 100644 index 0000000..63e0657 --- /dev/null +++ b/kadai3-2/README.md @@ -0,0 +1,15 @@ +## Usage + +### 基本的な使い方 + +``` +pdownload [URL] +``` + +#### オプション + +``` +-p ダウンロード分割数の指定(デフォルト: 5) +-o 出力先ディレクトリの指定(デフォルト: ".") +-t 分割ファイル一時格納先の指定(デフォルト: "/tmp/kaznishi_pdownload") +``` diff --git a/kadai3-2/pdownload.go b/kadai3-2/pdownload.go new file mode 100644 index 0000000..4fcc1ff --- /dev/null +++ b/kadai3-2/pdownload.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/gopherdojo/dojo2/kadai3-2/pdownload" +) + +var ( + pCountOpt = flag.Int("p", 5, "分割数") + outputDirOpt = flag.String("o", ".", "ダウンロードファイルの出力先ディレクトリ") + tmpDirOpt = flag.String("t", "/tmp/kaznishi_pdownload", "分割ファイルの一時格納ディレクトリ") +) + +func main() { + option := pdownload.Option{} + option.Init() + + flag.Parse() + if len(flag.Args()) == 0 { + fmt.Fprintf(os.Stderr, "ダウンロード対象のURLが指定されていません") + return + } + option.TargetURL = flag.Args()[0] + option.PCount = *pCountOpt + option.OutputDir = *outputDirOpt + option.TmpDir = *tmpDirOpt + + ctx, cancel := context.WithCancel(context.Background()) + + trapSignals := []os.Signal{ + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + } + sigCh := make(chan os.Signal, 1) + doneCh := make(chan int, 1) + signal.Notify(sigCh, trapSignals...) + + var wgMain sync.WaitGroup + go func() { + wgMain.Add(1) + if err := pdownload.Run(ctx, doneCh, option); err != nil { + fmt.Println(err) + } + wgMain.Done() + }() + select { + case sig := <-sigCh: + cancel() + wgMain.Wait() + fmt.Println("Got signal", sig) + case code := <-doneCh: + if code == 0 { + fmt.Println("Done!!!!!") + } else { + fmt.Println("Failed...") + } + } +} diff --git a/kadai3-2/pdownload/pdownload.go b/kadai3-2/pdownload/pdownload.go new file mode 100644 index 0000000..5a85dda --- /dev/null +++ b/kadai3-2/pdownload/pdownload.go @@ -0,0 +1,223 @@ +package pdownload + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "path" + "strconv" + "time" + + "golang.org/x/net/context/ctxhttp" + "golang.org/x/sync/errgroup" +) + +// Option はプログラムに与えるオプションをまとめた構造体です +type Option struct { + TargetURL string // ダウンロードの対象URL + PCount int // 分割数 + OutputDir string // 結合後のファイルの格納場所 + TmpDir string // 分割ファイルの一時格納場所 +} + +// Init は新しく生成したオブジェクトにデフォルト値を設定するための関数です +func (o *Option) Init() { + o.PCount = 5 + o.OutputDir = "." + o.TmpDir = "/tmp/kaznishi_pdownload" +} + +var ( + tmpDir = "/tmp/kaznishi_pdownload" // 分割ファイルを一時的に格納するディレクトリ +) + +// Run はpdownloadの処理を実行します +func Run(ctx context.Context, doneCh chan<- int, option Option) error { + errCh := make(chan error, 1) + eg, _ := errgroup.WithContext(ctx) + + // 一時保存ディレクトリの作成 + setTmpDir(option.TmpDir) + if err := mkTmpDir(); err != nil { + doneCh <- 1 + return err + } + //// チェック処理 + fullSize, err := sizeCheck(option.TargetURL) + if err != nil { + doneCh <- 1 + return err + } + //// ファイルサイズ分割処理 + fileName := path.Base(option.TargetURL) + parts := split(option.PCount, fullSize, fileName) + + go func() { + //// 分割ダウンロード処理 + for _, p := range parts { + fmt.Println("Downloding Part File Started. :" + p.FileName) + p := p + eg.Go(func() error { + return download(p, option.TargetURL) + }) + } + if err := eg.Wait(); err != nil { + errCh <- err + } else { + fmt.Println("Downloading Part Files completed.") + } + + //// 分割ファイルマージ処理 + if err := merge(parts, getNewFilePath(option.OutputDir, fileName)); err != nil { + errCh <- err + } else { + fmt.Println("Combining Part Files completed.") + } + + //// 分割ファイルクリア処理 + if err = clearPartFiles(parts); err != nil { + errCh <- err + } + + errCh <- nil + }() + + for { + select { + case err := <-errCh: + if err != nil { + clearWhenCancel(parts, getNewFilePath(option.OutputDir, fileName)) + doneCh <- 1 + return err + } + doneCh <- 0 + return nil + case <-ctx.Done(): + clearWhenCancel(parts, getNewFilePath(option.OutputDir, fileName)) + doneCh <- 0 + return nil + } + } +} + +/////////////////////////////////////////////////////////////////////////// + +func getNewFilePath(outputDir, fileName string) string { + return outputDir + "/" + fileName +} + +func setTmpDir(dirPath string) { + if dirPath != "" { + tmpDir = dirPath + } +} + +func mkTmpDir() error { + return os.MkdirAll(tmpDir, 0755) +} + +func sizeCheck(url string) (int, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second) + defer cancel() + res, err := ctxhttp.Head(ctx, http.DefaultClient, url) + if err != nil { + return 0, err + } + if res.Header.Get("Accept-Ranges") != "bytes" { + err = fmt.Errorf("Accept-Ranges = bytesではありません") + return 0, err + } + + l, err := strconv.Atoi(res.Header.Get("Content-Length")) + return l, err +} + +type part struct { + Low int + High int + FileName string +} + +func (p part) getFilePath() string { + return tmpDir + "/" + p.FileName +} + +func split(pCount int, fullSize int, fileName string) []part { + result := make([]part, pCount) + + var low, high int + for i := 0; i < pCount; i++ { + if i == 0 { + low = 0 + } else { + low = high + 1 + } + if i == pCount-1 { + high = fullSize - 1 + } else { + high = int(fullSize * (i + 1) / pCount) + } + fn := fileName + "_" + strconv.Itoa(i) + p := part{Low: low, High: high, FileName: fn} + result[i] = p + } + return result +} + +func download(p part, url string) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + low := p.Low + high := p.High + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", low, high)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + file, err := os.Create(p.getFilePath()) + if err != nil { + return err + } + + _, err = io.Copy(file, res.Body) + if err != nil { + return err + } + + return file.Close() +} + +func merge(parts []part, newFilePath string) error { + newFile, _ := os.Create(newFilePath) + for _, p := range parts { + pf, err := os.Open(p.getFilePath()) + if err != nil { + return err + } + io.Copy(newFile, pf) + pf.Close() + } + newFile.Close() + return nil +} + +func clearPartFiles(parts []part) error { + for _, p := range parts { + if err := os.Remove(p.getFilePath()); err != nil { + return err + } + } + return nil +} + +func clearWhenCancel(parts []part, newFilePath string) { + clearPartFiles(parts) + os.Remove(newFilePath) +}