Implement early version of command get

main
Stefano 2021-11-26 11:40:47 +01:00
parent 17f24f765c
commit d2c8df3593
5 changed files with 560 additions and 16 deletions

9
go.mod
View File

@ -2,4 +2,11 @@ module xdcc-cli
go 1.13 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
)

56
go.sum
View File

@ -1,11 +1,67 @@
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 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/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 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 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 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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-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-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/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/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-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=

159
main.go
View File

@ -1,9 +1,13 @@
package main package main
import ( import (
"bufio"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"strconv"
"strings"
"sync"
) )
var registry *XdccProviderRegistry = nil var registry *XdccProviderRegistry = nil
@ -13,20 +17,156 @@ func init() {
registry.AddProvider(&XdccEuProvider{}) registry.AddProvider(&XdccEuProvider{})
} }
func search(fileName string) { var defaultColWidths []int = []int{100, 10, -1}
printer := NewTablePrinter([]string{"File Name", "Network", "Channel"})
res, _ := registry.Search(fileName) const (
for _, fileInfo := range res { KiloByte = 1024
printer.AddRow(Row{fileInfo.Name, fileInfo.Network, fileInfo.Channel}) 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() 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() { func main() {
searchCmd := flag.NewFlagSet("foo", flag.ExitOnError)
fileName := searchCmd.String("f", "", "name of the file to search")
if len(os.Args) < 2 { if len(os.Args) < 2 {
fmt.Println("one of the following subcommands is expected: [search, get]") fmt.Println("one of the following subcommands is expected: [search, get]")
@ -35,10 +175,9 @@ func main() {
switch os.Args[1] { switch os.Args[1] {
case "search": case "search":
searchCmd.Parse(os.Args[2:]) searchCommand(os.Args[2:])
search(*fileName)
case "get": case "get":
break getCommand(os.Args[2:])
default: default:
fmt.Println("no such command: ", os.Args[1]) fmt.Println("no such command: ", os.Args[1])
os.Exit(1) os.Exit(1)

25
url.go
View File

@ -14,13 +14,19 @@ type IRCFileURL struct {
Slot int Slot int
} }
type IRCBot struct {
Network string
Channel string
Name string
}
const ircFileURLFields = 4 const ircFileURLFields = 4
func parseSlot(slotStr string) (int, error) { func parseSlot(slotStr string) (int, error) {
if !strings.HasPrefix(slotStr, "#") { if strings.HasPrefix(slotStr, "#") {
return -1, errors.New("invalid slot") 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 // url has the following format: irc://network/channel/bot/#slot
@ -39,12 +45,21 @@ func parseIRCFileURl(url string) (*IRCFileURL, error) {
return nil, err return nil, err
} }
return &IRCFileURL{ fileUrl := &IRCFileURL{
Network: fields[0], Network: fields[0],
Channel: fields[1], Channel: fields[1],
UserName: fields[2], UserName: fields[2],
Slot: slot, 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 { func (url *IRCFileURL) String() string {

327
xdcc.go Normal file
View File

@ -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)
}
}