aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md10
-rw-r--r--cmd/send-over-http/main.go48
-rw-r--r--go.mod7
-rw-r--r--go.sum9
-rw-r--r--qr_code.go39
-rw-r--r--send_over_http.go122
-rw-r--r--sync.go127
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()
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..0de3a6d
--- /dev/null
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e5a936f
--- /dev/null
+++ b/go.sum
@@ -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
+}
diff --git a/sync.go b/sync.go
new file mode 100644
index 0000000..b799554
--- /dev/null
+++ b/sync.go
@@ -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()
+}