liuyuqi-dellpc 5 years ago
commit
85b07ae7de
32 changed files with 1916 additions and 0 deletions
  1. 46 0
      .github/CODE_OF_CONDUCT.md
  2. 28 0
      .github/CONTRIBUTING.md
  3. 11 0
      .github/ISSUE_TEMPLATE.md
  4. 27 0
      .github/workflows/main.yml
  5. 3 0
      .gitignore
  6. 51 0
      .goreleaser.yml
  7. 26 0
      DEVELOPMENT.md
  8. 21 0
      LICENSE
  9. 286 0
      README.md
  10. 1 0
      _config.yml
  11. 18 0
      cmd/config.go
  12. 52 0
      cmd/qrcp.go
  13. 49 0
      cmd/receive.go
  14. 56 0
      cmd/send.go
  15. 16 0
      cmd/version.go
  16. 234 0
      config/config.go
  17. 16 0
      go.mod
  18. 171 0
      go.sum
  19. 24 0
      logger/logger.go
  20. 112 0
      logo.svg
  21. 13 0
      main.go
  22. BIN
      mobile-screenshot.png
  23. 15 0
      pages/pages.go
  24. 55 0
      payload/payload.go
  25. 16 0
      qr/qr.go
  26. BIN
      screenshot.png
  27. 278 0
      server/server.go
  28. 50 0
      server/tcpkeepalivelistener.go
  29. 38 0
      server/util.go
  30. 43 0
      util/net.go
  31. 146 0
      util/util.go
  32. 14 0
      version/version.go

+ 46 - 0
.github/CODE_OF_CONDUCT.md

@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at claudiodangelis@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/

+ 28 - 0
.github/CONTRIBUTING.md

@@ -0,0 +1,28 @@
+Contributions to this project are super welcome, so here's my recommendations:
+
+- Make sure that the no one is already working on what you are going to fix/implement
+
+    We use the [someone is working on this](https://github.com/claudiodangelis/qrcp/issues?q=is%3Aissue+is%3Aopen+label%3A%22someone+is+working+on+this%22) label to mark issues that are being taken care of by someone, so please have a look before starting coding
+
+- If you want to take on an open issue, please announce it in the thread
+
+    This does not mean you have to _ask_ first, but it makes sure that there won't be the case where more than one person are fixing the same bug or implementing the same feature without being aware of each other work, which usually results in a someone's time being wasted
+
+- Discuss implementation before writing the actual code
+
+    If you think what you are going to work on will take some time and effort, I recommend to share your thoughts in the thread first
+
+- Review pending pull requests
+
+    Help other users by reviewing their code
+
+- Explain the pull requests
+
+    Help reviewers by explaining how the patch works, what bugs/problems it addresses and _how_ it should be tested
+
+- Address one problem per pull request
+
+    When possible, avoid submitting a pull request that addresses more than problem
+
+- Run `go fmt` before submitting the pull request and address `golint` issues
+

+ 11 - 0
.github/ISSUE_TEMPLATE.md

@@ -0,0 +1,11 @@
+I'm opening this issue because:
+
+- [ ] I have found a bug
+- [ ] I want to request a feature
+- [ ] I have a question
+- [ ] Other
+
+
+- My Go version is: _(paste the output of `go version` and remember that qrcp requires at least version 1.8 of Go)_
+- My [GOPATH](https://github.com/golang/go/wiki/GOPATH) is set to: _(paste the output of `echo $GOPATH`)_
+

+ 27 - 0
.github/workflows/main.yml

@@ -0,0 +1,27 @@
+name: CI
+on:
+  push:
+    tags:
+    - '*'
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    # Required to build changeleg
+    - name: Unshallow
+      run: git fetch --prune --unshallow
+    # Setup go
+    - name: Set up Go
+      uses: actions/setup-go@v1
+      with:
+        go-version: 1.14.x
+    # Runs the goreleaser
+    - name: Run GoReleaser
+      uses: goreleaser/goreleaser-action@v1
+      with:
+        version: latest
+        args: release --rm-dist
+        key: ${{ secrets.YOUR_PRIVATE_KEY }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+qr-filetransfer
+qrcp
+dist

+ 51 - 0
.goreleaser.yml

@@ -0,0 +1,51 @@
+before:
+  hooks:
+    - go mod download
+builds:
+- env:
+  - CGO_ENABLED=0
+  ldflags:
+  - -s -w -X github.com/claudiodangelis/qrcp/version.version={{.Version}} -X github.com/claudiodangelis/qrcp/version.date={{.Date}}
+  goos:
+  - linux
+  - darwin
+  - windows
+  goarch:
+  - 386
+  - amd64
+  - arm
+  - arm64
+  goarm:
+  - 7
+  ignore:
+  - goos: darwin
+    goarch: 386
+archives:
+- replacements:
+    darwin: macOS
+    windows: Windows
+    386: i386
+    amd64: x86_64
+checksum:
+  name_template: 'checksums.txt'
+snapshot:
+  name_template: "{{ .Tag }}-next"
+changelog:
+  sort: asc
+  filters:
+    exclude:
+    - '^docs:'
+    - '^test:'
+nfpms:
+  - replacements:
+      darwin: macOS
+      windows: Windows
+      386: i386
+      amd64: x86_64
+    homepage: https://claudiodangelis.com/qrcp
+    maintainer: Claudio d'Angelis <claudiodangelis@gmail.com>
+    description: Transfer files over wifi from your computer to your mobile device by scanning a QR code without leaving the terminal.
+    license: MIT
+    formats:
+      - deb
+      - rpm

+ 26 - 0
DEVELOPMENT.md

@@ -0,0 +1,26 @@
+# Development
+
+## Versioning
+
+`qrcp` uses [semver](https://semver.org) for releases.
+
+Version number is defined in `cmd/version.go`.
+
+## Releases
+
+We are using [goreleases](https://goreleaser.com/), [nfpm](https://goreleaser.com/nfpm/) and [Github Actions](https://github.com/features/actions) to build, package and release `qrcp`.
+
+The relevant files are:
+
+- .goreleases.yml
+- .github/workflows/main.yml
+
+The release action is triggered when a tag is pushed to the master branch.
+
+## Development workflow
+
+1. Open a PR
+2. Let someone review it
+3. Squash commits and merge to master
+4. When ready to release, add a tag
+5. Wait for Github Action to process the release

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Claudio d'Angelis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 286 - 0
README.md

@@ -0,0 +1,286 @@
+![Logo](logo.svg)
+
+# $ qrcp
+
+Transfer files over Wi-Fi from your computer to a mobile device by scanning a QR code without leaving the terminal.
+
+[![Go Report Card](https://goreportcard.com/badge/github.com/claudiodangelis/qrcp)](https://goreportcard.com/report/github.com/claudiodangelis/qrcp)
+
+You can support development by donating with  [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/claudiodangelis).
+
+Join the **Telegram channel** [qrcp_dev](https://t.me/qrcp_dev) for news about the development.
+
+## How does it work?
+![Screenshot](screenshot.png)
+
+`qrcp` binds a web server to the address of your Wi-Fi network interface on a random port and creates a handler for it. The default handler serves the content and exits the program when the transfer is complete. When used to receive files, `qrcp` serves an upload page and handles the transfer.
+
+The tool prints a QR code that encodes the text:
+
+```
+http://{address}:{port}/{random_path}
+```
+
+
+Most QR apps can detect URLs in decoded text and act accordingly (i.e. open the decoded URL with the default browser), so when the QR code is scanned the content will begin downloading by the mobile browser.
+
+Send files to mobile:
+
+![screenshot](demo.gif)
+
+Receive files from mobile:
+
+![Screenshot](mobile-demo.gif)
+
+# Installation
+
+## Install the latest development version with Go
+    
+_Note: it requires go 1.8_
+
+    go get github.com/claudiodangelis/qrcp
+
+## Linux
+
+Download the latest Linux .tar.gz archive from the [Releases](https://github.com/claudiodangelis/qrcp/releases) page, extract it, move the binary to the proper directory, then set execution permissions.
+
+```sh
+# Extract the archive
+tar xf qrcp_0.5.0_linux_x86_64.tar.gz
+# Copy the binary
+sudo mv qrcp /usr/local/bin
+# Set execution permissions
+sudo chmod +x /usr/local/bin/qrcp
+```
+
+### Raspberry Pi
+
+The following ARM releases are available in the [Releases](https://github.com/claudiodangelis/qrcp/releases) page:
+
+- `armv7`
+- `arm64`
+
+
+### Using a package manager
+
+#### ArchLinux
+
+Packages available on AUR:
+-  [qrcp-bin](https://aur.archlinux.org/packages/qrcp-bin)
+-  [qrcp](https://aur.archlinux.org/packages/qrcp)
+
+#### Deb packages (Ubuntu, Debian, etc)
+
+Download the latest .deb package from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), then run `dpkg`:
+
+```sh
+sudo dpkg -i qrcp_0.5.0_linux_x86_64.deb
+# Confirm it's working:
+qrcp version
+```
+
+#### RPM packages (CentOS, Fedora, etc)
+
+Download the latest .rpm package from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), then run `rpm`:
+
+```sh
+sudo rpm -i qrcp_0.5.0_linux_x86_64.rpm
+# Confirm it's working:
+qrcp --help
+```
+
+## Windows
+
+Download the latest Windows .tar.gz archive from the [Releases page](https://github.com/claudiodangelis/qrcp/releases) and extract the EXE file.
+
+### Scoop 
+
+If you use [Scoop](https://scoop.sh/) for package management on Windows, you can install qrcp with the following one-liner:
+
+```
+scoop install qrcp
+```
+
+## MacOS
+
+Download the latest macOS .tar.gz archive from the [Releases page](https://github.com/claudiodangelis/qrcp/releases), extract it, move the binary to the proper directory, then set execution permissions.
+
+```sh
+# Extract the archive
+tar xf qrcp_0.5.0_macOS_x86_64.tar.gz
+# Copy the binary
+sudo mv qrcp /usr/local/bin
+# Set execution permissions
+sudo chmod +x /usr/local/bin/qrcp
+# Confirm it's working:
+qrcp --help
+```
+    
+# Usage
+
+## Send files
+
+### Send a file
+
+```sh
+qrcp MyDocument.pdf
+```
+
+### Send multiple files at once
+
+When sending multiple files at once, `qrcp` creates a zip archive of the files or folders you want to transfer, and deletes the zip archive once the transfer is complete.
+
+```sh
+# Multiple files
+qrcp MyDocument.pdf IMG0001.jpg
+```
+
+```sh
+# A whole folder
+qrcp Documents/
+```
+
+
+### Zip a file before transferring it
+You can choose to zip a file before transferring it.
+
+```sh
+qrcp --zip LongVideo.avi
+```
+
+
+## Receive files
+
+When receiving files, `qrcp` serves an "upload page" through which you can choose files from your mobile.
+
+### Receive files to the current directory
+
+```
+qrcp receive
+```
+
+### Receive files to a specific directory
+
+```sh
+# Note: the folder must exist
+qrcp receive --output=/tmp/dir
+```
+
+
+## Options
+
+`qrcp` works without any prior configuration, however, you can choose to configure to use specific values. The `config` command launches a wizard that lets you configure parameters like interface, port, fully-qualified domain name and keep alive.
+
+```sh
+qrcp config
+```
+
+Note: if some network interfaces are not showing up, use the `--list-all-interfaces` flag to suppress the interfaces' filter.
+
+```sh
+qrcp --list-all-interfaces config 
+```
+
+
+### Port
+
+By default `qrcp` listens on a random port. Pass the `--port` (or `-p`) flag to choose a specific one:
+
+```sh
+qrcp --port 8080 MyDocument.pdf
+```
+### Network Interface
+
+`qrcp` will try to automatically find the suitable network interface to use for the transfers. If more than one suitable interface is found, it asks you to choose one.
+
+If you want to use a specific interface, pass the `--interface` (or `-i`) flag:
+
+
+
+```sh
+# The webserver will be visible by
+# all computers on the tun0's interface network
+qrcp -i tun0 MyDocument.dpf
+```
+
+
+You can also use a special interface name, `any`, which binds the web server to `0.0.0.0`, making the web server visible by everyone on any network, even from an external network. 
+
+This is useful when you want to transfer files from your Amazon EC2, Digital Ocean Droplet, Google Cloud Platform Compute Instance or any other VPS.
+
+```sh
+qrcp -i any MyDocument.pdf
+```
+
+
+### URL
+
+`qrcp` uses two patterns for the URLs:
+
+- send: `http://{ip address}:{port}/send/{random path}`
+- receive: `http://{ip address}:{port}/receive/{random path}`
+
+A few options are available that override these patterns.
+
+
+Pass the `--path` flag to use a specific path for URLs, for example:
+
+```sh
+# The resulting URL will be
+# http://{ip address}:{port}/send/x
+qrcp --path=x MyDocument.pdf
+```
+
+Pass the `--fqdn` (or `-d`) to use a fully qualified domain name instead of the IP. This is useful in combination with `-i any` you are using it from a remote location:
+
+```sh
+# The resulting URL will be
+# http://example.com:8080/send/xYz9
+qrcp --fqdn example.com -i any -p 8080 MyRemoteDocument.pdf
+```
+
+
+
+### Keep the server alive
+
+It can be useful to keep the server alive after transferring the file, for example, when you want to transfer the same file to multiple devices. You can use the `--keep-alive` flag for that:
+
+```sh
+# The server will not shutdown automatically
+# after the first transfer
+qrcp --keep-alive MyDocument.pdf
+```
+
+## Authors
+
+**qrcp**, originally called **qr-filetransfer**, started from an idea of [Claudio d'Angelis](claudiodangelis@gmail.com) ([@daw985](https://twitter.com/daw985) on Twitter), the current maintainer, and it's [developed by the community](https://github.com/claudiodangelis/qrcp/graphs/contributors).
+
+
+[Join us!](https://github.com/claudiodangelis/qrcp/fork)
+
+## Credits
+
+Logo is provided by [@arasatasaygin](https://github.com/arasatasaygin) as part of the [openlogos](https://github.com/arasatasaygin/openlogos) initiative, a collection of free logos for open source projects.
+
+Check out the rules to claim one: [rules of openlogos](https://github.com/arasatasaygin/openlogos#rules).
+
+Releases are handled with [goreleaser](https://goreleaser.com).
+
+## Clones and Similar Projects
+
+- [qr-fileshare](https://github.com/shivensinha4/qr-fileshare) - A similar idea executed in NodeJS with a React interface.
+- [instant-file-transfer](https://github.com/maximumdata/instant-file-transfer) _(Uncredited)_ - Node.js project similar to this
+- [qr-filetransfer](https://github.com/sdushantha/qr-filetransfer) - Python clone of this project
+- [qr-filetransfer](https://github.com/svenkatreddy/qr-filetransfer) - Another Node.js clone of this project
+- [qr-transfer-node](https://github.com/codezoned/qr-transfer-node) - Another Node.js clone of this project
+- [QRDELIVER](https://github.com/realdennis/qrdeliver) - Node.js project similar to this
+- [qrfile](https://github.com/sgbj/qrfile) - Transfer files by scanning a QR code
+- [quick-transfer](https://github.com/CodeMan99/quick-transfer) - Node.js clone of this project
+- [share-file-qr](https://github.com/pwalch/share-file-qr) - Python re-implementation of this project
+- [share-files](https://github.com/antoaravinth/share-files) _(Uncredited)_  - Yet another Node.js clone of this project
+- [ezshare](https://github.com/mifi/ezshare) - Another Node.js two way file sharing tool supporting folders and multiple files
+- [local_file_share](https://github.com/woshimanong1990/local_file_share)  - _"share local file to other people, OR smartphone download files which is in pc"_
+- [qrcp](https://github.com/pearl2201/qrcp) - a desktop app clone of `qrcp`, writing with C# and .NET Core, work for Windows.
+## License
+
+MIT. See [LICENSE](LICENSE).

+ 1 - 0
_config.yml

@@ -0,0 +1 @@
+theme: jekyll-theme-cayman

+ 18 - 0
cmd/config.go

@@ -0,0 +1,18 @@
+package cmd
+
+import (
+	"github.com/claudiodangelis/qrcp/config"
+	"github.com/spf13/cobra"
+)
+
+func configCmdFunc(command *cobra.Command, args []string) error {
+	return config.Wizard()
+}
+
+var configCmd = &cobra.Command{
+	Use:     "config",
+	Short:   "Configure qrcp",
+	Long:    "Run an interactive configuration wizard for qrcp. With this command you can configure which network interface and port should be used to create the file server.",
+	Aliases: []string{"c", "cfg"},
+	RunE:    configCmdFunc,
+}

+ 52 - 0
cmd/qrcp.go

@@ -0,0 +1,52 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+func init() {
+	rootCmd.AddCommand(sendCmd)
+	rootCmd.AddCommand(receiveCmd)
+	rootCmd.AddCommand(configCmd)
+	rootCmd.AddCommand(versionCmd)
+	// Global command flags
+	rootCmd.PersistentFlags().BoolVarP(&quietFlag, "quiet", "q", false, "only print errors")
+	rootCmd.PersistentFlags().BoolVarP(&keepaliveFlag, "keep-alive", "k", false, "keep server alive after transferring")
+	rootCmd.PersistentFlags().BoolVarP(&listallinterfacesFlag, "list-all-interfaces", "l", false, "list all available interfaces when choosing the one to use")
+	rootCmd.PersistentFlags().IntVarP(&portFlag, "port", "p", 0, "port to use for the server")
+	rootCmd.PersistentFlags().StringVar(&pathFlag, "path", "", "path to use. Defaults to a random string")
+	rootCmd.PersistentFlags().StringVarP(&interfaceFlag, "interface", "i", "", "network interface to use for the server")
+	rootCmd.PersistentFlags().StringVarP(&fqdnFlag, "fqdn", "d", "", "fully-qualified domain name to use for the resulting URLs")
+	rootCmd.PersistentFlags().BoolVarP(&zipFlag, "zip", "z", false, "zip content before transferring")
+	// Receive command flags
+	receiveCmd.PersistentFlags().StringVarP(&outputFlag, "output", "o", "", "output directory for receiving files")
+}
+
+// Flags
+var zipFlag bool
+var portFlag int
+var interfaceFlag string
+var outputFlag string
+var keepaliveFlag bool
+var quietFlag bool
+var fqdnFlag string
+var pathFlag string
+var listallinterfacesFlag bool
+
+// The root command (`qrcp`) is like a shortcut of the `send` command
+var rootCmd = &cobra.Command{
+	Use:           "qrcp",
+	Args:          cobra.MinimumNArgs(1),
+	RunE:          sendCmdFunc,
+	SilenceErrors: true,
+	SilenceUsage:  true,
+}
+
+// Execute the root command
+func Execute() error {
+	if err := rootCmd.Execute(); err != nil {
+		rootCmd.PrintErrf("Error: %v\nRun `qrcp help` for help.\n", err)
+		return err
+	}
+	return nil
+}

+ 49 - 0
cmd/receive.go

@@ -0,0 +1,49 @@
+package cmd
+
+import (
+	"github.com/claudiodangelis/qrcp/config"
+	"github.com/claudiodangelis/qrcp/logger"
+	"github.com/claudiodangelis/qrcp/qr"
+	"github.com/claudiodangelis/qrcp/server"
+	"github.com/spf13/cobra"
+)
+
+func receiveCmdFunc(command *cobra.Command, args []string) error {
+	log := logger.New(quietFlag)
+	// Load configuration
+	cfg, err := config.New(interfaceFlag, portFlag, pathFlag, fqdnFlag, keepaliveFlag, listallinterfacesFlag)
+	if err != nil {
+		return err
+	}
+	// Create the server
+	srv, err := server.New(&cfg)
+	if err != nil {
+		return err
+	}
+	// Sets the output directory
+	if err := srv.ReceiveTo(outputFlag); err != nil {
+		return err
+	}
+	// Prints the URL to scan to screen
+	log.Print("Scan the following URL with a QR reader to start the file transfer:")
+	log.Print(srv.ReceiveURL)
+	// Renders the QR
+	qr.RenderString(srv.ReceiveURL)
+	if err := srv.Wait(); err != nil {
+		return err
+	}
+	return nil
+}
+
+var receiveCmd = &cobra.Command{
+	Use:     "receive",
+	Aliases: []string{"r"},
+	Short:   "Receive one or more files",
+	Long:    "Receive one or more files. If not specified with the --output flag, the current working directory will be used as a destination.",
+	Example: `# Receive files in the current directory
+qrcp receive
+# Receive files in a specific directory
+qrcp receive --output /tmp
+`,
+	RunE: receiveCmdFunc,
+}

+ 56 - 0
cmd/send.go

@@ -0,0 +1,56 @@
+package cmd
+
+import (
+	"github.com/claudiodangelis/qrcp/config"
+	"github.com/claudiodangelis/qrcp/logger"
+	"github.com/claudiodangelis/qrcp/payload"
+	"github.com/claudiodangelis/qrcp/qr"
+
+	"github.com/claudiodangelis/qrcp/server"
+	"github.com/spf13/cobra"
+)
+
+func sendCmdFunc(command *cobra.Command, args []string) error {
+	log := logger.New(quietFlag)
+	payload, err := payload.FromArgs(args, zipFlag)
+	if err != nil {
+		return err
+	}
+	cfg, err := config.New(interfaceFlag, portFlag, pathFlag, fqdnFlag, keepaliveFlag, listallinterfacesFlag)
+	if err != nil {
+		return err
+	}
+	srv, err := server.New(&cfg)
+	if err != nil {
+		return err
+	}
+	// Sets the payload
+	srv.Send(payload)
+	log.Print("Scan the following URL with a QR reader to start the file transfer:")
+	log.Print(srv.SendURL)
+	qr.RenderString(srv.SendURL)
+	if err := srv.Wait(); err != nil {
+		return err
+	}
+	return nil
+}
+
+var sendCmd = &cobra.Command{
+	Use:     "send",
+	Short:   "Send a file(s) or directories from this host",
+	Long:    "Send a file(s) or directories from this host",
+	Aliases: []string{"s"},
+	Example: `# Send /path/file.gif. Webserver listens on a random port
+qrcp send /path/file.gif
+# Shorter version:
+qrcp /path/file.gif
+# Zip file1.gif and file2.gif, then send the zip package
+qrcp /path/file1.gif /path/file2.gif
+# Zip the content of directory, then send the zip package
+qrcp /path/directory
+# Send file.gif by creating a webserver on port 8080
+qrcp --port 8080 /path/file.gif
+`,
+	Args: cobra.MinimumNArgs(1),
+	RunE: sendCmdFunc,
+}

+ 16 - 0
cmd/version.go

@@ -0,0 +1,16 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/claudiodangelis/qrcp/version"
+	"github.com/spf13/cobra"
+)
+
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "Print version number and build information.",
+	Run: func(c *cobra.Command, args []string) {
+		fmt.Println(version.String())
+	},
+}

+ 234 - 0
config/config.go

@@ -0,0 +1,234 @@
+package config
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os/user"
+	"path/filepath"
+	"strconv"
+
+	"github.com/asaskevich/govalidator"
+	"github.com/claudiodangelis/qrcp/util"
+	"github.com/manifoldco/promptui"
+)
+
+// Config of qrcp
+type Config struct {
+	FQDN      string `json:"fqdn"`
+	Interface string `json:"interface"`
+	Port      int    `json:"port"`
+	KeepAlive bool   `json:"keepAlive"`
+	Path      string `json:"path"`
+}
+
+func configFile() string {
+	currentUser, err := user.Current()
+	if err != nil {
+		panic(err)
+	}
+	return filepath.Join(currentUser.HomeDir, ".qrcp.json")
+}
+
+type configOptions struct {
+	interactive       bool
+	listAllInterfaces bool
+}
+
+func chooseInterface(opts configOptions) (string, error) {
+	interfaces, err := util.Interfaces(opts.listAllInterfaces)
+	if err != nil {
+		return "", err
+	}
+	if len(interfaces) == 0 {
+		return "", errors.New("no interfaces found")
+	}
+
+	if len(interfaces) == 1 && opts.interactive == false {
+		for name := range interfaces {
+			fmt.Printf("only one interface found: %s, using this one\n", name)
+			return name, nil
+		}
+	}
+	// Map for pretty printing
+	m := make(map[string]string)
+	items := []string{}
+	for name, ip := range interfaces {
+		label := fmt.Sprintf("%s (%s)", name, ip)
+		m[label] = name
+		items = append(items, label)
+	}
+	// Add the "any" interface
+	anyIP := "0.0.0.0"
+	anyName := "any"
+	anyLabel := fmt.Sprintf("%s (%s)", anyName, anyIP)
+	m[anyLabel] = anyName
+	items = append(items, anyLabel)
+	prompt := promptui.Select{
+		Items: items,
+		Label: "Choose interface",
+	}
+	_, result, err := prompt.Run()
+	if err != nil {
+		return "", err
+	}
+	return m[result], nil
+}
+
+// Load a new configuration
+func Load(opts configOptions) (Config, error) {
+	var cfg Config
+	// Read the configuration file, if it exists
+	if file, err := ioutil.ReadFile(configFile()); err == nil {
+		// Read the config
+		if err := json.Unmarshal(file, &cfg); err != nil {
+			return cfg, err
+		}
+	}
+	// Prompt if needed
+	if cfg.Interface == "" {
+		iface, err := chooseInterface(opts)
+		if err != nil {
+			return cfg, err
+		}
+		cfg.Interface = iface
+		// Write config
+		if err := write(cfg); err != nil {
+			return cfg, err
+		}
+	}
+	return cfg, nil
+}
+
+// Wizard starts an interactive configuration managements
+func Wizard() error {
+	var cfg Config
+	if file, err := ioutil.ReadFile(configFile()); err == nil {
+		// Read the config
+		if err := json.Unmarshal(file, &cfg); err != nil {
+			return err
+		}
+	}
+	// Ask for interface
+	opts := configOptions{
+		interactive: true,
+	}
+	iface, err := chooseInterface(opts)
+	if err != nil {
+		return err
+	}
+	cfg.Interface = iface
+	// Ask for fully qualified domain name
+	validateFqdn := func(input string) error {
+		if input != "" && govalidator.IsDNSName(input) == false {
+			return errors.New("invalid domain")
+		}
+		return nil
+	}
+	promptFqdn := promptui.Prompt{
+		Validate: validateFqdn,
+		Label:    "Choose fully-qualified domain name",
+		Default:  "",
+	}
+	if promptFqdnString, err := promptFqdn.Run(); err == nil {
+		cfg.FQDN = promptFqdnString
+	}
+	// Ask for port
+	validatePort := func(input string) error {
+		_, err := strconv.ParseInt(input, 10, 16)
+		if err != nil {
+			return errors.New("Invalid number")
+		}
+		return nil
+	}
+
+	promptPort := promptui.Prompt{
+		Validate: validatePort,
+		Label:    "Choose port, 0 means random port",
+		Default:  fmt.Sprintf("%d", cfg.Port),
+	}
+	if promptPortResultString, err := promptPort.Run(); err == nil {
+		if port, err := strconv.ParseInt(promptPortResultString, 10, 16); err == nil {
+			cfg.Port = int(port)
+		}
+	}
+
+	// Ask for path
+	promptPath := promptui.Prompt{
+		Label:   "Choose path, empty means random",
+		Default: cfg.Path,
+	}
+	if promptPathResultString, err := promptPath.Run(); err == nil {
+		if promptPathResultString != "" {
+			cfg.Path = promptPathResultString
+		}
+	}
+
+	// Ask for keep alive
+	promptKeepAlive := promptui.Select{
+		Items: []string{"No", "Yes"},
+		Label: "Should the server keep alive after transferring?",
+	}
+	if _, promptKeepAliveResultString, err := promptKeepAlive.Run(); err == nil {
+		if promptKeepAliveResultString == "Yes" {
+			cfg.KeepAlive = true
+		} else {
+			cfg.KeepAlive = false
+		}
+	}
+	// Write it down
+	if err := write(cfg); err != nil {
+		return err
+	}
+	b, err := json.MarshalIndent(cfg, "", "  ")
+	if err != nil {
+		return err
+	}
+	fmt.Printf("Configuration updated:\n%s\n", string(b))
+	return nil
+}
+
+// write the configuration file to disk
+func write(cfg Config) error {
+	j, err := json.MarshalIndent(cfg, "", "    ")
+	if err != nil {
+		return err
+	}
+	if err := ioutil.WriteFile(configFile(), j, 0644); err != nil {
+		return err
+	}
+	return nil
+}
+
+// New returns a new configuration struct. It loads defaults, then overrides
+// values if any.
+func New(iface string, port int, path string, fqdn string, keepAlive bool, listAllInterfaces bool) (Config, error) {
+	// Load saved file / defults
+	cfg, err := Load(configOptions{listAllInterfaces: listAllInterfaces})
+	if err != nil {
+		return cfg, err
+	}
+	if iface != "" {
+		cfg.Interface = iface
+	}
+	if fqdn != "" {
+		if govalidator.IsDNSName(fqdn) == false {
+			return cfg, errors.New("invalid value for fully-qualified domain name")
+		}
+		cfg.FQDN = fqdn
+	}
+	if port != 0 {
+		if port > 65535 {
+			return cfg, errors.New("invalid value for port")
+		}
+		cfg.Port = port
+	}
+	if keepAlive {
+		cfg.KeepAlive = true
+	}
+	if path != "" {
+		cfg.Path = path
+	}
+	return cfg, nil
+}

+ 16 - 0
go.mod

@@ -0,0 +1,16 @@
+module github.com/claudiodangelis/qrcp
+
+go 1.14
+
+require (
+	github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496
+	github.com/fatih/color v1.9.0 // indirect
+	github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd
+	github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681
+	github.com/manifoldco/promptui v0.7.0
+	github.com/mattn/go-colorable v0.1.6 // indirect
+	github.com/mattn/go-runewidth v0.0.9 // indirect
+	github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
+	github.com/spf13/cobra v1.0.0
+	gopkg.in/cheggaaa/pb.v1 v1.0.28
+)

+ 171 - 0
go.sum

@@ -0,0 +1,171 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
+github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd h1:1BzxHapafGJd/XlpMvocLeDBin2EKn90gXv2AQt5sfo=
+github.com/glendc/go-external-ip v0.0.0-20170425150139-139229dcdddd/go.mod h1:o9OoDQyE1WHvYVUH1FdFapy1/rCZHHq3O5wS4VA83ig=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681 h1:EiEjLram6Y0WXygV4WyzKmTr3XaR4CD3tvjdTrsk3cU=
+github.com/jhoonb/archivex v0.0.0-20180718040744-0488e4ce1681/go.mod h1:GN1Mg/uXQ6qwXA0HypnUO3xlcQJS9/y68EsHNeuuRa4=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
+github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+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/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
+github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4=
+github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs=
+github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
+gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 24 - 0
logger/logger.go

@@ -0,0 +1,24 @@
+package logger
+
+import (
+	"fmt"
+)
+
+// Print prints its argument if the --quiet flag is not passed
+func (l Logger) Print(args ...interface{}) {
+	if l.quiet == false {
+		fmt.Println(args...)
+	}
+}
+
+// Logger struct
+type Logger struct {
+	quiet bool
+}
+
+// New logger
+func New(quiet bool) Logger {
+	return Logger{
+		quiet: quiet,
+	}
+}

+ 112 - 0
logo.svg

@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="402.333px" height="153.07px" viewBox="0 0 804.666 306.14" enable-background="new 0 0 804.666 306.14"
+	 xml:space="preserve">
+<g>
+	<g>
+		<polygon fill="#00AEEF" points="246.355,182.111 82.612,290.467 82.688,198.443 254.354,116.233 		"/>
+	</g>
+	<g>
+		<g>
+			<g>
+				<g>
+					<polygon fill="#F9ED32" points="31.388,227.121 154.208,165.906 158.21,181.887 250.908,132.033 250.911,171.207 
+						386.376,31.251 284.368,75.32 284.368,37.369 195.419,82.664 177.044,36.152 31.388,91.638 					"/>
+				</g>
+				<g>
+					<path fill="#231F20" d="M26.388,235.199V88.193L179.88,29.721l18.17,45.992l91.318-46.502v38.502l118.027-50.989
+						L245.912,183.561l-0.003-43.162l-90.988,48.933l-4.052-16.176L26.388,235.199z M36.388,95.084v123.957l121.159-60.387
+						l3.952,15.785l94.408-50.773l0.003,35.185L365.356,45.779l-85.988,37.147v-37.4l-86.58,44.088l-18.58-47.031L36.388,95.084z"/>
+				</g>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g>
+			<g>
+				<path fill="#00AEEF" d="M217.093,114.078c0,7.662-5.496,15.098-11.062,17.955c-5.566,2.861-8.879,3.879-13.932,4.084
+					c-5.045,0.205-13.875-6.209-13.875-13.871V35.968c0-7.66,6.214-13.871,13.875-13.871h11.122c7.661,0,13.872,6.211,13.872,13.871
+					V114.078z"/>
+			</g>
+		</g>
+		<g>
+			<g>
+				<path fill="#231F20" d="M191.862,141.427c-8.348,0-18.938-9.035-18.938-19.182V35.968c0-10.574,8.604-19.18,19.174-19.18h11.122
+					c10.575,0,19.177,8.605,19.177,19.18v78.109c0,10.186-7.014,19.117-13.945,22.678c-6.338,3.254-10.411,4.43-16.136,4.662
+					C192.162,141.421,192.015,141.427,191.862,141.427z M192.099,27.4c-4.723,0-8.565,3.844-8.565,8.568v86.277
+					c0,4.402,6.062,8.57,8.329,8.57h0.021c4.183-0.168,6.7-0.922,11.721-3.5c3.939-2.02,8.189-7.654,8.189-13.238V35.968
+					c0-4.725-3.849-8.568-8.573-8.568H192.099z"/>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g>
+			<g>
+				<path fill="#00AEEF" d="M258.661,93.66c0,7.914-2.922,13.332-7.207,17.977c-4.081,4.428-10.127,9.779-17.783,9.779
+					c-7.662,0-13.875-6.211-13.875-13.871V45.755c0-7.66,6.213-13.871,13.875-13.871h11.121c7.661,0,13.869,6.211,13.869,13.871
+					V93.66z"/>
+			</g>
+		</g>
+		<g>
+			<g>
+				<path fill="#231F20" d="M233.671,126.718c-10.576,0-19.178-8.605-19.178-19.174V45.755c0-10.568,8.602-19.174,19.178-19.174
+					h11.121c10.574,0,19.178,8.605,19.178,19.174V93.66c0,8.346-2.738,15.199-8.615,21.572
+					C252.731,118.074,244.761,126.718,233.671,126.718z M233.671,37.193c-4.725,0-8.568,3.846-8.568,8.562v61.789
+					c0,4.725,3.844,8.562,8.568,8.562c4.397,0,8.809-2.562,13.885-8.066c4.066-4.41,5.803-8.713,5.803-14.381V45.755
+					c0-4.717-3.841-8.562-8.566-8.562H233.671z"/>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g>
+			<g>
+				<path fill="#00AEEF" d="M175.526,128.376c0,7.66-6.21,13.873-13.879,13.873H150.53c-7.661,0-13.871-6.213-13.871-13.873V33.933
+					c0-7.66,6.21-13.871,13.871-13.871h11.117c7.669,0,13.879,6.211,13.879,13.871V128.376z"/>
+			</g>
+		</g>
+		<g>
+			<g>
+				<path fill="#231F20" d="M161.647,147.554H150.53c-10.57,0-19.174-8.605-19.174-19.178V33.933c0-10.572,8.604-19.18,19.174-19.18
+					h11.117c10.58,0,19.182,8.607,19.182,19.18v94.443C180.829,138.949,172.228,147.554,161.647,147.554z M150.53,25.363
+					c-4.725,0-8.566,3.848-8.566,8.57v94.443c0,4.725,3.842,8.568,8.566,8.568h11.117c4.729,0,8.574-3.844,8.574-8.568V33.933
+					c0-4.723-3.846-8.57-8.574-8.57H150.53z"/>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g>
+			<g>
+				<path fill="#00AEEF" d="M133.958,115.712c0,7.66-6.211,13.871-13.871,13.871h-8.676c-7.66,0-13.87-6.211-13.87-13.871V45.771
+					c0-7.662,6.21-13.873,13.87-13.873h8.676c7.66,0,13.871,6.211,13.871,13.873V115.712z"/>
+			</g>
+		</g>
+		<g>
+			<g>
+				<path fill="#231F20" d="M120.087,134.884h-8.676c-10.57,0-19.174-8.6-19.174-19.172V45.771c0-10.576,8.604-19.182,19.174-19.182
+					h8.676c10.572,0,19.174,8.605,19.174,19.182v69.941C139.261,126.285,130.659,134.884,120.087,134.884z M111.411,37.201
+					c-4.721,0-8.566,3.842-8.566,8.57v69.941c0,4.725,3.846,8.568,8.566,8.568h8.676c4.721,0,8.565-3.844,8.565-8.568V45.771
+					c0-4.729-3.845-8.57-8.565-8.57H111.411z"/>
+			</g>
+		</g>
+	</g>
+	<g>
+		<g>
+			<g>
+				<path fill="#00AEEF" d="M83.612,222.221V108.378l85.373-53.645c0,0,14.189,23.855-2.943,41.402
+					c-17.142,17.547-29.383,22.033-29.383,22.033s29.383,29.385,21.627,67.336"/>
+			</g>
+		</g>
+		<g>
+			<g>
+				<path fill="#231F20" d="M83.612,291.561c-2.929,0-5.305-2.373-5.305-5.303V105.445l92.459-58.098l2.777,4.676
+					c5.627,9.463,12.094,31.637-3.71,47.816c-10.362,10.605-19.106,16.703-24.712,19.971c8.459,10.633,24.678,35.84,18.361,66.758
+					c-0.586,2.867-3.387,4.719-6.258,4.135c-2.869-0.584-4.721-3.393-4.135-6.26c7.097-34.756-19.906-62.254-20.182-62.524
+					l-5.873-5.877l7.795-2.855c0.092-0.031,11.52-4.488,27.412-20.758c5.785-5.92,7.975-13.449,6.52-22.369
+					c-0.478-2.912-1.277-5.531-2.062-7.627L88.918,111.31v174.948C88.918,289.188,86.542,291.561,83.612,291.561z"/>
+			</g>
+		</g>
+	</g>
+</g>
+</svg>

+ 13 - 0
main.go

@@ -0,0 +1,13 @@
+package main
+
+import (
+	"os"
+
+	"github.com/claudiodangelis/qrcp/cmd"
+)
+
+func main() {
+	if err := cmd.Execute(); err != nil {
+		os.Exit(1)
+	}
+}

BIN
mobile-screenshot.png


File diff suppressed because it is too large
+ 15 - 0
pages/pages.go


+ 55 - 0
payload/payload.go

@@ -0,0 +1,55 @@
+package payload
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/claudiodangelis/qrcp/util"
+)
+
+// Payload to transfer
+type Payload struct {
+	Filename            string
+	Path                string
+	DeleteAfterTransfer bool
+}
+
+// Delete the payload from disk
+func (p Payload) Delete() error {
+	return os.RemoveAll(p.Path)
+}
+
+// FromArgs returns a payload from args
+func FromArgs(args []string, zipFlag bool) (Payload, error) {
+	shouldzip := len(args) > 1 || zipFlag
+	var files []string
+	// Check if content exists
+	for _, arg := range args {
+		file, err := os.Stat(arg)
+		if err != nil {
+			return Payload{}, err
+		}
+		// If at least one argument is dir, the content will be zipped
+		if file.IsDir() {
+			shouldzip = true
+		}
+		files = append(files, arg)
+	}
+	// Prepare the content
+	// TODO: Research cleaner code
+	var content string
+	if shouldzip {
+		zip, err := util.ZipFiles(files)
+		if err != nil {
+			return Payload{}, err
+		}
+		content = zip
+	} else {
+		content = args[0]
+	}
+	return Payload{
+		Path:                content,
+		Filename:            filepath.Base(content),
+		DeleteAfterTransfer: shouldzip,
+	}, nil
+}

+ 16 - 0
qr/qr.go

@@ -0,0 +1,16 @@
+package qr
+
+import (
+	"fmt"
+
+	"github.com/skip2/go-qrcode"
+)
+
+// RenderString as a QR code
+func RenderString(s string) {
+	q, err := qrcode.New(s, qrcode.Medium)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(q.ToSmallString(false))
+}

BIN
screenshot.png


+ 278 - 0
server/server.go

@@ -0,0 +1,278 @@
+package server
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"github.com/claudiodangelis/qrcp/config"
+	"github.com/claudiodangelis/qrcp/pages"
+	"github.com/claudiodangelis/qrcp/payload"
+	"github.com/claudiodangelis/qrcp/util"
+	"gopkg.in/cheggaaa/pb.v1"
+)
+
+// Server is the server
+type Server struct {
+	// SendURL is the URL used to send the file
+	SendURL string
+	// ReceiveURL is the URL used to Receive the file
+	ReceiveURL  string
+	instance    *http.Server
+	payload     payload.Payload
+	outputDir   string
+	stopChannel chan bool
+	// expectParallelRequests is set to true when qrcp sends files, in order
+	// to support downloading of parallel chunks
+	expectParallelRequests bool
+}
+
+// ReceiveTo sets the output directory
+func (s *Server) ReceiveTo(dir string) error {
+	output, err := filepath.Abs(dir)
+	if err != nil {
+		return err
+	}
+	// Check if the output dir exists
+	fileinfo, err := os.Stat(output)
+	if err != nil {
+		return err
+	}
+	if !fileinfo.IsDir() {
+		return fmt.Errorf("%s is not a valid directory", output)
+	}
+	s.outputDir = output
+	return nil
+}
+
+// Send adds a handler for sending the file
+func (s *Server) Send(p payload.Payload) {
+	s.payload = p
+	s.expectParallelRequests = true
+}
+
+// Wait for transfer to be completed, it waits forever if kept awlive
+func (s Server) Wait() error {
+	<-s.stopChannel
+	if err := s.instance.Shutdown(context.Background()); err != nil {
+		log.Println(err)
+	}
+	if s.payload.DeleteAfterTransfer {
+		s.payload.Delete()
+	}
+	return nil
+}
+
+// New instance of the server
+func New(cfg *config.Config) (*Server, error) {
+	app := &Server{}
+	// Get the address of the configured interface to bind the server to
+	bind, err := util.GetInterfaceAddress(cfg.Interface)
+	if err != nil {
+		return &Server{}, err
+	}
+	// Create a listener. If `port: 0`, a random one is chosen
+	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", bind, cfg.Port))
+	if err != nil {
+		return nil, err
+	}
+	// Set the value of computed port
+	port := listener.Addr().(*net.TCPAddr).Port
+	// Set the host
+	host := fmt.Sprintf("%s:%d", bind, port)
+	// Get a random path to use
+	path := cfg.Path
+	if path == "" {
+		path = util.GetRandomURLPath()
+	}
+	// Set the hostname
+	hostname := fmt.Sprintf("%s:%d", bind, port)
+	// Use external IP when using `interface: any`, unless a FQDN is set
+	if bind == "0.0.0.0" && cfg.FQDN == "" {
+		fmt.Println("Retrieving the external IP...")
+		extIP, err := util.GetExernalIP()
+		if err != nil {
+			panic(err)
+		}
+		hostname = fmt.Sprintf("%s:%d", extIP.String(), port)
+	}
+	// Use a fully-qualified domain name if set
+	if cfg.FQDN != "" {
+		hostname = fmt.Sprintf("%s:%d", cfg.FQDN, port)
+	}
+	// Set send and receive URLs
+	app.SendURL = fmt.Sprintf("http://%s/send/%s",
+		hostname, path)
+	app.ReceiveURL = fmt.Sprintf("http://%s/receive/%s",
+		hostname, path)
+	// Create a server
+	httpserver := &http.Server{Addr: host}
+	// Create channel to send message to stop server
+	app.stopChannel = make(chan bool)
+	// Create cookie used to verify request is coming from first client to connect
+	cookie := http.Cookie{Name: "qrcp", Value: ""}
+	// Gracefully shutdown when an OS signal is received
+	sig := make(chan os.Signal, 1)
+	signal.Notify(sig, os.Interrupt)
+	go func() {
+		<-sig
+		app.stopChannel <- true
+	}()
+	// The handler adds and removes from the sync.WaitGroup
+	// When the group is zero all requests are completed
+	// and the server is shutdown
+	var waitgroup sync.WaitGroup
+	waitgroup.Add(1)
+	var initCookie sync.Once
+	// Create handlers
+	// Send handler (sends file to caller)
+	http.HandleFunc("/send/"+path, func(w http.ResponseWriter, r *http.Request) {
+		if cookie.Value == "" {
+			if !strings.HasPrefix(r.Header.Get("User-Agent"), "Mozilla") {
+				http.Error(w, "", http.StatusOK)
+				return
+			}
+			initCookie.Do(func() {
+				value, err := util.GetSessionID()
+				if err != nil {
+					log.Println("Unable to generate session ID", err)
+					app.stopChannel <- true
+					return
+				}
+				cookie.Value = value
+				http.SetCookie(w, &cookie)
+			})
+		} else {
+			// Check for the expected cookie and value
+			// If it is missing or doesn't match
+			// return a 404 status
+			rcookie, err := r.Cookie(cookie.Name)
+			if err != nil || rcookie.Value != cookie.Value {
+				http.Error(w, "", http.StatusNotFound)
+				return
+			}
+			// If the cookie exits and matches
+			// this is an aadditional request.
+			// Increment the waitgroup
+			waitgroup.Add(1)
+		}
+		// Remove connection from the waitfroup when done
+		defer waitgroup.Done()
+		w.Header().Set("Content-Disposition", "attachment; filename="+
+			app.payload.Filename)
+		http.ServeFile(w, r, app.payload.Path)
+	})
+	// Upload handler (serves the upload page)
+	http.HandleFunc("/receive/"+path, func(w http.ResponseWriter, r *http.Request) {
+		htmlVariables := struct {
+			Route string
+			File  string
+		}{}
+		htmlVariables.Route = "/receive/" + path
+		switch r.Method {
+		case "POST":
+			filenames := util.ReadFilenames(app.outputDir)
+			reader, err := r.MultipartReader()
+			if err != nil {
+				fmt.Fprintf(w, "Upload error: %v\n", err)
+				log.Printf("Upload error: %v\n", err)
+				app.stopChannel <- true
+				return
+			}
+			transferredFiles := []string{}
+			progressBar := pb.New64(r.ContentLength)
+			progressBar.ShowCounters = false
+			for {
+				part, err := reader.NextPart()
+				if err == io.EOF {
+					break
+				}
+				// iIf part.FileName() is empty, skip this iteration.
+				if part.FileName() == "" {
+					continue
+				}
+				// Prepare the destination
+				fileName := getFileName(part.FileName(), filenames)
+				out, err := os.Create(filepath.Join(app.outputDir, fileName))
+				if err != nil {
+					// Output to server
+					fmt.Fprintf(w, "Unable to create the file for writing: %s\n", err)
+					// Output to console
+					log.Printf("Unable to create the file for writing: %s\n", err)
+					// Send signal to server to shutdown
+					app.stopChannel <- true
+					return
+				}
+				defer out.Close()
+				// Add name of new file
+				filenames = append(filenames, fileName)
+				// Write the content from POSTed file to the out
+				fmt.Println("Transferring file: ", out.Name())
+				progressBar.Prefix(out.Name())
+				progressBar.Start()
+				buf := make([]byte, 1024)
+				for {
+					// Read a chunk
+					n, err := part.Read(buf)
+					if err != nil && err != io.EOF {
+						// Output to server
+						fmt.Fprintf(w, "Unable to write file to disk: %v", err)
+						// Output to console
+						fmt.Printf("Unable to write file to disk: %v", err)
+						// Send signal to server to shutdown
+						app.stopChannel <- true
+						return
+					}
+					if n == 0 {
+						break
+					}
+					// Write a chunk
+					if _, err := out.Write(buf[:n]); err != nil {
+						// Output to server
+						fmt.Fprintf(w, "Unable to write file to disk: %v", err)
+						// Output to console
+						log.Printf("Unable to write file to disk: %v", err)
+						// Send signal to server to shutdown
+						app.stopChannel <- true
+						return
+					}
+					progressBar.Add(n)
+				}
+				transferredFiles = append(transferredFiles, out.Name())
+			}
+			progressBar.FinishPrint("File transfer completed")
+			// Set the value of the variable to the actually transferred files
+			htmlVariables.File = strings.Join(transferredFiles, ", ")
+			serveTemplate("done", pages.Done, w, htmlVariables)
+			if cfg.KeepAlive == false {
+				app.stopChannel <- true
+			}
+		case "GET":
+			serveTemplate("upload", pages.Upload, w, htmlVariables)
+		}
+	})
+	// Wait for all wg to be done, then send shutdown signal
+	go func() {
+		waitgroup.Wait()
+		if cfg.KeepAlive || !app.expectParallelRequests {
+			return
+		}
+		app.stopChannel <- true
+	}()
+	// Receive handler (receives file from caller)
+	go func() {
+		if err := (httpserver.Serve(tcpKeepAliveListener{listener.(*net.TCPListener)})); err != http.ErrServerClosed {
+			log.Fatalln(err)
+		}
+	}()
+	app.instance = httpserver
+	return app, nil
+}

+ 50 - 0
server/tcpkeepalivelistener.go

@@ -0,0 +1,50 @@
+package server
+
+// Copyright (c) 2009 The Go Authors. All rights reserved.
+
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import (
+	"net"
+	"time"
+)
+
+// tcpKeepAliveListener applies TCP keepalives to the listener
+type tcpKeepAliveListener struct {
+	*net.TCPListener
+}
+
+// Accept accepts TCP
+func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
+	tc, err := ln.AcceptTCP()
+	if err != nil {
+		return nil, err
+	}
+	tc.SetKeepAlive(true)
+	tc.SetKeepAlivePeriod(3 * time.Minute)
+	return tc, nil
+}

+ 38 - 0
server/util.go

@@ -0,0 +1,38 @@
+package server
+
+import (
+	"fmt"
+	"html/template"
+	"io"
+	"path/filepath"
+	"strings"
+)
+
+func serveTemplate(name string, tmpl string, w io.Writer, data interface{}) {
+	t, err := template.New(name).Parse(tmpl)
+	if err != nil {
+		panic(err)
+	}
+	if err := t.Execute(w, data); err != nil {
+		panic(err)
+	}
+}
+
+// getFileName generates a file name based on the existing files in the directory
+// if name isn't taken leave it unchanged
+// else change name to format "name(number).ext"
+func getFileName(newFilename string, fileNamesInTargetDir []string) string {
+	fileExt := filepath.Ext(newFilename)
+	fileName := strings.TrimSuffix(newFilename, fileExt)
+	number := 1
+	i := 0
+	for i < len(fileNamesInTargetDir) {
+		if newFilename == fileNamesInTargetDir[i] {
+			newFilename = fmt.Sprintf("%s(%v)%s", fileName, number, fileExt)
+			number++
+			i = 0
+		}
+		i++
+	}
+	return newFilename
+}

+ 43 - 0
util/net.go

@@ -0,0 +1,43 @@
+package util
+
+import (
+	"net"
+	"regexp"
+
+	externalip "github.com/glendc/go-external-ip"
+)
+
+// Interfaces returns a `name:ip` map of the suitable interfaces found
+func Interfaces(listAll bool) (map[string]string, error) {
+	names := make(map[string]string)
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return names, err
+	}
+	var re = regexp.MustCompile(`^(veth|br\-|docker|lo|EHC|XHC|bridge|gif|stf|p2p|awdl|utun|tun|tap)`)
+	for _, iface := range ifaces {
+		if listAll == false && re.MatchString(iface.Name) {
+			continue
+		}
+		if iface.Flags&net.FlagUp == 0 {
+			continue
+		}
+		ip, err := FindIP(iface)
+		if err != nil {
+			continue
+		}
+		names[iface.Name] = ip
+	}
+	return names, nil
+}
+
+// GetExernalIP of this host
+func GetExernalIP() (net.IP, error) {
+	consensus := externalip.DefaultConsensus(nil, nil)
+	// Get your IP, which is never <nil> when err is <nil>
+	ip, err := consensus.ExternalIP()
+	if err != nil {
+		return nil, err
+	}
+	return ip, nil
+}

+ 146 - 0
util/util.go

@@ -0,0 +1,146 @@
+package util
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"errors"
+	"io"
+	"io/ioutil"
+	"net"
+	"os"
+	"regexp"
+	"strconv"
+	"time"
+
+	"github.com/jhoonb/archivex"
+)
+
+// ZipFiles and return the resulting zip's filename
+func ZipFiles(files []string) (string, error) {
+	zip := new(archivex.ZipFile)
+	tmpfile, err := ioutil.TempFile("", "qrcp")
+	if err != nil {
+		return "", err
+	}
+	tmpfile.Close()
+	if err := os.Rename(tmpfile.Name(), tmpfile.Name()+".zip"); err != nil {
+		return "", err
+	}
+	zip.Create(tmpfile.Name() + ".zip")
+	for _, filename := range files {
+		fileinfo, err := os.Stat(filename)
+		if err != nil {
+			return "", err
+		}
+		if fileinfo.IsDir() {
+			zip.AddAll(filename, true)
+		} else {
+			file, err := os.Open(filename)
+			if err != nil {
+				return "", err
+			}
+			defer file.Close()
+			if err := zip.Add(filename, file, fileinfo); err != nil {
+				return "", err
+			}
+		}
+	}
+	if err := zip.Close(); err != nil {
+		return "", nil
+	}
+	return zip.Name, nil
+}
+
+// GetRandomURLPath returns a random string of 4 alphanumeric characters
+func GetRandomURLPath() string {
+	timeNum := time.Now().UTC().UnixNano()
+	alphaString := strconv.FormatInt(timeNum, 36)
+	return alphaString[len(alphaString)-4:]
+}
+
+// GetSessionID returns a base64 encoded string of 40 random characters
+func GetSessionID() (string, error) {
+	randbytes := make([]byte, 40)
+	if _, err := io.ReadFull(rand.Reader, randbytes); err != nil {
+		return "", err
+	}
+	return base64.StdEncoding.EncodeToString(randbytes), nil
+}
+
+// GetInterfaceAddress returns the address of the network interface to
+// bind the server to. If the interface is "any", it will return 0.0.0.0.
+// If no interface is found with that name, an error is returned
+func GetInterfaceAddress(ifaceString string) (string, error) {
+	if ifaceString == "any" {
+		return "0.0.0.0", nil
+	}
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return "", err
+	}
+	var candidateInterface *net.Interface
+	for _, iface := range ifaces {
+		if iface.Name == ifaceString {
+			candidateInterface = &iface
+			break
+		}
+	}
+	if candidateInterface != nil {
+		ip, err := FindIP(*candidateInterface)
+		if err != nil {
+			return "", err
+		}
+		return ip, nil
+	}
+	return "", errors.New("unable to find interface")
+}
+
+// FindIP returns the IP address of the passed interface, and an error
+func FindIP(iface net.Interface) (string, error) {
+	addrs, err := iface.Addrs()
+	if err != nil {
+		return "", err
+	}
+	for _, addr := range addrs {
+		if ipnet, ok := addr.(*net.IPNet); ok {
+			if ipnet.IP.IsLinkLocalUnicast() {
+				continue
+			}
+			if ipnet.IP.To4() != nil {
+				return ipnet.IP.String(), nil
+			}
+			return "[" + ipnet.IP.String() + "]", nil
+		}
+	}
+	return "", errors.New("Unable to find an IP for this interface")
+}
+
+func filterInterfaces(ifaces []net.Interface) []net.Interface {
+	filtered := []net.Interface{}
+	var re = regexp.MustCompile(`^(veth|br\-|docker|lo|EHC|XHC|bridge|gif|stf|p2p|awdl|utun|tun|tap)`)
+	for _, iface := range ifaces {
+		if re.MatchString(iface.Name) {
+			continue
+		}
+		if iface.Flags&net.FlagUp == 0 {
+			continue
+		}
+		filtered = append(filtered, iface)
+	}
+	return filtered
+}
+
+// ReadFilenames from dir
+func ReadFilenames(dir string) []string {
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		panic(err)
+	}
+	// Create array of names of files which are stored in dir
+	// used later to set valid name for received files
+	filenames := make([]string, len(files))
+	for _, fi := range files {
+		filenames = append(filenames, fi.Name())
+	}
+	return filenames
+}

+ 14 - 0
version/version.go

@@ -0,0 +1,14 @@
+package version
+
+import "fmt"
+
+var (
+	app     = "qrcp"
+	version = "dev"
+	date    = "n/a"
+)
+
+// String returns a string representation of the build.
+func String() string {
+	return fmt.Sprintf("%s %s [date: %s]", app, version, date)
+}

Some files were not shown because too many files changed in this diff