From 912574506df1ee3129f90deb40e8f1917422ace7 Mon Sep 17 00:00:00 2001 From: Rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Tue, 6 Sep 2022 11:10:46 +0800 Subject: [PATCH] Add GUI support (#8) --- .github/workflows/pull-request.yaml | 3 - .gitignore | 2 + .goreleaser.yml | 51 ++++---- Makefile | 7 ++ cmd/send.go | 70 +++++++++++ cmd/wait.go | 42 +++++++ go.mod | 4 + go.sum | 10 ++ main.go | 38 +----- pkg/broadcast.go | 62 ++++++++++ pkg/common.go | 39 ++++++ main_test.go => pkg/common_test.go | 38 +----- header.go => pkg/header.go | 22 ++-- header_test.go => pkg/header_test.go | 2 +- queue.go => pkg/queue.go | 2 +- queue_test.go => pkg/queue_test.go | 2 +- safe_map.go => pkg/safe_map.go | 2 +- safe_map_test.go => pkg/safe_map_test.go | 2 +- send.go => pkg/sender.go | 108 ++++++++--------- pkg/sender_test.go | 42 +++++++ wait.go => pkg/waiter.go | 105 ++++------------ ui/component/input.go | 30 +++++ ui/index.html | 64 ++++++++++ ui/main.go | 146 +++++++++++++++++++++++ 24 files changed, 643 insertions(+), 250 deletions(-) create mode 100644 cmd/send.go create mode 100644 cmd/wait.go create mode 100644 pkg/broadcast.go create mode 100644 pkg/common.go rename main_test.go => pkg/common_test.go (59%) rename header.go => pkg/header.go (83%) rename header_test.go => pkg/header_test.go (97%) rename queue.go => pkg/queue.go (96%) rename queue_test.go => pkg/queue_test.go (98%) rename safe_map.go => pkg/safe_map.go (98%) rename safe_map_test.go => pkg/safe_map_test.go (97%) rename send.go => pkg/sender.go (63%) create mode 100644 pkg/sender_test.go rename wait.go => pkg/waiter.go (51%) create mode 100644 ui/component/input.go create mode 100644 ui/index.html create mode 100644 ui/main.go diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 71ad9ee..0f52f52 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -33,9 +33,6 @@ jobs: - name: Test run: | make test - - name: Test - run: | - make test - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: diff --git a/.gitignore b/.gitignore index 6df9c8e..36b37c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ bin/ .idea/ +coverage.out +release diff --git a/.goreleaser.yml b/.goreleaser.yml index d654adf..f10a629 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,48 +3,59 @@ project_name: transfer builds: - env: - CGO_ENABLED=0 + id: "cli" binary: transfer - goarch: - - amd64 - - arm64 - - arm - goarm: - - 7 - - 6 goos: - windows - linux - darwin - ignore: - - goos: windows - goarch: arm - - goos: windows - goarch: arm64 - - goos: darwin - goarch: arm - hooks: - post: - - upx "{{ .Path }}" ldflags: - -X github.com/linuxsuren/cobra-extension/version.version={{.Version}} - -X github.com/linuxsuren/cobra-extension/version.commit={{.ShortCommit}} - -X github.com/linuxsuren/cobra-extension/version.date={{.Date}} - -w - -s + - env: + - CGO_ENABLED=0 + id: "gui" + binary: transfer-gui + main: ./ui/main.go + goos: + - windows + - linux + - darwin + ldflags: + - -X github.com/linuxsuren/cobra-extension/version.version={{.Version}} + - -X github.com/linuxsuren/cobra-extension/version.commit={{.ShortCommit}} + - -X github.com/linuxsuren/cobra-extension/version.date={{.Date}} + - -w dist: release archives: - - name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}" + - name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}" + id: "cli" + builds: + - "cli" replacements: darwin: darwin linux: linux windows: windows - amd64: amd64 - arm64: arm64 format_overrides: - goos: windows format: zip files: - README.md + - LICENSE + - name_template: "transfer-gui-{{ .Os }}-{{ .Arch }}" + id: "gui" + builds: + - "gui" + replacements: + darwin: darwin + linux: linux + amd64: amd64 + files: + - README.md + - LICENSE checksum: name_template: 'checksums.txt' snapshot: diff --git a/Makefile b/Makefile index a6f4ac5..73ec45c 100644 --- a/Makefile +++ b/Makefile @@ -9,5 +9,12 @@ build-linux: build-all: build-darwin build-linux build-win +build-gui: + CGO_ENABLE=0 GOOS=windows go build -ldflags -H=windowsgui -o bin/win/transfer-gui.exe ui/main.go + CGO_ENABLE=0 GOOS=windows go build -o bin/win/transfer-gui.exe ui/main.go + +goreleaser: + goreleaser build --rm-dist --snapshot + test: go test ./... -coverprofile coverage.out diff --git a/cmd/send.go b/cmd/send.go new file mode 100644 index 0000000..1e1c789 --- /dev/null +++ b/cmd/send.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "context" + "fmt" + "github.com/linuxsuren/transfer/pkg" + "github.com/spf13/cobra" + "time" +) + +func NewSendCmd() (cmd *cobra.Command) { + opt := &sendOption{} + + cmd = &cobra.Command{ + Use: "send", + Short: "Send data with UDP protocol", + PreRunE: opt.preRunE, + RunE: opt.runE, + } + flags := cmd.Flags() + flags.IntVarP(&opt.port, "port", "p", 3000, "The port to send") + return +} + +type sendOption struct { + ip string + port int +} + +func (o *sendOption) preRunE(cmd *cobra.Command, args []string) (err error) { + if len(args) >= 2 { + o.ip = args[1] + return + } + + cmd.Println("no target ip provided, trying to find it") + + ctx, cancel := context.WithCancel(cmd.Context()) + + waiter := make(chan string, 10) + pkg.FindWaiters(ctx, waiter) + + o.ip = <-waiter + cancel() + return +} + +func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { + beginTime := time.Now() + if len(args) <= 0 { + cmd.PrintErrln("filename is required") + return + } + + file := args[0] + + sender := pkg.NewUDPSender(o.ip).WithPort(o.port) + msg := make(chan string, 10) + + go func() { + for a := range msg { + cmd.Println(a) + } + }() + + err = sender.Send(msg, file) + endTime := time.Now() + fmt.Printf("sent over with %f\n", endTime.Sub(beginTime).Seconds()) + return +} diff --git a/cmd/wait.go b/cmd/wait.go new file mode 100644 index 0000000..6da7f22 --- /dev/null +++ b/cmd/wait.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "github.com/linuxsuren/transfer/pkg" + "github.com/spf13/cobra" +) + +type waitOption struct { + port int + listen string +} + +func (o *waitOption) preRunE(cmd *cobra.Command, _ []string) (err error) { + err = pkg.Broadcast(cmd.Context()) + return +} + +func (o *waitOption) runE(cmd *cobra.Command, args []string) error { + waiter := pkg.NewUDPWaiter(o.port).ListenAddress(o.listen) + msg := make(chan string, 10) + + go func() { + for a := range msg { + cmd.Println(a) + } + }() + return waiter.Start(msg) +} + +func NewWaitCmd() (cmd *cobra.Command) { + opt := &waitOption{} + cmd = &cobra.Command{ + Use: "wait", + Short: "Wait the data from a UDP protocol", + PreRunE: opt.preRunE, + RunE: opt.runE, + } + flags := cmd.Flags() + flags.IntVarP(&opt.port, "port", "p", 3000, "The port to listen") + flags.StringVarP(&opt.listen, "listen", "l", "0.0.0.0", "The address that want to listen") + return +} diff --git a/go.mod b/go.mod index 2ddf941..06c803c 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,12 @@ require ( ) require ( + github.com/asticode/go-astikit v0.29.1 // indirect + github.com/asticode/go-astilectron v0.29.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/maxence-charriere/go-app/v9 v9.6.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 446c0b9..1869e05 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,17 @@ +github.com/asticode/go-astikit v0.29.1 h1:w27sLYXK84mDwArf/Vw1BiD5dfD5PBDB+iHoIcpYq0w= +github.com/asticode/go-astikit v0.29.1/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= +github.com/asticode/go-astilectron v0.29.0 h1:oeceXo55BwcOWUMF/9FHfURBCORt7tgMwh7zeYyFGj0= +github.com/asticode/go-astilectron v0.29.0/go.mod h1:o7wZ7KDr3XH3xcEwcxfpWzNVf63JsMKtif/6IP4mpHk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/maxence-charriere/go-app/v9 v9.6.4 h1:XYQZhr5AXiV+zw7eRQsLRNCicG7hdKYAB+BnNbpdRqw= +github.com/maxence-charriere/go-app/v9 v9.6.4/go.mod h1:UlniES44R5JoD4HsjMNrAqWXSzyw0smM0Ox+QwnO/IE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -13,11 +21,13 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index fb9dd00..18b2258 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,7 @@ package main import ( - "fmt" - "strings" - "time" - + cmd2 "github.com/linuxsuren/transfer/cmd" "github.com/spf13/cobra" ) @@ -13,41 +10,10 @@ func NewRoot() (cmd *cobra.Command) { Use: "transfer", } - cmd.AddCommand(newSendCmd(), newWaitCmd()) - return -} - -func retry(count int, callback func() error) (err error) { - if callback == nil { - return - } - - for i := 0; i < count; i++ { - if err = callback(); err == nil { - break - } - // mainly do this on the darwin - time.Sleep(500 * time.Millisecond) - } + cmd.AddCommand(cmd2.NewSendCmd(), cmd2.NewWaitCmd()) return } -func fillContainerWithNumber(num, size int) string { - return fillContainer(fmt.Sprintf("%d", num), size) -} - -func fillContainer(txt string, size int) string { - length := len(txt) - buf := strings.Builder{} - - prePendCount := size - length - for i := 0; i < prePendCount; i++ { - buf.WriteString(" ") - } - buf.WriteString(txt) - return buf.String() -} - func main() { cmd := NewRoot() err := cmd.Execute() diff --git a/pkg/broadcast.go b/pkg/broadcast.go new file mode 100644 index 0000000..28b6792 --- /dev/null +++ b/pkg/broadcast.go @@ -0,0 +1,62 @@ +package pkg + +import ( + "context" + "net" + "time" +) + +// Broadcast sends the broadcast message to all the potential ip addresses +func Broadcast(ctx context.Context) (err error) { + var ifaces []net.Interface + if ifaces, err = net.Interfaces(); err != nil { + return + } + + var addrs []net.Addr + var allIPs []net.IP + for _, i := range ifaces { + if addrs, err = i.Addrs(); err != nil { + continue + } + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + } + + if ip == nil || ip.IsLinkLocalUnicast() || ip.IsLoopback() || ip.To4() == nil { + continue + } + + allIPs = append(allIPs, ip) + } + } + + for _, ip := range allIPs { + go func(ctx context.Context, ip net.IP) { + broadcast(ctx, ip) + }(ctx, ip) + } + return +} + +func broadcast(ctx context.Context, ip net.IP) { + for { + select { + case <-ctx.Done(): + return + case <-time.After(3 * time.Second): + ip.To4()[3] = 255 + srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0} + dstAddr := &net.UDPAddr{IP: ip, Port: 9981} + conn, err := net.ListenUDP("udp", srcAddr) + if err != nil { + return + } + _, _ = conn.WriteToUDP([]byte("hello"), dstAddr) + } + } +} diff --git a/pkg/common.go b/pkg/common.go new file mode 100644 index 0000000..e36a6d4 --- /dev/null +++ b/pkg/common.go @@ -0,0 +1,39 @@ +package pkg + +import ( + "fmt" + "strings" + "time" +) + +// Retry run the callback for the specific times +func Retry(count int, callback func() error) (err error) { + if callback == nil { + return + } + + for i := 0; i < count; i++ { + if err = callback(); err == nil { + break + } + // mainly do this on the darwin + time.Sleep(500 * time.Millisecond) + } + return +} + +func fillContainerWithNumber(num, size int) string { + return fillContainer(fmt.Sprintf("%d", num), size) +} + +func fillContainer(txt string, size int) string { + length := len(txt) + buf := strings.Builder{} + + prePendCount := size - length + for i := 0; i < prePendCount; i++ { + buf.WriteString(" ") + } + buf.WriteString(txt) + return buf.String() +} diff --git a/main_test.go b/pkg/common_test.go similarity index 59% rename from main_test.go rename to pkg/common_test.go index 8e8a32b..e8a2afe 100644 --- a/main_test.go +++ b/pkg/common_test.go @@ -1,4 +1,4 @@ -package main +package pkg import ( "testing" @@ -69,39 +69,3 @@ func TestFillContainerWithNumber(t *testing.T) { }) } } - -func TestCheckMissing(t *testing.T) { - tests := []struct { - name string - message []byte - wantIndex int - wantOK bool - }{{ - name: "missing", - message: []byte("miss000123"), - wantIndex: 123, - wantOK: true, - }, { - name: "missing", - message: []byte("miss 123"), - wantIndex: 123, - wantOK: true, - }, { - name: "missing with invalid number", - message: []byte("miss000aaa"), - wantIndex: 0, - wantOK: false, - }, { - name: "done", - message: []byte("done0000"), - wantIndex: -1, - wantOK: true, - }} - for i, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - index, ok := checkMissing(tt.message) - assert.Equal(t, tt.wantIndex, index, "failed in case [%d]", i) - assert.Equal(t, tt.wantOK, ok, "failed in case [%d]", i) - }) - } -} diff --git a/header.go b/pkg/header.go similarity index 83% rename from header.go rename to pkg/header.go index c4d53c1..7593791 100644 --- a/header.go +++ b/pkg/header.go @@ -1,4 +1,4 @@ -package main +package pkg import ( "fmt" @@ -50,7 +50,7 @@ func readHeader(conn *net.UDPConn) (header dataHeader, err error) { return } -type headerBuilder struct { +type HeaderBuilder struct { file string filename string @@ -59,14 +59,14 @@ type headerBuilder struct { bufferCount int } -// NewHeaderBuilder creates an instance of the headerBuilder -func NewHeaderBuilder(file string) *headerBuilder { - return &headerBuilder{ +// NewHeaderBuilder creates an instance of the HeaderBuilder +func NewHeaderBuilder(file string) *HeaderBuilder { + return &HeaderBuilder{ file: file, } } -func (h *headerBuilder) Build() (err error) { +func (h *HeaderBuilder) Build() (err error) { var fi os.FileInfo if fi, err = os.Stat(h.file); err != nil { return @@ -98,7 +98,7 @@ func (h *headerBuilder) Build() (err error) { } // CreateHeader creates the header with index -func (h *headerBuilder) CreateHeader(index int, data []byte) []byte { +func (h *HeaderBuilder) CreateHeader(index int, data []byte) []byte { // length,filename,count,index header := fmt.Sprintf("%s%s%s%s%s", fillContainerWithNumber(int(h.GetFileSize()), 20), @@ -110,21 +110,21 @@ func (h *headerBuilder) CreateHeader(index int, data []byte) []byte { } // GetChunk returns the chunk size -func (h *headerBuilder) GetChunk() int { +func (h *HeaderBuilder) GetChunk() int { return h.chunk } // GetBufferCount returns the buffer count -func (h *headerBuilder) GetBufferCount() int { +func (h *HeaderBuilder) GetBufferCount() int { return h.bufferCount } // GetFileSize returns the file size -func (h *headerBuilder) GetFileSize() int64 { +func (h *HeaderBuilder) GetFileSize() int64 { return h.fileSize } // GetFilename returns the file name -func (h *headerBuilder) GetFilename() string { +func (h *HeaderBuilder) GetFilename() string { return h.filename } diff --git a/header_test.go b/pkg/header_test.go similarity index 97% rename from header_test.go rename to pkg/header_test.go index 893f355..8dc3ef8 100644 --- a/header_test.go +++ b/pkg/header_test.go @@ -1,4 +1,4 @@ -package main +package pkg import ( "github.com/stretchr/testify/assert" diff --git a/queue.go b/pkg/queue.go similarity index 96% rename from queue.go rename to pkg/queue.go index 986baab..62dfee7 100644 --- a/queue.go +++ b/pkg/queue.go @@ -1,4 +1,4 @@ -package main +package pkg import "sync" diff --git a/queue_test.go b/pkg/queue_test.go similarity index 98% rename from queue_test.go rename to pkg/queue_test.go index f0bfc94..0fbef51 100644 --- a/queue_test.go +++ b/pkg/queue_test.go @@ -1,4 +1,4 @@ -package main +package pkg import ( "github.com/stretchr/testify/assert" diff --git a/safe_map.go b/pkg/safe_map.go similarity index 98% rename from safe_map.go rename to pkg/safe_map.go index cdd8572..882075d 100644 --- a/safe_map.go +++ b/pkg/safe_map.go @@ -1,4 +1,4 @@ -package main +package pkg import "sync" diff --git a/safe_map_test.go b/pkg/safe_map_test.go similarity index 97% rename from safe_map_test.go rename to pkg/safe_map_test.go index f263618..6a98016 100644 --- a/safe_map_test.go +++ b/pkg/safe_map_test.go @@ -1,4 +1,4 @@ -package main +package pkg import ( "github.com/stretchr/testify/assert" diff --git a/send.go b/pkg/sender.go similarity index 63% rename from send.go rename to pkg/sender.go index adf38a8..8cff399 100644 --- a/send.go +++ b/pkg/sender.go @@ -1,9 +1,9 @@ -package main +package pkg import ( "bufio" + "context" "fmt" - "github.com/spf13/cobra" "io" "net" "os" @@ -15,54 +15,25 @@ import ( "time" ) -func newSendCmd() (cmd *cobra.Command) { - opt := &sendOption{} - - cmd = &cobra.Command{ - Use: "send", - Short: "Send data with UDP protocol", - PreRunE: opt.preRunE, - RunE: opt.runE, - } - flags := cmd.Flags() - flags.IntVarP(&opt.port, "port", "p", 3000, "The port to send") - return -} - -type sendOption struct { +type UDPSender struct { ip string port int } -func (o *sendOption) preRunE(cmd *cobra.Command, args []string) (err error) { - if len(args) >= 2 { - o.ip = args[1] - return - } - - cmd.Println("no target ip provided, trying to find it") - listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 9981}) - if err != nil { - return err +func NewUDPSender(ip string) *UDPSender { + return &UDPSender{ + ip: ip, + port: 3000, } - data := make([]byte, 1024) - _, remoteAddr, err := listener.ReadFromUDP(data) - if err != nil { - return err - } - o.ip = remoteAddr.IP.String() - cmd.Println("found target", o.ip) - return } -func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { - beginTime := time.Now() - if len(args) <= 0 { - cmd.PrintErrln("filename is required") - return - } +func (s *UDPSender) WithPort(port int) *UDPSender { + s.port = port + return s +} - file := args[0] +func (s *UDPSender) Send(msg chan string, file string) (err error) { + defer close(msg) var f *os.File if f, err = os.Open(file); err != nil { @@ -77,19 +48,19 @@ func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { chunk := builder.GetChunk() fileSize := builder.GetFileSize() - cmd.Println("sending chunk size", chunk) - cmd.Println("file length", fileSize) - cmd.Println("connect to", o.ip) + msg <- fmt.Sprintf("sending chunk size %d", chunk) + msg <- fmt.Sprintf("file length %d", fileSize) + msg <- fmt.Sprintf("connect to %s", s.ip) var conn net.Conn - if conn, err = net.Dial("udp", fmt.Sprintf("%s:%d", o.ip, o.port)); err != nil { + if conn, err = net.Dial("udp", fmt.Sprintf("%s:%d", s.ip, s.port)); err != nil { return } defer func() { _ = conn.Close() }() - cmd.Println("start to send data") + msg <- "start to send data" reader := bufio.NewReader(f) for i := 0; i < builder.GetBufferCount(); i++ { buf := make([]byte, builder.GetChunk()) @@ -100,7 +71,7 @@ func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { return } - err = retry(30, func() error { + err = Retry(30, func() error { // no buffer space available might happen on darwin _, err := conn.Write(builder.CreateHeader(i, buf[:n])) return err @@ -111,7 +82,7 @@ func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { time.Sleep(time.Second) } } - cmd.Println("all the data was sent, try to wait for the missing data") + msg <- "all the data was sent, try to wait for the missing data" mapBuffer := NewSafeMap(0) ck := atomic.Bool{} @@ -120,7 +91,7 @@ func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { wg.Add(1) go func() { defer wg.Done() - cmd.Print("checking") + msg <- "checking" for index := mapBuffer.GetLowestAndRemove(); ck.Load(); index = mapBuffer.GetLowestAndRemove() { if index != nil { @@ -149,7 +120,7 @@ func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { if match, _ := regexp.MatchString(".*connection refused.*", err.Error()); match { time.Sleep(time.Second * 2) - if conn, err = net.Dial("udp", fmt.Sprintf("%s:%d", o.ip, o.port)); err != nil { + if conn, err = net.Dial("udp", fmt.Sprintf("%s:%d", s.ip, s.port)); err != nil { fmt.Println(err) return } @@ -161,12 +132,10 @@ func (o *sendOption) runE(cmd *cobra.Command, args []string) (err error) { } wg.Wait() - endTime := time.Now() - cmd.Println("sent over with", endTime.Sub(beginTime).Seconds()) return } -func send(f *os.File, reader *bufio.Reader, conn net.Conn, index, chunk int, builder *headerBuilder) (err error) { +func send(f *os.File, reader *bufio.Reader, conn net.Conn, index, chunk int, builder *HeaderBuilder) (err error) { if _, err = f.Seek(int64(index*chunk), 0); err != nil { return } @@ -177,7 +146,7 @@ func send(f *os.File, reader *bufio.Reader, conn net.Conn, index, chunk int, bui return } - err = retry(30, func() error { + err = Retry(30, func() error { _, err := conn.Write(builder.CreateHeader(index, buf[:n])) return err }) @@ -216,7 +185,30 @@ func checkMissing(message []byte) (index int, ok bool) { return } -func requestMissing(conn *net.UDPConn, index int, remote *net.UDPAddr) (err error) { - _, err = conn.WriteTo([]byte("miss"+fillContainerWithNumber(index, 10)), remote) - return +func FindWaiters(ctx context.Context, waiter chan string) { + go func() { + for { + var listener *net.UDPConn + var err error + + select { + case <-ctx.Done(): + if listener != nil { + _ = listener.Close() + } + return + default: + listener, err = net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 9981}) + if err != nil { + continue + } + + data := make([]byte, 1024) + _, remoteAddr, err := listener.ReadFromUDP(data) + if err == nil { + waiter <- remoteAddr.IP.String() + } + } + } + }() } diff --git a/pkg/sender_test.go b/pkg/sender_test.go new file mode 100644 index 0000000..c0fd697 --- /dev/null +++ b/pkg/sender_test.go @@ -0,0 +1,42 @@ +package pkg + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCheckMissing(t *testing.T) { + tests := []struct { + name string + message []byte + wantIndex int + wantOK bool + }{{ + name: "missing", + message: []byte("miss000123"), + wantIndex: 123, + wantOK: true, + }, { + name: "missing", + message: []byte("miss 123"), + wantIndex: 123, + wantOK: true, + }, { + name: "missing with invalid number", + message: []byte("miss000aaa"), + wantIndex: 0, + wantOK: false, + }, { + name: "done", + message: []byte("done0000"), + wantIndex: -1, + wantOK: true, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + index, ok := checkMissing(tt.message) + assert.Equal(t, tt.wantIndex, index, "failed in case [%d]", i) + assert.Equal(t, tt.wantOK, ok, "failed in case [%d]", i) + }) + } +} diff --git a/wait.go b/pkg/waiter.go similarity index 51% rename from wait.go rename to pkg/waiter.go index 927f0c5..4a73ee3 100644 --- a/wait.go +++ b/pkg/waiter.go @@ -1,79 +1,40 @@ -package main +package pkg import ( - "context" "fmt" - "github.com/spf13/cobra" "net" "os" "sync" "time" ) -type waitOption struct { +// UDPWaiter represents a UDP component for receiving data +type UDPWaiter struct { port int listen string } -func (o *waitOption) preRunE(cmd *cobra.Command, args []string) (err error) { - var ifaces []net.Interface - if ifaces, err = net.Interfaces(); err != nil { - return +// NewUDPWaiter creates an instance of NewUDPWaiter +func NewUDPWaiter(port int) *UDPWaiter { + return &UDPWaiter{ + port: port, + listen: "0.0.0.0", } - - var addrs []net.Addr - var allIPs []net.IP - for _, i := range ifaces { - if addrs, err = i.Addrs(); err != nil { - continue - } - - for _, addr := range addrs { - var ip net.IP - switch v := addr.(type) { - case *net.IPNet: - ip = v.IP - } - - if ip == nil || ip.IsLinkLocalUnicast() || ip.IsLoopback() || ip.To4() == nil { - continue - } - - allIPs = append(allIPs, ip) - } - } - - for _, ip := range allIPs { - go func(ctx context.Context, ip net.IP) { - broadcast(ctx, ip) - }(cmd.Context(), ip) - } - return } -func broadcast(ctx context.Context, ip net.IP) { - for { - select { - case <-ctx.Done(): - return - case <-time.After(3 * time.Second): - ip.To4()[3] = 255 - srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0} - dstAddr := &net.UDPAddr{IP: ip, Port: 9981} - conn, err := net.ListenUDP("udp", srcAddr) - if err != nil { - return - } - _, _ = conn.WriteToUDP([]byte("hello"), dstAddr) - } - } +// ListenAddress set the listen address +func (w *UDPWaiter) ListenAddress(address string) *UDPWaiter { + w.listen = address + return w } -func (o *waitOption) runE(cmd *cobra.Command, args []string) error { +// Start starts UDP connection +func (w *UDPWaiter) Start(msg chan string) (err error) { udpAddress := &net.UDPAddr{ - Port: o.port, - IP: net.ParseIP(o.listen), + Port: w.port, + IP: net.ParseIP(w.listen), } + defer close(msg) conn, err := net.ListenUDP("udp", udpAddress) if err != nil { @@ -83,14 +44,12 @@ func (o *waitOption) runE(cmd *cobra.Command, args []string) error { _ = conn.Close() }() - cmd.Printf("server listening %s\n", conn.LocalAddr().String()) - + msg <- fmt.Sprintf("server listening %s", conn.LocalAddr().String()) header, err := readHeader(conn) if err != nil { - cmd.Println(err) return err } - cmd.Println("start to receive data from", header.remote) + msg <- fmt.Sprintf("start to receive data from %v", header.remote) f, err := os.OpenFile(header.filename, os.O_WRONLY|os.O_CREATE, 0640) if err != nil { @@ -119,14 +78,12 @@ func (o *waitOption) runE(cmd *cobra.Command, args []string) error { //startedMissingThread := false for size := mapBuffer.Size(); size > 0; size = mapBuffer.Size() { - header, err = readHeader(conn) + header, err := readHeader(conn) if err == nil { go func(header dataHeader) { _, err = f.WriteAt(header.data, int64(header.chrunk*header.index)) if err == nil { mapBuffer.Remove(header.index) - } else { - fmt.Println(err) } }(header) } @@ -143,8 +100,8 @@ func (o *waitOption) runE(cmd *cobra.Command, args []string) error { sendWaitingMissingRequest(&wg, &header, mapBuffer, conn) wg.Wait() - cmd.Println("wrote to file", f.Name()) - return nil + msg <- fmt.Sprintf("wrote to file %s", f.Name()) + return } func sendWaitingMissingRequest(wg *sync.WaitGroup, header *dataHeader, buffer *SafeMap, conn *net.UDPConn) { @@ -159,10 +116,7 @@ func sendWaitingMissingRequest(wg *sync.WaitGroup, header *dataHeader, buffer *S missing := buffer.GetKeys() //fmt.Println("missing", len(missing)) for _, i := range missing { - err := requestMissing(conn, i, header.remote) - if err != nil { - fmt.Println(err) - } + _ = requestMissing(conn, i, header.remote) } time.Sleep(time.Second) } @@ -182,16 +136,7 @@ func requestDone(conn *net.UDPConn, remote *net.UDPAddr) (err error) { return } -func newWaitCmd() (cmd *cobra.Command) { - opt := &waitOption{} - cmd = &cobra.Command{ - Use: "wait", - Short: "Wait the data from a UDP protocol", - PreRunE: opt.preRunE, - RunE: opt.runE, - } - flags := cmd.Flags() - flags.IntVarP(&opt.port, "port", "p", 3000, "The port to listen") - flags.StringVarP(&opt.listen, "listen", "l", "0.0.0.0", "The address that want to listen") +func requestMissing(conn *net.UDPConn, index int, remote *net.UDPAddr) (err error) { + _, err = conn.WriteTo([]byte("miss"+fillContainerWithNumber(index, 10)), remote) return } diff --git a/ui/component/input.go b/ui/component/input.go new file mode 100644 index 0000000..42bf926 --- /dev/null +++ b/ui/component/input.go @@ -0,0 +1,30 @@ +package component + +import "github.com/maxence-charriere/go-app/v9/pkg/app" + +type Hello struct { + app.Compo + + name string +} + +func (h *Hello) Render() app.UI { + return app.Div().Body( + app.H1().Body( + app.Text("Hello, "), + app.If(h.name != "", + app.Text(h.name), + ).Else( + app.Text("World!"), + ), + ), + app.P().Body( + app.Input(). + Type("text"). + Value(h.name). + Placeholder("What is your name?"). + AutoFocus(true). + OnChange(h.ValueTo(&h.name)), + ), + ) +} diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..720a414 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,64 @@ + + + + + Data transfer + + + + +
+ + Choose: + + + \ No newline at end of file diff --git a/ui/main.go b/ui/main.go new file mode 100644 index 0000000..22a85cd --- /dev/null +++ b/ui/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "github.com/asticode/go-astikit" + "github.com/asticode/go-astilectron" + "github.com/linuxsuren/transfer/pkg" + "log" + "net/http" +) + +func startHTTPServer() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(indexHTML)) + }) + _ = http.ListenAndServe(":9999", nil) +} + +func main() { + // Set logger + l := log.New(log.Writer(), log.Prefix(), log.Flags()) + + go func() { + startHTTPServer() + }() + + // Create astilectron + a, err := astilectron.New(l, astilectron.Options{ + AppName: "Test", + BaseDirectoryPath: "example", + }) + if err != nil { + l.Fatal(fmt.Errorf("main: creating astilectron failed: %w", err)) + } + defer a.Close() + + // Handle signals + a.HandleSignals() + + // Start + if err = a.Start(); err != nil { + l.Fatal(fmt.Errorf("main: starting astilectron failed: %w", err)) + } + + // New window + var w *astilectron.Window + if w, err = a.NewWindow("http://localhost:9999", &astilectron.WindowOptions{ + Center: astikit.BoolPtr(true), + Height: astikit.IntPtr(700), + Width: astikit.IntPtr(700), + }); err != nil { + l.Fatal(fmt.Errorf("main: new window failed: %w", err)) + } + //w.OpenDevTools() + w.OnMessage(func(m *astilectron.EventMessage) (v interface{}) { + fmt.Println("receive message") + var s string + err := m.Unmarshal(&s) + fmt.Println(err) + + fmt.Println(s) + data := make(map[string]string) + err = json.Unmarshal([]byte(s), &data) + fmt.Println(err) + + switch data["cmd"] { + case "wait": + waiter := pkg.NewUDPWaiter(3000) + msg := make(chan string, 10) + + go func() { + for m := range msg { + var n = a.NewNotification(&astilectron.NotificationOptions{ + Body: m, + HasReply: astikit.BoolPtr(true), // Only MacOSX + ReplyPlaceholder: "type your reply here", // Only MacOSX + Title: "Msg", + }) + // Create notification + n.Create() + // Show notification + n.Show() + } + }() + return waiter.Start(msg) + case "send": + file := data["message"] + if file != "" { + sender := pkg.NewUDPSender(data["ip"]) + msg := make(chan string, 10) + + go func() { + for m := range msg { + var n = a.NewNotification(&astilectron.NotificationOptions{ + Body: m, + HasReply: astikit.BoolPtr(true), // Only MacOSX + ReplyPlaceholder: "type your reply here", // Only MacOSX + Title: "Msg", + }) + // Create notification + n.Create() + // Show notification + n.Show() + } + }() + + err = sender.Send(msg, file) + } + } + return + }) + _ = pkg.Broadcast(context.TODO()) + + ctx, cancel := context.WithCancel(context.TODO()) + w.On(astilectron.EventNameAppClose, func(e astilectron.Event) (deleteListener bool) { + cancel() + return + }) + waiter := make(chan string, 10) + pkg.FindWaiters(ctx, waiter) + go func() { + for { + select { + case <-ctx.Done(): + return + case ip := <-waiter: + _ = w.SendMessage(ip, func(m *astilectron.EventMessage) { + }) + } + } + }() + + // Create windows + if err = w.Create(); err != nil { + l.Fatal(fmt.Errorf("main: creating window failed: %w", err)) + } + + // Blocking pattern + a.Wait() +} + +//go:embed index.html +var indexHTML string