diff options
-rw-r--r-- | README.md | 10 | ||||
-rw-r--r-- | cmd/send-over-http/main.go | 48 | ||||
-rw-r--r-- | go.mod | 7 | ||||
-rw-r--r-- | go.sum | 9 | ||||
-rw-r--r-- | qr_code.go | 39 | ||||
-rw-r--r-- | send_over_http.go | 122 | ||||
-rw-r--r-- | sync.go | 127 |
7 files changed, 362 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d54b1b --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Send Over HTTP + +Share a file with someone nearby over HTTP using a QR Code. + +Inspired by the android app [Share Via HTTP](https://github.com/marcosdiez/shareviahttp). + +## Usaage + +go run ./cmd/send-over-http --help + diff --git a/cmd/send-over-http/main.go b/cmd/send-over-http/main.go new file mode 100644 index 0000000..3c587ff --- /dev/null +++ b/cmd/send-over-http/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "flag" + "fmt" + "os" + "time" + + sendoverhttp "git.server.ky/cypher/send-over-http" +) + +func exitOnErr(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } +} + +func usage() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [target]\n", os.Args[0]) + fmt.Fprintln(flag.CommandLine.Output()) + fmt.Fprintf(flag.CommandLine.Output(), "\ttarget - file or directory to share (default: .)\n") + fmt.Fprintln(flag.CommandLine.Output()) +} + +func main() { + flag.Usage = usage + flag.Parse() + + var fp string + if len(os.Args) == 2 { + fp = os.Args[1] + } + if fp == "" { + v, err := os.Getwd() + exitOnErr(err) + fp = v + } + + s := sendoverhttp.NewServer(fp) + go s.Start() + + time.Sleep(250 * time.Millisecond) + fmt.Println("press enter to exit") + _, _ = fmt.Scanln() + + s.Stop() +} @@ -0,0 +1,7 @@ +module git.server.ky/cypher/send-over-http + +go 1.17 + +require github.com/mdp/qrterminal/v3 v3.0.0 + +require rsc.io/qr v0.2.0 // indirect @@ -0,0 +1,9 @@ +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= +github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= +github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= +github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/qr_code.go b/qr_code.go new file mode 100644 index 0000000..13a4aa0 --- /dev/null +++ b/qr_code.go @@ -0,0 +1,39 @@ +package sendoverhttp + +import ( + "fmt" + "os" + + "github.com/mdp/qrterminal/v3" + _ "github.com/mdp/qrterminal/v3" +) + +type TerminalQRShower struct { + waiter *Waiter + str string +} + +func NewTerminalQRShower(str string) *TerminalQRShower { + return &TerminalQRShower{ + waiter: NewWaiter(), + str: str, + } +} + +// Start the run, returning any error which occurs from start to +// finish. +func (s *TerminalQRShower) Start() error { + qrterminal.Generate(s.str, qrterminal.L, os.Stdout) + fmt.Println() + fmt.Printf("serving on '%s'", s.str) + fmt.Println() + fmt.Println() + s.waiter.Wait() + return nil +} + +// Stop the run and wait until it has ended. It can be called multiple +// times, ending immediately. +func (s *TerminalQRShower) Stop() { + s.waiter.Stop() +} diff --git a/send_over_http.go b/send_over_http.go new file mode 100644 index 0000000..c34145b --- /dev/null +++ b/send_over_http.go @@ -0,0 +1,122 @@ +package sendoverhttp + +import ( + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "path/filepath" +) + +func preferredIP() (*net.IP, error) { + ips, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + for _, addr := range ips { + ipNet, ok := addr.(*net.IPNet) + if !ok { + continue + } + if ipNet.IP.IsLoopback() { + continue + } + return &ipNet.IP, nil + } + return nil, nil +} + +type Server struct { + filepath string + + closeCh chan struct{} + mr *MultiRunner +} + +type SingleFileDir string + +func (s SingleFileDir) Open(name string) (http.File, error) { + if name == "index.html" { + return nil, fs.ErrNotExist + } + + fp := string(s) + stat, err := os.Stat(fp) + if err != nil { + return nil, err + } + if stat.IsDir() { + return http.Dir(fp).Open(name) + } + + switch name { + case "/": + return nil, os.ErrNotExist + case "/" + filepath.Base(fp): + return os.Open(fp) + default: + return nil, os.ErrNotExist + + } +} + +func NewServer(fp string) *Server { + return &Server{ + closeCh: make(chan struct{}), + filepath: fp, + mr: NewMultiRunner(), + } +} + +func (s *Server) Start() error { + ip, err := preferredIP() + if err != nil { + return err + } + + prot := "http" + port := "8081" + + url := fmt.Sprintf("%s://%s:%s", prot, ip, port) + if stat, _ := os.Stat(s.filepath); !stat.IsDir() { + url += "/" + filepath.Base(s.filepath) + } + + qrCodeShower := NewTerminalQRShower(url) + go s.mr.Run(qrCodeShower) + + httpRunner := NewRunner() + httpSrv := &http.Server{ + Addr: ":" + port, + } + + var d http.FileSystem = http.Dir(s.filepath) + if stat, _ := os.Stat(s.filepath); !stat.IsDir() { + // Show user the name of the file + d = SingleFileDir(s.filepath) + } + httpSrv.Handler = http.FileServer(d) + + httpRunner.OnStart = func() error { + l, err := net.Listen("tcp4", ip.String()+":"+port) + if err != nil { + log.Fatal(err) + } + return httpSrv.Serve(l) + } + httpRunner.OnStop = func() { + _ = httpSrv.Close() + } + go s.mr.Run(httpRunner) + + <-s.closeCh + close(s.closeCh) + return nil +} + +func (s *Server) Stop() { + s.mr.Stop() + return +} @@ -0,0 +1,127 @@ +package sendoverhttp + +import ( + "sync" +) + +type Waitable interface { + Wait() + Stop() +} + +type Waiter struct { + stopCh chan struct{} +} + +func NewWaiter() *Waiter { + return &Waiter{ + stopCh: make(chan struct{}), + } +} + +func (s *Waiter) Wait() { + <-s.stopCh +} + +func (s *Waiter) Stop() { + select { + case s.stopCh <- struct{}{}: + default: + } +} + +type Runnable interface { + // Start the run, returning any error which occurs from start to + // finish. + Start() error + // Stop the run and wait until it has ended. It can be called multiple + // times, ending immediately. + Stop() +} + +type NoOpRunner struct { + waiter *Waiter +} + +// Start the run, returning any error which occurs from start to +// finish. +func (s *NoOpRunner) Start() error { + s.waiter.Wait() + return nil +} + +// Stop the run and wait until it has ended. +func (s *NoOpRunner) Stop() { + s.waiter.Stop() + return +} + +type Runner struct { + OnStart func() error + OnStop func() +} + +func NewRunner() *Runner { + return &Runner{} +} + +func (s *Runner) Start() error { + return s.OnStart() +} + +func (s *Runner) Stop() { + s.OnStop() +} + +type MultiRunner struct { + runs []Runnable + runsMut sync.Mutex + err error + errMut sync.Mutex +} + +func NewMultiRunner() *MultiRunner { + return &MultiRunner{} +} + +// Error is the first error returned by any run. +func (s *MultiRunner) Error() error { + s.errMut.Lock() + defer s.errMut.Unlock() + return s.err +} + +func (s *MultiRunner) addRun(r Runnable) { + s.runsMut.Lock() + defer s.runsMut.Unlock() + s.runs = append(s.runs, r) +} + +// Run and wait for completion. If it ends with an error, stop all +// other runners. +func (s *MultiRunner) Run(r Runnable) { + s.addRun(r) + + err := r.Start() + + s.errMut.Lock() + defer s.errMut.Unlock() + if err != nil && s.err != nil { + s.err = err + //s.Stop() + } +} + +// Stop and wait for all runners to complete. +func (s *MultiRunner) Stop() { + // TODO: Allow multiple calls to stop + var wg sync.WaitGroup + for _, r := range s.runs { + wg.Add(1) + go func(r Runnable) { + defer wg.Done() + r.Stop() + }(r) + } + wg.Wait() +} |