diff --git a/kadai3/mpppk/.gitignore b/kadai3/mpppk/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/kadai3/mpppk/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/kadai3/mpppk/dpget/.gitignore b/kadai3/mpppk/dpget/.gitignore new file mode 100644 index 0000000..137eb3c --- /dev/null +++ b/kadai3/mpppk/dpget/.gitignore @@ -0,0 +1,3 @@ +.idea/ +*.zip +*.http \ No newline at end of file diff --git a/kadai3/mpppk/dpget/download/download.go b/kadai3/mpppk/dpget/download/download.go new file mode 100644 index 0000000..5d10629 --- /dev/null +++ b/kadai3/mpppk/dpget/download/download.go @@ -0,0 +1,155 @@ +package download + +import ( + "fmt" + "io" + "net/http" + "os" + "path" + "strconv" + + "sort" + + "golang.org/x/sync/errgroup" +) + +const chunkFileDir = "chunks" + +func DoParallel(urlPath, outputFilePath string, procs int) error { + chunkFilePaths, err := downloadAsChunkFiles(urlPath, procs) + if err != nil { + return err + } + + if err := concatChunkFiles(chunkFilePaths, outputFilePath); err != nil { + return err + } + + return nil +} + +func downloadAsChunkFiles(urlPath string, procs int) ([]string, error) { + contentLength, err := fetchContentLength(urlPath) + if err != nil { + return nil, err + } + + rangeHeaders := generateRangeHeaders(contentLength, procs) + chunkFilePathChan := make(chan string, 100) + var eg errgroup.Group + for i, rangeHeader := range rangeHeaders { + eg.Go(func(u, r string, i int) func() error { + return func() error { + return downloadChunk(u, r, i, chunkFilePathChan) + } + }(urlPath, rangeHeader, i)) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + close(chunkFilePathChan) + + var chunkFilePaths []string + for chunkFilePath := range chunkFilePathChan { + chunkFilePaths = append(chunkFilePaths, chunkFilePath) + } + + sort.Strings(chunkFilePaths) + return chunkFilePaths, nil +} + +func concatChunkFiles(chunkFilePaths []string, outputFilePath string) error { + dir := path.Dir(outputFilePath) + + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + resultFile, err := os.Create(outputFilePath) + if err != nil { + return err + } + + for _, chunkFilePath := range chunkFilePaths { + subfp, err := os.Open(chunkFilePath) + if err != nil { + return err + } + io.Copy(resultFile, subfp) + if err := os.Remove(chunkFilePath); err != nil { + return err + } + } + + if err := os.Remove(chunkFileDir); err != nil { + return err + } + + return nil +} + +func downloadChunk(urlPath, rangeHeader string, index int, filePathChan chan<- string) error { + client := &http.Client{} + + req, err := http.NewRequest("GET", urlPath, nil) + if err != nil { + return err + } + + req.Header.Add("Range", rangeHeader) + + resp, err := client.Do(req) + if err != nil { + return err + } + + chunkFilePath := createChunkFilePath(chunkFileDir, index) + if err := os.MkdirAll(chunkFileDir, 0755); err != nil { + return err + } + + file, err := os.Create(chunkFilePath) + defer file.Close() + if err != nil { + return err + } + + io.Copy(file, resp.Body) + filePathChan <- chunkFilePath + return nil +} + +func generateRangeHeaders(contentLength, splitNum int) (rangeHeaders []string) { + bytesPerRange := contentLength / splitNum + startByte := 0 + for i := 0; i < (splitNum - 1); i++ { + rangeHeaders = append(rangeHeaders, fmt.Sprintf("bytes=%d-%d", startByte, startByte+bytesPerRange-1)) + startByte += bytesPerRange + } + + rangeHeaders = append(rangeHeaders, fmt.Sprintf("bytes=%d-%d", startByte, contentLength-1)) + return +} + +func createChunkFilePath(dir string, index int) string { + return path.Join(dir, fmt.Sprintf("%04d_download", index)) +} + +func fetchContentLength(urlPath string) (int, error) { + client := &http.Client{} + req, err := http.NewRequest("HEAD", urlPath, nil) + if err != nil { + return 0, err + } + resp, err := client.Do(req) + if err != nil { + return 0, err + } + contentLengthHeader := resp.Header.Get("Content-Length") + contentLength, err := strconv.Atoi(contentLengthHeader) + if err != nil { + return 0, err + } + return contentLength, nil +} diff --git a/kadai3/mpppk/dpget/download/download_test.go b/kadai3/mpppk/dpget/download/download_test.go new file mode 100644 index 0000000..f5be9a3 --- /dev/null +++ b/kadai3/mpppk/dpget/download/download_test.go @@ -0,0 +1,50 @@ +package download + +import ( + "testing" +) + +func TestGenerateRangeHeaders(t *testing.T) { + cases := []struct { + contentLength int + splitNum int + expected []string + }{ + { + contentLength: 1, + splitNum: 1, + expected: []string{"bytes=0-0"}, + }, + { + contentLength: 2, + splitNum: 1, + expected: []string{"bytes=0-1"}, + }, + { + contentLength: 2, + splitNum: 2, + expected: []string{"bytes=0-0", "bytes=1-1"}, + }, + { + contentLength: 3, + splitNum: 2, + expected: []string{"bytes=0-0", "bytes=1-2"}, + }, + } + + for _, c := range cases { + headers := generateRangeHeaders(c.contentLength, c.splitNum) + + if len(headers) != len(c.expected) { + t.Fatalf("generateRangeHeaders is expected to return %d elements when contentLength is %d, but actually returns %d elements", + len(c.expected), c.contentLength, len(headers)) + } + + for i, header := range headers { + if header != c.expected[i] { + t.Fatalf("generateRangeHeaders is expected to return %q as an element with index %d if contentLength is %d, but acutually returns %q.", + c.expected, i, c.contentLength, header) + } + } + } +} diff --git a/kadai3/mpppk/dpget/main.go b/kadai3/mpppk/dpget/main.go new file mode 100644 index 0000000..7633416 --- /dev/null +++ b/kadai3/mpppk/dpget/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "flag" + "runtime" + + "path" + + "github.com/mpppk/dpget/download" +) + +var procs int +var outputFileName string +var targetDir string + +func main() { + flag.Parse() + urlPath := flag.Arg(0) + if outputFileName == "" { + outputFileName = path.Base(urlPath) + } + + outputFilePath := path.Join(targetDir, outputFileName) + + if err := download.DoParallel(urlPath, outputFilePath, procs); err != nil { + panic(err) + } +} + +func init() { + flag.IntVar(&procs, "procs", runtime.NumCPU(), "split ratio to download file") + flag.StringVar(&outputFileName, "output", "", "output file to ") + flag.StringVar(&targetDir, "target-dir", ".", "path to the directory to save the downloaded file") +} diff --git a/kadai3/mpppk/dpget/readme.md b/kadai3/mpppk/dpget/readme.md new file mode 100644 index 0000000..03b0a12 --- /dev/null +++ b/kadai3/mpppk/dpget/readme.md @@ -0,0 +1,13 @@ +``` +Usage of dpget: + -output string + output file to + -procs int + split ratio to download file (default 4) + -target-dir string + path to the directory to save the downloaded file (default ".") +``` + +## TODO +- [ ] download Resuming +- [ ] Cancel chunk downloading when error occured \ No newline at end of file diff --git a/kadai3/mpppk/typing-game/.gitignore b/kadai3/mpppk/typing-game/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/kadai3/mpppk/typing-game/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/kadai3/mpppk/typing-game/config.yaml b/kadai3/mpppk/typing-game/config.yaml new file mode 100644 index 0000000..e0cf64d --- /dev/null +++ b/kadai3/mpppk/typing-game/config.yaml @@ -0,0 +1,3 @@ +questions: + - hoge + - fuga \ No newline at end of file diff --git a/kadai3/mpppk/typing-game/main.go b/kadai3/mpppk/typing-game/main.go new file mode 100644 index 0000000..ff210f5 --- /dev/null +++ b/kadai3/mpppk/typing-game/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + + "time" + + "io/ioutil" + + "flag" + + "github.com/mpppk/go-typing-game/typing" + "gopkg.in/yaml.v2" +) + +var configFilePath string + +const timeLimitSec = 60 * time.Second + +type Config struct { + Questions []string +} + +func main() { + flag.Parse() + config, err := loadConfigFromYaml(configFilePath) + if err != nil { + fmt.Printf("failed to load config file from %s\n", configFilePath) + os.Exit(1) + } + ch := input(os.Stdin) + manager := typing.NewManager() + manager.AddQuestions(config.Questions) + + timeoutChan := time.After(timeLimitSec) + score := 0 + + for manager.SetNewQuestion() { + for { // 正しい解答が入力されるまでループ + fmt.Println("Q: " + manager.GetCurrentQuestion()) + fmt.Print(">") + answer, timeout := waitAnswerOrTimeout(ch, timeoutChan) + if timeout { + fmt.Printf("\ntime up! Your score is %d\n", score) + return + } + + if manager.ValidateAnswer(answer) { + fmt.Println("Correct!") + score++ + break + } else { + fmt.Println("invalid answer... try again") + } + } + } + fmt.Printf("all questions are answered! your score is %d\n", score) +} + +func waitAnswerOrTimeout(answerCh <-chan string, timeoutChan <-chan time.Time) (string, bool) { + for { + select { + case answer := <-answerCh: + return answer, false + case <-timeoutChan: + return "", true + } + } +} + +func loadConfigFromYaml(filePath string) (*Config, error) { + buf, err := ioutil.ReadFile(filePath) + if err != nil { + return nil, err + } + + var config Config + err = yaml.Unmarshal(buf, &config) + if err != nil { + return nil, err + } + return &config, nil +} + +func input(r io.Reader) <-chan string { + ch := make(chan string) + go func() { + s := bufio.NewScanner(r) + for s.Scan() { + ch <- s.Text() + } + close(ch) + }() + return ch +} + +func init() { + flag.StringVar(&configFilePath, "config", "config.yaml", "config file path") +} diff --git a/kadai3/mpppk/typing-game/readme.md b/kadai3/mpppk/typing-game/readme.md new file mode 100644 index 0000000..fa74838 --- /dev/null +++ b/kadai3/mpppk/typing-game/readme.md @@ -0,0 +1,5 @@ +``` +Usage of typing-game: + -config string + config file path (default "config.yaml") +``` diff --git a/kadai3/mpppk/typing-game/typing/manager.go b/kadai3/mpppk/typing-game/typing/manager.go new file mode 100644 index 0000000..4d7e7fb --- /dev/null +++ b/kadai3/mpppk/typing-game/typing/manager.go @@ -0,0 +1,62 @@ +package typing + +type Manager struct { + questions questions + currentQuestion string +} + +func (m *Manager) AddQuestions(qs []string) { + for _, q := range qs { + m.questions.addQuestion(q) + } +} + +func (m *Manager) AddQuestion(q string) { + m.questions.addQuestion(q) +} + +func (m *Manager) SetNewQuestion() bool { + if q, ok := m.questions.shiftQuestion(); ok { + m.currentQuestion = q + return true + } + return false +} + +func (m *Manager) GetCurrentQuestion() string { + return m.currentQuestion +} + +func (m *Manager) ValidateAnswer(a string) bool { + return a == m.currentQuestion +} + +func (m *Manager) HasQuestion() bool { + return m.questions.hasQuestion() +} + +func NewManager() *Manager { + return &Manager{ + questions: []string{}, + currentQuestion: "", + } +} + +type questions []string + +func (qs *questions) addQuestion(q string) { + *qs = append(*qs, q) +} + +func (qs *questions) shiftQuestion() (string, bool) { + if len(*qs) <= 0 { + return "", false + } + q := (*qs)[0] + *qs = (*qs)[1:] + return q, true +} + +func (qs *questions) hasQuestion() bool { + return len(*qs) > 0 +} diff --git a/kadai3/mpppk/typing-game/typing/manager_test.go b/kadai3/mpppk/typing-game/typing/manager_test.go new file mode 100644 index 0000000..4c4c883 --- /dev/null +++ b/kadai3/mpppk/typing-game/typing/manager_test.go @@ -0,0 +1,38 @@ +package typing + +import ( + "testing" +) + +func TestManager(t *testing.T) { + cases := []struct { + questions []string + }{ + { + questions: []string{"foo"}, + }, + { + questions: []string{"foo", "bar"}, + }, + } + + for _, c := range cases { + manager := NewManager() + manager.AddQuestions(c.questions) + cnt := 0 + for manager.SetNewQuestion() { + if !manager.ValidateAnswer(c.questions[cnt]) { + t.Fatalf("%dth question must be %q but actually %q (questions: %q)", + cnt+1, + c.questions[cnt], + manager.GetCurrentQuestion(), + c.questions) + } + cnt++ + } + + if cnt != len(c.questions) { + t.Fatalf("question num is %d, but cnt is %d", len(c.questions), cnt) + } + } +}