├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── SECURITY.md
├── assets
│ └── logo.svg
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── codeql.yml
│ ├── go.yml
│ └── release.yml
├── .gitignore
├── .gitpod.yml
├── .golangci.yml
├── .goreleaser.yml
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── Makefile
├── README.md
├── Taskfile.yaml
├── api
└── get-latest.go
├── app
├── app.go
├── checker
│ └── checker.go
├── config
│ └── config.go
├── gh.go
└── sync.go
├── cmd
├── factory
│ └── default.go
└── tran
│ ├── help-topic.go
│ ├── help.go
│ └── root.go
├── constants
├── commands.go
└── constants.go
├── core
├── crypt
│ └── crypt.go
├── receiver
│ ├── receive.go
│ ├── receiver.go
│ ├── state.go
│ └── tranx-client.go
├── sender
│ ├── handlers.go
│ ├── sender.go
│ ├── server.go
│ ├── state.go
│ ├── transfer.go
│ └── tranx-client.go
└── tranx
│ ├── handlers.go
│ ├── id.go
│ ├── mailbox.go
│ ├── routes.go
│ └── server.go
├── data
└── password-codes.go
├── dfs
└── directory-file-system.go
├── docker
├── container
│ └── Dockerfile
├── dev
│ └── Dockerfile
└── vm
│ └── Dockerfile
├── go.mod
├── go.sum
├── internal
├── config
│ └── config.go
├── theme
│ └── theme.go
└── tui
│ ├── cmds.go
│ ├── init.go
│ ├── keymap.go
│ ├── model.go
│ ├── receive.go
│ ├── receiver.go
│ ├── send.go
│ ├── sender.go
│ ├── sr-ui.go
│ ├── update.go
│ └── view.go
├── ios
├── color.go
├── console.go
├── console_windows.go
├── iostreams.go
├── tty_size.go
└── tty_size_windows.go
├── main.go
├── models
├── model.go
└── protocol
│ ├── transfer.go
│ └── tranx.go
├── renderer
└── renderer.go
├── schema.json
├── scripts
├── bfs.ps1
├── date.go
├── gh-tran
│ ├── gh-trn.js
│ ├── package.json
│ ├── templates
│ │ └── gh-tran
│ └── yarn.lock
├── install.ps1
├── install.sh
├── shell
│ ├── README
│ └── zshrc
└── tag.sh
└── tools
├── errors.go
├── files.go
├── json.go
├── math.go
├── messaging.go
├── password.go
├── ports.go
├── strings.go
├── text.go
└── websocket.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | # build dirs
2 | dist
3 |
4 | # tran files
5 | *.exe
6 | *.exe~
7 | tran
8 |
9 | # dependency directories
10 | vendor
11 | node_modules
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; http://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 |
11 | [*.go]
12 | indent_style = tab
13 | indent_size = 4
14 |
15 | [*.{json,md}]
16 | indent_style = space
17 | indent_size = 2
18 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.sh text eol=lf
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @abdfnx
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | abdfn@secman.dev.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 0.1.x | :white_check_mark: |
11 | | < 0.1 | :x: |
12 |
13 | ## Reporting a Vulnerability
14 |
15 | Use this section to tell people how to report a vulnerability.
16 |
17 | Tell them where to go, how often they can expect to get an update on a
18 | reported vulnerability, what to expect if the vulnerability is accepted or
19 | declined, etc.
20 |
--------------------------------------------------------------------------------
/.github/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 | updates:
4 | - package-ecosystem: "gomod"
5 | directory: "/"
6 | schedule:
7 | interval: "daily"
8 | reviewers:
9 | - abdfnx
10 | - package-ecosystem: npm
11 | directory: "/scripts/gh-tran"
12 | schedule:
13 | interval: "daily"
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tran CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | env:
10 | GITHUB_TOKEN: ${{ github.token }}
11 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true
12 |
13 | jobs:
14 | bfs: # build from source
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 |
20 | - name: Set up Go
21 | uses: actions/setup-go@v2
22 | with:
23 | go-version: 1.18
24 |
25 | - name: Building From Source
26 | run: |
27 | go mod tidy -compat=1.18
28 | go run ./scripts/date.go >> date.txt
29 | go build -ldflags "-X main.version=$(git describe --abbrev=0 --tags) -X main.buildDate=$(cat date.txt)" -o tran
30 |
31 | - name: Run Help
32 | run: ./tran help
33 |
34 | bfs_windows: # build from source (windows)
35 | runs-on: windows-latest
36 |
37 | steps:
38 | - uses: actions/checkout@v2
39 |
40 | - name: Set up Go
41 | uses: actions/setup-go@v2
42 | with:
43 | go-version: 1.18
44 |
45 | - name: Building From Source
46 | run: |
47 | .\scripts\bfs.ps1
48 | echo "::add-path::C:\Users\runneradmin\AppData\Local\tran\bin\;"
49 |
50 | - name: Run Help
51 | run: tran help
52 |
53 | from_script:
54 | needs: [ bfs ]
55 |
56 | runs-on: ubuntu-latest
57 |
58 | steps:
59 | - uses: actions/checkout@v2
60 |
61 | - name: Install from script
62 | run: curl -sL https://cutt.ly/tran-cli | bash
63 |
64 | - name: Run Help
65 | run: tran help
66 |
67 | from_script_windows:
68 | needs: [ bfs_windows ]
69 |
70 | runs-on: windows-latest
71 |
72 | steps:
73 | - uses: actions/checkout@v2
74 |
75 | - name: Install from script
76 | run: |
77 | iwr -useb https://cutt.ly/tran-win | iex
78 | echo "::add-path::C:\Users\runneradmin\AppData\Local\tran\bin\;"
79 |
80 | - name: Run Help
81 | run: tran help
82 |
83 | snapshot:
84 | needs: [ bfs, bfs_windows ]
85 |
86 | runs-on: ubuntu-latest
87 |
88 | steps:
89 | - uses: actions/checkout@v2
90 |
91 | - name: Set up Go
92 | uses: actions/setup-go@v2
93 | with:
94 | go-version: 1.18
95 |
96 | - name: Set up `GoReleaser`
97 | uses: goreleaser/goreleaser-action@v2
98 | with:
99 | install-only: true
100 |
101 | - name: Set up `Date`
102 | run: go run ./scripts/date.go >> date.txt
103 |
104 | - name: Build
105 | run: BuildDate="$(cat date.txt)" goreleaser release --snapshot --rm-dist --timeout 100m
106 |
107 | homebrew:
108 | needs: [ bfs, snapshot ]
109 |
110 | runs-on: macos-latest
111 |
112 | steps:
113 | - uses: actions/checkout@v2
114 |
115 | - name: Get Tran via homebrew
116 | run: brew install abdfnx/tap/tran
117 |
118 | - name: Run `tran help`
119 | run: tran help
120 |
121 | # via_docker:
122 | # needs: [ bfs, from_script, go ]
123 |
124 | # runs-on: ubuntu-latest
125 |
126 | # steps:
127 | # - uses: actions/checkout@v2
128 |
129 | # - name: Run in docker container
130 | # run: docker run --rm -iv trancli/tran -h
131 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 | schedule:
9 | - cron: '40 8 * * 0'
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ 'go', 'javascript' ]
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v2
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v1
31 | with:
32 | languages: ${{ matrix.language }}
33 |
34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
35 | # If this step fails, then you should remove it and run the build manually (see below)
36 | - name: Autobuild
37 | uses: github/codeql-action/autobuild@v1
38 |
39 | # ℹ️ Command-line programs to run using the OS shell.
40 | # 📚 https://git.io/JvXDl
41 |
42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
43 | # and modify them (or add more) to build your code if your project
44 | # uses a compiled language
45 |
46 | - name: Perform CodeQL Analysis
47 | uses: github/codeql-action/analyze@v1
48 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: Set up Go
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: 1.18
19 |
20 | - name: Build
21 | run: |
22 | go run ./scripts/date.go >> date.txt
23 | go build -ldflags "-X main.version=$(git describe --abbrev=0 --tags) -X main.buildDate=$(cat date.txt)" -o tran
24 |
25 | - name: Test
26 | run: go test -v ./...
27 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | env:
9 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
10 | GITHUB_ACTIONS_NAME: "github-actions[bot]"
11 | GITHUB_ACTIONS_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
12 |
13 | permissions: write-all
14 |
15 | jobs:
16 | build_tran:
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 | with:
22 | persist-credentials: false
23 | fetch-depth: 0
24 |
25 | - name: Set up Go
26 | uses: actions/setup-go@v2
27 | with:
28 | go-version: 1.18
29 |
30 | - name: Setup Node.js
31 | uses: actions/setup-node@v2.5.0
32 | with:
33 | node-version: 16
34 |
35 | - name: Set up GoReleaser
36 | uses: goreleaser/goreleaser-action@v2
37 | with:
38 | install-only: true
39 |
40 | - name: Set up Task
41 | uses: arduino/setup-task@v1
42 |
43 | - name: Set up Tag
44 | id: ghtag
45 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
46 |
47 | - name: Set up Date
48 | run: go run ./scripts/date.go >> date.txt
49 |
50 | - name: Build
51 | run: BuildDate="$(cat date.txt)" goreleaser release --rm-dist --timeout 100m
52 |
53 | - name: Build `gh-tran`
54 | env:
55 | TAG: ${{ steps.ghtag.outputs.tag }}
56 | run: |
57 | DATE="$(cat date.txt)"
58 | gh repo clone abdfnx/gh-tran
59 | cd gh-tran
60 | ./release.sh $TAG $DATE
61 | cd ../scripts/gh-tran
62 | yarn
63 | cd ../..
64 | task ght
65 |
66 | - name: Commit files
67 | env:
68 | TAG: ${{ steps.ghtag.outputs.tag }}
69 | run: |
70 | cd ./scripts/gh-tran/tmp/gh-tran
71 | git config --local user.email "${{ env.GITHUB_ACTIONS_EMAIL }}"
72 | git config --local user.name "${{ env.GITHUB_ACTIONS_NAME }}"
73 | git diff --cached
74 | git add .
75 | git commit -m "tran ${TAG}"
76 |
77 | - name: Push changes
78 | uses: ad-m/github-push-action@master
79 | with:
80 | repository: "abdfnx/gh-tran"
81 | github_token: ${{ secrets.ACCESS_TOKEN }}
82 | directory: ./scripts/gh-tran/tmp/gh-tran
83 |
84 | - name: Login to Docker Hub
85 | uses: docker/login-action@v1
86 | with:
87 | username: ${{ secrets.DOCKER_ID }}
88 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
89 |
90 | - name: Build tran from Docker
91 | run: |
92 | task build
93 | cp tran ./docker/vm
94 |
95 | - name: Build Tran Containers
96 | run: |
97 | task build-tran-container
98 | task build-tran-full-container
99 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | /tran
8 |
9 | # Test binary, built with `go test -c`
10 | *.test
11 |
12 | # Output of the go coverage tool, specifically when used with LiteIDE
13 | *.out
14 |
15 | # Dependency directories (remove the comment below to include it)
16 | vendor
17 | node_modules
18 |
19 | # Build Files
20 | date.txt
21 | tag.txt
22 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | - init: go get -d ./...
3 | command: |
4 | brew install go-task/tap/go-task
5 | curl -fsSL https://cutt.ly/tran-cli | bash
6 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters:
2 | enable:
3 | [ gofmt ]
4 |
5 | issues:
6 | max-issues-per-linter: 0
7 | max-same-issues: 0
8 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | project_name: tran
2 |
3 | env:
4 | - CGO_ENABLED=0
5 |
6 | release:
7 | prerelease: auto
8 | draft: true
9 | name_template: "Tran 💻 v{{ .Version }}"
10 |
11 | before:
12 | hooks:
13 | - go mod tidy -compat=1.18
14 |
15 | builds:
16 | - <<: &build_defaults
17 | binary: bin/tran
18 | main: ./
19 | ldflags:
20 | - -X main.version=v{{ .Version }}
21 | - -X main.buildDate={{ .Env.BuildDate }}
22 |
23 | id: macos
24 | goos: [ darwin ]
25 | goarch: [ amd64, arm64, arm ]
26 |
27 | - <<: *build_defaults
28 | id: linux
29 | goos: [ linux ]
30 | goarch: [ amd64, arm64, arm, 386 ]
31 |
32 | - <<: *build_defaults
33 | id: windows
34 | goos: [ windows ]
35 | goarch: [ amd64, arm64, arm, 386 ]
36 |
37 | - <<: *build_defaults
38 | id: freebsd
39 | goos: [ freebsd ]
40 | goarch: [ amd64, arm64, arm, 386 ]
41 |
42 | archives:
43 | - id: nix
44 | builds: [ macos, linux, freebsd ]
45 | <<: &archive_defaults
46 | name_template: "{{ .ProjectName }}_{{ .Os }}_v{{ .Version }}_{{ .Arch }}"
47 |
48 | wrap_in_directory: "true"
49 | replacements:
50 | darwin: macos
51 | format: zip
52 | files:
53 | - LICENSE
54 |
55 | - id: windows
56 | builds: [ windows ]
57 | <<: *archive_defaults
58 | wrap_in_directory: "false"
59 | format: zip
60 | files:
61 | - LICENSE
62 |
63 | nfpms:
64 | - license: MIT
65 | maintainer: abdfnx
66 | homepage: https://github.com/abdfnx/tran
67 | bindir: /usr
68 | file_name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Arch }}"
69 | description: "🖥️ Securely transfer and send anything between computers with TUI"
70 | formats:
71 | - apk
72 | - deb
73 | - rpm
74 |
75 | brews:
76 | - goarm: 6
77 | tap:
78 | owner: abdfnx
79 | name: homebrew-tap
80 | homepage: "https://github.com/abdfnx/tran"
81 | description: "🖥️ Securely transfer and send anything between computers with TUI"
82 |
83 | checksum:
84 | name_template: "checksums.txt"
85 |
86 | snapshot:
87 | name_template: "{{ .Tag }}-next"
88 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "golang.go",
4 | "ms-azuretools.vscode-docker",
5 | "esbenp.prettier-vscode",
6 | "PKief.material-icon-theme",
7 | "aaron-bond.better-comments"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "search.exclude": {
3 | "vendor/**": true
4 | },
5 | "editor.formatOnSave": true,
6 | "editor.fontLigatures": true,
7 | "git.autofetch": true,
8 | "git.confirmSync": false,
9 | "editor.defaultFormatter": "esbenp.prettier-vscode",
10 | "[go]": {
11 | "editor.formatOnSave": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Abdfn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 |
3 | TAG=$(shell git describe --abbrev=0 --tags)
4 | DATE=$(shell go run ./scripts/date.go)
5 |
6 | build:
7 | @go mod tidy && \
8 | go build -ldflags "-X main.version=$(TAG) -X main.buildDate=$(DATE)" -o tran
9 |
10 | install: tran
11 | @mv tran /usr/local/bin
12 |
13 | jbtc: # just build tran container without pushing it
14 | @docker build --file ./docker/vm/Dockerfile -t trancli/tran .
15 |
16 | btc: # build tran container
17 | @docker push trancli/tran
18 |
19 | btcwc: # build tran container with cache
20 | @docker pull trancli/tran:latest && \
21 | docker build -t trancli/tran --cache-from trancli/tran:latest . && \
22 | docker push trancli/tran
23 |
24 | jbftc: # just build full tran container without pushing it
25 | @docker build --file ./docker/container/Dockerfile -t trancli/tran-full .
26 |
27 | bftc: # build full tran container
28 | @docker push trancli/tran-full
29 |
30 | bftcwc: # build full tran container with cache
31 | @docker pull trancli/tran-full:latest && \
32 | docker build -t trancli/tran-full --cache-from trancli/tran-full:latest . && \
33 | docker push trancli/tran-full
34 |
35 | ght:
36 | @node ./scripts/gh-tran/gh-trn.js
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | > 🖥️ Securely transfer and send anything between computers with TUI.
14 |
15 | ## Installation
16 |
17 | ### Using script
18 |
19 | * Shell
20 |
21 | ```
22 | curl -fsSL https://cutt.ly/tran-cli | bash
23 | ```
24 |
25 | * PowerShell
26 |
27 | ```
28 | iwr -useb https://cutt.ly/tran-win | iex
29 | ```
30 |
31 | **then restart your powershell**
32 |
33 | ### Homebrew
34 |
35 | ```bash
36 | brew install abdfnx/tap/tran
37 | ```
38 |
39 | ### GitHub CLI
40 |
41 | ```bash
42 | gh extension install abdfnx/gh-tran
43 | ```
44 |
45 | ## Usage
46 |
47 | * Open Tran UI
48 |
49 | ```bash
50 | tran
51 | ```
52 |
53 | * Open with specific path
54 |
55 | ```
56 | tran --start-dir $PATH
57 | ```
58 |
59 | * Send files to a remote computer
60 |
61 | ```
62 | tran send
63 | ```
64 |
65 | * Receive files from a remote computer
66 |
67 | ```
68 | tran receive
69 | ```
70 |
71 | * Authenticate with github
72 |
73 | ```
74 | tran auth login
75 | ```
76 |
77 | * Sync your tran config file
78 |
79 | ```
80 | tran sync start
81 | ```
82 |
83 | ### Tran Config file
84 |
85 | > tran config file is located at `~/.tran/tran.yml`
86 |
87 | ```yml
88 | config:
89 | borderless: false
90 | editor: vim
91 | enable_mousewheel: true
92 | show_updates: true
93 | start_dir: .
94 | ```
95 |
96 | ### Flags
97 |
98 | ```
99 | --start-dir string Starting directory for Tran
100 | ```
101 |
102 | ### Shortkeys
103 |
104 | * tab: Switch between boxes
105 | * up: Move up
106 | * down: Move down
107 | * left: Go back a directory
108 | * right: Read file or enter directory
109 | * V: View directory
110 | * T: Go to top
111 | * G: Go to bottom
112 | * ~: Go to your home directory
113 | * /: Go to root directory
114 | * .: Toggle hidden files and directories
115 | * D: Only show directories
116 | * F: Only show files
117 | * E: Edit file
118 | * ctrl+s: Send files/directories to remote
119 | * ctrl+r: Receive files/directories from remote
120 | * ctrl+f: Find files and directories by name
121 | * q/ctrl+q: Quit
122 |
123 | ### Technologies Used in Tran
124 |
125 | - [**Charm**](https://charm.sh)
126 | - [**Chroma**](https://github.com/alecthomas/chroma)
127 | - [**Imaging**](https://github.com/disintegration/imaging)
128 | - [**Gorilla Websocket**](https://github.com/gorilla/websocket)
129 | - [**PAKE**](https://github.com/schollz/pake)
130 | - [**Cobra**](https://github.com/spf13/cobra)
131 | - [**Viper**](https://github.com/spf13/viper)
132 | - [**GJson**](https://github.com/tidwall/gjson)
133 | - [**Termenv**](https://github.com/muesli/termenv)
134 |
135 | ### Special thanks
136 |
137 | thanks to [**@ZinoKader**](https://github.com/ZinoKader) for his awesome repo [`portal`](https://github.com/ZinoKader/portal)
138 |
139 | ### License
140 |
141 | tran is licensed under the terms of [MIT](https://github.com/abdfnx/tran/blob/main/LICENSE) license.
142 |
143 |
144 | ## Stargazers over time
145 |
146 | [](https://starchart.cc/abdfnx/tran)
147 |
--------------------------------------------------------------------------------
/Taskfile.yaml:
--------------------------------------------------------------------------------
1 | # https://taskfile.dev
2 |
3 | version: "3"
4 |
5 | vars:
6 | TRAN_CONTAINER: trancli/tran
7 | TRAN_FULL_CONTAINER: trancli/tran-full
8 |
9 | tasks:
10 | default:
11 | deps: [ build, ght ]
12 |
13 | set-tag-and-date:
14 | cmds:
15 | - if [ -f "date.txt" ]; then rm date.txt; fi
16 | - if [ -f "tag.txt" ]; then rm tag.txt; fi
17 | - go run ./scripts/date.go >> date.txt
18 | - git describe --abbrev=0 --tags >> tag.txt
19 |
20 | build:
21 | cmds:
22 | - task: set-tag-and-date
23 | - go get -d
24 | # - go get -u
25 | - go build -ldflags "-X main.version=$(cat tag.txt) -X main.buildDate=$(cat date.txt)" -o tran
26 |
27 | install:
28 | deps: [ build ]
29 | cmds:
30 | - sudo mv tran /usr/local/bin
31 |
32 | remove:
33 | cmds:
34 | - sudo rm -rf /usr/local/bin/tran
35 |
36 | tran-container:
37 | deps: [ just-build-tran-container, build-tran-container, build-tran-container-with-cache ]
38 |
39 | tran-full-container:
40 | deps: [ just-build-tran-full-container, build-tran-full-container, build-full-tran-container-with-cache ]
41 |
42 | just-build-tran-container:
43 | dir: ./docker/vm
44 | cmds:
45 | - docker build -t "{{ .TRAN_CONTAINER }}" .
46 |
47 | build-tran-container:
48 | deps: [ just-build-tran-container ]
49 | dir: ./docker/vm
50 | cmds:
51 | - docker push "{{ .TRAN_CONTAINER }}"
52 |
53 | build-tran-container-with-cache:
54 | cmds:
55 | - docker pull "{{ .TRAN_CONTAINER }}":latest
56 | - docker build -t "{{ .TRAN_CONTAINER }}" --cache-from "{{ .TRAN_CONTAINER }}":latest .
57 | - docker push "{{ .TRAN_CONTAINER }}"
58 |
59 | just-build-tran-full-container:
60 | dir: ./docker/container
61 | cmds:
62 | - docker build -t "{{ .TRAN_CONTAINER }}" .
63 |
64 | build-tran-full-container:
65 | deps: [ just-build-tran-full-container ]
66 | dir: ./docker/container
67 | cmds:
68 | - docker push "{{ .TRAN_CONTAINER }}"
69 |
70 | build-full-tran-container-with-cache:
71 | cmds:
72 | - docker pull "{{ .TRAN_CONTAINER }}":latest && \
73 | - docker build -t "{{ .TRAN_CONTAINER }}" --cache-from "{{ .TRAN_CONTAINER }}":latest . && \
74 | - docker push "{{ .TRAN_CONTAINER }}"
75 |
76 | check_node_moduels:
77 | dir: ./scripts/gh-tran
78 | cmds:
79 | - if ! [ -d "node_modules" ]; then yarn; fi
80 |
81 | ght:
82 | deps: [ build ]
83 | cmds:
84 | - task: check_node_moduels
85 | - node ./scripts/gh-tran/gh-trn.js
86 |
--------------------------------------------------------------------------------
/api/get-latest.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | "net/http"
7 | "io/ioutil"
8 |
9 | "github.com/tidwall/gjson"
10 | "github.com/briandowns/spinner"
11 | httpClient "github.com/abdfnx/resto/client"
12 | )
13 |
14 | func GetLatest() string {
15 | url := "https://api.github.com/repos/abdfnx/tran/releases/latest"
16 |
17 | req, err := http.NewRequest("GET", url, nil)
18 |
19 | if err != nil {
20 | fmt.Errorf("Error creating request: %s", err.Error())
21 | }
22 |
23 | s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
24 | s.Suffix = " 🔍 Checking for updates..."
25 | s.Start()
26 |
27 | client := httpClient.HttpClient()
28 | res, err := client.Do(req)
29 |
30 | if err != nil {
31 | fmt.Printf("Error sending request: %s", err.Error())
32 | }
33 |
34 | defer res.Body.Close()
35 |
36 | b, err := ioutil.ReadAll(res.Body)
37 |
38 | if err != nil {
39 | fmt.Printf("Error reading response: %s", err.Error())
40 | }
41 |
42 | body := string(b)
43 |
44 | tag_name := gjson.Get(body, "tag_name")
45 |
46 | latestVersion := tag_name.String()
47 |
48 | s.Stop()
49 |
50 | return latestVersion
51 | }
52 |
--------------------------------------------------------------------------------
/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/spf13/cobra"
7 | "github.com/abdfnx/tran/tools"
8 | "github.com/abdfnx/tran/models"
9 | "github.com/abdfnx/tran/constants"
10 | "github.com/abdfnx/tran/internal/tui"
11 | "github.com/abdfnx/gh/pkg/cmd/factory"
12 | )
13 |
14 | var NewSendCmd = &cobra.Command{
15 | Use: "send",
16 | Short: "Send files/directories to remote",
17 | Long: "Send files/directories to remote",
18 | RunE: func(cmd *cobra.Command, args []string) error {
19 | tools.RandomSeed()
20 |
21 | err := tui.ValidateTranxAddress()
22 |
23 | if err != nil {
24 | log.Fatal(err)
25 | }
26 |
27 | tui.HandleSendCommand(models.TranOptions{
28 | TranxAddress: constants.DEFAULT_ADDRESS,
29 | TranxPort: constants.DEFAULT_PORT,
30 | }, args)
31 |
32 | return nil
33 | },
34 | }
35 |
36 | var NewReceiveCmd = &cobra.Command{
37 | Use: "receive",
38 | Short: "Receive files/directories from remote",
39 | Long: "Receive files/directories from remote",
40 | RunE: func(cmd *cobra.Command, args []string) error {
41 | err := tui.ValidateTranxAddress()
42 |
43 | if err != nil {
44 | return err
45 | }
46 |
47 | tui.HandleReceiveCommand(models.TranOptions{
48 | TranxAddress: constants.DEFAULT_ADDRESS,
49 | TranxPort: constants.DEFAULT_PORT,
50 | }, args[0])
51 |
52 | return nil
53 | },
54 | }
55 |
56 | var NewAuthCmd = Auth(factory.New())
57 | var NewGHConfigCmd = GHConfig(factory.New())
58 | var NewGHRepoCmd = Repo(factory.New())
59 |
--------------------------------------------------------------------------------
/app/checker/checker.go:
--------------------------------------------------------------------------------
1 | package checker
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/mgutz/ansi"
8 | "github.com/abdfnx/looker"
9 | "github.com/abdfnx/tran/api"
10 | "github.com/abdfnx/tran/cmd/factory"
11 | "github.com/abdfnx/tran/internal/config"
12 | )
13 |
14 | func Check(buildVersion string) {
15 | cmdFactory := factory.New()
16 | stderr := cmdFactory.IOStreams.ErrOut
17 | cfg := config.GetConfig()
18 |
19 | latestVersion := api.GetLatest()
20 | isFromHomebrewTap := isUnderHomebrew()
21 | isFromUsrBinDir := isUnderUsr()
22 | isFromGHCLI := isUnderGHCLI()
23 | isFromAppData := isUnderAppData()
24 |
25 | var command = func() string {
26 | if isFromHomebrewTap {
27 | return "brew upgrade tran"
28 | } else if isFromUsrBinDir {
29 | return "curl -fsSL https://cutt.ly/tran-cli | bash"
30 | } else if isFromGHCLI {
31 | return "gh extention upgrade tran"
32 | } else if isFromAppData {
33 | return "iwr -useb https://cutt.ly/tran-win | iex"
34 | }
35 |
36 | return ""
37 | }
38 |
39 | if buildVersion != latestVersion && cfg.Tran.ShowUpdates {
40 | fmt.Fprintf(stderr, "%s %s → %s\n",
41 | ansi.Color("There's a new version of ", "yellow") + ansi.Color("tran", "cyan") + ansi.Color(" is avalaible:", "yellow"),
42 | ansi.Color(buildVersion, "cyan"),
43 | ansi.Color(latestVersion, "cyan"))
44 |
45 | if command() != "" {
46 | fmt.Fprintf(stderr, ansi.Color("To upgrade, run: %s\n", "yellow"), ansi.Color(command(), "black:white"))
47 | }
48 | }
49 | }
50 |
51 | var tranExe, _ = looker.LookPath("tran")
52 |
53 | func isUnderHomebrew() bool {
54 | return strings.Contains(tranExe, "brew")
55 | }
56 |
57 | func isUnderUsr() bool {
58 | return strings.Contains(tranExe, "usr")
59 | }
60 |
61 | func isUnderAppData() bool {
62 | return strings.Contains(tranExe, "AppData")
63 | }
64 |
65 | func isUnderGHCLI() bool {
66 | return strings.Contains(tranExe, "gh")
67 | }
68 |
--------------------------------------------------------------------------------
/app/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "bytes"
6 | "io/ioutil"
7 | "path/filepath"
8 |
9 | "github.com/mgutz/ansi"
10 | "github.com/spf13/cobra"
11 | "github.com/spf13/viper"
12 | "github.com/abdfnx/tran/dfs"
13 | )
14 |
15 | var (
16 | homeDir, _ = dfs.GetHomeDirectory()
17 | tranConfigPath = filepath.Join(homeDir, ".tran", "tran.yml")
18 | tranConfig, err = ioutil.ReadFile(tranConfigPath)
19 | )
20 |
21 | func NewConfigCmd() *cobra.Command {
22 | cmd := &cobra.Command{
23 | Use: "config",
24 | Short: "Configure tran",
25 | Long: "Configure tran, including setting up tran editor, etc.",
26 | RunE: func(cmd *cobra.Command, args []string) error {
27 | return nil
28 | },
29 | }
30 |
31 | cmd.AddCommand(NewConfigSetCmd)
32 | cmd.AddCommand(NewConfigGetCmd)
33 | cmd.AddCommand(NewConfigListCmd)
34 |
35 | return cmd
36 | }
37 |
38 | var NewConfigSetCmd = &cobra.Command{
39 | Use: "set",
40 | Short: "Update tran configuration",
41 | Long: "Update tran configuration, such as editor, show updates, etc.",
42 | RunE: func(cmd *cobra.Command, args []string) error {
43 | if err != nil {
44 | return err
45 | }
46 |
47 | viper.SetConfigType("yaml")
48 |
49 | viper.ReadConfig(bytes.NewBuffer(tranConfig))
50 |
51 | // set new key value but keep existing values
52 | viper.Set("config." + args[0], args[1])
53 |
54 | // write config to file
55 | err := viper.WriteConfigAs(tranConfigPath)
56 |
57 | if err != nil {
58 | return err
59 | }
60 |
61 | fmt.Println(ansi.Color("Updated tran configuration", "green"))
62 |
63 | return nil
64 | },
65 | }
66 |
67 | var NewConfigGetCmd = &cobra.Command{
68 | Use: "get",
69 | Short: "Get tran configuration",
70 | Long: "Get tran configuration",
71 | RunE: func(cmd *cobra.Command, args []string) error {
72 | if err != nil {
73 | return err
74 | }
75 |
76 | viper.SetConfigType("yaml")
77 |
78 | viper.ReadConfig(bytes.NewBuffer(tranConfig))
79 |
80 | fmt.Println(viper.Get("config." + args[0]))
81 |
82 | return nil
83 | },
84 | }
85 |
86 | var NewConfigListCmd = &cobra.Command{
87 | Use: "list",
88 | Short: "List tran configuration",
89 | Long: "List tran configuration",
90 | RunE: func(cmd *cobra.Command, args []string) error {
91 | if err != nil {
92 | return err
93 | }
94 |
95 | viper.SetConfigType("yaml")
96 |
97 | viper.ReadConfig(bytes.NewBuffer(tranConfig))
98 |
99 | // get the config
100 | config := viper.GetStringMap("config")
101 |
102 | for k, v := range config {
103 | fmt.Println(k + " =", v)
104 | }
105 |
106 | return nil
107 | },
108 | }
109 |
--------------------------------------------------------------------------------
/app/gh.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "github.com/abdfnx/gh/api"
6 | "github.com/abdfnx/gh/context"
7 | "github.com/abdfnx/gh/core/ghrepo"
8 | "github.com/abdfnx/gh/pkg/cmdutil"
9 | aCmd "github.com/abdfnx/gh/pkg/cmd/auth"
10 | rCmd "github.com/abdfnx/gh/pkg/cmd/gh-repo"
11 | cCmd "github.com/abdfnx/gh/pkg/cmd/gh-config"
12 | )
13 |
14 | func Auth(f *cmdutil.Factory) *cobra.Command {
15 | cmd := aCmd.NewCmdAuth(f)
16 | return cmd
17 | }
18 |
19 | func GHConfig(f *cmdutil.Factory) *cobra.Command {
20 | cmd := cCmd.NewCmdConfig(f)
21 | return cmd
22 | }
23 |
24 | func Repo(f *cmdutil.Factory) *cobra.Command {
25 | repoResolvingCmdFactory := *f
26 | repoResolvingCmdFactory.BaseRepo = resolvedBaseRepo(f)
27 |
28 | cmd := rCmd.NewCmdRepo(&repoResolvingCmdFactory)
29 |
30 | return cmd
31 | }
32 |
33 | func resolvedBaseRepo(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
34 | return func() (ghrepo.Interface, error) {
35 | httpClient, err := f.HttpClient()
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | apiClient := api.NewClientFromHTTP(httpClient)
41 |
42 | remotes, err := f.Remotes()
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | baseRepo, err := repoContext.BaseRepo(f.IOStreams)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | return baseRepo, nil
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/sync.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 | "runtime"
8 |
9 | "github.com/abdfnx/gosh"
10 | "github.com/spf13/cobra"
11 | "github.com/abdfnx/gh/utils"
12 | "github.com/briandowns/spinner"
13 | "github.com/MakeNowJust/heredoc"
14 | "github.com/abdfnx/tran/constants"
15 | git_config "github.com/david-tomson/tran-git"
16 | )
17 |
18 | var username = git_config.GitConfig()
19 |
20 | var (
21 | NewCmdStart = &cobra.Command{
22 | Use: "start",
23 | Aliases: []string{"."},
24 | Example: "tran sync start",
25 | Short: "Start sync your tran config.",
26 | Run: func(cmd *cobra.Command, args []string) {
27 | if username != ":username" {
28 | exCmd := "echo '# My tran config - " + username + "\n\n## Clone\n\n```\ntran sync clone\n```\n\n**for more about sync command, run `tran sync -h`**' >> $HOME/.tran/README.md"
29 |
30 | gosh.Run(exCmd)
31 | gosh.RunMulti(constants.Start_ml(), constants.Start_w())
32 | } else {
33 | utils.AuthMessage()
34 | }
35 | },
36 | }
37 |
38 | NewCmdClone = &cobra.Command{
39 | Use: "clone",
40 | Aliases: []string{"cn"},
41 | Short: CloneHelp(),
42 | Run: func(cmd *cobra.Command, args []string) {
43 | if username != ":username" {
44 | gosh.RunMulti(constants.Clone_ml(), constants.Clone_w())
45 | gosh.RunMulti(constants.Clone_check_ml(), constants.Clone_check_w())
46 | } else {
47 | utils.AuthMessage()
48 | }
49 | },
50 | }
51 |
52 | NewCmdPush = &cobra.Command{
53 | Use: "push",
54 | Aliases: []string{"ph"},
55 | Short: "Push the new changes in tran config file.",
56 | Run: func(cmd *cobra.Command, args []string) {
57 | if username != ":username" {
58 | gosh.RunMulti(constants.Push_ml(), constants.Push_w())
59 | } else {
60 | utils.AuthMessage()
61 | }
62 | },
63 | }
64 |
65 | NewCmdPull = &cobra.Command{
66 | Use: "pull",
67 | Aliases: []string{"pl"},
68 | Short: PullHelp(),
69 | Run: func(cmd *cobra.Command, args []string) {
70 | if username != ":username" {
71 | gosh.RunMulti(constants.Pull_ml(), constants.Pull_w())
72 | } else {
73 | utils.AuthMessage()
74 | }
75 | },
76 | }
77 |
78 | FetchX = &cobra.Command{
79 | Use: "fetchx",
80 | Short: "Special command for windows",
81 | Run: func(cmd *cobra.Command, args []string) {
82 | if username != ":username" {
83 | if runtime.GOOS == "windows" {
84 | gosh.PowershellCommand(constants.Clone_w())
85 | } else {
86 | fmt.Println("This command isn't avaliable for this platform")
87 | }
88 | } else {
89 | utils.AuthMessage()
90 | }
91 | },
92 | }
93 | )
94 |
95 | func Sync() *cobra.Command {
96 | cmd := &cobra.Command{
97 | Use: "sync ",
98 | Short: "Sync your tran config file.",
99 | Long: SyncHelp(),
100 | Example: heredoc.Doc(`
101 | tran sync start
102 | tran sync clone
103 | `),
104 | }
105 |
106 | cmd.AddCommand(
107 | NewCmdStart,
108 | NewCmdClone,
109 | NewCmdPush,
110 | NewCmdPull,
111 | FetchX,
112 | )
113 |
114 | return cmd
115 | }
116 |
117 | const tranConfigPath string = "/.tran"
118 |
119 | func PullHelp() string {
120 | return git_config.GitConfigWithMsg("Pull the new changes from ", tranConfigPath)
121 | }
122 |
123 | func SyncHelp() string {
124 | return git_config.GitConfigWithMsg("Sync your config file, by create a private repo at ", tranConfigPath)
125 | }
126 |
127 | func CloneHelp() string {
128 | return git_config.GitConfigWithMsg("Clone your .tran from your private repo at https://github.com/", tranConfigPath)
129 | }
130 |
131 | func PushSync() {
132 | const Syncing string = " 📮 Syncing..."
133 |
134 | if runtime.GOOS == "windows" {
135 | err, out, errout := gosh.PowershellOutput(
136 | `
137 | $directoyPath = "~/.tran/.git"
138 |
139 | if (Test-Path -path $directoyPath) {
140 | Write-Host "Reading from .tran folder..."
141 | }
142 | `)
143 |
144 | fmt.Print(out)
145 |
146 | if err != nil {
147 | log.Printf("error: %v\n", err)
148 | fmt.Print(errout)
149 | } else if out != "" {
150 | s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
151 | s.Suffix = Syncing
152 | s.Start()
153 |
154 | gosh.PowershellCommand(constants.Push_w())
155 |
156 | s.Stop()
157 | }
158 | } else {
159 | err, out, errout := gosh.ShellOutput(
160 | `
161 | if [ -d ~/.tran/.git ]; then
162 | echo "📖 Reading from .tran folder..."
163 | fi
164 | `)
165 |
166 | fmt.Print(out)
167 |
168 | if err != nil {
169 | log.Printf("error: %v\n", err)
170 | fmt.Print(errout)
171 | } else if out != "" {
172 | s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
173 | s.Suffix = Syncing
174 | s.Start()
175 |
176 | gosh.ShellCommand(constants.Push_ml())
177 |
178 | s.Stop()
179 | }
180 | }
181 | }
182 |
183 |
--------------------------------------------------------------------------------
/cmd/factory/default.go:
--------------------------------------------------------------------------------
1 | package factory
2 |
3 | import (
4 | "github.com/abdfnx/tran/ios"
5 | )
6 |
7 | type Factory struct {
8 | IOStreams *ios.IOStreams
9 | }
10 |
11 | func New() *Factory {
12 | f := &Factory{}
13 |
14 | f.IOStreams = ioStreams(f)
15 |
16 | return f
17 | }
18 |
19 | func ioStreams(f *Factory) *ios.IOStreams {
20 | io := ios.System()
21 |
22 | return io
23 | }
24 |
--------------------------------------------------------------------------------
/cmd/tran/help-topic.go:
--------------------------------------------------------------------------------
1 | package tran
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | var HelpTopics = map[string]map[string]string{}
8 |
9 | func NewHelpTopic(topic string) *cobra.Command {
10 | cmd := &cobra.Command{
11 | Use: topic,
12 | Short: HelpTopics[topic]["short"],
13 | Long: HelpTopics[topic]["long"],
14 | Hidden: true,
15 | Annotations: map[string]string{
16 | "markdown:generate": "true",
17 | "markdown:basename": "tran_help_" + topic,
18 | },
19 | }
20 |
21 | cmd.SetHelpFunc(helpTopicHelpFunc)
22 | cmd.SetUsageFunc(helpTopicUsageFunc)
23 |
24 | return cmd
25 | }
26 |
27 | func helpTopicHelpFunc(command *cobra.Command, args []string) {
28 | command.Print(command.Long)
29 | }
30 |
31 | func helpTopicUsageFunc(command *cobra.Command) error {
32 | command.Printf("Usage: tran help %s", command.Use)
33 | return nil
34 | }
35 |
--------------------------------------------------------------------------------
/cmd/tran/help.go:
--------------------------------------------------------------------------------
1 | package tran
2 |
3 | import (
4 | "fmt"
5 | "bytes"
6 | "strings"
7 |
8 | "github.com/spf13/cobra"
9 | "github.com/spf13/pflag"
10 | "github.com/abdfnx/tran/ios"
11 | "github.com/abdfnx/tran/tools"
12 | )
13 |
14 | func rootUsageFunc(command *cobra.Command) error {
15 | command.Printf("Usage: %s", command.UseLine())
16 |
17 | subcommands := command.Commands()
18 |
19 | if len(subcommands) > 0 {
20 | command.Print("\n\nCommands:\n")
21 | for _, c := range subcommands {
22 | if c.Hidden {
23 | continue
24 | }
25 |
26 | command.Printf(" %s\n", c.Name())
27 | }
28 |
29 | return nil
30 | }
31 |
32 | flagUsages := command.LocalFlags().FlagUsages()
33 |
34 | if flagUsages != "" {
35 | command.Println("\n\nFlags:")
36 | command.Print(tools.Indent(dedent(flagUsages), " "))
37 | }
38 |
39 | return nil
40 | }
41 |
42 | func rootFlagErrorFunc(cmd *cobra.Command, err error) error {
43 | if err == pflag.ErrHelp {
44 | return err
45 | }
46 |
47 | return &tools.FlagError{Err: err}
48 | }
49 |
50 | var hasFailed bool
51 |
52 | func HasFailed() bool {
53 | return hasFailed
54 | }
55 |
56 | func nestedSuggestFunc(command *cobra.Command, arg string) {
57 | command.Printf("unknown command %q for %q\n", arg, command.CommandPath())
58 |
59 | var candidates []string
60 | if arg == "help" {
61 | candidates = []string{"--help"}
62 | } else {
63 | if command.SuggestionsMinimumDistance <= 0 {
64 | command.SuggestionsMinimumDistance = 2
65 | }
66 |
67 | candidates = command.SuggestionsFor(arg)
68 | }
69 |
70 | if len(candidates) > 0 {
71 | command.Print("\nDid you mean this?\n")
72 |
73 | for _, c := range candidates {
74 | command.Printf("\t%s\n", c)
75 | }
76 | }
77 |
78 | command.Print("\n")
79 | _ = rootUsageFunc(command)
80 | }
81 |
82 | func isRootCmd(command *cobra.Command) bool {
83 | return command != nil && !command.HasParent()
84 | }
85 |
86 | func rootHelpFunc(cs *ios.ColorScheme, command *cobra.Command, args []string) {
87 | if isRootCmd(command.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
88 | nestedSuggestFunc(command, args[1])
89 | hasFailed = true
90 | return
91 | }
92 |
93 | commands := []string{}
94 |
95 | for _, c := range command.Commands() {
96 | if c.Short == "" {
97 | continue
98 | }
99 | if c.Hidden {
100 | continue
101 | }
102 |
103 | s := rpad(c.Name()+":", c.NamePadding()) + c.Short
104 |
105 | commands = append(commands, s)
106 | }
107 |
108 | if len(commands) == 0 {
109 | commands = []string{}
110 | }
111 |
112 | type helpEntry struct {
113 | Title string
114 | Body string
115 | }
116 |
117 | helpEntries := []helpEntry{}
118 |
119 | if command.Long != "" {
120 | helpEntries = append(helpEntries, helpEntry{"", command.Long})
121 | } else if command.Short != "" {
122 | helpEntries = append(helpEntries, helpEntry{"", command.Short})
123 | }
124 |
125 | helpEntries = append(helpEntries, helpEntry{"USAGE", command.UseLine()})
126 |
127 | if len(commands) > 0 {
128 | helpEntries = append(helpEntries, helpEntry{"COMMANDS", strings.Join(commands, "\n")})
129 | }
130 |
131 | flagUsages := command.LocalFlags().FlagUsages()
132 |
133 | if flagUsages != "" {
134 | helpEntries = append(helpEntries, helpEntry{"FLAGS", dedent(flagUsages)})
135 | }
136 |
137 | if _, ok := command.Annotations["help:arguments"]; ok {
138 | helpEntries = append(helpEntries, helpEntry{"ARGUMENTS", command.Annotations["help:arguments"]})
139 | }
140 |
141 | if command.Example != "" {
142 | helpEntries = append(helpEntries, helpEntry{"EXAMPLES", command.Example})
143 | }
144 |
145 | helpEntries = append(helpEntries, helpEntry{"LEARN MORE", `
146 | Use 'tran --help' for more information about a command.`})
147 | if _, ok := command.Annotations["help:tellus"]; ok {
148 | helpEntries = append(helpEntries, helpEntry{"TELL US", command.Annotations["help:tellus"]})
149 | }
150 |
151 | out := command.OutOrStdout()
152 | for _, e := range helpEntries {
153 | if e.Title != "" {
154 | fmt.Fprintln(out, cs.Bold(e.Title))
155 | fmt.Fprintln(out, tools.Indent(strings.Trim(e.Body, "\r\n"), " "))
156 | } else {
157 | fmt.Fprintln(out, e.Body)
158 | }
159 |
160 | fmt.Fprintln(out)
161 | }
162 | }
163 |
164 | func rpad(s string, padding int) string {
165 | template := fmt.Sprintf("%%-%ds ", padding)
166 | return fmt.Sprintf(template, s)
167 | }
168 |
169 | func dedent(s string) string {
170 | lines := strings.Split(s, "\n")
171 | minIndent := -1
172 |
173 | for _, l := range lines {
174 | if len(l) == 0 {
175 | continue
176 | }
177 |
178 | indent := len(l) - len(strings.TrimLeft(l, " "))
179 |
180 | if minIndent == -1 || indent < minIndent {
181 | minIndent = indent
182 | }
183 | }
184 |
185 | if minIndent <= 0 {
186 | return s
187 | }
188 |
189 | var buf bytes.Buffer
190 |
191 | for _, l := range lines {
192 | fmt.Fprintln(&buf, strings.TrimPrefix(l, strings.Repeat(" ", minIndent)))
193 | }
194 |
195 | return strings.TrimSuffix(buf.String(), "\n")
196 | }
197 |
--------------------------------------------------------------------------------
/cmd/tran/root.go:
--------------------------------------------------------------------------------
1 | package tran
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/spf13/cobra"
8 | "github.com/abdfnx/tran/app"
9 | "github.com/MakeNowJust/heredoc"
10 | "github.com/abdfnx/tran/cmd/factory"
11 | "github.com/abdfnx/tran/internal/tui"
12 | "github.com/abdfnx/tran/internal/config"
13 | tea "github.com/charmbracelet/bubbletea"
14 | configCmd "github.com/abdfnx/tran/app/config"
15 | )
16 |
17 | // Execute start the CLI
18 | func Execute(f *factory.Factory, version string, buildDate string) *cobra.Command {
19 | const desc = `🖥️ Securely transfer and send anything between computers with TUI.`
20 |
21 | // Root command
22 | var rootCmd = &cobra.Command{
23 | Use: "tran [flags]",
24 | Short: desc,
25 | Long: desc,
26 | SilenceErrors: true,
27 | Example: heredoc.Doc(`
28 | # Open Tran UI
29 | tran
30 |
31 | # Open with specific path
32 | tran --start-dir $PATH
33 |
34 | # Send files to a remote computer
35 | tran send
36 |
37 | # Receive files from a remote computer
38 | tran receive
39 |
40 | # Authenticate
41 | tran auth login
42 |
43 | # Sync your tran config file
44 | tran sync start
45 | `),
46 | Annotations: map[string]string{
47 | "help:tellus": heredoc.Doc(`
48 | Open an issue at https://github.com/abdfnx/tran/issues
49 | `),
50 | },
51 | RunE: func(cmd *cobra.Command, args []string) error {
52 | startDir := cmd.Flags().Lookup("start-dir")
53 |
54 | config.LoadConfig(startDir)
55 | cfg := config.GetConfig()
56 |
57 | m := tui.New()
58 | var opts []tea.ProgramOption
59 |
60 | // Always append alt screen program option.
61 | opts = append(opts, tea.WithAltScreen())
62 |
63 | // If mousewheel is enabled, append it to the program options.
64 | if cfg.Tran.EnableMouseWheel {
65 | opts = append(opts, tea.WithMouseAllMotion())
66 | }
67 |
68 | // Initialize and start app.
69 | p := tea.NewProgram(m, opts...)
70 |
71 | if err := p.Start(); err != nil {
72 | log.Fatal("Failed to start tran", err)
73 | }
74 |
75 | return nil
76 | },
77 | }
78 |
79 | versionCmd := &cobra.Command{
80 | Use: "version",
81 | Aliases: []string{"ver"},
82 | Short: "Print the version of your tran binary.",
83 | Run: func(cmd *cobra.Command, args []string) {
84 | fmt.Println("tran version " + version + " " + buildDate)
85 | },
86 | }
87 |
88 | rootCmd.SetOut(f.IOStreams.Out)
89 | rootCmd.SetErr(f.IOStreams.ErrOut)
90 |
91 | cs := f.IOStreams.ColorScheme()
92 |
93 | helpHelper := func(command *cobra.Command, args []string) {
94 | rootHelpFunc(cs, command, args)
95 | }
96 |
97 | rootCmd.PersistentFlags().Bool("help", false, "Help for tran")
98 | rootCmd.PersistentFlags().String("start-dir", "", "Starting directory for Tran")
99 | rootCmd.SetHelpFunc(helpHelper)
100 | rootCmd.SetUsageFunc(rootUsageFunc)
101 | rootCmd.SetFlagErrorFunc(rootFlagErrorFunc)
102 |
103 | // Add sub-commands to root command
104 | rootCmd.AddCommand(
105 | app.NewAuthCmd,
106 | app.NewSendCmd,
107 | app.NewReceiveCmd,
108 | app.NewGHConfigCmd,
109 | app.NewGHRepoCmd,
110 | app.Sync(),
111 | configCmd.NewConfigCmd(),
112 | versionCmd,
113 | )
114 |
115 | return rootCmd
116 | }
117 |
--------------------------------------------------------------------------------
/constants/commands.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | func Fetch_w() string {
4 | return `
5 | Remove-Item $HOME\.tran -Recurse -Force
6 | tran sync fetchx
7 | Write-Host "Fetched Successfully"
8 | `
9 | }
10 |
11 | func Fetch_ml() string {
12 | return `
13 | cd $HOME/.tran
14 | git pull
15 | echo "Fetched Successfully ✅"
16 | `
17 | }
18 |
19 | func Start_w() string {
20 | return `
21 | $username = tran auth get-username
22 | cd $HOME\.tran
23 | git init
24 | tran gh-repo create .tran -d "My tran config - $username" --private -y
25 | git add .
26 | git commit -m "new .tran repo"
27 | git branch -M trunk
28 | git remote add origin https://github.com/$username/.tran
29 | git push -u origin trunk
30 | cd $lastDir
31 | `
32 | }
33 |
34 | func Start_ml() string {
35 | return `
36 | username=$(tran auth get-username)
37 | cd ~/.tran
38 | git init
39 | tran gh-repo create .tran -d "My tran config - $username" --private -y
40 | git add .
41 | git commit -m "new .tran repo"
42 | git branch -M trunk
43 | git remote add origin https://github.com/$username/.tran
44 | git push -u origin trunk
45 | `
46 | }
47 |
48 | func Push_w() string {
49 | return `
50 | $lastDir = pwd
51 | cd $HOME\.tran
52 | if (Test-Path -path .git) {
53 | git add .
54 | git commit -m "new change"
55 | git push
56 | }
57 |
58 | cd $lastDir
59 | `
60 | }
61 |
62 | func Push_ml() string {
63 | return `
64 | cd ~/.tran
65 | git add .
66 | git commit -m "new tran config"
67 | git push
68 | `
69 | }
70 |
71 | func Pull_w() string {
72 | return `
73 | $lastDir = pwd
74 | cd $HOME\.tran
75 |
76 | git pull
77 |
78 | cd $lastDir
79 | `
80 | }
81 |
82 | func Pull_ml() string {
83 | return `
84 | cd ~/.tran
85 | git pull
86 | `
87 | }
88 |
89 | func Clone_w() string {
90 | return `
91 | $TRANDIR = $HOME\.tran
92 |
93 | if (Test-Path -path $TRANDIR) {
94 | Remove-Item $TRANDIR -Recurse -Force
95 | } else {
96 | tran gh-repo clone .tran $TRANDIR
97 | }
98 | `
99 | }
100 |
101 | func Clone_ml() string {
102 | return `
103 | TRANDIR=~/.tran
104 |
105 | if [ -d $TRANDIR ]; then
106 | rm -rf $TRANDIR
107 | else
108 | tran gh-repo clone .tran $TRANDIR
109 | fi
110 | `
111 | }
112 |
113 | func Clone_check_w() string {
114 | return `
115 | if (Test-Path -path $HOME\.tran) {
116 | Write-Host "tran repo cloned successfully"
117 | }
118 | `
119 | }
120 |
121 | func Clone_check_ml() string {
122 | return `if [ -d $HOME/.tran ]; then echo "tran repo cloned successfully ✅"; fi`
123 | }
124 |
--------------------------------------------------------------------------------
/constants/constants.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | "runtime"
7 | "strings"
8 |
9 | "github.com/charmbracelet/lipgloss"
10 | "github.com/charmbracelet/bubbles/progress"
11 | )
12 |
13 | const DEFAULT_ADDRESS = "167.71.65.96"
14 | const DEFAULT_PORT = 80
15 |
16 | const MAX_CHUNK_BYTES = 1e6
17 | const MAX_SEND_CHUNKS = 2e8
18 |
19 | const RECEIVER_CONNECT_TIMEOUT time.Duration = 5 * time.Minute
20 |
21 | const SEND_TEMP_FILE_NAME_PREFIX = "tran-send-tmp"
22 | const RECEIVE_TEMP_FILE_NAME_PREFIX = "tran-receive-tmp"
23 |
24 | const (
25 | PrimaryBoxActive = iota
26 | SecondaryBoxActive
27 | ThirdBoxActive
28 | )
29 |
30 | const (
31 | StatusBarHeight = 1
32 | BoxPadding = 1
33 | EllipsisStyle = "..."
34 | FileSizeLoadingStyle = "---"
35 | )
36 |
37 | var BoldTextStyle = lipgloss.NewStyle().Bold(true)
38 |
39 | var Colors = map[string]lipgloss.Color{
40 | "black": "#000000",
41 | }
42 |
43 | const (
44 | PADDING = 2
45 | MAX_WIDTH = 80
46 | PRIMARY_COLOR = "#1E90FF"
47 | SECONDARY_COLOR = "#1E6AFF"
48 | DARK_GRAY_COLOR = "#3c3836"
49 | START_PERIOD = 1 * time.Millisecond
50 | SHUTDOWN_PERIOD = 1000 * time.Millisecond
51 | )
52 |
53 | var QuitKeys = []string{"q", "esc"}
54 | var PadText = strings.Repeat(" ", PADDING)
55 | var QuitCommandsHelpText = HelpStyle(fmt.Sprintf("(press one of [%s] keys to quit from tran)", (strings.Join(QuitKeys, ", "))))
56 | var ProgressBar = progress.NewModel(progress.WithGradient(SECONDARY_COLOR, PRIMARY_COLOR))
57 |
58 | var baseStyle = lipgloss.NewStyle()
59 | var InfoStyle = baseStyle.Copy().Foreground(lipgloss.Color(PRIMARY_COLOR)).Render
60 | var HelpStyle = baseStyle.Copy().Foreground(lipgloss.Color(DARK_GRAY_COLOR)).Render
61 | var ItalicText = baseStyle.Copy().Italic(true).Render
62 | var BoldText = baseStyle.Copy().Bold(true).Render
63 |
64 | func CtrlKey() string {
65 | // if os is macos, then return "⌘"
66 | if runtime.GOOS == "darwin" {
67 | return "⌘"
68 | } else {
69 | return "ctrl"
70 | }
71 | }
72 |
73 | func AltKey() string {
74 | // if os is macos, then return "⌥"
75 | if runtime.GOOS == "darwin" {
76 | return "⌥"
77 | } else {
78 | return "alt"
79 | }
80 | }
81 |
82 | var HelpContent = `# Help Guide` + "\n" +
83 | "* `tab`: Switch between boxes\n" +
84 | "* `up`: Move up\n" +
85 | "* `down`: Move down\n" +
86 | "* `left`: Go back a directory\n" +
87 | "* `right`: Read file or enter directory\n" +
88 | "* `V`: View directory\n" +
89 | "* `T`: Go to top\n" +
90 | "* `G`: Go to bottom\n" +
91 | "* `~`: Go to your home directory\n" +
92 | "* `/`: Go to root directory\n" +
93 | "* `.`: Toggle hidden files and directories\n" +
94 | "* `D`: Only show directories\n" +
95 | "* `F`: Only show files\n" +
96 | "* `E`: Edit file\n" +
97 | "* `" + CtrlKey() + "+s`: Send files/directories to remote\n" +
98 | "* `" + CtrlKey() + "+r`: Receive files/directories from remote\n" +
99 | "* `" + CtrlKey() + "+f`: Find files and directories by name\n" +
100 | "* `q`/`" + CtrlKey() + "+q`: Quit"
101 |
102 | var InfoContent = `# Info` + "\n" +
103 | "* Address: **" + DEFAULT_ADDRESS + "**\n" +
104 | "* Port: **" + fmt.Sprintf("%d", DEFAULT_PORT) + "**\n" +
105 | "* OS: **" + runtime.GOOS + "**\n" +
106 | "* Arch: **" + runtime.GOARCH + "**\n" +
107 | "* Author: " + "[**@abdfnx**](https://github.com/abdfnx)"
108 |
--------------------------------------------------------------------------------
/core/crypt/crypt.go:
--------------------------------------------------------------------------------
1 | package crypt
2 |
3 | import (
4 | "fmt"
5 | "crypto/aes"
6 | "crypto/rand"
7 | "crypto/cipher"
8 | "crypto/sha256"
9 |
10 | "golang.org/x/crypto/pbkdf2"
11 | )
12 |
13 | type Crypt struct {
14 | Key []byte
15 | Salt []byte
16 | }
17 |
18 | // New returns a new Crypt object, with a sha256 cryptographic key and corresponding salt.
19 | // Parameters:
20 | // sessionkey A sessionkey (preferably generated with PAKE2).
21 | // salt Salt used in generating the key. If not supplied a random salt will be generated.
22 | func New(sessionkey []byte, salt ...[]byte) (*Crypt, error) {
23 | var s []byte
24 |
25 | if len(salt) < 1 {
26 | s = make([]byte, 8)
27 |
28 | if _, err := rand.Read(s); err != nil {
29 | return nil, fmt.Errorf("unable to generate random salt: %v", err)
30 | }
31 | } else {
32 | s = salt[0]
33 | }
34 |
35 | key := pbkdf2.Key(sessionkey, s, 100, 32, sha256.New)
36 |
37 | crypt := &Crypt{
38 | Key: key,
39 | Salt: s,
40 | }
41 |
42 | return crypt, nil
43 | }
44 |
45 | // Encrypt encrypts the provided message using shared key and a random nonce that is appended to the message.
46 | func (s *Crypt) Encrypt(unencrypted []byte) (encrypted []byte, err error) {
47 | block, err := aes.NewCipher(s.Key)
48 |
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | nonce := make([]byte, 12)
54 |
55 | if _, err := rand.Read(nonce); err != nil {
56 | return nil, fmt.Errorf("unable to generate random nonce: %v", err)
57 | }
58 |
59 | aescgm, err := cipher.NewGCM(block)
60 |
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | encrypted = aescgm.Seal(nil, nonce, unencrypted, nil)
66 | encrypted = append(nonce, encrypted...)
67 |
68 | return encrypted, nil
69 | }
70 |
71 | // Decrypt decrypts the provided message with the the shared key.
72 | func (s *Crypt) Decrypt(encrypted []byte) (decrypted []byte, err error) {
73 | block, err := aes.NewCipher(s.Key)
74 |
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | aescgm, err := cipher.NewGCM(block)
80 |
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | decrypted, err = aescgm.Open(nil, encrypted[:12], encrypted[12:], nil)
86 |
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | return decrypted, nil
92 | }
93 |
--------------------------------------------------------------------------------
/core/receiver/receive.go:
--------------------------------------------------------------------------------
1 | package receiver
2 |
3 | import (
4 | "io"
5 | "encoding/json"
6 |
7 | "github.com/gorilla/websocket"
8 | "github.com/abdfnx/tran/tools"
9 | "github.com/abdfnx/tran/models/protocol"
10 | )
11 |
12 | func (r *Receiver) Receive(wsConn *websocket.Conn, buffer io.Writer) error {
13 | // request payload
14 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.ReceiverRequestPayload}, r.crypt)
15 |
16 | var writtenBytes int64
17 | for {
18 | _, encBytes, err := wsConn.ReadMessage()
19 | if err != nil {
20 | return err
21 | }
22 |
23 | decBytes, err := r.crypt.Decrypt(encBytes)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | transferMsg := protocol.TransferMessage{}
29 | err = json.Unmarshal(decBytes, &transferMsg)
30 | if err != nil {
31 | buffer.Write(decBytes)
32 | writtenBytes += int64(len(decBytes))
33 | r.updateUI(float32(writtenBytes) / float32(r.payloadSize))
34 | } else {
35 | if transferMsg.Type != protocol.SenderPayloadSent {
36 | return protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.SenderPayloadSent}, transferMsg.Type)
37 | }
38 | break
39 | }
40 | }
41 |
42 | // ACK received payload
43 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.ReceiverPayloadAck}, r.crypt)
44 |
45 | transferMsg, err := tools.ReadEncryptedMessage(wsConn, r.crypt)
46 | if err != nil {
47 | return err
48 | }
49 | if transferMsg.Type != protocol.SenderClosing {
50 | return protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.SenderClosing}, transferMsg.Type)
51 | }
52 |
53 | // ACK SenderClosing with ReceiverClosing
54 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.ReceiverClosingAck}, r.crypt)
55 |
56 | return err
57 | }
58 |
--------------------------------------------------------------------------------
/core/receiver/receiver.go:
--------------------------------------------------------------------------------
1 | package receiver
2 |
3 | import (
4 | "github.com/abdfnx/tran/models"
5 | "github.com/abdfnx/tran/core/crypt"
6 | )
7 |
8 | type Receiver struct {
9 | crypt *crypt.Crypt
10 | payloadSize int64
11 | tranxAddress string
12 | tranxPort int
13 | ui chan<- UIUpdate
14 | usedRelay bool
15 | }
16 |
17 | func NewReceiver(programOptions models.TranOptions) *Receiver {
18 | return &Receiver{
19 | tranxAddress: programOptions.TranxAddress,
20 | tranxPort: programOptions.TranxPort,
21 | }
22 | }
23 |
24 | func WithUI(r *Receiver, ui chan<- UIUpdate) *Receiver {
25 | r.ui = ui
26 | return r
27 | }
28 |
29 | func (r *Receiver) UsedRelay() bool {
30 | return r.usedRelay
31 | }
32 |
33 | func (r *Receiver) PayloadSize() int64 {
34 | return r.payloadSize
35 | }
36 |
37 | func (r *Receiver) TranxAddress() string {
38 | return r.tranxAddress
39 | }
40 |
41 | func (r *Receiver) TranxPort() int {
42 | return r.tranxPort
43 | }
44 |
45 | func (r *Receiver) updateUI(progress float32) {
46 | if r.ui == nil {
47 | return
48 | }
49 |
50 | r.ui <- UIUpdate{Progress: progress}
51 | }
52 |
--------------------------------------------------------------------------------
/core/receiver/state.go:
--------------------------------------------------------------------------------
1 | package receiver
2 |
3 | type UIUpdate struct {
4 | Progress float32
5 | }
6 |
--------------------------------------------------------------------------------
/core/receiver/tranx-client.go:
--------------------------------------------------------------------------------
1 | package receiver
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "time"
7 | "context"
8 |
9 | "github.com/schollz/pake/v3"
10 | "github.com/abdfnx/tran/tools"
11 | "github.com/gorilla/websocket"
12 | "github.com/abdfnx/tran/models"
13 | "github.com/abdfnx/tran/core/crypt"
14 | "github.com/abdfnx/tran/models/protocol"
15 | )
16 |
17 | func (r *Receiver) ConnectToTranx(tranxAddress string, tranxPort int, password models.Password) (*websocket.Conn, error) {
18 | // establish websocket connection to tranx server
19 | tranxConn, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://%s:%d/establish-receiver", tranxAddress, tranxPort), nil)
20 |
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | err = r.establishSecureConnection(tranxConn, password)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | senderIP, senderPort, err := r.doTransferHandshake(tranxConn)
31 |
32 | if err != nil {
33 | return nil, err
34 | }
35 |
36 | directConn, err := r.probeSender(senderIP, senderPort)
37 |
38 | if err == nil {
39 | // notify sender through tranx that we will be using direct communication
40 | tools.WriteEncryptedMessage(tranxConn, protocol.TransferMessage{Type: protocol.ReceiverDirectCommunication}, r.crypt)
41 | // tell tranx to close the connection
42 | tranxConn.WriteJSON(protocol.TranxMessage{Type: protocol.ReceiverToTranxClose})
43 |
44 | return directConn, nil
45 | }
46 |
47 | r.usedRelay = true
48 | tools.WriteEncryptedMessage(tranxConn, protocol.TransferMessage{Type: protocol.ReceiverRelayCommunication}, r.crypt)
49 |
50 | transferMsg, err := tools.ReadEncryptedMessage(tranxConn, r.crypt)
51 |
52 | if err != nil {
53 | return nil, err
54 | }
55 |
56 | if transferMsg.Type != protocol.SenderRelayAck {
57 | return nil, err
58 | }
59 |
60 | return tranxConn, nil
61 | }
62 |
63 | func (r *Receiver) probeSender(senderIP net.IP, senderPort int) (*websocket.Conn, error) {
64 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
65 | defer cancel()
66 |
67 | d := 250 * time.Millisecond
68 |
69 | for {
70 | select {
71 | case <-ctx.Done():
72 | return nil, fmt.Errorf("could not establish a connection to the sender server")
73 |
74 | default:
75 | dialer := websocket.Dialer{HandshakeTimeout: d}
76 | wsConn, _, err := dialer.Dial(fmt.Sprintf("ws://%s:%d/tran", senderIP.String(), senderPort), nil)
77 |
78 | if err != nil {
79 | time.Sleep(d)
80 | d = d * 2
81 | continue
82 | }
83 |
84 | return wsConn, nil
85 | }
86 | }
87 | }
88 |
89 | func (r *Receiver) doTransferHandshake(wsConn *websocket.Conn) (net.IP, int, error) {
90 | tcpAddr, _ := wsConn.LocalAddr().(*net.TCPAddr)
91 |
92 | msg := protocol.TransferMessage{
93 | Type: protocol.ReceiverHandshake,
94 | Payload: protocol.ReceiverHandshakePayload{
95 | IP: tcpAddr.IP,
96 | },
97 | }
98 |
99 | err := tools.WriteEncryptedMessage(wsConn, msg, r.crypt)
100 | if err != nil {
101 | return nil, 0, err
102 | }
103 |
104 | msg, err = tools.ReadEncryptedMessage(wsConn, r.crypt)
105 | if err != nil {
106 | return nil, 0, err
107 | }
108 |
109 | if msg.Type != protocol.SenderHandshake {
110 | return nil, 0, protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.SenderHandshake}, msg.Type)
111 | }
112 |
113 | handshakePayload := protocol.SenderHandshakePayload{}
114 | err = tools.DecodePayload(msg.Payload, &handshakePayload)
115 |
116 | if err != nil {
117 | return nil, 0, err
118 | }
119 |
120 | r.payloadSize = handshakePayload.PayloadSize
121 |
122 | return handshakePayload.IP, handshakePayload.Port, nil
123 | }
124 |
125 | func (r *Receiver) establishSecureConnection(wsConn *websocket.Conn, password models.Password) error {
126 | // init curve in background
127 | pakeCh := make(chan *pake.Pake)
128 | pakeErr := make(chan error)
129 |
130 | go func() {
131 | var err error
132 | p, err := pake.InitCurve([]byte(password), 1, "p256")
133 | pakeErr <- err
134 | pakeCh <- p
135 | }()
136 |
137 | wsConn.WriteJSON(protocol.TranxMessage{
138 | Type: protocol.ReceiverToTranxEstablish,
139 | Payload: protocol.PasswordPayload{
140 | Password: tools.HashPassword(password),
141 | },
142 | })
143 |
144 | msg, err := tools.ReadTranxMessage(wsConn, protocol.TranxToReceiverPAKE)
145 | if err != nil {
146 | return err
147 | }
148 |
149 | pakePayload := protocol.PakePayload{}
150 | err = tools.DecodePayload(msg.Payload, &pakePayload)
151 |
152 | if err != nil {
153 | return err
154 | }
155 |
156 | // check if we had an issue with the PAKE2 initialization error
157 | if err = <-pakeErr; err != nil {
158 | return err
159 | }
160 |
161 | p := <-pakeCh
162 |
163 | err = p.Update(pakePayload.Bytes)
164 | if err != nil {
165 | return err
166 | }
167 |
168 | wsConn.WriteJSON(protocol.TranxMessage{
169 | Type: protocol.ReceiverToTranxPAKE,
170 | Payload: protocol.PakePayload{
171 | Bytes: p.Bytes(),
172 | },
173 | })
174 |
175 | msg, err = tools.ReadTranxMessage(wsConn, protocol.TranxToReceiverSalt)
176 |
177 | if err != nil {
178 | return err
179 | }
180 |
181 | saltPayload := protocol.SaltPayload{}
182 | err = tools.DecodePayload(msg.Payload, &saltPayload)
183 |
184 | if err != nil {
185 | return err
186 | }
187 |
188 | sessionKey, err := p.SessionKey()
189 | if err != nil {
190 | return err
191 | }
192 |
193 | r.crypt, err = crypt.New(sessionKey, saltPayload.Salt)
194 | if err != nil {
195 | return err
196 | }
197 |
198 | return nil
199 | }
200 |
--------------------------------------------------------------------------------
/core/sender/handlers.go:
--------------------------------------------------------------------------------
1 | package sender
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "syscall"
8 | "net/http"
9 | )
10 |
11 | // handleTransfer creates a HandlerFunc to handle serving the transfer of files over a websocket connection
12 | func (s *Sender) handleTransfer() http.HandlerFunc {
13 | return func(w http.ResponseWriter, r *http.Request) {
14 | if s.receiverIP.Equal(net.ParseIP(r.RemoteAddr)) {
15 | w.WriteHeader(http.StatusForbidden)
16 | fmt.Fprintf(w, "No Tran for You!")
17 | log.Printf("Unauthorized Tran attempt from alien species with IP: %s\n", r.RemoteAddr)
18 |
19 | return
20 | }
21 |
22 | wsConn, err := s.senderServer.upgrader.Upgrade(w, r, nil)
23 | if err != nil {
24 | log.Printf("Unable to initialize Tran due to technical error: %s\n", err)
25 | s.closeServer <- syscall.SIGTERM
26 |
27 | return
28 | }
29 |
30 | // Start transfer sequence.
31 | s.Transfer(wsConn)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/core/sender/sender.go:
--------------------------------------------------------------------------------
1 | package sender
2 |
3 | import (
4 | "os"
5 | "io"
6 | "fmt"
7 | "net"
8 | "time"
9 | "syscall"
10 | "net/http"
11 | "os/signal"
12 |
13 | "github.com/gorilla/websocket"
14 | "github.com/abdfnx/tran/models"
15 | "github.com/abdfnx/tran/core/crypt"
16 | )
17 |
18 | // Sender represents the sender client, handles tranx communication and file transfer.
19 | type Sender struct {
20 | payload io.Reader
21 | payloadSize int64
22 | senderServer *Server
23 | closeServer chan os.Signal
24 | receiverIP net.IP
25 | tranxAddress string
26 | tranxPort int
27 | ui chan<- UIUpdate
28 | crypt *crypt.Crypt
29 | state TransferState
30 | }
31 |
32 | // NewSender returns a bare bones Sender.
33 | func NewSender(programOptions models.TranOptions) *Sender {
34 | closeServerCh := make(chan os.Signal, 1)
35 | signal.Notify(closeServerCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
36 |
37 | return &Sender{
38 | closeServer: closeServerCh,
39 | tranxAddress: programOptions.TranxAddress,
40 | tranxPort: programOptions.TranxPort,
41 | state: Initial,
42 | }
43 | }
44 |
45 | // WithPayload specifies the payload that will be transfered.
46 | func WithPayload(s *Sender, payload io.Reader, payloadSize int64) *Sender {
47 | s.payload = payload
48 | s.payloadSize = payloadSize
49 |
50 | return s
51 | }
52 |
53 | // WithServer specifies the option to run the sender by hosting a server which the receiver establishes a connection to.
54 | func WithServer(s *Sender, options ServerOptions) *Sender {
55 | s.receiverIP = options.receiverIP
56 | router := &http.ServeMux{}
57 | s.senderServer = &Server{
58 | router: router,
59 | server: &http.Server{
60 | Addr: fmt.Sprintf(":%d", options.port),
61 | ReadTimeout: 30 * time.Second,
62 | WriteTimeout: 30 * time.Second,
63 | Handler: router,
64 | },
65 | upgrader: websocket.Upgrader{},
66 | }
67 |
68 | // setup routes
69 | router.HandleFunc("/tran", s.handleTransfer())
70 | return s
71 | }
72 |
73 | // WithUI specifies the option to run the sender with an UI channel that reports the state of the transfer.
74 | func WithUI(s *Sender, ui chan<- UIUpdate) *Sender {
75 | s.ui = ui
76 |
77 | return s
78 | }
79 |
80 | func (s *Sender) TranxAddress() string {
81 | return s.tranxAddress
82 | }
83 |
84 | func (s *Sender) TranxPort() int {
85 | return s.tranxPort
86 | }
87 |
88 | // updateUI is a helper function that checks if we have a UI channel and reports the state.
89 | func (s *Sender) updateUI(progress ...float32) {
90 | if s.ui == nil {
91 | return
92 | }
93 |
94 | var p float32
95 |
96 | if len(progress) > 0 {
97 | p = progress[0]
98 | }
99 |
100 | s.ui <- UIUpdate{State: s.state, Progress: p}
101 | }
102 |
--------------------------------------------------------------------------------
/core/sender/server.go:
--------------------------------------------------------------------------------
1 | package sender
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "time"
8 | "syscall"
9 | "context"
10 | "net/http"
11 |
12 | "github.com/gorilla/websocket"
13 | )
14 |
15 | // Server specifies the webserver that will be used for direct file transfer.
16 | type Server struct {
17 | server *http.Server
18 | router *http.ServeMux
19 | upgrader websocket.Upgrader
20 | }
21 |
22 | // Specifies the necessary options for initializing the webserver.
23 | type ServerOptions struct {
24 | port int
25 | receiverIP net.IP
26 | }
27 |
28 | // Start starts the sender.Server webserver and setups graceful shutdown
29 | func (s *Sender) StartServer() error {
30 | if s.senderServer == nil {
31 | return fmt.Errorf("start called with uninitialized senderServer")
32 | }
33 |
34 | // context used for graceful shutdown
35 | ctx, cancel := context.WithCancel(context.Background())
36 |
37 | go func() {
38 | <-s.closeServer
39 | cancel()
40 | }()
41 |
42 | // serve the webserver, and report errors
43 | if err := serve(s, ctx); err != nil {
44 | return err
45 | }
46 |
47 | return nil
48 | }
49 |
50 | func (s *Sender) CloseServer() {
51 | s.closeServer <- syscall.SIGTERM
52 | }
53 |
54 | // serve is helper function that serves the webserver while providing graceful shutdown.
55 | func serve(s *Sender, ctx context.Context) (err error) {
56 | if s.senderServer == nil {
57 | return fmt.Errorf("serve called with uninitialized senderServer")
58 | }
59 |
60 | go func() {
61 | if err = s.senderServer.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
62 | log.Fatalf("Tran sender-server crashed due to an error: %s\n", err)
63 | }
64 | }()
65 |
66 | <-ctx.Done() // wait for the shutdown sequence to start.
67 |
68 | ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
69 | defer func() {
70 | cancel()
71 | }()
72 |
73 | // shutdown and report errors
74 | if err = s.senderServer.server.Shutdown(ctxShutdown); err != nil {
75 | log.Fatalf("Tran shutdown sequence failed to due error: %s\n", err)
76 | }
77 |
78 | // strip error in this case, as we deal with this gracefully
79 | if err == http.ErrServerClosed {
80 | err = nil
81 | }
82 |
83 | return err
84 | }
85 |
--------------------------------------------------------------------------------
/core/sender/state.go:
--------------------------------------------------------------------------------
1 | package sender
2 |
3 | import "fmt"
4 |
5 | type TransferState int
6 |
7 | const (
8 | Initial TransferState = iota
9 | WaitForFileRequest
10 | SendingData
11 | WaitForFileAck
12 | WaitForCloseMessage
13 | WaitForCloseAck
14 | )
15 |
16 | // UIUpdate is a struct that is continously communicated to the UI (if sender has attached a UI)
17 | type UIUpdate struct {
18 | State TransferState
19 | Progress float32
20 | }
21 |
22 | // WrongStateError is a custom error for the Transfer sequence
23 | type WrongStateError struct {
24 | expected TransferState
25 | got TransferState
26 | }
27 |
28 | // WrongStateError constructor
29 | func NewWrongStateError(expected, got TransferState) *WrongStateError {
30 | return &WrongStateError{
31 | expected: expected,
32 | got: got,
33 | }
34 | }
35 |
36 | func (e *WrongStateError) Error() string {
37 | return fmt.Sprintf("wrong message type, expected type: %d(%s), got: %d(%s)", e.expected, e.expected.Name(), e.got, e.got.Name())
38 | }
39 |
40 | // Name returns the associated to the state enum.
41 | func (s TransferState) Name() string {
42 | switch s {
43 | case Initial:
44 | return "Initial"
45 |
46 | case WaitForFileRequest:
47 | return "WaitForFileRequest"
48 |
49 | case SendingData:
50 | return "SendingData"
51 |
52 | case WaitForFileAck:
53 | return "WaitForFileAck"
54 |
55 | case WaitForCloseMessage:
56 | return "WaitForCloseMessage"
57 |
58 | case WaitForCloseAck:
59 | return "WaitForCloseAck"
60 |
61 | default:
62 | return ""
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/core/sender/transfer.go:
--------------------------------------------------------------------------------
1 | package sender
2 |
3 | import (
4 | "io"
5 | "fmt"
6 | "log"
7 | "bufio"
8 | "syscall"
9 |
10 | "github.com/gorilla/websocket"
11 | "github.com/abdfnx/tran/tools"
12 | "github.com/abdfnx/tran/constants"
13 | "github.com/abdfnx/tran/models/protocol"
14 | )
15 |
16 | // Transfer is the file transfer sequence, can be via relay or tranx.
17 | func (s *Sender) Transfer(wsConn *websocket.Conn) error {
18 | s.state = WaitForFileRequest
19 |
20 | for {
21 | // Read incoming message.
22 | receivedMsg, err := tools.ReadEncryptedMessage(wsConn, s.crypt)
23 | if err != nil {
24 | wsConn.Close()
25 | s.closeServer <- syscall.SIGTERM
26 | return fmt.Errorf("shutting down tran due to websocket error: %s", err)
27 | }
28 |
29 | // main switch for action based on incoming message.
30 | // The states flows from top down. States checks are performend at each step.
31 | switch receivedMsg.Type {
32 | case protocol.ReceiverRequestPayload:
33 | if s.state != WaitForFileRequest {
34 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{
35 | Type: protocol.TransferError,
36 | Payload: fmt.Sprintf("Tran unsynchronized, expected state: %s, actual: %s", WaitForFileRequest.Name(), s.state.Name()),
37 | }, s.crypt)
38 |
39 | if err != nil {
40 | return err
41 | }
42 |
43 | wsConn.Close()
44 | s.closeServer <- syscall.SIGTERM
45 |
46 | return NewWrongStateError(WaitForFileRequest, s.state)
47 | }
48 |
49 | err = s.streamPayload(wsConn)
50 | if err != nil {
51 | log.Println("error in payload streaming:", err)
52 |
53 | return err
54 | }
55 |
56 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{
57 | Type: protocol.SenderPayloadSent,
58 | Payload: "Tran transfer completed",
59 | }, s.crypt)
60 |
61 | if err != nil {
62 | return err
63 | }
64 |
65 | s.state = WaitForFileAck
66 | s.updateUI()
67 |
68 | case protocol.ReceiverPayloadAck:
69 | if s.state != WaitForFileAck {
70 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{
71 | Type: protocol.TransferError,
72 | Payload: fmt.Sprintf("Tran unsynchronized, expected state: %s, actual: %s", WaitForFileAck.Name(), s.state.Name()),
73 | }, s.crypt)
74 |
75 | if err != nil {
76 | return err
77 | }
78 |
79 | wsConn.Close()
80 | s.closeServer <- syscall.SIGTERM
81 |
82 | return NewWrongStateError(WaitForFileAck, s.state)
83 | }
84 |
85 | s.state = WaitForCloseMessage
86 | s.updateUI()
87 |
88 | err = tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{
89 | Type: protocol.SenderClosing,
90 | Payload: "Closing down Tran as requested",
91 | }, s.crypt)
92 |
93 | if err != nil {
94 | return err
95 | }
96 |
97 | s.state = WaitForCloseAck
98 | s.updateUI()
99 |
100 | case protocol.ReceiverClosingAck:
101 | wsConn.Close()
102 | s.closeServer <- syscall.SIGTERM
103 |
104 | if s.state != WaitForCloseAck {
105 | return NewWrongStateError(WaitForCloseAck, s.state)
106 | }
107 |
108 | return nil
109 |
110 | case protocol.TransferError:
111 | s.updateUI()
112 | log.Println("Shutting down Tran due to a transfer error")
113 | wsConn.Close()
114 | s.closeServer <- syscall.SIGTERM
115 |
116 | return fmt.Errorf("TransferError during file transfer")
117 | }
118 | }
119 | }
120 |
121 | // streamPayload streams the payload over the provided websocket connection while reporting the progress.
122 | func (s *Sender) streamPayload(wsConn *websocket.Conn) error {
123 | bufReader := bufio.NewReader(s.payload)
124 | chunkSize := ChunkSize(s.payloadSize)
125 | buffer := make([]byte, chunkSize)
126 |
127 | var bytesSent int
128 |
129 | for {
130 | n, err := bufReader.Read(buffer)
131 | bytesSent += n
132 | enc, encErr := s.crypt.Encrypt(buffer[:n])
133 |
134 | if encErr != nil {
135 | return encErr
136 | }
137 |
138 | wsConn.WriteMessage(websocket.BinaryMessage, enc)
139 | progress := float32(bytesSent) / float32(s.payloadSize)
140 | s.updateUI(progress)
141 |
142 | if err == io.EOF {
143 | break
144 | }
145 | }
146 |
147 | return nil
148 | }
149 |
150 | // ChunkSize returns an appropriate chunk size for the payload size
151 | func ChunkSize(payloadSize int64) int64 {
152 | // clamp amount of chunks to be at most MAX_SEND_CHUNKS if it exceeds
153 | if payloadSize / constants.MAX_CHUNK_BYTES > constants.MAX_SEND_CHUNKS {
154 | return int64(payloadSize) / constants.MAX_SEND_CHUNKS
155 | }
156 | // if not exceeding MAX_SEND_CHUNKS, divide up no. of chunks to MAX_CHUNK_BYTES-sized chunks
157 | chunkSize := int64(payloadSize) / constants.MAX_CHUNK_BYTES
158 | // clamp amount of chunks to be at least MAX_CHUNK_BYTES
159 | if chunkSize <= constants.MAX_CHUNK_BYTES {
160 | return constants.MAX_CHUNK_BYTES
161 | }
162 |
163 | return chunkSize
164 | }
165 |
--------------------------------------------------------------------------------
/core/sender/tranx-client.go:
--------------------------------------------------------------------------------
1 | package sender
2 |
3 | import (
4 | "fmt"
5 | "net"
6 |
7 | "github.com/schollz/pake/v3"
8 | "github.com/gorilla/websocket"
9 | "github.com/abdfnx/tran/tools"
10 | "github.com/abdfnx/tran/models"
11 | "github.com/abdfnx/tran/core/crypt"
12 | "github.com/abdfnx/tran/models/protocol"
13 | )
14 |
15 | // ConnectToTranx, establishes the connection with the tranx server.
16 | // Paramaters:
17 | // tranxAddress - IP or hostname of the tranx server
18 | // tranxPort - port of the tranx server
19 | // startServerCh - channel to communicate to the caller when to start the server, and with which options.
20 | // passwordCh - channel to communicate the password to the caller.
21 | // startServerCh - channel to communicate to the caller when to start the server, and with which options.
22 | // payloadReady - channel over which the caller can communicate when the payload is ready.
23 | // relayCh - channel to commuincate if we are using relay (tranx) for transfer.
24 | func (s *Sender) ConnectToTranx(
25 | tranxAddress string,
26 | tranxPort int,
27 | passwordCh chan<- models.Password,
28 | startServerCh chan<- ServerOptions,
29 | payloadReady <-chan bool,
30 | relayCh chan<- *websocket.Conn,
31 | ) error {
32 | // establish websocket connection to tranx server
33 | wsConn, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://%s:%d/establish-sender", tranxAddress, tranxPort), nil)
34 | if err != nil {
35 | return err
36 | }
37 |
38 | // bind connection
39 | tranxMsg, err := tools.ReadTranxMessage(wsConn, protocol.TranxToSenderBind)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | bindPayload := protocol.TranxToSenderBindPayload{}
45 | err = tools.DecodePayload(tranxMsg.Payload, &bindPayload)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | // establish sender
51 | password := tools.GeneratePassword(bindPayload.ID)
52 | hashed := tools.HashPassword(password)
53 |
54 | wsConn.WriteJSON(protocol.TranxMessage{
55 | Type: protocol.SenderToTranxEstablish,
56 | Payload: protocol.PasswordPayload{
57 | Password: hashed,
58 | },
59 | })
60 |
61 | // send the generated password to the UI so it can be displayed
62 | passwordCh <- password
63 |
64 | // setup the encryption
65 | err = s.establishSecureConnection(wsConn, password)
66 | if err != nil {
67 | return err
68 | }
69 |
70 | // do the transfer handshake over the tranx
71 | err = s.doHandshake(wsConn, payloadReady, startServerCh)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | transferMsg, err := tools.ReadEncryptedMessage(wsConn, s.crypt)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | switch transferMsg.Type {
82 | // we will do direct communication with the receiver
83 | case protocol.ReceiverDirectCommunication:
84 | close(relayCh)
85 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.SenderDirectAck}, s.crypt)
86 |
87 | return nil
88 |
89 | // we will do relay communication with receiver using the same websocket connection as with tranx
90 | case protocol.ReceiverRelayCommunication:
91 | tools.WriteEncryptedMessage(wsConn, protocol.TransferMessage{Type: protocol.SenderRelayAck}, s.crypt)
92 | relayCh <- wsConn
93 |
94 | return nil
95 |
96 | default:
97 | return protocol.NewWrongMessageTypeError(
98 | []protocol.TransferMessageType{protocol.ReceiverDirectCommunication, protocol.ReceiverRelayCommunication},
99 | transferMsg.Type)
100 | }
101 | }
102 |
103 | // establishSecureConnection setups the PAKE2 key exchange and the crypt struct in the sender.
104 | func (s *Sender) establishSecureConnection(wsConn *websocket.Conn, password models.Password) error {
105 | // init PAKE2 (NOTE: This takes a couple of seconds, here it is fine as we have to wait for the receiver)
106 | pake, err := pake.InitCurve([]byte(password), 0, "p256")
107 |
108 | if err != nil {
109 | return err
110 | }
111 |
112 | // Wait for receiver to be ready to exchange crypto information.
113 | msg, err := tools.ReadTranxMessage(wsConn, protocol.TranxToSenderReady)
114 | if err != nil {
115 | return err
116 | }
117 |
118 | // PAKE sender -> receiver.
119 | wsConn.WriteJSON(protocol.TranxMessage{
120 | Type: protocol.SenderToTranxPAKE,
121 | Payload: protocol.PakePayload{
122 | Bytes: pake.Bytes(),
123 | },
124 | })
125 |
126 | // PAKE receiver -> sender.
127 | msg, err = tools.ReadTranxMessage(wsConn, protocol.TranxToSenderPAKE)
128 | if err != nil {
129 | return err
130 | }
131 |
132 | pakePayload := protocol.PakePayload{}
133 | err = tools.DecodePayload(msg.Payload, &pakePayload)
134 | if err != nil {
135 | return err
136 | }
137 |
138 | err = pake.Update(pakePayload.Bytes)
139 | if err != nil {
140 | return err
141 | }
142 |
143 | // Setup crypt.Crypt struct in Sender.
144 | sessionkey, err := pake.SessionKey()
145 | if err != nil {
146 | return err
147 | }
148 |
149 | s.crypt, err = crypt.New(sessionkey)
150 | if err != nil {
151 | return err
152 | }
153 |
154 | // Send salt to receiver.
155 | wsConn.WriteJSON(protocol.TranxMessage{
156 | Type: protocol.SenderToTranxSalt,
157 | Payload: protocol.SaltPayload{
158 | Salt: s.crypt.Salt,
159 | },
160 | })
161 |
162 | return nil
163 | }
164 |
165 | // doHandshake does the transfer handshake over the tranx connection
166 | func (s *Sender) doHandshake(wsConn *websocket.Conn, payloadReady <-chan bool, startServerCh chan<- ServerOptions) error {
167 | transferMsg, err := tools.ReadEncryptedMessage(wsConn, s.crypt)
168 | if err != nil {
169 | return err
170 | }
171 |
172 | if transferMsg.Type != protocol.ReceiverHandshake {
173 | return protocol.NewWrongMessageTypeError([]protocol.TransferMessageType{protocol.ReceiverHandshake}, transferMsg.Type)
174 | }
175 |
176 | handshakePayload := protocol.ReceiverHandshakePayload{}
177 | err = tools.DecodePayload(transferMsg.Payload, &handshakePayload)
178 | if err != nil {
179 | return err
180 | }
181 |
182 | senderPort, err := tools.GetOpenPort()
183 | if err != nil {
184 | return err
185 | }
186 |
187 | // wait for payload to be ready
188 | <-payloadReady
189 | startServerCh <- ServerOptions{port: senderPort, receiverIP: handshakePayload.IP}
190 |
191 | tcpAddr, _ := wsConn.LocalAddr().(*net.TCPAddr)
192 | handshake := protocol.TransferMessage{
193 | Type: protocol.SenderHandshake,
194 | Payload: protocol.SenderHandshakePayload{
195 | IP: tcpAddr.IP,
196 | Port: senderPort,
197 | PayloadSize: s.payloadSize,
198 | },
199 | }
200 |
201 | tools.WriteEncryptedMessage(wsConn, handshake, s.crypt)
202 |
203 | return nil
204 | }
205 |
--------------------------------------------------------------------------------
/core/tranx/handlers.go:
--------------------------------------------------------------------------------
1 | package tranx
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 | "encoding/json"
8 |
9 | "github.com/gorilla/websocket"
10 | "github.com/abdfnx/tran/tools"
11 | "github.com/abdfnx/tran/constants"
12 | "github.com/abdfnx/tran/models/protocol"
13 | )
14 |
15 | // handleEstablishSender returns a websocket handler that communicates with the sender.
16 | func (s *Server) handleEstablishSender() tools.WsHandlerFunc {
17 | return func(wsConn *websocket.Conn) {
18 | // Bind an ID to this communication and send ot to the sender
19 | id := s.ids.Bind()
20 | wsConn.WriteJSON(protocol.TranxMessage{
21 | Type: protocol.TranxToSenderBind,
22 | Payload: protocol.TranxToSenderBindPayload{
23 | ID: id,
24 | },
25 | })
26 |
27 | msg := protocol.TranxMessage{}
28 | err := wsConn.ReadJSON(&msg)
29 |
30 | if err != nil {
31 | log.Println("message did not follow protocol:", err)
32 | return
33 | }
34 |
35 | if !isExpected(msg.Type, protocol.SenderToTranxEstablish) {
36 | return
37 | }
38 |
39 | // receive the password (hashed) from the sender.
40 | establishPayload := protocol.PasswordPayload{}
41 | err = tools.DecodePayload(msg.Payload, &establishPayload)
42 | if err != nil {
43 | log.Println("error in SenderToTranxEstablish payload:", err)
44 |
45 | return
46 | }
47 |
48 | // Allocate a mailbox for this communication.
49 | mailbox := &Mailbox{
50 | Sender: &protocol.TranxSender{
51 | TranxClient: *NewClient(wsConn),
52 | },
53 | CommunicationChannel: make(chan []byte),
54 | Quit: make(chan bool),
55 | }
56 |
57 | s.mailboxes.StoreMailbox(establishPayload.Password, mailbox)
58 | _, err = s.mailboxes.GetMailbox(establishPayload.Password)
59 |
60 | if err != nil {
61 | log.Println("The created mailbox could not be retrieved")
62 |
63 | return
64 | }
65 |
66 | // wait for receiver to connect
67 | timeout := time.NewTimer(constants.RECEIVER_CONNECT_TIMEOUT)
68 |
69 | select {
70 | case <-timeout.C:
71 | s.ids.Delete(id)
72 | return
73 |
74 | case <-mailbox.CommunicationChannel:
75 | // receiver connected
76 | s.ids.Delete(id)
77 | break
78 | }
79 |
80 | wsConn.WriteJSON(protocol.TranxMessage{
81 | Type: protocol.TranxToSenderReady,
82 | })
83 |
84 | msg = protocol.TranxMessage{}
85 | err = wsConn.ReadJSON(&msg)
86 |
87 | if err != nil {
88 | log.Println("message did not follow protocol:", err)
89 |
90 | return
91 | }
92 |
93 | if !isExpected(msg.Type, protocol.SenderToTranxPAKE) {
94 | return
95 | }
96 |
97 | pakePayload := protocol.PakePayload{}
98 | err = tools.DecodePayload(msg.Payload, &pakePayload)
99 |
100 | if err != nil {
101 | log.Println("error in SenderToTranxPAKE payload:", err)
102 | return
103 | }
104 |
105 | // send PAKE bytes to receiver
106 | mailbox.CommunicationChannel <- pakePayload.Bytes
107 | // respond with receiver PAKE bytes
108 | wsConn.WriteJSON(protocol.TranxMessage{
109 | Type: protocol.TranxToSenderPAKE,
110 | Payload: protocol.PakePayload{
111 | Bytes: <-mailbox.CommunicationChannel,
112 | },
113 | })
114 |
115 | msg = protocol.TranxMessage{}
116 | err = wsConn.ReadJSON(&msg)
117 |
118 | if err != nil {
119 | log.Println("message did not follow protocol:", err)
120 |
121 | return
122 | }
123 |
124 | if !isExpected(msg.Type, protocol.SenderToTranxSalt) {
125 | return
126 | }
127 |
128 | saltPayload := protocol.SaltPayload{}
129 | err = tools.DecodePayload(msg.Payload, &saltPayload)
130 |
131 | if err != nil {
132 | log.Println("error in SenderToTranxSalt payload:", err)
133 | return
134 | }
135 |
136 | // Send the salt to the receiver.
137 | mailbox.CommunicationChannel <- saltPayload.Salt
138 | // Start the relay of messgaes between the sender and receiver handlers.
139 | startRelay(s, wsConn, mailbox, establishPayload.Password)
140 | }
141 | }
142 |
143 | // handleEstablishReceiver returns a websocket handler that that communicates with the sender.
144 | func (s *Server) handleEstablishReceiver() tools.WsHandlerFunc {
145 | return func(wsConn *websocket.Conn) {
146 | // Establish receiver.
147 | msg := protocol.TranxMessage{}
148 | err := wsConn.ReadJSON(&msg)
149 |
150 | if err != nil {
151 | log.Println("message did not follow protocol:", err)
152 | return
153 | }
154 |
155 | if !isExpected(msg.Type, protocol.ReceiverToTranxEstablish) {
156 | return
157 | }
158 |
159 | establishPayload := protocol.PasswordPayload{}
160 | err = tools.DecodePayload(msg.Payload, &establishPayload)
161 | if err != nil {
162 | log.Println("error in ReceiverToTranxEstablish payload:", err)
163 | return
164 | }
165 |
166 | mailbox, err := s.mailboxes.GetMailbox(establishPayload.Password)
167 |
168 | if err != nil {
169 | log.Println("failed to get mailbox:", err)
170 | return
171 | }
172 |
173 | if mailbox.Receiver != nil {
174 | log.Println("mailbox already has a receiver:", err)
175 | return
176 | }
177 |
178 | // this reveiver was first, reserve this mailbox for it to receive
179 | mailbox.Receiver = NewClient(wsConn)
180 | s.mailboxes.StoreMailbox(establishPayload.Password, mailbox)
181 |
182 | // notify sender we are connected
183 | mailbox.CommunicationChannel <- nil
184 | // send back received sender PAKE bytes
185 | wsConn.WriteJSON(protocol.TranxMessage{
186 | Type: protocol.TranxToReceiverPAKE,
187 | Payload: protocol.PakePayload{
188 | Bytes: <-mailbox.CommunicationChannel,
189 | },
190 | })
191 |
192 | msg = protocol.TranxMessage{}
193 | err = wsConn.ReadJSON(&msg)
194 |
195 | if err != nil {
196 | log.Println("message did not follow protocol:", err)
197 | return
198 | }
199 |
200 | if !isExpected(msg.Type, protocol.ReceiverToTranxPAKE) {
201 | return
202 | }
203 |
204 | receiverPakePayload := protocol.PakePayload{}
205 | err = tools.DecodePayload(msg.Payload, &receiverPakePayload)
206 |
207 | if err != nil {
208 | log.Println("error in ReceiverToTranxPAKE payload:", err)
209 | return
210 | }
211 |
212 | mailbox.CommunicationChannel <- receiverPakePayload.Bytes
213 | wsConn.WriteJSON(protocol.TranxMessage{
214 | Type: protocol.TranxToReceiverSalt,
215 | Payload: protocol.SaltPayload{
216 | Salt: <-mailbox.CommunicationChannel,
217 | },
218 | })
219 |
220 | startRelay(s, wsConn, mailbox, establishPayload.Password)
221 | }
222 | }
223 |
224 | // starts the relay service, closing it on request (if i.e. clients can communicate directly)
225 | func startRelay(s *Server, wsConn *websocket.Conn, mailbox *Mailbox, mailboxPassword string) {
226 | relayForwardCh := make(chan []byte)
227 | // listen for incoming websocket messages from currently handled client
228 | go func() {
229 | for {
230 | _, p, err := wsConn.ReadMessage()
231 |
232 | if err != nil {
233 | log.Println("error when listening to incoming client messages:", err)
234 | fmt.Printf("closed by: %s\n", wsConn.RemoteAddr())
235 | mailbox.Quit <- true
236 |
237 | return
238 | }
239 |
240 | relayForwardCh <- p
241 | }
242 | }()
243 |
244 | for {
245 | select {
246 | case relayReceivePayload := <-mailbox.CommunicationChannel:
247 | wsConn.WriteMessage(websocket.BinaryMessage, relayReceivePayload)
248 |
249 | // received payload from __currently handled__ client, relay it to other client
250 | case relayForwardPayload := <-relayForwardCh:
251 | msg := protocol.TranxMessage{}
252 | err := json.Unmarshal(relayForwardPayload, &msg)
253 | // failed to unmarshal, we are in (encrypted) relay-mode, forward message directly to client
254 | if err != nil {
255 | mailbox.CommunicationChannel <- relayForwardPayload
256 | } else {
257 | // close the relay service if sender requested it
258 | if isExpected(msg.Type, protocol.ReceiverToTranxClose) {
259 | mailbox.Quit <- true
260 |
261 | return
262 | }
263 | }
264 |
265 | // deallocate mailbox and quit
266 | case <-mailbox.Quit:
267 | s.mailboxes.Delete(mailboxPassword)
268 |
269 | return
270 | }
271 | }
272 | }
273 |
274 | // isExpected is a convience helper function that checks message types and logs errors.
275 | func isExpected(actual protocol.TranxMessageType, expected protocol.TranxMessageType) bool {
276 | wasExpected := actual == expected
277 |
278 | if !wasExpected {
279 | log.Printf("Expected message of type: %d. Got type %d\n", expected, actual)
280 | }
281 |
282 | return wasExpected
283 | }
284 |
--------------------------------------------------------------------------------
/core/tranx/id.go:
--------------------------------------------------------------------------------
1 | package tranx
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | // IDs is a threadsafe set of numbers.
8 | type IDs struct{ *sync.Map }
9 |
10 | type void struct{} // empty struct complies to 0 bytes
11 | var member void
12 |
13 | // Bind binds an id to connection.
14 | func (ids *IDs) Bind() int {
15 | id := 1
16 |
17 | for {
18 | val, _ := ids.Load(id)
19 | if val == nil {
20 | break
21 | }
22 |
23 | id++
24 | }
25 |
26 | ids.Store(id, member)
27 |
28 | return id
29 | }
30 |
31 | // DeleteID Deletes a bound ID.
32 | func (ids *IDs) DeleteID(id int) {
33 | ids.Delete(id)
34 | }
35 |
--------------------------------------------------------------------------------
/core/tranx/mailbox.go:
--------------------------------------------------------------------------------
1 | package tranx
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 |
8 | "github.com/gorilla/websocket"
9 | "github.com/abdfnx/tran/models/protocol"
10 | )
11 |
12 | // Mailbox is a data structure that links together a sender and a receiver client.
13 | type Mailbox struct {
14 | Sender *protocol.TranxSender
15 | Receiver *protocol.TranxReceiver
16 | CommunicationChannel chan []byte
17 | Quit chan bool
18 | }
19 |
20 | type Mailboxes struct{ *sync.Map }
21 |
22 | // StoreMailbox allocates a mailbox.
23 | func (mailboxes *Mailboxes) StoreMailbox(p string, m *Mailbox) {
24 | mailboxes.Store(p, m)
25 | }
26 |
27 | // GetMailbox returns the decired mailbox.
28 | func (mailboxes *Mailboxes) GetMailbox(p string) (*Mailbox, error) {
29 | mailbox, ok := mailboxes.Load(p)
30 |
31 | if !ok {
32 | return nil, fmt.Errorf("no mailbox with password '%s'", p)
33 | }
34 |
35 | return mailbox.(*Mailbox), nil
36 | }
37 |
38 | // DeleteMailbox deallocates a mailbox.
39 | func (mailboxes *Mailboxes) DeleteMailbox(p string) {
40 | mailboxes.Delete(p)
41 | }
42 |
43 | // NewClient returns a new client struct.
44 | func NewClient(wsConn *websocket.Conn) *protocol.TranxClient {
45 | return &protocol.TranxClient{
46 | Conn: wsConn,
47 | IP: wsConn.RemoteAddr().(*net.TCPAddr).IP,
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/core/tranx/routes.go:
--------------------------------------------------------------------------------
1 | package tranx
2 |
3 | import "github.com/abdfnx/tran/tools"
4 |
5 | func (s *Server) routes() {
6 | s.router.HandleFunc("/establish-sender", tools.WebsocketHandler(s.handleEstablishSender()))
7 | s.router.HandleFunc("/establish-receiver", tools.WebsocketHandler(s.handleEstablishReceiver()))
8 | }
9 |
--------------------------------------------------------------------------------
/core/tranx/server.go:
--------------------------------------------------------------------------------
1 | package tranx
2 |
3 | import (
4 | "os"
5 | "fmt"
6 | "log"
7 | "sync"
8 | "time"
9 | "context"
10 | "net/http"
11 | )
12 |
13 | // Server is contains the necessary data to run the tranx server.
14 | type Server struct {
15 | httpServer *http.Server
16 | router *http.ServeMux
17 | mailboxes *Mailboxes
18 | ids *IDs
19 | signal chan os.Signal
20 | }
21 |
22 | // NewServer constructs a new Server struct and setups the routes.
23 | func NewServer(port int) *Server {
24 | router := &http.ServeMux{}
25 |
26 | s := &Server{
27 | httpServer: &http.Server{
28 | Addr: fmt.Sprintf(":%d", port),
29 | ReadTimeout: 30 * time.Second,
30 | WriteTimeout: 30 * time.Second,
31 | Handler: router,
32 | },
33 | router: router,
34 | mailboxes: &Mailboxes{&sync.Map{}},
35 | ids: &IDs{&sync.Map{}},
36 | }
37 |
38 | s.routes()
39 |
40 | return s
41 | }
42 |
43 | // Start runs the tranx server.
44 | func (s *Server) Start() {
45 | ctx, cancel := context.WithCancel(context.Background())
46 |
47 | go func() {
48 | <-s.signal
49 | cancel()
50 | }()
51 |
52 | if err := serve(s, ctx); err != nil {
53 | log.Printf("Error serving Tran tranx server: %s\n", err)
54 | }
55 | }
56 |
57 | // serve is a helper function providing graceful shutdown of the server.
58 | func serve(s *Server, ctx context.Context) (err error) {
59 | go func() {
60 | if err = s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
61 | log.Fatalf("Serving Tran: %s\n", err)
62 | }
63 | }()
64 |
65 | log.Printf("Tran Tranx Server started at \"%s\" \n", s.httpServer.Addr)
66 | <-ctx.Done()
67 |
68 | ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
69 | defer func() {
70 | cancel()
71 | }()
72 |
73 | if err = s.httpServer.Shutdown(ctxShutdown); err != nil {
74 | log.Fatalf("Tran tranx shutdown failed: %s", err)
75 | }
76 |
77 | if err == http.ErrServerClosed {
78 | err = nil
79 | }
80 |
81 | return err
82 | }
83 |
--------------------------------------------------------------------------------
/data/password-codes.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | var PasswordList = []string{
4 | "go",
5 | "rust",
6 | "solar",
7 | "moon",
8 | "cli",
9 | "planet",
10 | "byte",
11 | "comet",
12 | "star",
13 | "orbit",
14 | "galaxy",
15 | "lunar",
16 | "vue",
17 | "meteor",
18 | "mass",
19 | "football",
20 | "nebula",
21 | "orbit",
22 | "ios",
23 | "lightyear",
24 | "parsec",
25 | "neutron",
26 | "supernova",
27 | "quasar",
28 | "cluster",
29 | "redshift",
30 | "gamma",
31 | "flare",
32 | "ray",
33 | "tidal",
34 | "xcode",
35 | "hub",
36 | "compose",
37 | "docker",
38 | "kubernetes",
39 | "computer",
40 | "cloud",
41 | "react",
42 | "secman",
43 | "x",
44 | "base",
45 | }
46 |
--------------------------------------------------------------------------------
/dfs/directory-file-system.go:
--------------------------------------------------------------------------------
1 | package dfs
2 |
3 | import (
4 | "os"
5 | "fmt"
6 | "io/fs"
7 | "errors"
8 | "strings"
9 | "path/filepath"
10 | )
11 |
12 | // Directory shortcuts.
13 | const (
14 | CurrentDirectory = "."
15 | PreviousDirectory = ".."
16 | HomeDirectory = "~"
17 | RootDirectory = "/"
18 | )
19 |
20 | // Different types of listings.
21 | const (
22 | DirectoriesListingType = "directories"
23 | FilesListingType = "files"
24 | )
25 |
26 | // RenameDirectoryItem renames a directory or files given a source and destination.
27 | func RenameDirectoryItem(src, dst string) error {
28 | err := os.Rename(src, dst)
29 |
30 | return err
31 | }
32 |
33 | // CreateDirectory creates a new directory given a name.
34 | func CreateDirectory(name string) error {
35 | if _, err := os.Stat(name); errors.Is(err, os.ErrNotExist) {
36 | err := os.Mkdir(name, os.ModePerm)
37 | if err != nil {
38 | return err
39 | }
40 | }
41 |
42 | return nil
43 | }
44 |
45 | // GetDirectoryListing returns a list of files and directories within a given directory.
46 | func GetDirectoryListing(dir string, showHidden bool) ([]fs.DirEntry, error) {
47 | n := 0
48 |
49 | files, err := os.ReadDir(dir)
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | if !showHidden {
55 | for _, file := range files {
56 | // If the file or directory starts with a dot,
57 | // we know its hidden so dont add it to the array
58 | // of files to return.
59 | if !strings.HasPrefix(file.Name(), ".") {
60 | files[n] = file
61 | n++
62 | }
63 | }
64 |
65 | // Set files to the list that does not include hidden files.
66 | files = files[:n]
67 | }
68 |
69 | return files, nil
70 | }
71 |
72 | // GetDirectoryListingByType returns a directory listing based on type (directories | files).
73 | func GetDirectoryListingByType(dir, listingType string, showHidden bool) ([]fs.DirEntry, error) {
74 | n := 0
75 |
76 | files, err := os.ReadDir(dir)
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | for _, file := range files {
82 | switch {
83 | case file.IsDir() && listingType == DirectoriesListingType && !showHidden:
84 | if !strings.HasPrefix(file.Name(), ".") {
85 | files[n] = file
86 | n++
87 | }
88 |
89 | case file.IsDir() && listingType == DirectoriesListingType && showHidden:
90 | files[n] = file
91 | n++
92 |
93 | case !file.IsDir() && listingType == FilesListingType && !showHidden:
94 | if !strings.HasPrefix(file.Name(), ".") {
95 | files[n] = file
96 | n++
97 | }
98 |
99 | case !file.IsDir() && listingType == FilesListingType && showHidden:
100 | files[n] = file
101 | n++
102 | }
103 | }
104 |
105 | return files[:n], nil
106 | }
107 |
108 | // DeleteDirectory deletes a directory given a name.
109 | func DeleteDirectory(name string) error {
110 | err := os.RemoveAll(name)
111 |
112 | return err
113 | }
114 |
115 | // GetHomeDirectory returns the users home directory.
116 | func GetHomeDirectory() (string, error) {
117 | home, err := os.UserHomeDir()
118 | if err != nil {
119 | return "", err
120 | }
121 |
122 | return home, nil
123 | }
124 |
125 | // GetWorkingDirectory returns the current working directory.
126 | func GetWorkingDirectory() (string, error) {
127 | workingDir, err := os.Getwd()
128 | if err != nil {
129 | return "", err
130 | }
131 |
132 | return workingDir, nil
133 | }
134 |
135 | // DeleteFile deletes a file given a name.
136 | func DeleteFile(name string) error {
137 | err := os.Remove(name)
138 |
139 | return err
140 | }
141 |
142 | // MoveDirectoryItem moves a file from one place to another.
143 | func MoveDirectoryItem(src, dst string) error {
144 | err := os.Rename(src, dst)
145 |
146 | return err
147 | }
148 |
149 | // ReadFileContent returns the contents of a file given a name.
150 | func ReadFileContent(name string) (string, error) {
151 | fileContent, err := os.ReadFile(name)
152 | if err != nil {
153 | return "", err
154 | }
155 |
156 | return string(fileContent), nil
157 | }
158 |
159 | // GetDirectoryItemSize calculates the size of a directory or file.
160 | func GetDirectoryItemSize(path string) (int64, error) {
161 | curFile, err := os.Stat(path)
162 | if err != nil {
163 | return 0, err
164 | }
165 |
166 | if !curFile.IsDir() {
167 | return curFile.Size(), nil
168 | }
169 |
170 | var size int64
171 |
172 | err = filepath.WalkDir(path, func(path string, d os.DirEntry, err error) error {
173 | if err != nil {
174 | return err
175 | }
176 |
177 | fileInfo, err := d.Info()
178 | if err != nil {
179 | return err
180 | }
181 |
182 | if !d.IsDir() {
183 | size += fileInfo.Size()
184 | }
185 |
186 | return err
187 | })
188 |
189 | return size, err
190 | }
191 |
192 | func FindFilesByName(name, dir string) ([]string, []fs.DirEntry, error) {
193 | var paths []string
194 | var entries []fs.DirEntry
195 |
196 | err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error {
197 | if err != nil {
198 | return filepath.SkipDir
199 | }
200 |
201 | if strings.Contains(entry.Name(), name) {
202 | paths = append(paths, path)
203 | entries = append(entries, entry)
204 | }
205 |
206 | return err
207 | })
208 |
209 | return paths, entries, err
210 | }
211 |
212 | // WriteToFile writes content to a file, overwriting content if it exists.
213 | func WriteToFile(path, content string) error {
214 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
215 | if err != nil {
216 | return err
217 | }
218 |
219 | workingDir, err := os.Getwd()
220 | if err != nil {
221 | return err
222 | }
223 |
224 | _, err = f.WriteString(fmt.Sprintf("%s\n", filepath.Join(workingDir, content)))
225 |
226 | if err != nil {
227 | f.Close()
228 | return err
229 | }
230 |
231 | err = f.Close()
232 | if err != nil {
233 | return err
234 | }
235 |
236 | return err
237 | }
238 |
--------------------------------------------------------------------------------
/docker/container/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:latest
2 |
3 | ### variables ###
4 | ARG UPD="apt-get update"
5 | ARG UPD_s="sudo $UPD"
6 | ARG INS="apt-get install"
7 | ARG INS_s="sudo $INS"
8 | ENV PKGS="zip unzip multitail curl lsof wget ssl-cert asciidoctor apt-transport-https ca-certificates gnupg-agent bash-completion build-essential htop jq software-properties-common less llvm locales man-db nano vim ruby-full build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev libbz2-dev"
9 |
10 | RUN $UPD && $INS -y $PKGS && $UPD && \
11 | locale-gen en_US.UTF-8 && \
12 | mkdir /var/lib/apt/abdcodedoc-marks && \
13 | apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* && \
14 | $UPD
15 |
16 | ENV LANG=en_US.UTF-8
17 |
18 | ### git ###
19 | RUN $INS -y git && \
20 | rm -rf /var/lib/apt/lists/* && \
21 | $UPD
22 |
23 | ### sudo ###
24 | RUN $UPD && $INS -y sudo && \
25 | adduser --disabled-password --gecos '' trn && \
26 | adduser trn sudo && \
27 | echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
28 |
29 | ENV HOME="/home/trn"
30 | WORKDIR $HOME
31 | USER trn
32 |
33 | ### go ###
34 | COPY --from=golang /usr/local/go/ /usr/local/go/
35 | ENV PATH="/usr/local/go/bin:${PATH}"
36 |
37 | ### tran ###
38 | RUN curl -sL https://cutt.ly/tran-cli | bash
39 |
40 | ### zsh ###
41 | ENV src=".zshrc"
42 |
43 | RUN $INS_s zsh -y
44 | RUN zsh && \
45 | sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" && \
46 | $UPD_s && \
47 | git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting && \
48 | git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
49 |
50 | ### rm old ~/.zshrc ###
51 | RUN sudo rm -rf $src
52 |
53 | ### wget new files ###
54 | RUN wget https://abdfnx.github.io/tran/scripts/shell/zshrc -o $src
55 | RUN wget https://abdfnx.github.io/tran/scripts/shell/README
56 |
57 | CMD /bin/bash -c "cat README && zsh"
58 |
--------------------------------------------------------------------------------
/docker/dev/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:latest
2 |
3 | ### variables ###
4 | ARG UPD="apt-get update"
5 | ARG UPD_s="sudo $UPD"
6 | ARG INS="apt-get install"
7 | ARG INS_s="sudo $INS"
8 |
9 | RUN $UPD && $INS -y build-essential software-properties-common && $UPD && \
10 | locale-gen en_US.UTF-8 && \
11 | mkdir /var/lib/apt/abdcodedoc-marks && \
12 | apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* && \
13 | $UPD
14 |
15 | ENV LANG=en_US.UTF-8
16 |
17 | ### git ###
18 | RUN $INS -y git && \
19 | rm -rf /var/lib/apt/lists/* && \
20 | $UPD
21 |
22 | ### sudo ###
23 | RUN $UPD && $INS -y sudo && \
24 | adduser --disabled-password --gecos '' trn && \
25 | adduser trn sudo && \
26 | echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
27 |
28 | ENV HOME="/home/trn"
29 | WORKDIR $HOME
30 | USER trn
31 |
32 | ### go ###
33 | COPY --from=golang /usr/local/go/ /usr/local/go/
34 | ENV PATH="/usr/local/go/bin:${PATH}"
35 |
36 | ### tran ###
37 | RUN go install github.com/abdfnx/tran@latest
38 |
--------------------------------------------------------------------------------
/docker/vm/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | RUN apk update && apk upgrade && apk add --no-cache ca-certificates
4 |
5 | COPY tran /usr/bin/tran
6 |
7 | ENTRYPOINT ["/usr/bin/tran"]
8 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/abdfnx/tran
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/AlecAivazis/survey/v2 v2.3.7
7 | github.com/MakeNowJust/heredoc v1.0.0
8 | github.com/abdfnx/gh v0.1.5
9 | github.com/abdfnx/gosh v0.4.0
10 | github.com/abdfnx/looker v0.1.0
11 | github.com/abdfnx/resto v0.1.6
12 | github.com/alecthomas/chroma v0.10.0
13 | github.com/briandowns/spinner v1.23.0
14 | github.com/charmbracelet/bubbles v0.18.0
15 | github.com/charmbracelet/bubbletea v0.25.0
16 | github.com/charmbracelet/glamour v0.7.0
17 | github.com/charmbracelet/lipgloss v0.10.0
18 | github.com/david-tomson/tran-git v0.0.3
19 | github.com/disintegration/imaging v1.6.2
20 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
21 | github.com/gorilla/websocket v1.5.1
22 | github.com/klauspost/pgzip v1.2.6
23 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80
24 | github.com/lucasb-eyer/go-colorful v1.2.0
25 | github.com/mattn/go-colorable v0.1.13
26 | github.com/mattn/go-isatty v0.0.20
27 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
28 | github.com/muesli/reflow v0.3.0
29 | github.com/muesli/termenv v0.15.2
30 | github.com/schollz/pake/v3 v3.0.5
31 | github.com/spf13/cobra v1.8.0
32 | github.com/spf13/pflag v1.0.5
33 | github.com/spf13/viper v1.18.2
34 | github.com/tidwall/gjson v1.17.1
35 | golang.org/x/crypto v0.21.0
36 | golang.org/x/sys v0.19.0
37 | golang.org/x/term v0.18.0
38 | )
39 |
40 | require (
41 | github.com/alecthomas/chroma/v2 v2.8.0 // indirect
42 | github.com/atotto/clipboard v0.1.4 // indirect
43 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
44 | github.com/aymerick/douceur v0.2.0 // indirect
45 | github.com/charmbracelet/harmonica v0.2.0 // indirect
46 | github.com/cli/browser v1.1.0 // indirect
47 | github.com/cli/oauth v0.9.0 // indirect
48 | github.com/cli/safeexec v1.0.0 // indirect
49 | github.com/cli/shurcooL-graphql v0.0.1 // indirect
50 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
51 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
52 | github.com/dlclark/regexp2 v1.7.0 // indirect
53 | github.com/fatih/color v1.14.1 // indirect
54 | github.com/fsnotify/fsnotify v1.7.0 // indirect
55 | github.com/gorilla/css v1.0.0 // indirect
56 | github.com/hashicorp/hcl v1.0.0 // indirect
57 | github.com/henvic/httpretty v0.1.0 // indirect
58 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
59 | github.com/itchyny/gojq v0.12.8 // indirect
60 | github.com/itchyny/timefmt-go v0.1.3 // indirect
61 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
62 | github.com/klauspost/compress v1.17.0 // indirect
63 | github.com/magiconair/properties v1.8.7 // indirect
64 | github.com/mattn/go-localereader v0.0.1 // indirect
65 | github.com/mattn/go-runewidth v0.0.15 // indirect
66 | github.com/microcosm-cc/bluemonday v1.0.25 // indirect
67 | github.com/mitchellh/mapstructure v1.5.0 // indirect
68 | github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
69 | github.com/muesli/cancelreader v0.2.2 // indirect
70 | github.com/olekukonko/tablewriter v0.0.5 // indirect
71 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect
72 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
73 | github.com/rivo/uniseg v0.4.7 // indirect
74 | github.com/sagikazarmark/locafero v0.4.0 // indirect
75 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect
76 | github.com/scmn-dev/browser v0.1.3 // indirect
77 | github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00 // indirect
78 | github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect
79 | github.com/sourcegraph/conc v0.3.0 // indirect
80 | github.com/spf13/afero v1.11.0 // indirect
81 | github.com/spf13/cast v1.6.0 // indirect
82 | github.com/stretchr/objx v0.5.0 // indirect
83 | github.com/stretchr/testify v1.8.4 // indirect
84 | github.com/subosito/gotenv v1.6.0 // indirect
85 | github.com/tidwall/match v1.1.1 // indirect
86 | github.com/tidwall/pretty v1.2.0 // indirect
87 | github.com/tscholl2/siec v0.0.0-20210707234609-9bdfc483d499 // indirect
88 | github.com/yuin/goldmark v1.5.4 // indirect
89 | github.com/yuin/goldmark-emoji v1.0.2 // indirect
90 | go.uber.org/atomic v1.9.0 // indirect
91 | go.uber.org/multierr v1.9.0 // indirect
92 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
93 | golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
94 | golang.org/x/net v0.21.0 // indirect
95 | golang.org/x/sync v0.5.0 // indirect
96 | golang.org/x/text v0.14.0 // indirect
97 | gopkg.in/ini.v1 v1.67.0 // indirect
98 | gopkg.in/yaml.v3 v3.0.1 // indirect
99 | )
100 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 | "log"
6 | "runtime"
7 | "path/filepath"
8 |
9 | "github.com/spf13/pflag"
10 | "github.com/spf13/viper"
11 | "github.com/abdfnx/tran/dfs"
12 | )
13 |
14 | // TranConfig struct represents the config for the config.
15 | type TranConfig struct {
16 | StartDir string `mapstructure:"start_dir"`
17 | Borderless bool `mapstructure:"borderless"`
18 | Editor string `mapstructure:"editor"`
19 | EnableMouseWheel bool `mapstructure:"enable_mousewheel"`
20 | ShowUpdates bool `mapstructure:"show_updates"`
21 | }
22 |
23 | // Config represents the main config for the application.
24 | type Config struct {
25 | Tran TranConfig `mapstructure:"config"`
26 | }
27 |
28 | func defualtEditor() string {
29 | if runtime.GOOS == "windows" {
30 | return "notepad.exe"
31 | }
32 |
33 | return "vim"
34 | }
35 |
36 | // LoadConfig loads a users config and creates the config if it does not exist
37 | // located at `~/.tran/tran.yml`
38 | func LoadConfig(startDir *pflag.Flag) {
39 | var err error
40 |
41 | homeDir, err := dfs.GetHomeDirectory()
42 |
43 | if err != nil {
44 | log.Fatal(err)
45 | }
46 |
47 | err = dfs.CreateDirectory(filepath.Join(homeDir, ".tran"))
48 | if err != nil {
49 | log.Fatal(err)
50 | }
51 |
52 | // Windows doesn't use the $HOME env variable,
53 | // check if running on Windows and if so, use $USERPROFILE
54 | if runtime.GOOS == "windows" {
55 | viper.AddConfigPath(`$USERPROFILE\\.tran`)
56 | } else {
57 | viper.AddConfigPath("$HOME/.tran")
58 | }
59 |
60 | viper.SetConfigName("tran")
61 | viper.SetConfigType("yml")
62 |
63 | // Setup config defaults.
64 | viper.SetDefault("config.start_dir", ".")
65 | viper.SetDefault("config.enable_mousewheel", true)
66 | viper.SetDefault("config.borderless", false)
67 | viper.SetDefault("config.editor", defualtEditor())
68 | viper.SetDefault("config.show_updates", true)
69 |
70 | if err := viper.SafeWriteConfig(); err != nil {
71 | if os.IsNotExist(err) {
72 | err = viper.WriteConfig()
73 |
74 | if err != nil {
75 | log.Fatal(err)
76 | }
77 | }
78 | }
79 |
80 | if err := viper.ReadInConfig(); err != nil {
81 | if _, ok := err.(viper.ConfigFileNotFoundError); ok {
82 | log.Fatal(err)
83 | }
84 | }
85 |
86 | // Setup flags.
87 | err = viper.BindPFlag("start-dir", startDir)
88 | if err != nil {
89 | log.Fatal(err)
90 | }
91 |
92 | // Setup flag defaults.
93 | viper.SetDefault("start-dir", "")
94 | }
95 |
96 | // GetConfig returns the users config.
97 | func GetConfig() (config Config) {
98 | if err := viper.Unmarshal(&config); err != nil {
99 | log.Fatal("Error parsing config", err)
100 | }
101 |
102 | return
103 | }
104 |
--------------------------------------------------------------------------------
/internal/theme/theme.go:
--------------------------------------------------------------------------------
1 | package theme
2 |
3 | import (
4 | "github.com/abdfnx/tran/constants"
5 | "github.com/charmbracelet/lipgloss"
6 | )
7 |
8 | type Theme struct {
9 | SelectedTreeItemColor lipgloss.AdaptiveColor
10 | UnselectedTreeItemColor lipgloss.AdaptiveColor
11 | ActiveBoxBorderColor lipgloss.AdaptiveColor
12 | InactiveBoxBorderColor lipgloss.AdaptiveColor
13 | SpinnerColor lipgloss.AdaptiveColor
14 | StatusBarSelectedFileForegroundColor lipgloss.AdaptiveColor
15 | StatusBarSelectedFileBackgroundColor lipgloss.AdaptiveColor
16 | StatusBarBarForegroundColor lipgloss.AdaptiveColor
17 | StatusBarBarBackgroundColor lipgloss.AdaptiveColor
18 | StatusBarTotalFilesForegroundColor lipgloss.AdaptiveColor
19 | StatusBarTotalFilesBackgroundColor lipgloss.AdaptiveColor
20 | StatusBarLogoForegroundColor lipgloss.AdaptiveColor
21 | StatusBarLogoBackgroundColor lipgloss.AdaptiveColor
22 | ErrorColor lipgloss.AdaptiveColor
23 | DefaultTextColor lipgloss.AdaptiveColor
24 | }
25 |
26 | // appColors contains the different types of colors.
27 | type appColors struct {
28 | white string
29 | darkGray string
30 | red string
31 | black string
32 | }
33 |
34 | // Colors contains the different kinds of colors and their values.
35 | var colors = appColors{
36 | white: "#FFFDF5",
37 | darkGray: constants.DARK_GRAY_COLOR,
38 | red: "#cc241d",
39 | black: "#000000",
40 | }
41 |
42 | // themeMap represents the mapping of different themes.
43 | var themeMap = map[string]Theme{
44 | "default": {
45 | SelectedTreeItemColor: lipgloss.AdaptiveColor{Dark: constants.PRIMARY_COLOR, Light: constants.PRIMARY_COLOR},
46 | UnselectedTreeItemColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.black},
47 | ActiveBoxBorderColor: lipgloss.AdaptiveColor{Dark: constants.PRIMARY_COLOR, Light: constants.PRIMARY_COLOR},
48 | InactiveBoxBorderColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.black},
49 | SpinnerColor: lipgloss.AdaptiveColor{Dark: constants.PRIMARY_COLOR, Light: constants.PRIMARY_COLOR},
50 | StatusBarSelectedFileForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white},
51 | StatusBarSelectedFileBackgroundColor: lipgloss.AdaptiveColor{Dark: "#4880EC", Light: "#4880EC"},
52 | StatusBarBarForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white},
53 | StatusBarBarBackgroundColor: lipgloss.AdaptiveColor{Dark: colors.darkGray, Light: colors.darkGray},
54 | StatusBarTotalFilesForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white},
55 | StatusBarTotalFilesBackgroundColor: lipgloss.AdaptiveColor{Dark: "#1E6AFF", Light: "#1E6AFF"},
56 | StatusBarLogoForegroundColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.white},
57 | StatusBarLogoBackgroundColor: lipgloss.AdaptiveColor{Dark: "#1747A6", Light: "#1747A6"},
58 | ErrorColor: lipgloss.AdaptiveColor{Dark: colors.red, Light: colors.red},
59 | DefaultTextColor: lipgloss.AdaptiveColor{Dark: colors.white, Light: colors.black},
60 | },
61 | }
62 |
63 | // GetTheme returns a theme based on the given name.
64 | func GetTheme(theme string) Theme {
65 | return themeMap["default"]
66 | }
67 |
--------------------------------------------------------------------------------
/internal/tui/cmds.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "os"
5 | "image"
6 | "io/fs"
7 | _ "image/png"
8 | _ "image/jpeg"
9 | "path/filepath"
10 |
11 | "github.com/abdfnx/tran/dfs"
12 | "github.com/abdfnx/tran/models"
13 | "github.com/abdfnx/tran/renderer"
14 | "github.com/abdfnx/tran/constants"
15 | "github.com/charmbracelet/lipgloss"
16 | tea "github.com/charmbracelet/bubbletea"
17 | )
18 |
19 | type updateDirectoryListingMsg []fs.DirEntry
20 | type viewDirectoryListingMsg []fs.DirEntry
21 | type errorMsg string
22 | type convertImageToStringMsg string
23 | type directoryItemSizeMsg struct {
24 | index int
25 | size string
26 | }
27 |
28 | type findFilesByNameMsg struct {
29 | paths []string
30 | entries []fs.DirEntry
31 | }
32 |
33 | type readFileContentMsg struct {
34 | rawContent string
35 | markdown string
36 | code string
37 | imageString string
38 | pdfContent string
39 | image image.Image
40 | }
41 |
42 | // updateDirectoryListingCmd updates the directory listing based on the name of the directory provided.
43 | func (b Bubble) updateDirectoryListingCmd(name string) tea.Cmd {
44 | return func() tea.Msg {
45 | files, err := dfs.GetDirectoryListing(name, b.showHiddenFiles)
46 | if err != nil {
47 | return errorMsg(err.Error())
48 | }
49 |
50 | err = os.Chdir(name)
51 | if err != nil {
52 | return errorMsg(err.Error())
53 | }
54 |
55 | return updateDirectoryListingMsg(files)
56 | }
57 | }
58 |
59 | // viewDirectoryListingCmd updates the directory listing based on the name of the directory provided.
60 | func (b Bubble) viewDirectoryListingCmd(name string) tea.Cmd {
61 | return func() tea.Msg {
62 | currentDir, err := dfs.GetWorkingDirectory()
63 | if err != nil {
64 | return errorMsg(err.Error())
65 | }
66 |
67 | files, err := dfs.GetDirectoryListing(filepath.Join(currentDir, name), b.showHiddenFiles)
68 | if err != nil {
69 | return errorMsg(err.Error())
70 | }
71 |
72 | return viewDirectoryListingMsg(files)
73 | }
74 | }
75 |
76 | // convertImageToStringCmd redraws the image based on the width provided.
77 | func (b Bubble) convertImageToStringCmd(width int) tea.Cmd {
78 | return func() tea.Msg {
79 | imageString := renderer.ImageToString(width, b.currentImage)
80 |
81 | return convertImageToStringMsg(imageString)
82 | }
83 | }
84 |
85 | // readFileContentCmd reads the content of a file and returns it.
86 | func (b Bubble) readFileContentCmd(fileName string, width int) tea.Cmd {
87 | return func() tea.Msg {
88 | content, err := dfs.ReadFileContent(fileName)
89 |
90 | if err != nil {
91 | return errorMsg(err.Error())
92 | }
93 |
94 | switch {
95 | case filepath.Ext(fileName) == ".md":
96 | markdownContent, err := renderer.RenderMarkdown(width, content)
97 |
98 | if err != nil {
99 | return errorMsg(err.Error())
100 | }
101 |
102 | return readFileContentMsg{
103 | rawContent: content,
104 | markdown: markdownContent,
105 | code: "",
106 | imageString: "",
107 | pdfContent: "",
108 | image: nil,
109 | }
110 |
111 | case filepath.Ext(fileName) == ".png" || filepath.Ext(fileName) == ".jpg" || filepath.Ext(fileName) == ".jpeg":
112 | imageContent, err := os.Open(fileName)
113 |
114 | if err != nil {
115 | return errorMsg(err.Error())
116 | }
117 |
118 | img, _, err := image.Decode(imageContent)
119 |
120 | if err != nil {
121 | return errorMsg(err.Error())
122 | }
123 |
124 | imageString := renderer.ImageToString(width, img)
125 |
126 | return readFileContentMsg{
127 | rawContent: content,
128 | code: "",
129 | markdown: "",
130 | imageString: imageString,
131 | pdfContent: "",
132 | image: img,
133 | }
134 |
135 | case filepath.Ext(fileName) == ".pdf":
136 | pdfContent, err := renderer.ReadPdf(fileName)
137 | if err != nil {
138 | return errorMsg(err.Error())
139 | }
140 |
141 | return readFileContentMsg{
142 | rawContent: content,
143 | code: "",
144 | markdown: "",
145 | imageString: "",
146 | pdfContent: pdfContent,
147 | image: nil,
148 | }
149 |
150 | default:
151 | syntaxTheme := "solarized-light"
152 |
153 | if lipgloss.HasDarkBackground() {
154 | syntaxTheme = "solarized-dark"
155 | }
156 |
157 | code, err := renderer.Highlight(content, filepath.Ext(fileName), syntaxTheme)
158 |
159 | if err != nil {
160 | return errorMsg(err.Error())
161 | }
162 |
163 | return readFileContentMsg{
164 | rawContent: content,
165 | code: code,
166 | markdown: "",
167 | imageString: "",
168 | pdfContent: "",
169 | image: nil,
170 | }
171 | }
172 | }
173 | }
174 |
175 | // getDirectoryItemSizeCmd calculates the size of a directory or file.
176 | func (b Bubble) getDirectoryItemSizeCmd(name string, i int) tea.Cmd {
177 | return func() tea.Msg {
178 | size, err := dfs.GetDirectoryItemSize(name)
179 |
180 | if err != nil {
181 | return directoryItemSizeMsg{size: "N/A", index: i}
182 | }
183 |
184 | sizeString := renderer.ConvertBytesToSizeString(size)
185 |
186 | return directoryItemSizeMsg{
187 | size: sizeString,
188 | index: i,
189 | }
190 | }
191 | }
192 |
193 | // handleErrorCmd returns an error message to the UI.
194 | func (b Bubble) handleErrorCmd(err error) tea.Cmd {
195 | return func() tea.Msg {
196 | return errorMsg(err.Error())
197 | }
198 | }
199 |
200 | // getDirectoryListingByTypeCmd returns only directories in the current directory.
201 | func (b Bubble) getDirectoryListingByTypeCmd(listType string) tea.Cmd {
202 | return func() tea.Msg {
203 | workingDir, err := dfs.GetWorkingDirectory()
204 | if err != nil {
205 | return errorMsg(err.Error())
206 | }
207 |
208 | directories, err := dfs.GetDirectoryListingByType(workingDir, listType, b.showHiddenFiles)
209 | if err != nil {
210 | return errorMsg(err.Error())
211 | }
212 |
213 | return updateDirectoryListingMsg(directories)
214 | }
215 | }
216 |
217 | // findFilesByNameCmd finds files based on name.
218 | func (b Bubble) findFilesByNameCmd(name string) tea.Cmd {
219 | return func() tea.Msg {
220 | workingDir, err := dfs.GetWorkingDirectory()
221 | if err != nil {
222 | return errorMsg(err.Error())
223 | }
224 |
225 | paths, entries, err := dfs.FindFilesByName(name, workingDir)
226 | if err != nil {
227 | return errorMsg(err.Error())
228 | }
229 |
230 | return findFilesByNameMsg{
231 | paths: paths,
232 | entries: entries,
233 | }
234 | }
235 | }
236 |
237 | func (b Bubble) receiveFileCmd(password string) tea.Cmd {
238 | return func() tea.Msg {
239 | err := ValidateTranxAddress()
240 |
241 | if err != nil {
242 | return err
243 | }
244 |
245 | HandleReceiveCommand(models.TranOptions{
246 | TranxAddress: constants.DEFAULT_ADDRESS,
247 | TranxPort: constants.DEFAULT_PORT,
248 | }, password)
249 |
250 | return nil
251 | }
252 | }
253 |
254 | // redrawCmd redraws the UI.
255 | func (b Bubble) redrawCmd() tea.Cmd {
256 | return func() tea.Msg {
257 | return tea.WindowSizeMsg{
258 | Width: b.width,
259 | Height: b.height,
260 | }
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/internal/tui/init.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "os"
5 | "log"
6 | "strings"
7 | "path/filepath"
8 |
9 | "github.com/spf13/viper"
10 | "github.com/abdfnx/tran/dfs"
11 | tea "github.com/charmbracelet/bubbletea"
12 | "github.com/charmbracelet/bubbles/spinner"
13 | "github.com/charmbracelet/bubbles/textinput"
14 | )
15 |
16 | // Init initializes the UI and sets up initial data.
17 | func (b Bubble) Init() tea.Cmd {
18 | var cmds []tea.Cmd
19 | startDir := viper.GetString("start-dir")
20 |
21 | switch {
22 | case startDir != "":
23 | _, err := os.Stat(startDir)
24 | if err != nil {
25 | return nil
26 | }
27 |
28 | if strings.HasPrefix(startDir, dfs.RootDirectory) {
29 | cmds = append(cmds, b.updateDirectoryListingCmd(startDir))
30 | } else {
31 | path, err := os.Getwd()
32 |
33 | if err != nil {
34 | log.Fatal(err)
35 | }
36 |
37 | filePath := filepath.Join(path, startDir)
38 |
39 | cmds = append(cmds, b.updateDirectoryListingCmd(filePath))
40 | }
41 |
42 | case b.appConfig.Tran.StartDir == dfs.HomeDirectory:
43 | homeDir, err := dfs.GetHomeDirectory()
44 | if err != nil {
45 | log.Fatal(err)
46 | }
47 |
48 | cmds = append(cmds, b.updateDirectoryListingCmd(homeDir))
49 |
50 | default:
51 | cmds = append(cmds, b.updateDirectoryListingCmd(b.appConfig.Tran.StartDir))
52 | }
53 |
54 | cmds = append(cmds, spinner.Tick)
55 | cmds = append(cmds, textinput.Blink)
56 |
57 | return tea.Batch(cmds...)
58 | }
59 |
--------------------------------------------------------------------------------
/internal/tui/keymap.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import "github.com/charmbracelet/bubbles/key"
4 |
5 | type KeyMap struct {
6 | Quit key.Binding
7 | Down key.Binding
8 | Up key.Binding
9 | Left key.Binding
10 | Right key.Binding
11 | View key.Binding
12 | Receive key.Binding
13 | GotoBottom key.Binding
14 | HomeShortcut key.Binding
15 | RootShortcut key.Binding
16 | ToggleHidden key.Binding
17 | ShowDirectoriesOnly key.Binding
18 | ShowFilesOnly key.Binding
19 | Enter key.Binding
20 | Edit key.Binding
21 | Find key.Binding
22 | Send key.Binding
23 | Command key.Binding
24 | Escape key.Binding
25 | ToggleBox key.Binding
26 | }
27 |
28 | // DefaultKeyMap returns a set of default keybindings.
29 | var Keys = KeyMap{
30 | Quit: key.NewBinding(
31 | key.WithKeys("q", "ctrl+q"),
32 | ),
33 | Down: key.NewBinding(
34 | key.WithKeys("down"),
35 | ),
36 | Up: key.NewBinding(
37 | key.WithKeys("up"),
38 | ),
39 | Left: key.NewBinding(
40 | key.WithKeys("left"),
41 | ),
42 | Right: key.NewBinding(
43 | key.WithKeys("right"),
44 | ),
45 | Enter: key.NewBinding(
46 | key.WithKeys("enter"),
47 | ),
48 | Escape: key.NewBinding(
49 | key.WithKeys("esc"),
50 | ),
51 | View: key.NewBinding(
52 | key.WithKeys("V"),
53 | ),
54 | GotoBottom: key.NewBinding(
55 | key.WithKeys("G"),
56 | ),
57 | HomeShortcut: key.NewBinding(
58 | key.WithKeys("~"),
59 | ),
60 | RootShortcut: key.NewBinding(
61 | key.WithKeys("/"),
62 | ),
63 | ToggleHidden: key.NewBinding(
64 | key.WithKeys("."),
65 | ),
66 | ShowDirectoriesOnly: key.NewBinding(
67 | key.WithKeys("D"),
68 | ),
69 | ShowFilesOnly: key.NewBinding(
70 | key.WithKeys("F"),
71 | ),
72 | Edit: key.NewBinding(
73 | key.WithKeys("E"),
74 | ),
75 | Find: key.NewBinding(
76 | key.WithKeys("ctrl+f"),
77 | ),
78 | ToggleBox: key.NewBinding(
79 | key.WithKeys("tab"),
80 | ),
81 | Receive: key.NewBinding(
82 | key.WithKeys("ctrl+r"),
83 | ),
84 | Send: key.NewBinding(
85 | key.WithKeys("ctrl+s"),
86 | ),
87 | }
88 |
--------------------------------------------------------------------------------
/internal/tui/model.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "image"
5 | "io/fs"
6 |
7 | "github.com/abdfnx/tran/constants"
8 | "github.com/charmbracelet/lipgloss"
9 | "github.com/abdfnx/tran/internal/theme"
10 | tea "github.com/charmbracelet/bubbletea"
11 | "github.com/abdfnx/tran/internal/config"
12 | "github.com/charmbracelet/bubbles/spinner"
13 | "github.com/charmbracelet/bubbles/viewport"
14 | "github.com/charmbracelet/bubbles/textinput"
15 | )
16 |
17 | // Bubble represents the state of the UI.
18 | type Bubble struct {
19 | appConfig config.Config
20 | theme theme.Theme
21 | currentImage image.Image
22 | spinner spinner.Model
23 | textinput textinput.Model
24 | primaryViewport viewport.Model
25 | secondaryViewport viewport.Model
26 | thirdViewport viewport.Model
27 | treeFiles []fs.DirEntry
28 | treePreviewFiles []fs.DirEntry
29 | previousKey tea.KeyMsg
30 | keyMap KeyMap
31 | width int
32 | height int
33 | activeBox int
34 | treeCursor int
35 | showHiddenFiles bool
36 | ready bool
37 | showCommandInput bool
38 | showFilesOnly bool
39 | showDirectoriesOnly bool
40 | showFileTreePreview bool
41 | findMode bool
42 | sendMode bool
43 | receiveMode bool
44 | showBoxSpinner bool
45 | foundFilesPaths []string
46 | fileSizes []string
47 | secondaryBoxContent string
48 | errorMsg string
49 | }
50 |
51 | // New creates an instance of the entire application.
52 | func New() Bubble {
53 | cfg := config.GetConfig()
54 | theme := theme.GetTheme("default")
55 |
56 | primaryBoxBorder := lipgloss.RoundedBorder()
57 | secondaryBoxBorder := lipgloss.RoundedBorder()
58 | thirdBoxBorder := lipgloss.RoundedBorder()
59 | primaryBoxBorderColor := theme.ActiveBoxBorderColor
60 | secondaryBoxBorderColor := theme.InactiveBoxBorderColor
61 | thirdBoxBorderColor := theme.InactiveBoxBorderColor
62 |
63 | if cfg.Tran.Borderless {
64 | primaryBoxBorder = lipgloss.HiddenBorder()
65 | secondaryBoxBorder = lipgloss.HiddenBorder()
66 | thirdBoxBorder = lipgloss.HiddenBorder()
67 | }
68 |
69 | pvp := viewport.New(0, 0)
70 | pvp.Style = lipgloss.NewStyle().
71 | PaddingLeft(constants.BoxPadding).
72 | PaddingRight(constants.BoxPadding).
73 | Border(primaryBoxBorder).
74 | BorderForeground(primaryBoxBorderColor)
75 |
76 | svp := viewport.New(0, 0)
77 | svp.Style = lipgloss.NewStyle().
78 | PaddingLeft(constants.BoxPadding).
79 | PaddingRight(constants.BoxPadding).
80 | Border(secondaryBoxBorder).
81 | BorderForeground(secondaryBoxBorderColor)
82 |
83 | tvp := viewport.New(0, 0)
84 | tvp.Style = lipgloss.NewStyle().
85 | PaddingLeft(constants.BoxPadding).
86 | PaddingRight(constants.BoxPadding).
87 | Border(thirdBoxBorder).
88 | BorderForeground(thirdBoxBorderColor)
89 |
90 | s := spinner.New()
91 | s.Spinner = spinner.Dot
92 | s.Style = lipgloss.NewStyle().Foreground(theme.SpinnerColor)
93 |
94 | t := textinput.New()
95 | t.Prompt = "❯ "
96 | t.CharLimit = 250
97 | t.PlaceholderStyle = lipgloss.NewStyle().
98 | Background(theme.StatusBarBarBackgroundColor).
99 | Foreground(theme.StatusBarBarForegroundColor)
100 |
101 | return Bubble{
102 | appConfig: cfg,
103 | theme: theme,
104 | showHiddenFiles: true,
105 | spinner: s,
106 | textinput: t,
107 | primaryViewport: pvp,
108 | secondaryViewport: svp,
109 | thirdViewport: tvp,
110 | keyMap: Keys,
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/internal/tui/receive.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "os"
5 | "fmt"
6 | "math"
7 | "time"
8 |
9 | "github.com/gorilla/websocket"
10 | "github.com/abdfnx/tran/tools"
11 | "github.com/abdfnx/tran/models"
12 | "github.com/abdfnx/tran/constants"
13 | "github.com/abdfnx/tran/core/receiver"
14 | "github.com/abdfnx/tran/models/protocol"
15 | tea "github.com/charmbracelet/bubbletea"
16 | )
17 |
18 | // HandleReceiveCommand is the receive application.
19 | func HandleReceiveCommand(programOptions models.TranOptions, password string) {
20 | // communicate ui updates on this channel between receiverClient and handleReceiveCmmand
21 | uiCh := make(chan receiver.UIUpdate)
22 | // initialize a receiverClient with a UI
23 | receiverClient := receiver.WithUI(receiver.NewReceiver(programOptions), uiCh)
24 | // initialize and start receiver-UI
25 | receiverUI := NewReceiverUI()
26 | // clean up temporary files previously created by this command
27 | tools.RemoveTemporaryFiles(constants.RECEIVE_TEMP_FILE_NAME_PREFIX)
28 |
29 | go initReceiverUI(receiverUI)
30 | time.Sleep(constants.START_PERIOD)
31 | go listenForReceiverUIUpdates(receiverUI, uiCh)
32 |
33 | parsedPassword, err := tools.ParsePassword(password)
34 | if err != nil {
35 | receiverUI.Send(ErrorMsg{Message: "Error parsing password, make sure you entered a correctly formatted password (e.g. 1-gamma-ray-quasar)."})
36 | GracefulUIQuit()
37 | }
38 |
39 | // initiate communications with tranx-server
40 | wsConnCh := make(chan *websocket.Conn)
41 | go initiateReceiverTranxCommunication(receiverClient, receiverUI, parsedPassword, wsConnCh)
42 |
43 | // keeps program alive until finished
44 | doneCh := make(chan bool)
45 | // start receiving files
46 | go startReceiving(receiverClient, receiverUI, <-wsConnCh, doneCh)
47 |
48 | // wait for shut down to render final UI
49 | <-doneCh
50 | GracefulUIQuit()
51 | }
52 |
53 | func initReceiverUI(receiverUI *tea.Program) {
54 | go func() {
55 | if err := receiverUI.Start(); err != nil {
56 | fmt.Println("Error initializing UI", err)
57 | os.Exit(1)
58 | }
59 |
60 | os.Exit(0)
61 | }()
62 | }
63 |
64 | func listenForReceiverUIUpdates(receiverUI *tea.Program, uiCh chan receiver.UIUpdate) {
65 | latestProgress := 0
66 |
67 | for uiUpdate := range uiCh {
68 | // limit progress update ui-send events
69 | newProgress := int(math.Ceil(100 * float64(uiUpdate.Progress)))
70 | if newProgress > latestProgress {
71 | latestProgress = newProgress
72 | receiverUI.Send(ProgressMsg{Progress: uiUpdate.Progress})
73 | }
74 | }
75 | }
76 |
77 | func initiateReceiverTranxCommunication(receiverClient *receiver.Receiver, receiverUI *tea.Program, password models.Password, connectionCh chan *websocket.Conn) {
78 | wsConn, err := receiverClient.ConnectToTranx(receiverClient.TranxAddress(), receiverClient.TranxPort(), password)
79 | if err != nil {
80 | receiverUI.Send(ErrorMsg{Message: "Something went wrong during connection-negotiation (did you enter the correct password?)"})
81 | GracefulUIQuit()
82 | }
83 |
84 | receiverUI.Send(FileInfoMsg{Bytes: receiverClient.PayloadSize()})
85 | connectionCh <- wsConn
86 | }
87 |
88 | func startReceiving(receiverClient *receiver.Receiver, receiverUI *tea.Program, wsConnection *websocket.Conn, doneCh chan bool) {
89 | tempFile, err := os.CreateTemp(os.TempDir(), constants.RECEIVE_TEMP_FILE_NAME_PREFIX)
90 |
91 | if err != nil {
92 | receiverUI.Send(ErrorMsg{Message: "Something went wrong when creating the received file container."})
93 | GracefulUIQuit()
94 | }
95 |
96 | defer os.Remove(tempFile.Name())
97 | defer tempFile.Close()
98 |
99 | // start receiving files from sender
100 | err = receiverClient.Receive(wsConnection, tempFile)
101 | if err != nil {
102 | receiverUI.Send(ErrorMsg{Message: "Something went wrong during file transfer."})
103 | GracefulUIQuit()
104 | }
105 |
106 | if receiverClient.UsedRelay() {
107 | wsConnection.WriteJSON(protocol.TranxMessage{Type: protocol.ReceiverToTranxClose})
108 | }
109 |
110 | // reset file position for reading
111 | tempFile.Seek(0, 0)
112 |
113 | // read received bytes from tmpFile
114 | receivedFileNames, decompressedSize, err := tools.DecompressAndUnarchiveBytes(tempFile)
115 | if err != nil {
116 | receiverUI.Send(ErrorMsg{Message: "Something went wrong when expanding the received files."})
117 | GracefulUIQuit()
118 | }
119 |
120 | receiverUI.Send(FinishedMsg{Files: receivedFileNames, PayloadSize: decompressedSize})
121 | doneCh <- true
122 | }
123 |
--------------------------------------------------------------------------------
/internal/tui/receiver.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/abdfnx/tran/tools"
8 | "github.com/muesli/reflow/indent"
9 | "github.com/abdfnx/tran/constants"
10 | "github.com/muesli/reflow/wordwrap"
11 | "github.com/charmbracelet/lipgloss"
12 | tea "github.com/charmbracelet/bubbletea"
13 | "github.com/charmbracelet/bubbles/spinner"
14 | "github.com/charmbracelet/bubbles/progress"
15 | )
16 |
17 | type uiState int
18 |
19 | // ui state flows from the top down
20 | const (
21 | showEstablishing uiState = iota
22 | showReceivingProgress
23 | showFinished
24 | showError
25 | )
26 |
27 | type receiverUIModel struct {
28 | state uiState
29 | receivedFiles []string
30 | payloadSize int64
31 | decompressedPayloadSize int64
32 | spinner spinner.Model
33 | progressBar progress.Model
34 | errorMessage string
35 | }
36 |
37 | func NewReceiverUI() *tea.Program {
38 | m := receiverUIModel{
39 | progressBar: constants.ProgressBar,
40 | }
41 |
42 | m.resetSpinner()
43 | var opts []tea.ProgramOption
44 |
45 | opts = append(opts, tea.WithAltScreen())
46 |
47 | return tea.NewProgram(m, opts...)
48 | }
49 |
50 | func (receiverUIModel) Init() tea.Cmd {
51 | return spinner.Tick
52 | }
53 |
54 | func (m receiverUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
55 | switch msg := msg.(type) {
56 | case FileInfoMsg:
57 | m.payloadSize = msg.Bytes
58 | if m.state != showReceivingProgress {
59 | m.state = showReceivingProgress
60 | m.resetSpinner()
61 |
62 | return m, spinner.Tick
63 | }
64 |
65 | return m, nil
66 |
67 | case ProgressMsg:
68 | m.state = showReceivingProgress
69 | cmd := m.progressBar.SetPercent(float64(msg.Progress))
70 |
71 | return m, cmd
72 |
73 | case FinishedMsg:
74 | m.state = showFinished
75 | m.receivedFiles = msg.Files
76 | m.decompressedPayloadSize = msg.PayloadSize
77 | cmd := m.progressBar.SetPercent(1.0)
78 |
79 | return m, cmd
80 |
81 | case ErrorMsg:
82 | m.state = showError
83 | m.errorMessage = msg.Message
84 |
85 | return m, nil
86 |
87 | case tea.KeyMsg:
88 | if tools.Contains(constants.QuitKeys, strings.ToLower(msg.String())) {
89 | return m, tea.Quit
90 | }
91 |
92 | return m, nil
93 |
94 | case tea.WindowSizeMsg:
95 | m.progressBar.Width = msg.Width - 2 * constants.PADDING - 4
96 | if m.progressBar.Width > constants.MAX_WIDTH {
97 | m.progressBar.Width = constants.MAX_WIDTH
98 | }
99 |
100 | return m, nil
101 |
102 | // FrameMsg is sent when the progress bar wants to animate itself
103 | case progress.FrameMsg:
104 | progressModel, cmd := m.progressBar.Update(msg)
105 | m.progressBar = progressModel.(progress.Model)
106 |
107 | return m, cmd
108 |
109 | default:
110 | var cmd tea.Cmd
111 | m.spinner, cmd = m.spinner.Update(msg)
112 |
113 | return m, cmd
114 | }
115 | }
116 |
117 | func (m receiverUIModel) View() string {
118 | switch m.state {
119 | case showEstablishing:
120 | return "\n" +
121 | constants.PadText + constants.InfoStyle(fmt.Sprintf("%s Establishing connection with sender", m.spinner.View())) + "\n\n"
122 |
123 | case showReceivingProgress:
124 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize))
125 | receivingText := fmt.Sprintf("%s Receiving files (total size %s)", m.spinner.View(), payloadSize)
126 |
127 | return "\n" +
128 | constants.PadText + constants.InfoStyle(receivingText) + "\n\n" +
129 | constants.PadText + m.progressBar.View() + "\n\n" +
130 | constants.PadText + constants.QuitCommandsHelpText + "\n\n"
131 |
132 | case showFinished:
133 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize))
134 | indentedWrappedFiles := indent.String(fmt.Sprintf("Received: %s", wordwrap.String(constants.ItalicText(TopLevelFilesText(m.receivedFiles)), constants.MAX_WIDTH)), constants.PADDING)
135 | finishedText := fmt.Sprintf("Received %d files (%s decompressed)\n\n%s", len(m.receivedFiles), payloadSize, indentedWrappedFiles)
136 |
137 | return "\n" +
138 | constants.PadText + constants.InfoStyle(finishedText) + "\n\n" +
139 | constants.PadText + m.progressBar.View() + "\n\n" +
140 | constants.PadText + constants.QuitCommandsHelpText + "\n\n"
141 |
142 | case showError:
143 | return m.errorMessage
144 |
145 | default:
146 | return ""
147 | }
148 | }
149 |
150 | func (m *receiverUIModel) resetSpinner() {
151 | m.spinner = spinner.NewModel()
152 | m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(constants.PRIMARY_COLOR))
153 |
154 | if m.state == showEstablishing {
155 | m.spinner.Spinner = WaitingSpinner
156 | }
157 |
158 | if m.state == showReceivingProgress {
159 | m.spinner.Spinner = TransferSpinner
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/internal/tui/send.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "os"
5 | "fmt"
6 | "net"
7 | "math"
8 | "time"
9 | "errors"
10 |
11 | "github.com/gorilla/websocket"
12 | "github.com/abdfnx/tran/tools"
13 | "github.com/abdfnx/tran/models"
14 | "github.com/abdfnx/tran/constants"
15 | "github.com/abdfnx/tran/core/sender"
16 | tea "github.com/charmbracelet/bubbletea"
17 | )
18 |
19 | func HandleSendCommand(programOptions models.TranOptions, fileNames []string) {
20 | // communicate ui updates on this channel between senderClient and HandleSendCommand
21 | uiCh := make(chan sender.UIUpdate)
22 | // initialize a senderClient with a UI
23 | senderClient := sender.WithUI(sender.NewSender(programOptions), uiCh)
24 | // initialize and start sender-UI
25 | senderUI := NewSenderUI()
26 | // clean up temporary files previously created by this command
27 | tools.RemoveTemporaryFiles(constants.SEND_TEMP_FILE_NAME_PREFIX)
28 |
29 | go initSenderUI(senderUI)
30 | time.Sleep(constants.START_PERIOD)
31 | go listenForSenderUIUpdates(senderUI, uiCh)
32 |
33 | closeFileCh := make(chan *os.File)
34 | senderReadyCh := make(chan bool, 1)
35 | // read, archive and compress files in parallel
36 | go prepareFiles(senderClient, senderUI, fileNames, senderReadyCh, closeFileCh)
37 |
38 | // initiate communications with tranx-server
39 | startServerCh := make(chan sender.ServerOptions)
40 | relayCh := make(chan *websocket.Conn)
41 | passCh := make(chan models.Password)
42 | go initiateSenderTranxCommunication(senderClient, senderUI, passCh, startServerCh, senderReadyCh, relayCh)
43 | senderUI.Send(PasswordMsg{Password: string(<-passCh)})
44 |
45 | // keeps program alive until finished
46 | doneCh := make(chan bool)
47 | // attach server to senderClient
48 | senderClient = sender.WithServer(senderClient, <-startServerCh)
49 |
50 | go startDirectCommunicationServer(senderClient, senderUI, doneCh)
51 | // prepare a fallback to relay communications through tranx if direct communications unavailble
52 | prepareRelayCommunicationFallback(senderClient, senderUI, relayCh, doneCh)
53 |
54 | <-doneCh
55 | senderUI.Send(FinishedMsg{})
56 | tempFile := <-closeFileCh
57 | os.Remove(tempFile.Name())
58 | tempFile.Close()
59 | GracefulUIQuit()
60 | }
61 |
62 | func initSenderUI(senderUI *tea.Program) {
63 | if err := senderUI.Start(); err != nil {
64 | fmt.Println("Error initializing UI", err)
65 | os.Exit(1)
66 | }
67 |
68 | os.Exit(0)
69 | }
70 |
71 | func listenForSenderUIUpdates(senderUI *tea.Program, uiCh chan sender.UIUpdate) {
72 | latestProgress := 0
73 | for uiUpdate := range uiCh {
74 | // make sure progress is 100 if connection is to be closed
75 | if uiUpdate.State == sender.WaitForCloseMessage {
76 | latestProgress = 100
77 | senderUI.Send(ProgressMsg{Progress: 1})
78 | continue
79 | }
80 |
81 | // limit progress update ui-send events
82 | newProgress := int(math.Ceil(100 * float64(uiUpdate.Progress)))
83 | if newProgress > latestProgress {
84 | latestProgress = newProgress
85 | senderUI.Send(ProgressMsg{Progress: uiUpdate.Progress})
86 | }
87 | }
88 | }
89 |
90 | func prepareFiles(senderClient *sender.Sender, senderUI *tea.Program, fileNames []string, readyCh chan bool, closeFileCh chan *os.File) {
91 | files, err := tools.ReadFiles(fileNames)
92 |
93 | if err != nil {
94 | senderUI.Send(ErrorMsg{Message: "Error reading files."})
95 | GracefulUIQuit()
96 | }
97 |
98 | uncompressedFileSize, err := tools.FilesTotalSize(files)
99 | if err != nil {
100 | senderUI.Send(ErrorMsg{Message: "Error during file preparation."})
101 | GracefulUIQuit()
102 | }
103 |
104 | senderUI.Send(FileInfoMsg{FileNames: fileNames, Bytes: uncompressedFileSize})
105 |
106 | tempFile, fileSize, err := tools.ArchiveAndCompressFiles(files)
107 | for _, file := range files {
108 | file.Close()
109 | }
110 |
111 | if err != nil {
112 | senderUI.Send(ErrorMsg{Message: "Error compressing files."})
113 | GracefulUIQuit()
114 | }
115 |
116 | sender.WithPayload(senderClient, tempFile, fileSize)
117 | senderUI.Send(FileInfoMsg{FileNames: fileNames, Bytes: fileSize})
118 | readyCh <- true
119 | senderUI.Send(ReadyMsg{})
120 | closeFileCh <- tempFile
121 | }
122 |
123 | func initiateSenderTranxCommunication(senderClient *sender.Sender, senderUI *tea.Program, passCh chan models.Password,
124 | startServerCh chan sender.ServerOptions, readyCh chan bool, relayCh chan *websocket.Conn) {
125 | err := senderClient.ConnectToTranx(
126 | senderClient.TranxAddress(), senderClient.TranxPort(), passCh, startServerCh, readyCh, relayCh)
127 |
128 | if err != nil {
129 | senderUI.Send(ErrorMsg{Message: "Failed to communicate with tranx server."})
130 | GracefulUIQuit()
131 | }
132 | }
133 |
134 | func startDirectCommunicationServer(senderClient *sender.Sender, senderUI *tea.Program, doneCh chan bool) {
135 | if err := senderClient.StartServer(); err != nil {
136 | senderUI.Send(ErrorMsg{Message: fmt.Sprintf("Something went wrong during file transfer: %e", err)})
137 | GracefulUIQuit()
138 | }
139 |
140 | doneCh <- true
141 | }
142 |
143 | func prepareRelayCommunicationFallback(senderClient *sender.Sender, senderUI *tea.Program, relayCh chan *websocket.Conn, doneCh chan bool) {
144 | if relayWsConn, closed := <-relayCh; closed {
145 | // start transferring to the tranx-relay
146 | go func() {
147 | if err := senderClient.Transfer(relayWsConn); err != nil {
148 | senderUI.Send(ErrorMsg{Message: fmt.Sprintf("Something went wrong during file transfer: %e", err)})
149 | GracefulUIQuit()
150 | }
151 |
152 | doneCh <- true
153 | }()
154 | }
155 | }
156 |
157 | func ValidateTranxAddress() error {
158 | address := net.ParseIP(constants.DEFAULT_ADDRESS)
159 | err := tools.ValidateHostname(constants.DEFAULT_ADDRESS)
160 |
161 | // neither a valid IP nor a valid hostname was provided
162 | if (address == nil) && err != nil {
163 | return errors.New("invalid IP or hostname provided")
164 | }
165 |
166 | return nil
167 | }
168 |
--------------------------------------------------------------------------------
/internal/tui/sender.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 |
8 | "github.com/abdfnx/tran/constants"
9 | "github.com/abdfnx/tran/tools"
10 | "github.com/charmbracelet/bubbles/progress"
11 | "github.com/charmbracelet/bubbles/spinner"
12 | tea "github.com/charmbracelet/bubbletea"
13 | "github.com/charmbracelet/lipgloss"
14 | "github.com/muesli/reflow/indent"
15 | "github.com/muesli/reflow/wordwrap"
16 | "github.com/muesli/termenv"
17 | )
18 |
19 | // ui state flows from the top down
20 | const (
21 | showPasswordWithCopy uiState = iota
22 | showPassword
23 | showSendingProgress
24 | showSFinished
25 | showSError
26 | )
27 |
28 | type senderUIModel struct {
29 | state uiState
30 | fileNames []string
31 | payloadSize int64
32 | password string
33 | readyToSend bool
34 | spinner spinner.Model
35 | progressBar progress.Model
36 | errorMessage string
37 | }
38 |
39 | type ReadyMsg struct{}
40 |
41 | type PasswordMsg struct {
42 | Password string
43 | }
44 |
45 | func NewSenderUI() *tea.Program {
46 | m := senderUIModel{progressBar: constants.ProgressBar}
47 | m.resetSpinner()
48 | var opts []tea.ProgramOption
49 |
50 | termenv.AltScreen()
51 |
52 | opts = append(opts, tea.WithAltScreen())
53 |
54 | return tea.NewProgram(m, opts...)
55 | }
56 |
57 | func (senderUIModel) Init() tea.Cmd {
58 | return spinner.Tick
59 | }
60 |
61 | func (m senderUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
62 | switch msg := msg.(type) {
63 | case FileInfoMsg:
64 | m.fileNames = msg.FileNames
65 | m.payloadSize = msg.Bytes
66 |
67 | return m, nil
68 |
69 | case ReadyMsg:
70 | m.readyToSend = true
71 | m.resetSpinner()
72 |
73 | return m, spinner.Tick
74 |
75 | case PasswordMsg:
76 | m.password = msg.Password
77 |
78 | return m, nil
79 |
80 | case ProgressMsg:
81 | if m.state != showSendingProgress {
82 | m.state = showSendingProgress
83 | m.resetSpinner()
84 | return m, spinner.Tick
85 | }
86 |
87 | if m.progressBar.Percent() == 1.0 {
88 | return m, nil
89 | }
90 |
91 | cmd := m.progressBar.SetPercent(float64(msg.Progress))
92 |
93 | return m, cmd
94 |
95 | case FinishedMsg:
96 | m.state = showSFinished
97 | cmd := m.progressBar.SetPercent(1.0)
98 |
99 | return m, cmd
100 |
101 | case ErrorMsg:
102 | m.state = showSError
103 | m.errorMessage = msg.Message
104 |
105 | return m, nil
106 |
107 | case tea.KeyMsg:
108 | if tools.Contains(constants.QuitKeys, strings.ToLower(msg.String())) {
109 | return m, tea.Quit
110 | }
111 |
112 | return m, nil
113 |
114 | case tea.WindowSizeMsg:
115 | m.progressBar.Width = msg.Width - 2 * constants.PADDING - 4
116 |
117 | if m.progressBar.Width > constants.MAX_WIDTH {
118 | m.progressBar.Width = constants.MAX_WIDTH
119 | }
120 |
121 | return m, nil
122 |
123 | // FrameMsg is sent when the progress bar wants to animate itself
124 | case progress.FrameMsg:
125 | progressModel, cmd := m.progressBar.Update(msg)
126 | m.progressBar = progressModel.(progress.Model)
127 |
128 | return m, cmd
129 |
130 | default:
131 | var cmd tea.Cmd
132 | m.spinner, cmd = m.spinner.Update(msg)
133 |
134 | return m, cmd
135 | }
136 | }
137 |
138 | func (m senderUIModel) View() string {
139 | readiness := fmt.Sprintf("%s Compressing objects, preparing to send", m.spinner.View())
140 |
141 | if m.readyToSend {
142 | readiness = fmt.Sprintf("%s Awaiting receiver, ready to send", m.spinner.View())
143 | }
144 |
145 | if m.state == showSendingProgress {
146 | readiness = fmt.Sprintf("%s Sending", m.spinner.View())
147 | }
148 |
149 | fileInfoText := fmt.Sprintf("%s object(s)...", readiness)
150 |
151 | if m.fileNames != nil && m.payloadSize != 0 {
152 | sort.Strings(m.fileNames)
153 | filesToSend := constants.ItalicText(strings.Join(m.fileNames, ", "))
154 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize))
155 | fileInfoText = fmt.Sprintf("%s %d objects (%s)", readiness, len(m.fileNames), payloadSize)
156 |
157 | indentedWrappedFiles := indent.String(wordwrap.String(fmt.Sprintf("Sending: %s", filesToSend), constants.MAX_WIDTH), constants.PADDING)
158 | fileInfoText = fmt.Sprintf("%s\n\n%s", fileInfoText, indentedWrappedFiles)
159 | }
160 |
161 | switch m.state {
162 | case showPassword, showPasswordWithCopy:
163 | return "\n" +
164 | constants.PadText + constants.InfoStyle(fileInfoText) + "\n\n" +
165 | constants.PadText + "On the other computer, press " + constants.HelpStyle("`ctrl+r`") + " to enable receive mode and then enter the password:" + "\n\n" +
166 | constants.PadText + "This is the password: " + constants.BoldText(m.password) + "\n\n"
167 |
168 | case showSendingProgress:
169 | return "\n" +
170 | constants.PadText + constants.InfoStyle(fileInfoText) + "\n\n" +
171 | constants.PadText + m.progressBar.View() + "\n\n" +
172 | constants.PadText + constants.QuitCommandsHelpText + "\n\n"
173 |
174 | case showSFinished:
175 | payloadSize := constants.BoldText(tools.ByteCountSI(m.payloadSize))
176 | indentedWrappedFiles := indent.String(fmt.Sprintf("Sent: %s", wordwrap.String(constants.ItalicText(TopLevelFilesText(m.fileNames)), constants.MAX_WIDTH)), constants.PADDING)
177 | finishedText := fmt.Sprintf("Sent %d objects (%s decompressed)\n\n%s", len(m.fileNames), payloadSize, indentedWrappedFiles)
178 |
179 | return "\n" +
180 | constants.PadText + constants.InfoStyle(finishedText) + "\n\n" +
181 | constants.PadText + m.progressBar.View() + "\n\n" +
182 | constants.PadText + constants.QuitCommandsHelpText + "\n\n"
183 |
184 | case showSError:
185 | return m.errorMessage
186 |
187 | default:
188 | return ""
189 | }
190 | }
191 |
192 | func (m *senderUIModel) resetSpinner() {
193 | m.spinner = spinner.NewModel()
194 | m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(constants.PRIMARY_COLOR))
195 |
196 | if m.readyToSend {
197 | m.spinner.Spinner = WaitingSpinner
198 | } else {
199 | m.spinner.Spinner = CompressingSpinner
200 | }
201 |
202 | if m.state == showSendingProgress {
203 | m.spinner.Spinner = TransferSpinner
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/internal/tui/sr-ui.go:
--------------------------------------------------------------------------------
1 | package tui
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "time"
7 | "strings"
8 |
9 | "github.com/abdfnx/tran/constants"
10 | "github.com/charmbracelet/bubbles/spinner"
11 | )
12 |
13 | type UIUpdate struct {
14 | Progress float32
15 | }
16 |
17 | type FileInfoMsg struct {
18 | FileNames []string
19 | Bytes int64
20 | }
21 |
22 | type ErrorMsg struct {
23 | Message string
24 | }
25 |
26 | type ProgressMsg struct {
27 | Progress float32
28 | }
29 |
30 | type FinishedMsg struct {
31 | Files []string
32 | PayloadSize int64
33 | }
34 |
35 | var WaitingSpinner = spinner.Dot
36 |
37 | var CompressingSpinner = spinner.Globe
38 |
39 | var TransferSpinner = spinner.Spinner{
40 | Frames: []string{"» ", "»» ", "»»»", " "},
41 | FPS: time.Millisecond * 400,
42 | }
43 |
44 | var ReceivingSpinner = spinner.Spinner{
45 | Frames: []string{" ", " «", " ««", "«««"},
46 | FPS: time.Second / 2,
47 | }
48 |
49 | func TopLevelFilesText(fileNames []string) string {
50 | // parse top level file names and attach number of subfiles in them
51 | topLevelFileChildren := make(map[string]int)
52 |
53 | for _, f := range fileNames {
54 | fileTopPath := strings.Split(f, "/")[0]
55 |
56 | subfileCount, wasPresent := topLevelFileChildren[fileTopPath]
57 |
58 | if wasPresent {
59 | topLevelFileChildren[fileTopPath] = subfileCount + 1
60 | } else {
61 | topLevelFileChildren[fileTopPath] = 0
62 | }
63 | }
64 |
65 | // read map into formatted strings
66 | var topLevelFilesText []string
67 |
68 | for fileName, subFileCount := range topLevelFileChildren {
69 | formattedFileName := fileName
70 |
71 | if subFileCount > 0 {
72 | formattedFileName = fmt.Sprintf("%s (%d subfiles)", fileName, subFileCount)
73 | }
74 |
75 | topLevelFilesText = append(topLevelFilesText, formattedFileName)
76 | }
77 |
78 | sort.Strings(topLevelFilesText)
79 |
80 | return strings.Join(topLevelFilesText, ", ")
81 | }
82 |
83 | func GracefulUIQuit() {
84 | time.Sleep(constants.SHUTDOWN_PERIOD)
85 | }
86 |
--------------------------------------------------------------------------------
/ios/color.go:
--------------------------------------------------------------------------------
1 | package ios
2 |
3 | import (
4 | "os"
5 | "fmt"
6 | "strings"
7 | "strconv"
8 |
9 | "github.com/mgutz/ansi"
10 | )
11 |
12 | var (
13 | magenta = ansi.ColorFunc("magenta")
14 | cyan = ansi.ColorFunc("cyan")
15 | red = ansi.ColorFunc("red")
16 | yellow = ansi.ColorFunc("yellow")
17 | blue = ansi.ColorFunc("blue")
18 | green = ansi.ColorFunc("green")
19 | gray = ansi.ColorFunc("black+h")
20 | bold = ansi.ColorFunc("default+b")
21 | cyanBold = ansi.ColorFunc("cyan+b")
22 |
23 | gray256 = func(t string) string {
24 | return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t)
25 | }
26 | )
27 |
28 | func EnvColorDisabled() bool {
29 | return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0"
30 | }
31 |
32 | func EnvColorForced() bool {
33 | return os.Getenv("CLICOLOR_FORCE") != "" && os.Getenv("CLICOLOR_FORCE") != "0"
34 | }
35 |
36 | func Is256ColorSupported() bool {
37 | return IsTrueColorSupported() ||
38 | strings.Contains(os.Getenv("TERM"), "256") ||
39 | strings.Contains(os.Getenv("COLORTERM"), "256")
40 | }
41 |
42 | func IsTrueColorSupported() bool {
43 | term := os.Getenv("TERM")
44 | colorterm := os.Getenv("COLORTERM")
45 |
46 | return strings.Contains(term, "24bit") ||
47 | strings.Contains(term, "truecolor") ||
48 | strings.Contains(colorterm, "24bit") ||
49 | strings.Contains(colorterm, "truecolor")
50 | }
51 |
52 | func NewColorScheme(enabled, is256enabled bool) *ColorScheme {
53 | return &ColorScheme{
54 | enabled: enabled,
55 | is256enabled: is256enabled,
56 | }
57 | }
58 |
59 | type ColorScheme struct {
60 | enabled bool
61 | is256enabled bool
62 | hasTrueColor bool
63 | }
64 |
65 | func (c *ColorScheme) Bold(t string) string {
66 | if !c.enabled {
67 | return t
68 | }
69 |
70 | return bold(t)
71 | }
72 |
73 | func (c *ColorScheme) Boldf(t string, args ...interface{}) string {
74 | return c.Bold(fmt.Sprintf(t, args...))
75 | }
76 |
77 | func (c *ColorScheme) Red(t string) string {
78 | if !c.enabled {
79 | return t
80 | }
81 |
82 | return red(t)
83 | }
84 |
85 | func (c *ColorScheme) Redf(t string, args ...interface{}) string {
86 | return c.Red(fmt.Sprintf(t, args...))
87 | }
88 |
89 | func (c *ColorScheme) Yellow(t string) string {
90 | if !c.enabled {
91 | return t
92 | }
93 |
94 | return yellow(t)
95 | }
96 |
97 | func (c *ColorScheme) Yellowf(t string, args ...interface{}) string {
98 | return c.Yellow(fmt.Sprintf(t, args...))
99 | }
100 |
101 | func (c *ColorScheme) Green(t string) string {
102 | if !c.enabled {
103 | return t
104 | }
105 |
106 | return green(t)
107 | }
108 |
109 | func (c *ColorScheme) Greenf(t string, args ...interface{}) string {
110 | return c.Green(fmt.Sprintf(t, args...))
111 | }
112 |
113 | func (c *ColorScheme) Gray(t string) string {
114 | if !c.enabled {
115 | return t
116 | }
117 |
118 | if c.is256enabled {
119 | return gray256(t)
120 | }
121 |
122 | return gray(t)
123 | }
124 |
125 | func (c *ColorScheme) Grayf(t string, args ...interface{}) string {
126 | return c.Gray(fmt.Sprintf(t, args...))
127 | }
128 |
129 | func (c *ColorScheme) Magenta(t string) string {
130 | if !c.enabled {
131 | return t
132 | }
133 |
134 | return magenta(t)
135 | }
136 |
137 | func (c *ColorScheme) Magentaf(t string, args ...interface{}) string {
138 | return c.Magenta(fmt.Sprintf(t, args...))
139 | }
140 |
141 | func (c *ColorScheme) Cyan(t string) string {
142 | if !c.enabled {
143 | return t
144 | }
145 |
146 | return cyan(t)
147 | }
148 |
149 | func (c *ColorScheme) Cyanf(t string, args ...interface{}) string {
150 | return c.Cyan(fmt.Sprintf(t, args...))
151 | }
152 |
153 | func (c *ColorScheme) CyanBold(t string) string {
154 | if !c.enabled {
155 | return t
156 | }
157 |
158 | return cyanBold(t)
159 | }
160 |
161 | func (c *ColorScheme) Blue(t string) string {
162 | if !c.enabled {
163 | return t
164 | }
165 |
166 | return blue(t)
167 | }
168 |
169 | func (c *ColorScheme) Bluef(t string, args ...interface{}) string {
170 | return c.Blue(fmt.Sprintf(t, args...))
171 | }
172 |
173 | func (c *ColorScheme) SuccessIcon() string {
174 | return c.SuccessIconWithColor(c.Green)
175 | }
176 |
177 | func (c *ColorScheme) SuccessIconWithColor(colo func(string) string) string {
178 | return colo("✓")
179 | }
180 |
181 | func (c *ColorScheme) WarningIcon() string {
182 | return c.Yellow("!")
183 | }
184 |
185 | func (c *ColorScheme) FailureIcon() string {
186 | return c.FailureIconWithColor(c.Red)
187 | }
188 |
189 | func (c *ColorScheme) FailureIconWithColor(colo func(string) string) string {
190 | return colo("X")
191 | }
192 |
193 | func (c *ColorScheme) ColorFromString(s string) func(string) string {
194 | s = strings.ToLower(s)
195 | var fn func(string) string
196 |
197 | switch s {
198 | case "bold":
199 | fn = c.Bold
200 | case "red":
201 | fn = c.Red
202 | case "yellow":
203 | fn = c.Yellow
204 | case "green":
205 | fn = c.Green
206 | case "gray":
207 | fn = c.Gray
208 | case "magenta":
209 | fn = c.Magenta
210 | case "cyan":
211 | fn = c.Cyan
212 | case "blue":
213 | fn = c.Blue
214 | default:
215 | fn = func(s string) string {
216 | return s
217 | }
218 | }
219 |
220 | return fn
221 | }
222 |
223 | func (c *ColorScheme) HexToRGB(hex string, x string) string {
224 | if !c.enabled || !c.hasTrueColor {
225 | return x
226 | }
227 |
228 | r, _ := strconv.ParseInt(hex[0:2], 16, 64)
229 | g, _ := strconv.ParseInt(hex[2:4], 16, 64)
230 | b, _ := strconv.ParseInt(hex[4:6], 16, 64)
231 |
232 | return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x)
233 | }
234 |
--------------------------------------------------------------------------------
/ios/console.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package ios
4 |
5 | import (
6 | "os"
7 | "errors"
8 | )
9 |
10 | func (s *IOStreams) EnableVirtualTerminalProcessing() error {
11 | return nil
12 | }
13 |
14 | func enableVirtualTerminalProcessing(f *os.File) error {
15 | return errors.New("not implemented")
16 | }
17 |
--------------------------------------------------------------------------------
/ios/console_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package ios
4 |
5 | import (
6 | "os"
7 |
8 | "golang.org/x/sys/windows"
9 | )
10 |
11 | func (s *IOStreams) EnableVirtualTerminalProcessing() error {
12 | if !s.IsStdoutTTY() {
13 | return nil
14 | }
15 |
16 | f, ok := s.originalOut.(*os.File)
17 | if !ok {
18 | return nil
19 | }
20 |
21 | return enableVirtualTerminalProcessing(f)
22 | }
23 |
24 | func enableVirtualTerminalProcessing(f *os.File) error {
25 | stdout := windows.Handle(f.Fd())
26 |
27 | var originalMode uint32
28 | windows.GetConsoleMode(stdout, &originalMode)
29 |
30 | return windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
31 | }
32 |
--------------------------------------------------------------------------------
/ios/tty_size.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package ios
5 |
6 | import (
7 | "os"
8 |
9 | "golang.org/x/term"
10 | )
11 |
12 | // ttySize measures the size of the controlling terminal for the current process
13 | func ttySize() (int, int, error) {
14 | f, err := os.Open("/dev/tty")
15 |
16 | if err != nil {
17 | return -1, -1, err
18 | }
19 |
20 | defer f.Close()
21 |
22 | return term.GetSize(int(f.Fd()))
23 | }
24 |
--------------------------------------------------------------------------------
/ios/tty_size_windows.go:
--------------------------------------------------------------------------------
1 | package ios
2 |
3 | import (
4 | "os"
5 |
6 | "golang.org/x/term"
7 | )
8 |
9 | func ttySize() (int, int, error) {
10 | f, err := os.Open("CONOUT$")
11 |
12 | if err != nil {
13 | return -1, -1, err
14 | }
15 |
16 | defer f.Close()
17 |
18 | return term.GetSize(int(f.Fd()))
19 | }
20 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "fmt"
6 | "errors"
7 | "runtime"
8 |
9 | "github.com/mgutz/ansi"
10 | "github.com/spf13/cobra"
11 | "github.com/abdfnx/tran/tools"
12 | "github.com/abdfnx/tran/cmd/tran"
13 | "github.com/abdfnx/tran/cmd/factory"
14 | "github.com/abdfnx/tran/app/checker"
15 | "github.com/AlecAivazis/survey/v2/terminal"
16 | surveyCore "github.com/AlecAivazis/survey/v2/core"
17 | )
18 |
19 | var (
20 | version string
21 | buildDate string
22 | )
23 |
24 | type exitCode int
25 |
26 | const (
27 | exitOK exitCode = 0
28 | exitError exitCode = 1
29 | exitCancel exitCode = 2
30 | )
31 |
32 | func main() {
33 | code := mainRun()
34 | os.Exit(int(code))
35 | }
36 |
37 | func mainRun() exitCode {
38 | runtime.LockOSThread()
39 |
40 | cmdFactory := factory.New()
41 | hasDebug := os.Getenv("DEBUG") != ""
42 | stderr := cmdFactory.IOStreams.ErrOut
43 |
44 | if !cmdFactory.IOStreams.ColorEnabled() {
45 | surveyCore.DisableColor = true
46 | } else {
47 | surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
48 | switch style {
49 | case "white":
50 | if cmdFactory.IOStreams.ColorSupport256() {
51 | return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242)
52 | }
53 |
54 | return ansi.ColorCode("default")
55 |
56 | default:
57 | return ansi.ColorCode(style)
58 | }
59 | }
60 | }
61 |
62 | if len(os.Args) > 1 && os.Args[1] != "" {
63 | cobra.MousetrapHelpText = ""
64 | }
65 |
66 | RootCmd := tran.Execute(cmdFactory, version, buildDate)
67 |
68 | if cmd, err := RootCmd.ExecuteC(); err != nil {
69 | if err == tools.SilentError {
70 | return exitError
71 | } else if tools.IsUserCancellation(err) {
72 | if errors.Is(err, terminal.InterruptErr) {
73 | fmt.Fprint(stderr, "\n")
74 | }
75 |
76 | return exitCancel
77 | }
78 |
79 | tools.PrintError(stderr, err, cmd, hasDebug)
80 |
81 | return exitError
82 | }
83 |
84 | if tran.HasFailed() {
85 | return exitError
86 | }
87 |
88 | if len(os.Args) > 1 && os.Args[1] != "tran" {
89 | checker.Check(version)
90 | }
91 |
92 | return exitOK
93 | }
94 |
--------------------------------------------------------------------------------
/models/model.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "github.com/abdfnx/tran/ios"
4 |
5 | type TranOptions struct {
6 | TranxAddress string
7 | TranxPort int
8 | Auth AuthLogin
9 | }
10 |
11 | type AuthLogin struct {
12 | Token string
13 | Hostname string
14 | IO *ios.IOStreams
15 | }
16 |
17 | type Password string
18 |
--------------------------------------------------------------------------------
/models/protocol/transfer.go:
--------------------------------------------------------------------------------
1 | package protocol
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "strings"
7 | )
8 |
9 | // TransferMessageType specifies the message type for the messages in the transfer protocol.
10 | type TransferMessageType int
11 |
12 | const (
13 | TransferError TransferMessageType = iota // An error has occured in transferProtocol
14 | ReceiverHandshake // Receiver exchange its IP via the tranx server to the sender
15 | SenderHandshake // Sender exchanges IP, port and payload size to the receiver via the tranx server
16 | ReceiverDirectCommunication
17 | SenderDirectAck // Sender ACKs the request for direct communication
18 | ReceiverRelayCommunication // Receiver has tried to probe the sender but cannot find it on the subnet, relay communication will be used
19 | SenderRelayAck // Sender ACKs the request for relay communication
20 | ReceiverRequestPayload // Receiver request the payload from the sender
21 | SenderPayloadSent // Sender announces that the entire file has been transfered
22 | ReceiverPayloadAck // Receiver ACKs that is has received the payload
23 | SenderClosing // Sender announces that it is closing the connection
24 | ReceiverClosingAck // Receiver ACKs the closing of the connection
25 | )
26 |
27 | // TransferMessage specifies a message in the transfer protocol.
28 | type TransferMessage struct {
29 | Type TransferMessageType `json:"type"`
30 | Payload interface{} `json:"payload,omitempty"`
31 | }
32 |
33 | func (t TransferMessage) Bytes() []byte {
34 | return []byte(fmt.Sprintf("%v", t))
35 | }
36 |
37 | type ReceiverHandshakePayload struct {
38 | IP net.IP `json:"ip"`
39 | }
40 |
41 | // SenderHandshakePayload specifies a payload type for announcing the payload size.
42 | type SenderHandshakePayload struct {
43 | IP net.IP `json:"ip"`
44 | Port int `json:"port"`
45 | PayloadSize int64 `json:"payload_size"`
46 | }
47 |
48 | type WrongMessageTypeError struct {
49 | expected []TransferMessageType
50 | got TransferMessageType
51 | }
52 |
53 | func NewWrongMessageTypeError(expected []TransferMessageType, got TransferMessageType) *WrongMessageTypeError {
54 | return &WrongMessageTypeError{
55 | expected: expected,
56 | got: got,
57 | }
58 | }
59 |
60 | func (e *WrongMessageTypeError) Error() string {
61 | var expectedMessageTypes []string
62 |
63 | for _, expectedType := range e.expected {
64 | expectedMessageTypes = append(expectedMessageTypes, expectedType.Name())
65 | }
66 |
67 | oneOfExpected := strings.Join(expectedMessageTypes, ", ")
68 |
69 | return fmt.Sprintf("wrong message type, expected one of: (%s), got: (%s)", oneOfExpected, e.got.Name())
70 | }
71 |
72 | func (t TransferMessageType) Name() string {
73 | switch t {
74 | case TransferError:
75 | return "TransferError"
76 |
77 | case ReceiverHandshake:
78 | return "ReceiverHandshake"
79 |
80 | case SenderHandshake:
81 | return "SenderHandshake"
82 |
83 | case ReceiverRelayCommunication:
84 | return "ReceiverRelayCommunication"
85 |
86 | case SenderRelayAck:
87 | return "SenderRelayAck"
88 |
89 | case ReceiverRequestPayload:
90 | return "ReceiverRequestPayload"
91 |
92 | case SenderPayloadSent:
93 | return "SenderPayloadSent"
94 |
95 | case ReceiverPayloadAck:
96 | return "ReceiverAckPayload"
97 |
98 | case SenderClosing:
99 | return "SenderClosing"
100 |
101 | case ReceiverClosingAck:
102 | return "ReceiverClosingAck"
103 |
104 | default:
105 | return ""
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/models/protocol/tranx.go:
--------------------------------------------------------------------------------
1 | package protocol
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/gorilla/websocket"
7 | )
8 |
9 | type TranxMessageType int
10 |
11 | const (
12 | TranxToSenderBind TranxMessageType = iota // An ID for this connection is bound and communicated
13 | SenderToTranxEstablish // Sender has generated and hashed password
14 | ReceiverToTranxEstablish // Passsword has been communicated to receiver who has hashed it
15 | TranxToSenderReady // Tranx announces to sender that receiver is connected
16 | SenderToTranxPAKE // Sender sends PAKE information to tranx
17 | TranxToReceiverPAKE // Tranx forwards PAKE information to receiver
18 | ReceiverToTranxPAKE // Receiver sends PAKE information to tranx
19 | TranxToSenderPAKE // Tranx forwards PAKE information to receiver
20 | SenderToTranxSalt // Sender sends cryptographic salt to tranx
21 | TranxToReceiverSalt // Rendevoux forwards cryptographic salt to receiver
22 | ReceiverToTranxClose // Receiver can connect directly to sender, close receiver connection -> close sender connection
23 | SenderToTranxClose // Transit sequence is completed, close sender connection -> close receiver connection
24 | )
25 |
26 | type TranxMessage struct {
27 | Type TranxMessageType `json:"type"`
28 | Payload interface{} `json:"payload"`
29 | }
30 |
31 | type TranxClient struct {
32 | Conn *websocket.Conn
33 | IP net.IP
34 | }
35 |
36 | type TranxSender struct {
37 | TranxClient
38 | Port int
39 | }
40 |
41 | type TranxReceiver = TranxClient
42 |
43 | /* [💻 Receiver <-> Sender 💻] messages */
44 |
45 | type PasswordPayload struct {
46 | Password string `json:"password"`
47 | }
48 | type PakePayload struct {
49 | Bytes []byte `json:"pake_bytes"`
50 | }
51 |
52 | type SaltPayload struct {
53 | Salt []byte `json:"salt"`
54 | }
55 |
56 | /* [Tranx -> Sender] messages */
57 |
58 | type TranxToSenderBindPayload struct {
59 | ID int `json:"id"`
60 | }
61 |
--------------------------------------------------------------------------------
/renderer/renderer.go:
--------------------------------------------------------------------------------
1 | package renderer
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "bytes"
7 | "strings"
8 |
9 | "github.com/ledongthuc/pdf"
10 | "github.com/charmbracelet/glamour"
11 | "github.com/charmbracelet/lipgloss"
12 | "github.com/disintegration/imaging"
13 | "github.com/lucasb-eyer/go-colorful"
14 | "github.com/alecthomas/chroma/quick"
15 | )
16 |
17 | // ConvertByesToSizeString converts a byte count to a human readable string.
18 | func ConvertBytesToSizeString(size int64) string {
19 | if size < 1000 {
20 | return fmt.Sprintf("%dB", size)
21 | }
22 |
23 | suffix := []string{
24 | "K", // kilo
25 | "M", // mega
26 | "G", // giga
27 | "T", // tera
28 | "P", // peta
29 | "E", // exa
30 | "Z", // zeta
31 | "Y", // yotta
32 | }
33 |
34 | curr := float64(size) / 1000
35 | for _, s := range suffix {
36 | if curr < 10 {
37 | return fmt.Sprintf("%.1f%s", curr-0.0499, s)
38 | } else if curr < 1000 {
39 | return fmt.Sprintf("%d%s", int(curr), s)
40 | }
41 |
42 | curr /= 1000
43 | }
44 |
45 | return ""
46 | }
47 |
48 | // ImageToString converts an image to a string representation of an image.
49 | func ImageToString(width int, img image.Image) string {
50 | img = imaging.Resize(img, width, 0, imaging.Lanczos)
51 | b := img.Bounds()
52 | w := b.Max.X
53 | h := b.Max.Y
54 | str := strings.Builder{}
55 |
56 | for y := 0; y < h; y += 2 {
57 | for x := w; x < width; x += 2 {
58 | str.WriteString(" ")
59 | }
60 |
61 | for x := 0; x < w; x++ {
62 | c1, _ := colorful.MakeColor(img.At(x, y))
63 | color1 := lipgloss.Color(c1.Hex())
64 | c2, _ := colorful.MakeColor(img.At(x, y+1))
65 | color2 := lipgloss.Color(c2.Hex())
66 | str.WriteString(lipgloss.NewStyle().Foreground(color1).
67 | Background(color2).Render("▀"))
68 | }
69 |
70 | str.WriteString("\n")
71 | }
72 |
73 | return str.String()
74 | }
75 |
76 | // RenderMarkdown renders the markdown content with glamour.
77 | func RenderMarkdown(width int, content string) (string, error) {
78 | bg := "light"
79 |
80 | if lipgloss.HasDarkBackground() {
81 | bg = "dark"
82 | }
83 |
84 | r, _ := glamour.NewTermRenderer(
85 | glamour.WithWordWrap(width),
86 | glamour.WithStandardStyle(bg),
87 | )
88 |
89 | out, err := r.Render(content)
90 | if err != nil {
91 | return "", err
92 | }
93 |
94 | return out, nil
95 | }
96 |
97 | // ReadPdf reads a PDF file given a name.
98 | func ReadPdf(name string) (string, error) {
99 | f, r, err := pdf.Open(name)
100 | if err != nil {
101 | return "", err
102 | }
103 |
104 | defer f.Close()
105 |
106 | buf := new(bytes.Buffer)
107 | b, err := r.GetPlainText()
108 |
109 | if err != nil {
110 | return "", err
111 | }
112 |
113 | _, err = buf.ReadFrom(b)
114 | if err != nil {
115 | return "", err
116 | }
117 |
118 | return buf.String(), nil
119 | }
120 |
121 | // Highlight returns a syntax highlighted string of text.
122 | func Highlight(content, extension, syntaxTheme string) (string, error) {
123 | buf := new(bytes.Buffer)
124 | if err := quick.Highlight(buf, content, extension, "terminal256", syntaxTheme); err != nil {
125 | return "", err
126 | }
127 |
128 | return buf.String(), nil
129 | }
130 |
--------------------------------------------------------------------------------
/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "tran settings",
4 | "description": "tran settings\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file",
5 | "type": "object",
6 | "properties": {
7 | "config": {
8 | "title": "config",
9 | "description": "tran settings\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file",
10 | "type": "object",
11 | "properties": {
12 | "borderless": {
13 | "title": "borderless",
14 | "description": "Whether to disable borders or not\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file",
15 | "type": "boolean",
16 | "default": true
17 | },
18 | "editor": {
19 | "title": "editor",
20 | "description": "An editor\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file",
21 | "type": "string",
22 | "minLength": 1,
23 | "pattern": "[^ ]",
24 | "default": "vim"
25 | },
26 | "enable_mousewheel": {
27 | "title": "enable mouse wheel",
28 | "description": "Whether to enable a mouse wheel or not\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file",
29 | "type": "boolean",
30 | "default": true
31 | },
32 | "show_updates": {
33 | "title": "show updates",
34 | "description": "Whether to show updates or not\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file",
35 | "type": "boolean",
36 | "default": true
37 | },
38 | "start_dir": {
39 | "title": "starting directory",
40 | "description": "A starting directory\nhttps://github.com/abdfnx/tran?tab=readme-ov-file#tran-config-file",
41 | "type": "string",
42 | "minLength": 1,
43 | "pattern": "[^ ]",
44 | "default": "."
45 | }
46 | },
47 | "minProperties": 1,
48 | "additionalProperties": false
49 | }
50 | },
51 | "minProperties": 1,
52 | "additionalProperties": false
53 | }
54 |
--------------------------------------------------------------------------------
/scripts/bfs.ps1:
--------------------------------------------------------------------------------
1 | # Build From Source
2 | $loc = "$HOME\AppData\Local\tran"
3 |
4 | go run scripts/date.go >> date.txt
5 |
6 | $LATEST_VERSION=git describe --abbrev=0 --tags
7 | $DATE=cat date.txt
8 |
9 | # Build
10 | go mod tidy
11 | go build -o tran.exe -ldflags "-X main.version=$LATEST_VERSION -X main.versionDate=$DATE"
12 |
13 | # Setup
14 | $BIN = "$loc\bin"
15 | New-Item -ItemType "directory" -Path $BIN
16 | Move-Item tran.exe -Destination $BIN
17 | [System.Environment]::SetEnvironmentVariable("Path", $Env:Path + ";$BIN", [System.EnvironmentVariableTarget]::User)
18 |
19 | if (Test-Path -path $loc) {
20 | Write-Host "Tran was built successfully, refresh your powershell and then run 'tran --help'" -ForegroundColor DarkGreen
21 | } else {
22 | Write-Host "Build failed" -ForegroundColor Red
23 | }
24 |
--------------------------------------------------------------------------------
/scripts/date.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func main() {
9 | currentTime := time.Now()
10 |
11 | fmt.Println("(" + currentTime.Format("2006-01-02") + ")")
12 | }
13 |
--------------------------------------------------------------------------------
/scripts/gh-tran/gh-trn.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const rm = require("rimraf");
4 | const mkdirp = require("mkdirp");
5 | const sh = require("shelljs");
6 |
7 | const VERSION_CMD = sh.exec("git describe --abbrev=0 --tags");
8 | const VERSION_DATE_CMD = sh.exec("go run ./scripts/date.go");
9 |
10 | const VERSION = VERSION_CMD.replace("\n", "").replace("\r", "");
11 | const VERSION_DATE = VERSION_DATE_CMD.replace("\n", "").replace("\r", "");
12 |
13 | const ROOT = __dirname;
14 | const TEMPLATES = path.join(ROOT, "templates");
15 |
16 | async function updateTranExtension(ghTranDir) {
17 | const templatePath = path.join(TEMPLATES, "gh-tran");
18 | const template = fs.readFileSync(templatePath).toString("utf-8");
19 |
20 | const templateReplaced = template
21 | .replace("CLI_VERSION", VERSION)
22 | .replace("CLI_VERSION_DATE", VERSION_DATE);
23 |
24 | fs.writeFileSync(path.join(ghTranDir, "gh-tran"), templateReplaced);
25 | }
26 |
27 | async function updateExtension() {
28 | const tmp = path.join(__dirname, "tmp");
29 | const extensionDir = path.join(tmp, "gh-tran");
30 |
31 | mkdirp.sync(tmp);
32 | rm.sync(extensionDir);
33 |
34 | console.log(`cloning https://github.com/abdfnx/gh-tran to ${extensionDir}`);
35 |
36 | sh.exec(`git clone https://github.com/abdfnx/gh-tran.git ${extensionDir}`)
37 |
38 | console.log(`done cloning abdfnx/gh-tran to ${extensionDir}`);
39 |
40 | console.log("updating local git...");
41 |
42 | await updateTranExtension(extensionDir);
43 | }
44 |
45 | updateExtension().catch((err) => {
46 | console.error(`error running scripts/gh-tran/gh-trn.js`, err);
47 | process.exit(1);
48 | });
49 |
--------------------------------------------------------------------------------
/scripts/gh-tran/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gh-trn-brew",
3 | "version": "0.0.0",
4 | "description": "gh cli extension of tran",
5 | "author": "@abdfnx",
6 | "main": "gh-trn.js",
7 | "scripts": {
8 | "start": "node gh-trn.js"
9 | },
10 | "keywords": [
11 | "tran",
12 | "github"
13 | ],
14 | "dependencies": {
15 | "mkdirp": "^3.0.1",
16 | "rimraf": "^6.0.1",
17 | "shelljs": "^0.8.5"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/gh-tran/templates/gh-tran:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | repo="abdfnx/gh-tran"
5 | tag="CLI_VERSION"
6 | buildDate="CLI_VERSION_DATE"
7 |
8 | extensionPath="$(dirname "$0")"
9 | arch="$(uname -m)"
10 |
11 | exe=""
12 |
13 | if uname -a | grep Msys > /dev/null; then
14 | if [ $arch = "x86_64" ]; then
15 | exe="windows-x86_64"
16 | elif [ $arch = "i686" ]; then
17 | exe="windows-i386"
18 | elif [ $arch = "i386" ]; then
19 | exe="windows-i386"
20 | fi
21 | elif uname -a | grep Darwin > /dev/null; then
22 | if [ $arch = "x86_64" ]; then
23 | exe="darwin-x86_64"
24 | fi
25 | elif uname -a | grep Linux > /dev/null; then
26 | if [ $arch = "x86_64" ]; then
27 | exe="linux-x86_64"
28 | elif [ $arch = "i686" ]; then
29 | exe="linux-i38"
30 | elif [ $arch = "i386" ]; then
31 | exe="linux-i386"
32 | fi
33 | fi
34 |
35 | if [ "${exe}" == "" ]; then
36 | if [ "$(which go)" = "" ]; then
37 | echo "go must be installed to use this gh extension on this platform"
38 | exit 1
39 | fi
40 |
41 | exe="cmd.out"
42 |
43 | cd "${extensionPath}" > /dev/null
44 | go build -o "${exe}" -ldflags "-X main.version=${tag} -X main.buildDate=${buildDate}"
45 | cd - > /dev/null
46 | else
47 | if [[ ! -x "${extensionPath}/bin/${exe}" ]]; then
48 | mkdir -p "${extensionPath}/bin"
49 | rm -f "${extensionPath}/bin/*"
50 | gh release -R"${repo}" download "${tag}" -p "${exe}" --dir="${extensionPath}/bin"
51 | chmod +x "${extensionPath}/bin/${exe}"
52 | fi
53 | fi
54 |
55 | exec "${extensionPath}/bin/${exe}" "$@"
56 |
--------------------------------------------------------------------------------
/scripts/install.ps1:
--------------------------------------------------------------------------------
1 | # get latest release
2 | $release_url = "https://api.github.com/repos/abdfnx/tran/releases"
3 | $tag = (Invoke-WebRequest -Uri $release_url -UseBasicParsing | ConvertFrom-Json)[0].tag_name
4 | $loc = "$HOME\AppData\Local\tran"
5 | $url = ""
6 | $arch = $env:PROCESSOR_ARCHITECTURE
7 | $releases_api_url = "https://github.com/abdfnx/tran/releases/download/$tag/tran_windows_${tag}"
8 |
9 | if ($arch -eq "AMD64") {
10 | $url = "${releases_api_url}_amd64.zip"
11 | } elseif ($arch -eq "x86") {
12 | $url = "${releases_api_url}_386.zip"
13 | } elseif ($arch -eq "arm") {
14 | $url = "${releases_api_url}_arm.zip"
15 | } elseif ($arch -eq "arm64") {
16 | $url = "${releases_api_url}_arm64.zip"
17 | }
18 |
19 | if (Test-Path -path $loc) {
20 | Remove-Item $loc -Recurse -Force
21 | }
22 |
23 | Write-Host "Installing tran version $tag" -ForegroundColor DarkCyan
24 |
25 | Invoke-WebRequest $url -outfile tran_windows.zip
26 |
27 | Expand-Archive tran_windows.zip
28 |
29 | New-Item -ItemType "directory" -Path $loc
30 |
31 | Move-Item -Path tran_windows\bin -Destination $loc
32 |
33 | Remove-Item tran_windows* -Recurse -Force
34 |
35 | [System.Environment]::SetEnvironmentVariable("Path", $Env:Path + ";$loc\bin", [System.EnvironmentVariableTarget]::User)
36 |
37 | if (Test-Path -path $loc) {
38 | Write-Host "Thanks for installing Tran! Now Refresh your powershell" -ForegroundColor DarkGreen
39 | Write-Host "If this is your first time using the CLI, be sure to run 'tran --help' first." -ForegroundColor DarkGreen
40 | } else {
41 | Write-Host "Download failed" -ForegroundColor Red
42 | Write-Host "Please try again later" -ForegroundColor Red
43 | }
44 |
--------------------------------------------------------------------------------
/scripts/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | installPath=$1
4 | tranPath=""
5 |
6 | if [ "$installPath" != "" ]; then
7 | tranPath=$installPath
8 | else
9 | tranPath=/usr/local/bin
10 | fi
11 |
12 | UNAME=$(uname)
13 | ARCH=$(uname -m)
14 |
15 | rmOldFiles() {
16 | if [ -f $tranPath/tran ]; then
17 | sudo rm -rf $tranPath/tran*
18 | fi
19 | }
20 |
21 | v=$(curl --silent "https://api.github.com/repos/abdfnx/tran/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
22 |
23 | releases_api_url=https://github.com/abdfnx/tran/releases/download
24 |
25 | successInstall() {
26 | echo "🙏 Thanks for installing Tran! If this is your first time using the CLI, be sure to run `tran --help` first."
27 | }
28 |
29 | mainCheck() {
30 | echo "Installing tran version $v"
31 | name=""
32 |
33 | if [ "$UNAME" == "Linux" ]; then
34 | if [ $ARCH = "x86_64" ]; then
35 | name="tran_linux_${v}_amd64"
36 | elif [ $ARCH = "i686" ]; then
37 | name="tran_linux_${v}_386"
38 | elif [ $ARCH = "i386" ]; then
39 | name="tran_linux_${v}_386"
40 | elif [ $ARCH = "arm64" ]; then
41 | name="tran_linux_${v}_arm64"
42 | elif [ $ARCH = "arm" ]; then
43 | name="tran_linux_${v}_arm"
44 | fi
45 |
46 | tranURL=$releases_api_url/$v/$name.zip
47 |
48 | wget $tranURL
49 | sudo chmod 755 $name.zip
50 | unzip $name.zip
51 | rm $name.zip
52 |
53 | # tran
54 | sudo mv $name/bin/tran $tranPath
55 |
56 | rm -rf $name
57 |
58 | elif [ "$UNAME" == "Darwin" ]; then
59 | if [ $ARCH = "x86_64" ]; then
60 | name="tran_macos_${v}_amd64"
61 | elif [ $ARCH = "arm64" ]; then
62 | name="tran_macos_${v}_arm64"
63 | fi
64 |
65 | tranURL=$releases_api_url/$v/$name.zip
66 |
67 | wget $tranURL
68 | sudo chmod 755 $name.zip
69 | unzip $name.zip
70 | rm $name.zip
71 |
72 | # tran
73 | sudo mv $name/bin/tran $tranPath
74 |
75 | rm -rf $name
76 |
77 | elif [ "$UNAME" == "FreeBSD" ]; then
78 | if [ $ARCH = "x86_64" ]; then
79 | name="tran_freebsd_${v}_amd64"
80 | elif [ $ARCH = "i386" ]; then
81 | name="tran_freebsd_${v}_386"
82 | elif [ $ARCH = "i686" ]; then
83 | name="tran_freebsd_${v}_386"
84 | elif [ $ARCH = "arm64" ]; then
85 | name="tran_freebsd_${v}_arm64"
86 | elif [ $ARCH = "arm" ]; then
87 | name="tran_freebsd_${v}_arm"
88 | fi
89 |
90 | tranURL=$releases_api_url/$v/$name.zip
91 |
92 | wget $tranURL
93 | sudo chmod 755 $name.zip
94 | unzip $name.zip
95 | rm $name.zip
96 |
97 | # tran
98 | sudo mv $name/bin/tran $tranPath
99 |
100 | rm -rf $name
101 | fi
102 |
103 | # chmod
104 | sudo chmod 755 $tranPath/tran
105 | }
106 |
107 | rmOldFiles
108 | mainCheck
109 |
110 | if [ -x "$(command -v tran)" ]; then
111 | successInstall
112 | else
113 | echo "Download failed 😔"
114 | echo "Please try again."
115 | fi
116 |
--------------------------------------------------------------------------------
/scripts/shell/README:
--------------------------------------------------------------------------------
1 | Welcome to Tran 👋
2 |
3 | you can do your first steps
4 |
5 | - `$ tran`
6 | - `$ tran send README.md`
7 | - `$ tran receive $CODE`
8 |
9 | run `tran -h` for get help 📄
10 |
--------------------------------------------------------------------------------
/scripts/shell/zshrc:
--------------------------------------------------------------------------------
1 | export ZSH="/home/trn/.oh-my-zsh"
2 |
3 | ZSH_THEME="af-magic"
4 |
5 | plugins=( git zsh-syntax-highlighting zsh-autosuggestions )
6 |
7 | source $ZSH/oh-my-zsh.sh
8 |
9 | alias s="source ~/.zshrc"
10 | alias n="nano ~/.zshrc"
11 | alias update="sudo apt update"
12 | alias upgrade="sudo apt upgrade"
13 | alias py="python3"
14 |
--------------------------------------------------------------------------------
/scripts/tag.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | tag="${1}"
4 |
5 | while (($#)); do
6 | case "$2" in
7 |
8 | -a)
9 | git tag -a "${tag}" -m ""
10 | exit 0
11 | ;;
12 |
13 | -p)
14 | git push origin "${tag}"
15 | exit 0
16 | ;;
17 |
18 | -x)
19 | git tag -a "${tag}" -m ""
20 | git push origin "${tag}"
21 | exit 0
22 | ;;
23 |
24 | -d)
25 | git tag -d "${tag}"
26 | git push --delete origin "${tag}"
27 | exit 0
28 | ;;
29 |
30 | esac
31 | done
32 |
--------------------------------------------------------------------------------
/tools/errors.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "io"
5 | "fmt"
6 | "net"
7 | "errors"
8 | "strings"
9 |
10 | "github.com/spf13/cobra"
11 | "github.com/AlecAivazis/survey/v2/terminal"
12 | )
13 |
14 | // FlagError is the kind of error raised in flag processing
15 | type FlagError struct {
16 | Err error
17 | }
18 |
19 | func (fe FlagError) Error() string {
20 | return fe.Err.Error()
21 | }
22 |
23 | func (fe FlagError) Unwrap() error {
24 | return fe.Err
25 | }
26 |
27 | // SilentError is an error that triggers exit code 1 without any error messaging
28 | var SilentError = errors.New("SilentError")
29 |
30 | // CancelError signals user-initiated cancellation
31 | var CancelError = errors.New("CancelError")
32 |
33 | func IsUserCancellation(err error) bool {
34 | return errors.Is(err, CancelError) || errors.Is(err, terminal.InterruptErr)
35 | }
36 |
37 | func MutuallyExclusive(message string, conditions ...bool) error {
38 | numTrue := 0
39 |
40 | for _, ok := range conditions {
41 | if ok {
42 | numTrue++
43 | }
44 | }
45 |
46 | if numTrue > 1 {
47 | return &FlagError{Err: errors.New(message)}
48 | }
49 |
50 | return nil
51 | }
52 |
53 | func PrintError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
54 | var dnsError *net.DNSError
55 |
56 | if errors.As(err, &dnsError) {
57 | fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)
58 |
59 | if debug {
60 | fmt.Fprintln(out, dnsError)
61 | }
62 |
63 | return
64 | }
65 |
66 | fmt.Fprintln(out, err)
67 |
68 | var flagError *FlagError
69 | if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") {
70 | if !strings.HasSuffix(err.Error(), "\n") {
71 | fmt.Fprintln(out)
72 | }
73 |
74 | fmt.Fprintln(out, cmd.UsageString())
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tools/files.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "io"
5 | "os"
6 | "fmt"
7 | "bufio"
8 | "strings"
9 | "archive/tar"
10 | "path/filepath"
11 |
12 | "github.com/klauspost/pgzip"
13 | "github.com/abdfnx/tran/constants"
14 | )
15 |
16 | func ReadFiles(fileNames []string) ([]*os.File, error) {
17 | var files []*os.File
18 |
19 | for _, fileName := range fileNames {
20 | f, err := os.Open(fileName)
21 | if err != nil {
22 | return nil, fmt.Errorf("file '%s' not found", fileName)
23 | }
24 |
25 | files = append(files, f)
26 | }
27 |
28 | return files, nil
29 | }
30 |
31 | // ArchiveAndCompressFiles tars and gzip-compresses files into a temporary file, returning it
32 | // along with the resulting size
33 | func ArchiveAndCompressFiles(files []*os.File) (*os.File, int64, error) {
34 | // chained writers -> writing to tw writes to gw -> writes to temporary file
35 | tempFile, err := os.CreateTemp(os.TempDir(), constants.SEND_TEMP_FILE_NAME_PREFIX)
36 |
37 | if err != nil {
38 | return nil, 0, err
39 | }
40 |
41 | tempFileWriter := bufio.NewWriter(tempFile)
42 | gw := pgzip.NewWriter(tempFileWriter)
43 | tw := tar.NewWriter(gw)
44 |
45 | for _, file := range files {
46 | err := addToTarArchive(tw, file)
47 | if err != nil {
48 | return nil, 0, err
49 | }
50 | }
51 |
52 | tw.Close()
53 | gw.Close()
54 |
55 | tempFileWriter.Flush()
56 | fileInfo, err := tempFile.Stat()
57 |
58 | if err != nil {
59 | return nil, 0, err
60 | }
61 |
62 | tempFile.Seek(0, io.SeekStart)
63 |
64 | return tempFile, fileInfo.Size(), nil
65 | }
66 |
67 | // DecompressAndUnarchiveBytes gzip-decompresses and un-tars files into the current working directory
68 | // and returns the names and decompressed size of the created files
69 | func DecompressAndUnarchiveBytes(reader io.Reader) ([]string, int64, error) {
70 | // chained readers -> gr reads from reader -> tr reads from gr
71 | gr, err := pgzip.NewReader(reader)
72 |
73 | if err != nil {
74 | return nil, 0, err
75 | }
76 |
77 | defer gr.Close()
78 |
79 | tr := tar.NewReader(gr)
80 |
81 | var createdFiles []string
82 | var decompressedSize int64
83 |
84 | for {
85 | header, err := tr.Next()
86 |
87 | if err == io.EOF {
88 | break
89 | }
90 |
91 | if err != nil {
92 | return nil, 0, err
93 | }
94 | if header == nil {
95 | continue
96 | }
97 |
98 | cwd, err := os.Getwd()
99 |
100 | if err != nil {
101 | return nil, 0, err
102 | }
103 |
104 | fileTarget := filepath.Join(cwd, header.Name)
105 |
106 | switch header.Typeflag {
107 | case tar.TypeDir:
108 | if _, err := os.Stat(fileTarget); err != nil {
109 | if err := os.MkdirAll(fileTarget, 0755); err != nil {
110 | return nil, 0, err
111 | }
112 | }
113 |
114 | case tar.TypeReg:
115 | f, err := os.OpenFile(fileTarget, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
116 |
117 | if err != nil {
118 | return nil, 0, err
119 | }
120 |
121 | if _, err := io.Copy(f, tr); err != nil {
122 | return nil, 0, err
123 | }
124 |
125 | fileInfo, err := f.Stat()
126 |
127 | if err != nil {
128 | return nil, 0, err
129 | }
130 |
131 | decompressedSize += fileInfo.Size()
132 | createdFiles = append(createdFiles, header.Name)
133 |
134 | f.Close()
135 | }
136 | }
137 |
138 | return createdFiles, decompressedSize, nil
139 | }
140 |
141 | // Traverses files and directories (recursively) for total size in bytes
142 | func FilesTotalSize(files []*os.File) (int64, error) {
143 | var size int64
144 |
145 | for _, file := range files {
146 | err := filepath.Walk(file.Name(), func(_ string, info os.FileInfo, err error) error {
147 | if err != nil {
148 | return err
149 | }
150 |
151 | if !info.IsDir() {
152 | size += info.Size()
153 | }
154 |
155 | return err
156 | })
157 |
158 | if err != nil {
159 | return 0, err
160 | }
161 | }
162 |
163 | return size, nil
164 | }
165 |
166 | // source: https://gist.github.com/mimoo/25fc9716e0f1353791f5908f94d6e726
167 | func addToTarArchive(tw *tar.Writer, file *os.File) error {
168 | return filepath.Walk(file.Name(), func(file string, fi os.FileInfo, err error) error {
169 | header, e := tar.FileInfoHeader(fi, file)
170 | if e != nil {
171 | return err
172 | }
173 |
174 | header.Name = filepath.ToSlash(file)
175 |
176 | if err := tw.WriteHeader(header); err != nil {
177 | return err
178 | }
179 |
180 | if !fi.IsDir() {
181 | data, err := os.Open(file)
182 | if err != nil {
183 | return err
184 | }
185 |
186 | defer data.Close()
187 |
188 | if _, err := io.Copy(tw, data); err != nil {
189 | return err
190 | }
191 | }
192 |
193 | return nil
194 | })
195 | }
196 |
197 | func RemoveTemporaryFiles(prefix string) {
198 | tempFiles, err := os.ReadDir(os.TempDir())
199 |
200 | if err != nil {
201 | return
202 | }
203 |
204 | for _, tempFile := range tempFiles {
205 | fileInfo, err := tempFile.Info()
206 | if err != nil {
207 | continue
208 | }
209 |
210 | fileName := fileInfo.Name()
211 | if strings.HasPrefix(fileName, prefix) {
212 | os.Remove(filepath.Join(os.TempDir(), fileName))
213 | }
214 | }
215 | }
216 |
217 | // source: https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format
218 | func ByteCountSI(b int64) string {
219 | const unit = 1000
220 |
221 | if b < unit {
222 | return fmt.Sprintf("%d B", b)
223 | }
224 |
225 | div, exp := int64(unit), 0
226 |
227 | for n := b / unit; n >= unit; n /= unit {
228 | div *= unit
229 | exp++
230 | }
231 |
232 | return fmt.Sprintf("%.1f %cB",
233 | float64(b)/float64(div), "kMGTPE"[exp])
234 | }
235 |
--------------------------------------------------------------------------------
/tools/json.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "fmt"
5 | "encoding/json"
6 | )
7 |
8 | func prettyJSONFormat(i interface{}) string {
9 | s, _ := json.MarshalIndent(i, "", " ")
10 | return string(s)
11 | }
12 |
13 | func DecodePayload(payload interface{}, target interface{}) (err error) {
14 | bytes, err := json.Marshal(payload)
15 |
16 | if err != nil {
17 | return fmt.Errorf("could not marshal payload into bytes:%e", err)
18 | }
19 |
20 | err = json.Unmarshal(bytes, &target)
21 | if err != nil {
22 | return fmt.Errorf("faulty payload format\nexpected format:\n%s\ngot:\n%s",
23 | prettyJSONFormat(&target),
24 | prettyJSONFormat(&payload))
25 | }
26 |
27 | return nil
28 | }
29 |
--------------------------------------------------------------------------------
/tools/math.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "encoding/binary"
5 | mrand "math/rand"
6 | crand "crypto/rand"
7 | )
8 |
9 | func RandomSeed() {
10 | var b [8]byte
11 |
12 | _, err := crand.Read(b[:])
13 |
14 | if err != nil {
15 | panic("failed to seed math/rand")
16 | }
17 |
18 | mrand.Seed(int64(binary.LittleEndian.Uint64(b[:])))
19 | }
20 |
--------------------------------------------------------------------------------
/tools/messaging.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "fmt"
5 | "encoding/json"
6 |
7 | "github.com/gorilla/websocket"
8 | "github.com/abdfnx/tran/core/crypt"
9 | "github.com/abdfnx/tran/models/protocol"
10 | )
11 |
12 | func ReadTranxMessage(wsConn *websocket.Conn, expected protocol.TranxMessageType) (protocol.TranxMessage, error) {
13 | msg := protocol.TranxMessage{}
14 | err := wsConn.ReadJSON(&msg)
15 |
16 | if err != nil {
17 | return protocol.TranxMessage{}, err
18 | }
19 |
20 | if msg.Type != expected {
21 | return protocol.TranxMessage{}, fmt.Errorf("expected message type: %d. Got type: %d", expected, msg.Type)
22 | }
23 |
24 | return msg, nil
25 | }
26 |
27 | func WriteEncryptedMessage(wsConn *websocket.Conn, msg protocol.TransferMessage, crypt *crypt.Crypt) error {
28 | json, err := json.Marshal(msg)
29 |
30 | if err != nil {
31 | return nil
32 | }
33 |
34 | enc, err := crypt.Encrypt(json)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | wsConn.WriteMessage(websocket.BinaryMessage, enc)
40 |
41 | return nil
42 | }
43 |
44 | func ReadEncryptedMessage(wsConn *websocket.Conn, crypt *crypt.Crypt) (protocol.TransferMessage, error) {
45 | _, enc, err := wsConn.ReadMessage()
46 |
47 | if err != nil {
48 | return protocol.TransferMessage{}, err
49 | }
50 |
51 | dec, err := crypt.Decrypt(enc)
52 | if err != nil {
53 | return protocol.TransferMessage{}, err
54 | }
55 |
56 | msg := protocol.TransferMessage{}
57 | err = json.Unmarshal(dec, &msg)
58 |
59 | if err != nil {
60 | return protocol.TransferMessage{}, err
61 | }
62 |
63 | return msg, nil
64 | }
65 |
--------------------------------------------------------------------------------
/tools/password.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "math/rand"
7 | "encoding/hex"
8 | "crypto/sha256"
9 |
10 | "github.com/abdfnx/tran/data"
11 | "github.com/abdfnx/tran/models"
12 | )
13 |
14 | const passwordLength = 4
15 |
16 | // GeneratePassword generates a random password prefixed with the supplied id.
17 | func GeneratePassword(id int) models.Password {
18 | var words []string
19 | hitlistSize := len(data.PasswordList)
20 |
21 | // generate three unique words
22 | for len(words) != passwordLength {
23 | candidateWord := data.PasswordList[rand.Intn(hitlistSize)]
24 | if !Contains(words, candidateWord) {
25 | words = append(words, candidateWord)
26 | }
27 | }
28 |
29 | password := formatPassword(id, words)
30 | return models.Password(password)
31 | }
32 |
33 | func ParsePassword(passStr string) (models.Password, error) {
34 | re := regexp.MustCompile(`^\d+-[a-z]+-[a-z]+-[a-z]+$`)
35 | ok := re.MatchString(passStr)
36 |
37 | if !ok {
38 | return models.Password(""), fmt.Errorf("password: %q is on wrong format", passStr)
39 | }
40 |
41 | return models.Password(passStr), nil
42 | }
43 |
44 | func formatPassword(prefixIndex int, words []string) string {
45 | return fmt.Sprintf("%d-%s-%s-%s", prefixIndex, words[0], words[1], words[2])
46 | }
47 |
48 | func HashPassword(password models.Password) string {
49 | h := sha256.New()
50 | h.Write([]byte(password))
51 |
52 | return hex.EncodeToString(h.Sum(nil))
53 | }
54 |
--------------------------------------------------------------------------------
/tools/ports.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import "net"
4 |
5 | func GetOpenPort() (int, error) {
6 | addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
7 |
8 | if err != nil {
9 | return 0, err
10 | }
11 |
12 | listener, err := net.ListenTCP("tcp", addr)
13 |
14 | if err != nil {
15 | return 0, err
16 | }
17 |
18 | defer listener.Close()
19 |
20 | return listener.Addr().(*net.TCPAddr).Port, nil
21 | }
22 |
--------------------------------------------------------------------------------
/tools/strings.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "fmt"
5 | "unicode/utf8"
6 | )
7 |
8 | func Contains(s []string, str string) bool {
9 | for _, v := range s {
10 | if v == str {
11 | return true
12 | }
13 | }
14 |
15 | return false
16 | }
17 |
18 | // ValidateHostname returns an error if the domain name is not valid
19 | // source: https://gist.github.com/chmike/d4126a3247a6d9a70922fc0e8b4f4013
20 | func ValidateHostname(name string) error {
21 | switch {
22 | case len(name) == 0:
23 | return nil
24 |
25 | case len(name) > 255:
26 | return fmt.Errorf("name length is %d, can't exceed 255", len(name))
27 | }
28 |
29 | var l int
30 |
31 | for i := 0; i < len(name); i++ {
32 | b := name[i]
33 |
34 | if b == '.' {
35 | // check domain labels validity
36 | switch {
37 | case i == l:
38 | return fmt.Errorf("invalid character '%c' at offset %d: label can't begin with a period", b, i)
39 |
40 | case i-l > 63:
41 | return fmt.Errorf("byte length of label '%s' is %d, can't exceed 63", name[l:i], i-l)
42 |
43 | case name[l] == '-':
44 | return fmt.Errorf("label '%s' at offset %d begins with a hyphen", name[l:i], l)
45 |
46 | case name[i-1] == '-':
47 | return fmt.Errorf("label '%s' at offset %d ends with a hyphen", name[l:i], l)
48 | }
49 |
50 | l = i + 1
51 |
52 | continue
53 | }
54 |
55 | if !(b >= 'a' && b <= 'z' || b >= '0' && b <= '9' || b == '-' || b >= 'A' && b <= 'Z') {
56 | // show the printable unicode character starting at byte offset i
57 | c, _ := utf8.DecodeRuneInString(name[i:])
58 |
59 | if c == utf8.RuneError {
60 | return fmt.Errorf("invalid rune at offset %d", i)
61 | }
62 |
63 | return fmt.Errorf("invalid character '%c' at offset %d", c, i)
64 | }
65 | }
66 |
67 | // check top level domain validity
68 | switch {
69 | case l == len(name):
70 | return fmt.Errorf("missing top level domain, domain can't end with a period")
71 |
72 | case len(name)-l > 63:
73 | return fmt.Errorf("byte length of top level domain '%s' is %d, can't exceed 63", name[l:], len(name)-l)
74 |
75 | case name[l] == '-':
76 | return fmt.Errorf("top level domain '%s' at offset %d begins with a hyphen", name[l:], l)
77 |
78 | case name[len(name)-1] == '-':
79 | return fmt.Errorf("top level domain '%s' at offset %d ends with a hyphen", name[l:], l)
80 |
81 | case name[l] >= '0' && name[l] <= '9':
82 | return fmt.Errorf("top level domain '%s' at offset %d begins with a digit", name[l:], l)
83 | }
84 |
85 | return nil
86 | }
87 |
--------------------------------------------------------------------------------
/tools/text.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 |
7 | "github.com/muesli/reflow/ansi"
8 | "github.com/muesli/reflow/truncate"
9 | )
10 |
11 | var (
12 | ellipsis = "..."
13 | minWidthForEllipsis = len(ellipsis) + 2
14 | lineRE = regexp.MustCompile(`(?m)^`)
15 | ws = regexp.MustCompile(`\s+`)
16 | )
17 |
18 | func Indent(s, indent string) string {
19 | if len(strings.TrimSpace(s)) == 0 {
20 | return s
21 | }
22 |
23 | return lineRE.ReplaceAllLiteralString(s, indent)
24 | }
25 |
26 | func ReplaceExcessiveWhitespace(s string) string {
27 | return ws.ReplaceAllString(strings.TrimSpace(s), " ")
28 | }
29 |
30 | // DisplayWidth calculates what the rendered width of a string may be
31 | func DisplayWidth(s string) int {
32 | return ansi.PrintableRuneWidth(s)
33 | }
34 |
35 | // Truncate shortens a string to fit the maximum display width
36 | func Truncate(maxWidth int, s string) string {
37 | w := DisplayWidth(s)
38 | if w <= maxWidth {
39 | return s
40 | }
41 |
42 | tail := ""
43 | if maxWidth >= minWidthForEllipsis {
44 | tail = ellipsis
45 | }
46 |
47 | r := truncate.StringWithTail(s, uint(maxWidth), tail)
48 | if DisplayWidth(r) < maxWidth {
49 | r += " "
50 | }
51 |
52 | return r
53 | }
54 |
55 | // TruncateColumn replaces the first new line character with an ellipsis
56 | // and shortens a string to fit the maximum display width
57 | func TruncateColumn(maxWidth int, s string) string {
58 | if i := strings.IndexAny(s, "\r\n"); i >= 0 {
59 | s = s[:i] + ellipsis
60 | }
61 |
62 | return Truncate(maxWidth, s)
63 | }
64 |
--------------------------------------------------------------------------------
/tools/websocket.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "github.com/gorilla/websocket"
8 | )
9 |
10 | type WsHandlerFunc func(*websocket.Conn)
11 |
12 | func WebsocketHandler(wsHandler WsHandlerFunc) http.HandlerFunc {
13 | wsUpgrader := websocket.Upgrader{}
14 |
15 | return func(w http.ResponseWriter, r *http.Request) {
16 | wsConn, err := wsUpgrader.Upgrade(w, r, nil)
17 |
18 | if err != nil {
19 | log.Println("failed to upgrade connection: ", err)
20 | return
21 | }
22 |
23 | defer wsConn.Close()
24 |
25 | wsHandler(wsConn)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------