From d2c8df35930a89c5816895083a3572209d277f26 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 26 Nov 2021 11:40:47 +0100 Subject: [PATCH] Implement early version of command get --- go.mod | 9 +- go.sum | 56 ++++++++++ main.go | 159 +++++++++++++++++++++++++-- url.go | 25 ++++- xdcc.go | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 560 insertions(+), 16 deletions(-) create mode 100644 xdcc.go diff --git a/go.mod b/go.mod index 17128f5..a9032b9 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,11 @@ module xdcc-cli go 1.13 -require github.com/PuerkitoBio/goquery v1.8.0 +require ( + github.com/PuerkitoBio/goquery v1.8.0 + github.com/Workiva/go-datastructures v1.0.53 + github.com/fluffle/goirc v1.1.1 + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/vbauerster/mpb v3.4.0+incompatible + github.com/vbauerster/mpb/v7 v7.1.5 +) diff --git a/go.sum b/go.sum index d5e55e1..a53952c 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,67 @@ github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= +github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fluffle/goirc v1.1.1 h1:6nO+7rrED3Kp3mngoi9OmQmQHevNwDfjGpYUdWc1s0k= +github.com/fluffle/goirc v1.1.1/go.mod h1:iRzPLv2vkuZEtbns5LioYguJkRh/bvshuWg7izf1yeE= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= +github.com/vbauerster/mpb v3.4.0+incompatible h1:mfiiYw87ARaeRW6x5gWwYRUawxaW1tLAD8IceomUCNw= +github.com/vbauerster/mpb v3.4.0+incompatible/go.mod h1:zAHG26FUhVKETRu+MWqYXcI70POlC6N8up9p1dID7SU= +github.com/vbauerster/mpb/v7 v7.1.5 h1:vtUEUfQHmNeJETyF4AcRCOV6RC4wqFwNORy52UMXPbQ= +github.com/vbauerster/mpb/v7 v7.1.5/go.mod h1:4M8+qAoQqV60WDNktBM5k05i1iTrXE7rjKOHEVkVlec= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg= +golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index f266a9f..33a286f 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,13 @@ package main import ( + "bufio" "flag" "fmt" "os" + "strconv" + "strings" + "sync" ) var registry *XdccProviderRegistry = nil @@ -13,20 +17,156 @@ func init() { registry.AddProvider(&XdccEuProvider{}) } -func search(fileName string) { - printer := NewTablePrinter([]string{"File Name", "Network", "Channel"}) +var defaultColWidths []int = []int{100, 10, -1} - res, _ := registry.Search(fileName) - for _, fileInfo := range res { - printer.AddRow(Row{fileInfo.Name, fileInfo.Network, fileInfo.Channel}) +const ( + KiloByte = 1024 + MegaByte = KiloByte * 1024 + GigaByte = MegaByte * 1024 +) + +func FloatToString(value float64) string { + if value-float64(int64(value)) > 0 { + return strconv.FormatFloat(value, 'f', 2, 32) + } + return strconv.FormatFloat(value, 'f', 0, 32) +} + +func formatSize(size int64) string { + if size < 0 { + return "--" } + if size >= GigaByte { + return FloatToString(float64(size)/float64(GigaByte)) + "GB" + } else if size >= MegaByte { + return FloatToString(float64(size)/float64(MegaByte)) + "MB" + } else if size >= KiloByte { + return FloatToString(float64(size)/float64(KiloByte)) + "KB" + } + return FloatToString(float64(size)) + "B" +} + +func searchCommand(args []string) { + printer := NewTablePrinter([]string{"File Name", "Size", "URL"}) + printer.SetMaxWidths(defaultColWidths) + + res, _ := registry.Search(args[0]) + for _, fileInfo := range res { + printer.AddRow(Row{fileInfo.Name, formatSize(fileInfo.Size), fileInfo.Url}) + } + + printer.SortByColumn(0) // sort by filename printer.Print() } +func doTransfer(transfer *XdccTransfer) { + //pb := NewProgressBar() + + err := transfer.Start() + + fmt.Println(err) + + if err != nil { + fmt.Println(err) + return + } + + evts := transfer.PollEvents() + quit := false + for !quit { + e := <-evts + switch evtType := e.(type) { + case *TransferStartedEvent: + // pb.SetTotal(int(evtType.FileSize)) + // pb.SetFileName(evtType.FileName) + // pb.SetState(ProgressStateDownloading) + case *TransferProgessEvent: + // pb.Increment(int(evtType.transferBytes)) + case *TransferCompletedEvent: + // pb.SetState(ProgressStateCompleted) + print(evtType) + quit = true + } + } + // TODO: do clean-up operations here +} + +func parseFlags(flagSet *flag.FlagSet, args []string) []string { + findFirstFlag := func(args []string) int { + for i, arg := range args { + if strings.HasPrefix(arg, "-") || strings.HasPrefix(arg, "--") { + return i + } + } + return -1 + } + flagIdx := findFirstFlag(args) + if flagIdx >= 0 { + flagSet.Parse(args[flagIdx:]) + return args[:flagIdx] + } + return args +} + +func loadUrlListFile(filePath string) []string { + file, err := os.Open(filePath) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + urlList := make([]string, 0) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + urlList = append(urlList, line) + } + + if err := scanner.Err(); err != nil { + fmt.Println(err) + os.Exit(1) + } + + return urlList +} + +func getCommand(args []string) { + searchCmd := flag.NewFlagSet("get", flag.ExitOnError) + path := searchCmd.String("o", ".", "output folder of dowloaded file") + inputFile := searchCmd.String("i", "", "input file containing a list of urls") + + urlList := parseFlags(searchCmd, args) + + if *inputFile != "" { + urlList = append(urlList, loadUrlListFile(*inputFile)...) + } + + wg := sync.WaitGroup{} + for _, urlStr := range urlList { + if strings.HasPrefix(urlStr, "irc://") { + url, err := parseIRCFileURl(urlStr) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + wg.Add(1) + transfer := NewXdccTransfer(*url, *path) + go func(transfer *XdccTransfer) { + doTransfer(transfer) + wg.Done() + }(transfer) + } else { + + } + } + wg.Wait() +} + func main() { - searchCmd := flag.NewFlagSet("foo", flag.ExitOnError) - fileName := searchCmd.String("f", "", "name of the file to search") if len(os.Args) < 2 { fmt.Println("one of the following subcommands is expected: [search, get]") @@ -35,10 +175,9 @@ func main() { switch os.Args[1] { case "search": - searchCmd.Parse(os.Args[2:]) - search(*fileName) + searchCommand(os.Args[2:]) case "get": - break + getCommand(os.Args[2:]) default: fmt.Println("no such command: ", os.Args[1]) os.Exit(1) diff --git a/url.go b/url.go index 6d4da9e..877cfe8 100644 --- a/url.go +++ b/url.go @@ -14,13 +14,19 @@ type IRCFileURL struct { Slot int } +type IRCBot struct { + Network string + Channel string + Name string +} + const ircFileURLFields = 4 func parseSlot(slotStr string) (int, error) { - if !strings.HasPrefix(slotStr, "#") { - return -1, errors.New("invalid slot") + if strings.HasPrefix(slotStr, "#") { + strconv.Atoi(strings.TrimPrefix(slotStr, "#")) } - return strconv.Atoi(strings.TrimPrefix(slotStr, "#")) + return strconv.Atoi(slotStr) } // url has the following format: irc://network/channel/bot/#slot @@ -39,12 +45,21 @@ func parseIRCFileURl(url string) (*IRCFileURL, error) { return nil, err } - return &IRCFileURL{ + fileUrl := &IRCFileURL{ Network: fields[0], Channel: fields[1], UserName: fields[2], Slot: slot, - }, nil + } + + if !strings.HasPrefix(fileUrl.Channel, "#") { + fileUrl.Channel = "#" + fileUrl.Channel + } + return fileUrl, nil +} + +func (url *IRCFileURL) GetBot() IRCBot { + return IRCBot{Network: url.Network, Channel: url.Channel, Name: url.UserName} } func (url *IRCFileURL) String() string { diff --git a/xdcc.go b/xdcc.go new file mode 100644 index 0000000..58d726a --- /dev/null +++ b/xdcc.go @@ -0,0 +1,327 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "log" + "math/rand" + "net" + "os" + "strconv" + "strings" + "time" + + irc "github.com/fluffle/goirc/client" +) + +const IRCClientUserName = "xdcc-cli" + +type CTCPRequest interface { + String() string +} + +type CTCPResponse interface { + Parse(args []string) error + Name() string +} + +type XdccSendReq struct { + Slot int +} + +func (send *XdccSendReq) String() string { + return fmt.Sprintf("xdcc send #%d", send.Slot) +} + +type XdccSendRes struct { + FileName string + IP net.IP + Port int + FileSize int +} + +func uint32ToIP(n int) net.IP { + a := byte((n >> 24) & 255) + b := byte((n >> 16) & 255) + c := byte((n >> 8) & 255) + d := byte(n & 255) + return net.IPv4(a, b, c, d) +} + +const XdccSendResArgs = 4 + +func (send *XdccSendRes) Name() string { + return SEND +} + +func (send *XdccSendRes) Parse(args []string) error { + if len(args) != XdccSendResArgs { + return errors.New("invalid number of arguments") + } + + send.FileName = args[0] + + ipUint32, err := strconv.Atoi(args[1]) + + if err != nil { + return err + } + + send.IP = uint32ToIP(ipUint32) + send.Port, err = strconv.Atoi(args[2]) + + if err != nil { + return err + } + + send.FileSize, err = strconv.Atoi(args[3]) + + if err != nil { + return err + } + return nil +} + +const ( + SEND = "SEND" + VERSION = "\x01VERSION\x01" +) + +func parseCTCPRes(text string) (CTCPResponse, error) { + fields := strings.Fields(text) + + var resp CTCPResponse = nil + + switch strings.TrimSpace(fields[0]) { + case SEND: + resp = &XdccSendRes{} + case VERSION: + return nil, nil + } + + if resp == nil { + return nil, errors.New("invalid command: " + fields[0]) + } + + err := resp.Parse(fields[1:]) + if err != nil { + return nil, err + } + return resp, nil +} + +const defaultEventChanSize = 1024 + +func (transfer *XdccTransfer) Start() error { + return transfer.conn.Connect() +} + +type XdccEvent interface{} + +type TransferAbortedEvent struct { + Error string +} + +type XdccTransfer struct { + filePath string + url IRCFileURL + conn *irc.Conn + started bool + events chan XdccEvent +} + +type TransferManager struct { + transfers map[IRCBot]*XdccTransfer +} + +func (tm *TransferManager) addTransfer(fileUrl *IRCFileURL, filePath string) { + transfer, ok := tm.transfers[fileUrl.GetBot()] + + if !ok { + transfer = NewXdccTransfer(*fileUrl, filePath) + tm.transfers[fileUrl.GetBot()] = transfer + // add transfer + } + +} + +func NewXdccTransfer(url IRCFileURL, filePath string) *XdccTransfer { + conn := irc.SimpleClient(IRCClientUserName + strconv.Itoa(int(rand.Uint32()))) + conn.Config().Server = url.Network + conn.Config().NewNick = func(nick string) string { + return nick + "" + strconv.Itoa(int(rand.Uint32())) + } + + t := &XdccTransfer{ + conn: conn, + url: url, + filePath: filePath, + started: false, + events: make(chan XdccEvent, defaultEventChanSize), + } + t.setupHandlers(url.Channel, url.UserName, url.Slot) + return t +} + +func (transfer *XdccTransfer) send(req CTCPRequest) { + transfer.conn.Privmsg(transfer.url.UserName, req.String()) +} + +func (transfer *XdccTransfer) setupHandlers(channel string, userName string, slot int) { + conn := transfer.conn + + // e.g. join channel on connect. + conn.HandleFunc(irc.CONNECTED, + func(conn *irc.Conn, line *irc.Line) { + fmt.Println("connected ", channel) + conn.Join(channel) + }) + + // send xdcc send on successfull join + conn.HandleFunc(irc.JOIN, + func(conn *irc.Conn, line *irc.Line) { + if line.Args[0] == channel && !transfer.started { + fmt.Println("contacting ", transfer.url.UserName) + transfer.send(&XdccSendReq{Slot: slot}) + } + }) + + conn.HandleFunc(irc.CTCP, + func(conn *irc.Conn, line *irc.Line) { + fmt.Println(line.Text()) + res, err := parseCTCPRes(line.Text()) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) // TODO: correct clean up + } + transfer.handleCTCPRes(res) + }) + + conn.HandleFunc(irc.DISCONNECTED, + func(conn *irc.Conn, line *irc.Line) { + if !transfer.started { + transfer.notifyEvent(&TransferAbortedEvent{Error: "disconnected from server"}) + } + }) +} + +func (transfer *XdccTransfer) PollEvents() chan XdccEvent { + return transfer.events +} + +type TransferProgessEvent struct { + transferBytes uint64 + transferRate float32 +} + +const downloadBufSize = 1024 + +type TransferStartedEvent struct { + FileName string + FileSize uint64 +} + +type TransferCompletedEvent struct{} + +func (transfer *XdccTransfer) notifyEvent(e XdccEvent) { + select { + case transfer.events <- e: + default: + break + } +} + +type SpeedMonitorReader struct { + reader io.Reader + elapsedTime time.Duration + currValue uint64 + currentSpeed float64 + onUpdate func(amount int, speed float64) +} + +func NewSpeedMonitorReader(reader io.Reader, onUpdate func(int, float64)) *SpeedMonitorReader { + return &SpeedMonitorReader{ + reader: reader, + elapsedTime: time.Duration(0), + currValue: 0, + currentSpeed: 0, + onUpdate: onUpdate, + } +} + +func (monitor *SpeedMonitorReader) Read(buf []byte) (int, error) { + now := time.Now() + n, err := monitor.reader.Read(buf) + elapsedTime := time.Since(now) + monitor.currValue += uint64(n) + monitor.elapsedTime += elapsedTime + + if monitor.elapsedTime > time.Second { + monitor.currentSpeed = float64(monitor.currValue) / monitor.elapsedTime.Seconds() + monitor.onUpdate(int(monitor.currValue), monitor.currentSpeed) + monitor.currValue = 0 + monitor.elapsedTime = time.Duration(0) + } + return n, err +} + +func (transfer *XdccTransfer) handleXdccSendRes(send *XdccSendRes) { + go func() { + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: send.IP, Port: send.Port}) + if err != nil { + log.Fatalf("unable to reach host %s:%d", send.IP.String(), send.Port) + return + } + + file, err := os.OpenFile(transfer.filePath+"/"+send.FileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + fileWriter := bufio.NewWriter(file) + + if err != nil { + log.Fatal(err.Error()) + return + } + + transfer.notifyEvent(&TransferStartedEvent{ + FileName: send.FileName, + FileSize: uint64(send.FileSize), + }) + transfer.started = true + + reader := NewSpeedMonitorReader(conn, func(dowloadedAmount int, speed float64) { + transfer.notifyEvent(&TransferProgessEvent{ + transferRate: float32(speed), + transferBytes: uint64(dowloadedAmount), + }) + }) + + // download loop + downloadedBytesTotal := 0 + buf := make([]byte, downloadBufSize) + for downloadedBytesTotal < send.FileSize { + n, err := reader.Read(buf) + + if err != nil { + log.Fatal(err.Error()) + return + } + + if _, err := fileWriter.Write(buf[:n]); err != nil { + log.Fatal(err.Error()) + return + } + + downloadedBytesTotal += n + } + + transfer.notifyEvent(&TransferCompletedEvent{}) + }() +} + +func (transfer *XdccTransfer) handleCTCPRes(resp CTCPResponse) { + switch r := resp.(type) { + case *XdccSendRes: + transfer.handleXdccSendRes(r) + } +}