Skip to content

kadai3 by mpppk #49

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

Open
wants to merge 1 commit 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
1 change: 1 addition & 0 deletions kadai3/mpppk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
3 changes: 3 additions & 0 deletions kadai3/mpppk/dpget/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea/
*.zip
*.http
155 changes: 155 additions & 0 deletions kadai3/mpppk/dpget/download/download.go
Original file line number Diff line number Diff line change
@@ -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
}
50 changes: 50 additions & 0 deletions kadai3/mpppk/dpget/download/download_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
34 changes: 34 additions & 0 deletions kadai3/mpppk/dpget/main.go
Original file line number Diff line number Diff line change
@@ -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 <filename>")
flag.StringVar(&targetDir, "target-dir", ".", "path to the directory to save the downloaded file")
}
13 changes: 13 additions & 0 deletions kadai3/mpppk/dpget/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
```
Usage of dpget:
-output string
output file to <filename>
-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
1 change: 1 addition & 0 deletions kadai3/mpppk/typing-game/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
3 changes: 3 additions & 0 deletions kadai3/mpppk/typing-game/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
questions:
- hoge
- fuga
102 changes: 102 additions & 0 deletions kadai3/mpppk/typing-game/main.go
Original file line number Diff line number Diff line change
@@ -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")
}
5 changes: 5 additions & 0 deletions kadai3/mpppk/typing-game/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
```
Usage of typing-game:
-config string
config file path (default "config.yaml")
```
Loading