diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d386ed6 --- /dev/null +++ b/Makefile @@ -0,0 +1,66 @@ +#VERSION = $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || echo v0) +VERSION = $(shell git describe --tags --match=v* 2> /dev/null || echo 0.0.0) + +APPID = com.github.fabiodcorreia.catch-my-file +ICON = assets/icons/icon-512.png +NAME = CatchMyFile + +format: + gofmt -s -w main.go + gofmt -s -w internal/**/*.go + gofmt -s -w cmd/**/*.go + +review: format + @echo "============= Spell Check ============= " + @misspell . + + @echo "============= Ineffectual Assignments Check ============= " + @ineffassign ./... + + @echo "============= Cyclomatic Complexity Check ============= " + @gocyclo -total -over 5 -avg . + + @echo "============= Duplication Check ============= " + @dupl -t 25 + + @echo "============= Repeated Strings Check ============= " + @goconst ./... + + @echo "============= Vet Check ============= " + @go vet ./... + +build: + go mod tidy + go build -tags release -ldflags="-s -w" -o $(NAME) + +darwin: + fyne-cross darwin -arch amd64,arm64 -app-id $(APPID) -icon $(ICON) -app-version $(VERSION) -output $(NAME) + +linux: + fyne-cross linux -arch amd64,arm64 -app-id $(APPID) -icon $(ICON) -app-version $(VERSION) + +windows: + fyne-cross windows -arch amd64 -app-id $(APPID) -icon $(ICON) -app-version $(VERSION) + +bundle: + rm -fr dist + mkdir dist + + mv fyne-cross/dist/linux-amd64/$(NAME).tar.gz $(NAME)-$(VERSION)-linux-amd64.tar.gz + mv fyne-cross/dist/linux-arm64/$(NAME).tar.gz $(NAME)-$(VERSION)-linux-arm64.tar.gz + + (cd fyne-cross/dist/darwin-amd64/ && zip -r $(NAME)-darwin-amd64.zip $(NAME).app/) + mv fyne-cross/dist/darwin-amd64/$(NAME)-darwin-amd64.zip dist/$(NAME)-$(VERSION)-darwin-amd64.zip + + (cd fyne-cross/dist/darwin-arm64/ && zip -r $(NAME)-darwin-arm64.zip $(NAME).app/) + mv fyne-cross/dist/darwin-arm64/$(NAME)-darwin-arm64.zip dist/$(NAME)-$(VERSION)-darwin-arm64.zip + + mv fyne-cross/dist/windows-amd64/$(NAME).exe.zip $(NAME)-$(VERSION)-windows-amd64.zip + +release: darwin freebsd linux windows bundle + +tools: + go get -u github.com/jgautheron/goconst/cmd/goconst + go get -u github.com/mdempsky/unconvert + go get -u github.com/securego/gosec/v2/cmd/gosec + go get -u github.com/alexkohler/prealloc \ No newline at end of file diff --git a/cmd/frontend/frontend.go b/cmd/frontend/frontend.go new file mode 100644 index 0000000..ba8a3c8 --- /dev/null +++ b/cmd/frontend/frontend.go @@ -0,0 +1,89 @@ +package frontend + +import ( + "os" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "github.com/fabiodcorreia/catch-my-file/cmd/internal/backend" +) + +const ( + prefPort = "port" + prefHostname = "hostname" +) + +type Frontend struct { + a fyne.App + w fyne.Window +} + +func New() *Frontend { + f := &Frontend{ + a: app.NewWithID("github.fabiodcorreia.catch-my-file"), + } + f.w = f.a.NewWindow("Catch My File") + f.w.Resize(fyne.NewSize(880, 600)) + return f +} + +func (f *Frontend) Run() error { + pl := newPeersList() + rl := newTransferList() + sl := newTransferList() + lt := newLogTable() + f.w.SetContent(container.NewAppTabs( + container.NewTabItemWithIcon("Receiving", theme.DownloadIcon(), rl), + container.NewTabItemWithIcon("Sending", theme.UploadIcon(), sl), + container.NewTabItemWithIcon("Peers", theme.ComputerIcon(), pl), + container.NewTabItemWithIcon("Log", theme.ErrorIcon(), lt), + )) + + hn, err := os.Hostname() + if err != nil { + return err + } + + f.a.Preferences().SetString(prefHostname, strings.ReplaceAll(hn, ".", "-")) //! ? + f.a.Preferences().SetInt(prefPort, 8820) + + e := backend.NewEngine(f.a.Preferences().String(prefHostname), f.a.Preferences().Int(prefPort)) + + go func() { + for { + select { + case p := <-e.DiscoverPeers(): + pl.NewPeer(p) + case t := <-e.ReceiveTransferNotification(): + rl.NewTransfer(t) + case t := <-pl.SendTransfer: + sl.NewTransfer(t) + case rds := <-e.DiscoverServerError(): + lt.NewLogRecord(time.Now(), rds) + case rdc := <-e.DiscoverClientError(): + lt.NewLogRecord(time.Now(), rdc) + case rts := <-e.TransferServerError(): + lt.NewLogRecord(time.Now(), rts) + } + } + }() + + f.w.CenterOnScreen() + f.w.SetMaster() + f.w.SetOnClosed(func() { + e.Shutdown() + }) + + err = e.Start() + if err != nil { + f.a.SendNotification(fyne.NewNotification("Catch My File - Fail to Start", err.Error())) + return err + } + + f.w.ShowAndRun() + return nil +} diff --git a/cmd/frontend/logs.go b/cmd/frontend/logs.go new file mode 100644 index 0000000..d2ffa17 --- /dev/null +++ b/cmd/frontend/logs.go @@ -0,0 +1,50 @@ +package frontend + +import ( + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +type logTable struct { + widget.Table + items []error +} + +func newLogTable() *logTable { + lt := &logTable{ + items: make([]error, 0, 20), + } + lt.Table.Length = lt.Length + lt.Table.CreateCell = lt.CreateCell + lt.Table.UpdateCell = lt.UpdateCell + lt.Table.SetColumnWidth(0, 160) + lt.ExtendBaseWidget(lt) + + return lt +} + +func (lt *logTable) Length() (int, int) { + return len(lt.items), 2 +} + +func (lt *logTable) CreateCell() fyne.CanvasObject { + return widget.NewLabel("Name") +} + +func (lt *logTable) UpdateCell(id widget.TableCellID, item fyne.CanvasObject) { + switch id.Col { + case 0: + item.(*widget.Label).SetText(time.Now().Format("2006-01-02 15:04:05")) + case 1: + item.(*widget.Label).SetText(lt.items[id.Row].Error()) + } +} + +func (lt *logTable) NewLogRecord(t time.Time, err error) { + if err != nil { + lt.items = append(lt.items, err) + lt.Refresh() + } +} diff --git a/cmd/frontend/peers.go b/cmd/frontend/peers.go new file mode 100644 index 0000000..8acb81d --- /dev/null +++ b/cmd/frontend/peers.go @@ -0,0 +1,104 @@ +package frontend + +import ( + "fmt" + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" + "github.com/fabiodcorreia/catch-my-file/internal/store" + "github.com/fabiodcorreia/catch-my-file/internal/transfer" +) + +type peersList struct { + widget.List + items []store.Peer + SendTransfer chan *store.Transfer +} + +func newPeersList() *peersList { + p := &peersList{ + items: make([]store.Peer, 0, 1), + SendTransfer: make(chan *store.Transfer, 1), + } + + p.List.Length = p.Length + p.List.CreateItem = p.CreateItem + p.List.UpdateItem = p.UpdateItem + p.ExtendBaseWidget(p) + + return p +} + +func (pl *peersList) Length() int { + return len(pl.items) +} + +func (pl *peersList) CreateItem() fyne.CanvasObject { + return container.NewAdaptiveGrid( + 4, + widget.NewLabel(""), //Name + widget.NewLabel(""), //Ip Address + widget.NewLabel(""), //Port + widget.NewButton("", func() {}), + ) +} + +func (pl *peersList) UpdateItem(i int, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[0].(*widget.Label).SetText(pl.items[i].Name) + item.(*fyne.Container).Objects[1].(*widget.Label).SetText(pl.items[i].Address.String()) + item.(*fyne.Container).Objects[2].(*widget.Label).SetText(fmt.Sprintf("%d", pl.items[i].Port)) + item.(*fyne.Container).Objects[3] = widget.NewButton("Send File", func() { + dialog.ShowFileOpen(func(uc fyne.URIReadCloser, err error) { + if err != nil || uc == nil { + return + } + + ft, err := transfer.NewFileTransfer(uc.URI().Path()) + if err != nil { + return + } + + rect := canvas.NewRectangle(color.Transparent) + rect.SetMinSize(fyne.NewSize(200, 0)) + + d := dialog.NewCustom( + "Send File Request", + fmt.Sprintf("Preparing %s to send to %s", ft.FileName, pl.items[i].Name), + container.NewMax(rect, widget.NewProgressBarInfinite()), + fyne.CurrentApp().Driver().AllWindows()[0], + ) + + tc := transfer.NewClient(pl.items[i].Address, pl.items[i].Port, ft) + + tt := store.NewTransfer("", ft.FileName, float64(ft.FileSize), pl.items[i].Name, nil) + tt.IsToSend = true + + d.Show() + err = tc.SendRequest() + d.Hide() + + if err != nil { + return + } + + go tc.WaitSendOrStop(tt) + pl.SendTransfer <- tt + }, fyne.CurrentApp().Driver().AllWindows()[0]) + }) + item.Refresh() +} +func (pl *peersList) RemoveItem(i int) { + copy(pl.items[i:], pl.items[i+1:]) + //pl.items[pl.Length()-1] = nil + pl.items = pl.items[:pl.Length()-1] + pl.Refresh() +} + +func (pl *peersList) NewPeer(p store.Peer) { + pl.items = append(pl.items, p) + pl.Refresh() +} diff --git a/cmd/frontend/transfers.go b/cmd/frontend/transfers.go new file mode 100644 index 0000000..2dcf3a7 --- /dev/null +++ b/cmd/frontend/transfers.go @@ -0,0 +1,180 @@ +package frontend + +import ( + "fmt" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/fabiodcorreia/catch-my-file/internal/store" +) + +type transferList struct { + widget.List + items []*store.Transfer +} + +func newTransferList() *transferList { + p := &transferList{ + items: make([]*store.Transfer, 0, 5), + } + p.List.Length = p.Length + p.List.CreateItem = p.CreateItem + p.List.UpdateItem = p.UpdateItem + p.ExtendBaseWidget(p) + + return p +} + +func (tl *transferList) Length() int { + return len(tl.items) +} + +func (tl *transferList) CreateItem() fyne.CanvasObject { + return container.New(&transferLayout{}, + widget.NewLabel(""), // Name + widget.NewLabel(""), // Sender + widget.NewLabel(""), // Size + container.NewHBox( + widget.NewProgressBar(), + widget.NewButton("", func() {}), // Accept + widget.NewButton("", func() {})), // Reject + ) +} + +func onConfirm(t *store.Transfer, cProAction *fyne.Container) { + saveDialog := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) { + if err != nil || uc == nil { + return + } + t.Accept <- uc.URI().Path() + close(t.Accept) + showHideButtons(true, cProAction) + }, fyne.CurrentApp().Driver().AllWindows()[0]) + saveDialog.SetFileName(t.Name) + saveDialog.Show() +} + +func showHideButtons(pbVisible bool, cProAction *fyne.Container) { + if pbVisible { + cProAction.Objects[0].(*widget.ProgressBar).Show() + cProAction.Objects[1].(*widget.Button).Hide() + cProAction.Objects[2].(*widget.Button).Hide() + } else { + cProAction.Objects[0].(*widget.ProgressBar).Hide() + cProAction.Objects[1].(*widget.Button).Show() + cProAction.Objects[2].(*widget.Button).Show() + } +} + +func (tl *transferList) UpdateItem(i int, item fyne.CanvasObject) { + wName := item.(*fyne.Container).Objects[0].(*widget.Label) + wSource := item.(*fyne.Container).Objects[1].(*widget.Label) + wSize := item.(*fyne.Container).Objects[2].(*widget.Label) + cProAction := item.(*fyne.Container).Objects[3].(*fyne.Container) + + // If label size is not set it's the frist update of the item + if wSize.Text == "" { + wName.SetText(tl.items[i].Name) + wSource.SetText(tl.items[i].SourceName) + wSize.SetText(byteCountSI(tl.items[i].Size)) + + showHideButtons(tl.items[i].IsToSend, cProAction) + + tl.items[i].OnProgressChange(func(progress float64) { + cProAction.Objects[0].(*widget.ProgressBar).SetValue(progress) + }) + + if cProAction.Objects[1].Visible() { + cProAction.Objects[1] = widget.NewButtonWithIcon("", theme.ConfirmIcon(), func() { + onConfirm(tl.items[i], cProAction) + }) + + cProAction.Objects[2] = widget.NewButtonWithIcon("", theme.CancelIcon(), func() { + close(tl.items[i].Accept) + cProAction.Objects[1].(*widget.Button).Hide() + cProAction.Objects[2].(*widget.Button).Hide() + }) + } + } +} + +func (tl *transferList) RemoveItem(i int) { + copy(tl.items[i:], tl.items[i+1:]) + tl.items[tl.Length()-1] = nil + tl.items = tl.items[:tl.Length()-1] + tl.Refresh() +} + +func (tl *transferList) NewTransfer(t *store.Transfer) { + tl.items = append(tl.items, t) + //tl.Refresh() + if !t.IsToSend { + go dialog.ShowInformation("Transfer Request Received", t.Name, fyne.CurrentApp().Driver().AllWindows()[0]) + } +} + +func byteCountSI(b float64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%f B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) +} + +var maxMinSizeHeight float32 // Keeping all instances of the list layout consistent in height + +type transferLayout struct{} + +// Layout is called to pack all child objects into a specified size. +// The objects is the list of UI objects inside the layout and the +// size is the size of the container. +func (l *transferLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + col1Width := size.Width * 0.60 + col2Width := size.Width * 0.15 + col3Width := size.Width * 0.10 + col4Width := size.Width - col1Width - col2Width - col3Width + + resizeAndMove(objects[0], col1Width, 0, 0) + resizeAndMove(objects[1], col2Width, objects[0].Position().X, objects[0].Size().Width) + resizeAndMove(objects[2], col3Width, objects[1].Position().X, objects[1].Size().Width) + resizeAndMove(objects[3], col4Width, objects[2].Position().X, objects[2].Size().Width) + + cont := objects[3].(*fyne.Container) + // ProgressBar is visible + if cont.Objects[0].Visible() { + cont.Objects[0].Resize(fyne.NewSize(col4Width, objects[3].Size().Height)) + return + } + + // Buttons are visible + cont.Objects[1].Resize(fyne.NewSize(col4Width/2, objects[3].Size().Height)) + cont.Objects[2].Resize(fyne.NewSize(col4Width/2, objects[3].Size().Height)) + cont.Objects[2].Move(fyne.NewPos(cont.Objects[1].Position().X+cont.Objects[1].Size().Width, 0)) +} + +func resizeAndMove(obj fyne.CanvasObject, width float32, prevPositionX float32, prevObjWidth float32) { + obj.Resize(fyne.NewSize(width, obj.Size().Height)) + obj.Move(fyne.NewPos(prevPositionX+prevObjWidth, 0)) +} + +// MinSize finds the smallest size that satisfies all the child objects. +// Height will stay consistent between each each instance. +func (g *transferLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + maxMinSizeWidth := float32(0) + for _, child := range objects { + if child.Visible() { + maxMinSizeWidth += child.MinSize().Width + maxMinSizeHeight = fyne.Max(child.MinSize().Height, maxMinSizeHeight) + } + } + + return fyne.NewSize(maxMinSizeWidth, maxMinSizeHeight+theme.Padding()) +} diff --git a/cmd/internal/backend/backend.go b/cmd/internal/backend/backend.go new file mode 100644 index 0000000..adbd777 --- /dev/null +++ b/cmd/internal/backend/backend.go @@ -0,0 +1,143 @@ +package backend + +import ( + "context" + "fmt" + + "github.com/fabiodcorreia/catch-my-file/internal/discover" + "github.com/fabiodcorreia/catch-my-file/internal/store" + "github.com/fabiodcorreia/catch-my-file/internal/transfer" + "github.com/grandcat/zeroconf" +) + +type appState uint + +const ( + // All the components are off + down appState = iota + // Broadcast Server is running + discoverServerUp + // Broadcast Client is running + discoverClientUp + // Transfer Server is running + transferServerUp +) + +type Engine struct { + ds *discover.Server + dc *discover.Client + ts *transfer.Server + ctx context.Context + cancel context.CancelFunc + state appState + stream chan store.Peer +} + +func NewEngine(hostname string, transferPort int) *Engine { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + e := &Engine{ + ds: discover.NewServer(ctx, hostname, transferPort), + dc: discover.NewClient(ctx), + ts: transfer.NewServer(ctx, transferPort), + state: down, + ctx: ctx, + cancel: cancel, + stream: make(chan store.Peer, 1), + } + + return e +} + +func (e *Engine) DiscoverPeers() chan store.Peer { + go func(results <-chan *zeroconf.ServiceEntry) { + for entry := range results { + e.stream <- store.NewPeer(entry.HostName, entry.AddrIPv4[0], entry.Port) + } + }(e.dc.PeerStream) + return e.stream +} + +func (e *Engine) ReceiveTransferNotification() chan *store.Transfer { + return e.ts.TransferStream +} + +func (e *Engine) DiscoverClientError() chan error { + return e.ds.Err +} + +func (e *Engine) DiscoverServerError() chan error { + return e.dc.Err +} + +func (e *Engine) TransferServerError() chan error { + return e.ts.Err +} + +func (e *Engine) Start() error { + err := e.bootstrap() + + if err != nil { + e.Shutdown() + return err + } + + return err +} + +// Shutdown will cancel the context to finish all the backend components. +func (e *Engine) Shutdown() { + e.cancel() + // Wait until brocast server exits + if e.state >= discoverServerUp { + <-e.ds.Exit + } + // Wait until brocast client exits + if e.state >= discoverClientUp { + <-e.dc.Exit + } + // Wait until transfer exits + if e.state >= transferServerUp { + e.ts.Stop() + <-e.ts.Exit + } +} + +// bootstrap will start a goroutine for each component and wait for the startup to finish. +// For each component ready it will also update the state of the application. +// +// If a component fails it will interrupt the sequence and return an error. +func (e *Engine) bootstrap() error { + go e.ds.Run() + + err := waitForReady(e.ds.Ready, e.ds.Err) + if err != nil { + return fmt.Errorf("discover server fail before ready: %w", err) + } + + go e.dc.Run() + + err = waitForReady(e.dc.Ready, e.dc.Err) + if err != nil { + return fmt.Errorf("discover client fail before ready: %w", err) + } + + go e.ts.Run() + + err = waitForReady(e.ts.Ready, e.ts.Err) + if err != nil { + return fmt.Errorf("transfer client fail before ready: %w", err) + } + + return nil +} + +func waitForReady(ready chan interface{}, err chan error) error { + select { + case <-ready: + return nil + case e := <-err: + return e + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..124f383 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/fabiodcorreia/catch-my-file + +go 1.16 + +require ( + fyne.io/fyne/v2 v2.0.3 + github.com/grandcat/zeroconf v1.0.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b0ecdd --- /dev/null +++ b/go.sum @@ -0,0 +1,94 @@ +fyne.io/fyne/v2 v2.0.3 h1:qzd2uLLrAVrNeqnLY44QZCsMxZwjoo1my+lMzHicMXY= +fyne.io/fyne/v2 v2.0.3/go.mod h1:nNpgL7sZkDVLraGtQII2ArNRnnl6kHup/KfQRxIhbvs= +github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +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/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fyne-io/mobile v0.1.3-0.20210412090810-650a3139866a h1:3TAJhl8vXyli0tooKB0vd6gLCyBdWL4QEYbDoJpHEZk= +github.com/fyne-io/mobile v0.1.3-0.20210412090810-650a3139866a/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= +github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= +github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= +github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= +github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.1.25/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/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666 h1:gVCS+QOncANNPlmlO1AhlU3oxs4V9z+gTtPwIk3p2N8= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/discover/client.go b/internal/discover/client.go new file mode 100644 index 0000000..6ee3442 --- /dev/null +++ b/internal/discover/client.go @@ -0,0 +1,52 @@ +package discover + +import ( + "context" + + "github.com/grandcat/zeroconf" +) + +type Client struct { + PeerStream chan *zeroconf.ServiceEntry + Ready chan interface{} + Exit chan interface{} + Err chan error + ctx context.Context +} + +func NewClient(ctx context.Context) *Client { + return &Client{ + PeerStream: make(chan *zeroconf.ServiceEntry), + Ready: make(chan interface{}), + Exit: make(chan interface{}, 1), + Err: make(chan error, 1), + ctx: ctx, + } +} + +func (bc *Client) stop(err error) { + bc.Err <- err + close(bc.Err) + close(bc.Exit) +} + +func (bc *Client) Run() { + + resolver, err := zeroconf.NewResolver(zeroconf.SelectIPTraffic(zeroconf.IPv4)) + if err != nil { + bc.stop(err) + return + } + + zeroconf.SelectIPTraffic(zeroconf.IPv4) + + close(bc.Ready) + + err = resolver.Browse(bc.ctx, serviceName, serviceDomain, bc.PeerStream) + if err != nil { + bc.Err <- err + } + + <-bc.ctx.Done() + bc.stop(err) +} diff --git a/internal/discover/discover.go b/internal/discover/discover.go new file mode 100644 index 0000000..71bf20c --- /dev/null +++ b/internal/discover/discover.go @@ -0,0 +1,11 @@ +// Discover package is responsible for the components that allow +// the peer discovery on the local network. +// +// It contains a server that register the peers to be discovered +// and the client that will discover the peers +package discover + +const ( + serviceName = "_catchmyfile._tcp" + serviceDomain = "local." +) diff --git a/internal/discover/server.go b/internal/discover/server.go new file mode 100644 index 0000000..3e4a803 --- /dev/null +++ b/internal/discover/server.go @@ -0,0 +1,47 @@ +package discover + +import ( + "context" + "fmt" + + "github.com/grandcat/zeroconf" +) + +type Server struct { + Name string + Port int + Ready chan interface{} + Exit chan interface{} + Err chan error + ctx context.Context +} + +func NewServer(ctx context.Context, name string, port int) *Server { + return &Server{ + Name: name, + Port: port, + Ready: make(chan interface{}), + Exit: make(chan interface{}, 1), + Err: make(chan error, 1), + ctx: ctx, + } +} + +func (bs *Server) Run() { + server, err := zeroconf.Register(fmt.Sprintf("catch-%s", bs.Name), serviceName, serviceDomain, bs.Port, nil, nil) + if err != nil { + bs.Err <- err + close(bs.Err) + close(bs.Exit) + return + } + + defer server.Shutdown() + + close(bs.Ready) + + <-bs.ctx.Done() + + close(bs.Err) + close(bs.Exit) +} diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 0000000..4ab9763 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,63 @@ +package store + +import ( + "net" + "strings" + "time" +) + +type Peer struct { + Name string + Address net.IP + Port int + Timestamp time.Time +} + +func NewPeer(name string, address net.IP, port int) Peer { + return Peer{ + Name: strings.Replace(name, ".local.", "", 1), + Address: address, + Port: port, + Timestamp: time.Now(), + } +} + +type Transfer struct { + Checksum string + Name string + Size float64 + SourceAddr net.Addr + SourceName string + Accept chan string + IsToSend bool + transferred float64 + progressCallback func(progress float64) +} + +func NewTransfer(checksum string, name string, size float64, sourceName string, sourceAddr net.Addr) *Transfer { + return &Transfer{ + Checksum: checksum, + Name: name, + Size: size, + SourceName: sourceName, + SourceAddr: sourceAddr, + Accept: make(chan string), + } +} + +func (t *Transfer) OnProgressChange(callback func(progress float64)) { + if callback != nil { + t.progressCallback = callback + } +} + +func (t *Transfer) UpdateTransferred(value float64) { + t.transferred += value + if t.progressCallback != nil { + t.progressCallback(t.transferred / t.Size) + } +} + +func (t *Transfer) Transferred() float64 { + return t.transferred +} diff --git a/internal/transfer/client.go b/internal/transfer/client.go new file mode 100644 index 0000000..e92b257 --- /dev/null +++ b/internal/transfer/client.go @@ -0,0 +1,123 @@ +package transfer + +import ( + "fmt" + "io" + "net" + "os" + "strconv" + "time" + + "github.com/fabiodcorreia/catch-my-file/internal/store" +) + +type Client struct { + ServerAddress net.IP + Port int + ft *FileTransfer + conn net.Conn +} + +func NewClient(addr net.IP, port int, ft *FileTransfer) *Client { + return &Client{ + ServerAddress: addr, + Port: port, + ft: ft, + } +} + +func (tc *Client) SendRequest() error { + ck, err := tc.ft.Checksum() + if err != nil { + return err + } + + ckm, err := fillMessage(ck, transferChecksumBufferLen) + if err != nil { + return err + } + + sz, err := fillMessage(strconv.FormatInt(tc.ft.FileSize, 10), transferSizeBufferLen) + if err != nil { + return err + } + + fm, err := fillMessage(tc.ft.FileName, transferNameLen) + if err != nil { + return err + } + + h, err := os.Hostname() + if err != nil { + return err + } + + hn, err := fillMessage(h, transferHostnameLen) + if err != nil { + return err + } + + message := make([]byte, 0, messageSize()) + message = append(message, append(ckm, append(sz, append(fm, hn...)...)...)...) + + conn, err := net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", tc.ServerAddress, tc.Port), time.Second*10) + if err != nil { + fmt.Println(err.Error()) + return err + } + + tc.conn = conn + + conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) + _, err = conn.Write(message) + if err != nil { + tc.conn.Close() + return err + } + + return nil +} + +func (tc *Client) WaitSendOrStop(trans *store.Transfer) { + defer tc.conn.Close() + + bufferConfirm := make([]byte, 1) + _, err := tc.conn.Read(bufferConfirm) + if err != nil { + fmt.Println(err.Error()) + return + } + + if string(bufferConfirm) != "1" { + fmt.Println("file rejected") + return + } + + r, err := tc.ft.Open() + if err != nil { + fmt.Println(err.Error()) + return + } + + defer r.Close() + + sendBuffer := make([]byte, transferBufferLen) + + for { + rc, err := r.Read(sendBuffer) + if err == io.EOF { + break + } + if err != nil { + fmt.Println("read error:" + err.Error()) + return + } + tc.conn.SetWriteDeadline(time.Time{}) + w, err := tc.conn.Write(sendBuffer[:rc]) + if err != nil { + fmt.Println("client:" + err.Error()) + return + } + trans.UpdateTransferred(float64(w)) + } +} diff --git a/internal/transfer/filetransfer.go b/internal/transfer/filetransfer.go new file mode 100644 index 0000000..f0bb073 --- /dev/null +++ b/internal/transfer/filetransfer.go @@ -0,0 +1,53 @@ +package transfer + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +type FileTransfer struct { + FileFullPath string + FileName string + FileSize int64 + FileExt string + checksum string +} + +func NewFileTransfer(filePath string) (*FileTransfer, error) { + st, err := os.Stat(filePath) + if err != nil { + return nil, err + } + + if st.IsDir() || !st.Mode().IsRegular() { + return nil, fmt.Errorf("file is not valid") + } + + return &FileTransfer{ + FileFullPath: filePath, + FileName: filepath.Base(filePath), + FileSize: st.Size(), + FileExt: filepath.Ext(filePath), + }, nil +} + +func (ft *FileTransfer) Open() (io.ReadCloser, error) { + return os.OpenFile(ft.FileFullPath, os.O_RDONLY, os.ModePerm) +} + +func (ft *FileTransfer) Checksum() (string, error) { + if ft.checksum != "" { + return ft.checksum, nil + } + + f, err := ft.Open() + if err != nil { + return "", err + } + defer f.Close() + chks, err := fileChecksum(f) + ft.checksum = chks + return chks, err +} diff --git a/internal/transfer/server.go b/internal/transfer/server.go new file mode 100644 index 0000000..0f83ee3 --- /dev/null +++ b/internal/transfer/server.go @@ -0,0 +1,154 @@ +package transfer + +import ( + "context" + "fmt" + "net" + "os" + "strconv" + + "github.com/fabiodcorreia/catch-my-file/internal/store" +) + +type Server struct { + Port int + Ready chan interface{} + Exit chan interface{} + Err chan error + TransferStream chan *store.Transfer + listener net.Listener + ctx context.Context +} + +func NewServer(ctx context.Context, port int) *Server { + return &Server{ + Port: port, + Ready: make(chan interface{}), + Exit: make(chan interface{}, 1), + Err: make(chan error, 1), + TransferStream: make(chan *store.Transfer), + ctx: ctx, + } + +} + +func (ts *Server) Stop() { + ts.stop(nil) +} + +func (ts *Server) stop(err error) { + if err != nil { + ts.Err <- err + } + err = ts.listener.Close() + if err != nil { + ts.Err <- err + } + close(ts.Err) + close(ts.Exit) +} + +func (ts *Server) Run() { + listener, err := net.Listen("tcp4", fmt.Sprintf(":%d", ts.Port)) + if err != nil { + ts.Err <- err + return + } + + ts.listener = listener + + close(ts.Ready) + + for { + c, err := ts.listener.Accept() + + if err != nil { + ts.Err <- err + return + } + + if ts.ctx.Err() != nil { + ts.stop(ts.ctx.Err()) + return + } + + go ts.transfer(c) + } + +} + +func (ts *Server) transfer(conn net.Conn) { + + tName, tCheck, tHostname, tSize, err := receiveTransferData(conn) + if err != nil { + ts.Err <- err + return + } + + tt := store.NewTransfer(tCheck, tName, float64(tSize), tHostname, conn.RemoteAddr()) + + ts.TransferStream <- tt + + filePath, open := <-tt.Accept + + if !open { + ts.Err <- fmt.Errorf("transfer %s rejected", tt.Name) + conn.Write([]byte("0")) + return + } + + conn.Write([]byte("1")) + + err = WriteFile(ts.ctx, conn, filePath, tt) + if err != nil { + ts.Err <- err + return + } + + err = verifyFile(filePath, tt.Checksum) + if err != nil { + ts.Err <- err + return + } + ts.Err <- fmt.Errorf("file %s transferred and verified", tName) +} + +func verifyFile(filePath string, checksum string) error { + ft, err := NewFileTransfer(filePath) + if err != nil { + return err + } + + c, err := ft.Checksum() + + if err != nil { + return err + } + + if c != checksum { + err = fmt.Errorf("file checksum don't match") + e := os.Remove(ft.FileFullPath) + if e != nil { + return fmt.Errorf("%s - %w", err, e) + } + return err + } + return nil +} + +func receiveTransferData(conn net.Conn) (string, string, string, int64, error) { + bufferMessage := make([]byte, messageSize()) + _, err := conn.Read(bufferMessage) + if err != nil { + return "", "", "", 0, err + } + + bufferChecksum := bufferMessage[:messageCheckIdx()] + bufferFileSize := bufferMessage[messageCheckIdx():messageSizeIdx()] + bufferFileName := bufferMessage[messageSizeIdx():messageNameIdx()] + bufferHostName := bufferMessage[messageNameIdx():messageHostIdx()] + + tSize, err := strconv.ParseInt(trimMessage(bufferFileSize), 10, 64) + + return trimMessage(bufferFileName), trimMessage(bufferChecksum), trimMessage(bufferHostName), tSize, err +} diff --git a/internal/transfer/transfer.go b/internal/transfer/transfer.go new file mode 100644 index 0000000..24b81c6 --- /dev/null +++ b/internal/transfer/transfer.go @@ -0,0 +1,104 @@ +package transfer + +import ( + "context" + "crypto/sha256" + "fmt" + "io" + "os" + + "github.com/fabiodcorreia/catch-my-file/internal/store" +) + +const ( + transferBufferLen int = 65536 //64kb + transferChecksumBufferLen int = 64 + transferSizeBufferLen int = 10 + transferNameLen int = 128 + transferHostnameLen int = 32 +) + +func messageSize() int { + return transferChecksumBufferLen + transferSizeBufferLen + transferNameLen + transferHostnameLen +} + +func messageCheckIdx() int { + return transferChecksumBufferLen +} + +func messageSizeIdx() int { + return messageCheckIdx() + transferSizeBufferLen +} + +func messageNameIdx() int { + return messageCheckIdx() + transferNameLen +} + +func messageHostIdx() int { + return messageNameIdx() + transferHostnameLen +} + +func fileChecksum(r io.Reader) (string, error) { + hash := sha256.New() + + if _, err := io.Copy(hash, r); err != nil { + return "", err + } + + return fmt.Sprintf("%x", hash.Sum(nil)), nil +} + +func WriteFile(ctx context.Context, r io.Reader, filePath string, t *store.Transfer) error { + w, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, os.ModePerm) + + if err != nil && err != io.EOF { + return err + } + + buf := make([]byte, transferBufferLen) + for { + if ctx.Err() != nil { + return ctx.Err() + } + + nr, err := r.Read(buf) + if err != nil && err != io.EOF { + return err + } + + if nr == 0 { + break + } + + nw, err := w.Write(buf[:nr]) + if err != nil { + return err + } + + t.UpdateTransferred(float64(nw)) + } + /* + if t.Transferred() != t.Size { + return fmt.Errorf("file size and transferred size are different") + }*/ + return nil +} + +func fillMessage(content string, toFill int) ([]byte, error) { + if len(content) > toFill { + return nil, fmt.Errorf("message is longer than the fill length") + } + + buffer := make([]byte, toFill) + copy(buffer, content) + return buffer, nil +} + +func trimMessage(message []byte) string { + for i := range message { + if message[i] == 0 { + return string(message[:i]) + } + } + return string(message) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..249eab2 --- /dev/null +++ b/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "os" + + "github.com/fabiodcorreia/catch-my-file/cmd/frontend" +) + +func main() { + f := frontend.New() + + if err := f.Run(); err != nil { + os.Exit(1) + } + +}