├── .bundle └── config ├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── close-inactive.yaml │ └── unit.yml ├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── TODO.md ├── bin └── haste ├── haste.gemspec ├── lib ├── haste.rb └── haste │ ├── cli.rb │ ├── exception.rb │ ├── uploader.rb │ └── version.rb └── spec ├── examples └── uploader_spec.rb └── spec_helper.rb /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "3" 3 | BUNDLE_JOBS: "4" 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @toptal/marketing-tools-2-eng 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | [ABC-1234](https://toptal-core.atlassian.net/browse/ABC-1234) 2 | 3 | ### Description 4 | 5 | Describe the changes and motivations for the pull request, unless obvious from the title. 6 | 7 | [Designs](https://share.abstract.com) (if applicable) 8 | 9 | ### How to test 10 | 11 | - `bundle install` 12 | - FIXME: Add the steps describing how to test your changes manually 13 | 14 | ### Environment variables 15 | 16 | Include a snapshot of the ENV vars used in your local env when this PR was created. 17 | 18 | ``` 19 | PUBLIC_ENV_VAR=public-value 20 | GLOBAL_SECRET_ENV_VAR=xxx # Added in 1Password in Shared-Utilities-Environment group 21 | OWN_PRIVATE_ENV_VAR # Different for everyone 22 | ``` 23 | 24 | ### Acceptance Criteria 25 | 26 | - [ ] Add acceptance criterias from Jira task [ABC-1234] 27 | 28 | ### Pre-merge checklist 29 | 30 | - [ ] The PR relates to a single subject with a clear title and description in grammatically correct, complete sentences. 31 | - [ ] Verify that feature branch is up-to-date with `master` (if not - rebase it). 32 | - [ ] Double check the quality of [commit messages](http://chris.beams.io/posts/git-commit/). 33 | - [ ] A snapshot of the author's env vars has been added 34 | -------------------------------------------------------------------------------- /.github/workflows/close-inactive.yaml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues and PRs 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | close-stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@v6 15 | with: 16 | days-before-stale: 30 17 | days-before-close: 14 18 | stale-issue-label: "stale" 19 | stale-pr-label: "stale" 20 | 21 | exempt-issue-labels: backlog,triage,nostale 22 | exempt-pr-labels: backlog,triage,nostale 23 | 24 | stale-pr-message: "This PR is stale because it has been open for 30 days with no activity." 25 | close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale." 26 | 27 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 28 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 29 | 30 | repo-token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/unit.yml: -------------------------------------------------------------------------------- 1 | name: Unit 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | unit_tests: 10 | name: Unit tests 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 5 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: '3.0' 19 | bundler-cache: true 20 | 21 | - name: Run bundle install 22 | run: | 23 | gem install bundler 24 | bundle install --jobs 4 --retry 3 25 | 26 | - name: Build and test with rspec 27 | run: bundle exec rspec spec 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.gem 4 | Gemfile.lock 5 | .rspec 6 | .DS_STORE 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: "rspec spec" 2 | branches: 3 | only: 4 | - master 5 | rvm: 6 | - 1.8.7 7 | - 1.9.3 8 | - 2.0.0 9 | - 2.4.0 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 Toptal LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haste Client 2 | 3 | Haste-client is a simple client for uploading data to `Haste` server. All you need to do is to pipe data in STDIN: 4 | 5 | `cat file | HASTE_SERVER_TOKEN=mytoken haste` 6 | 7 | And once the output makes it to the server, it will print the `Haste` share page URL to STDOUT. 8 | 9 | This can be combined with `pbcopy`, like: 10 | 11 | * mac osx: `cat file | haste | pbcopy` 12 | * linux: `cat file | haste | xsel` 13 | 14 | after which the contents of `file` will be accessible at a URL which has been copied to your pasteboard. 15 | 16 | ## Installation 17 | 18 | ``` bash 19 | gem install haste 20 | ``` 21 | 22 | ## Configuration 23 | 24 | Most of the configuration is controlled by env variables. Here is the all environment variables that you can use. 25 | 26 | ``` 27 | HASTE_SERVER: Haste server domain url 28 | HASTE_SERVER_TOKEN: Github authentication token 29 | HASTE_SHARE_SERVER: Haste share server domain url 30 | HASTE_USER: Basic authentication user name 31 | HASTE_PASS: Basic authentication user pass 32 | HASTE_SSL_CERTS: SSL certs path 33 | ``` 34 | 35 | To add these environment variables, you should simply add them to your ~.bash_profile: 36 | 37 | ```bash 38 | export VARIABLE="value" 39 | ``` 40 | 41 | ### Authentication 42 | 43 | If you are using default `HASTE_SERVER`, you need to have an GitHub authentication token. 44 | You can get the information about authentication and how to generate token [here](https://www.toptal.com/developers/hastebin/documentation) 45 | 46 | After you have generated your token, you should add it to your ~.bash_profile: 47 | 48 | ```bash 49 | export HASTE_SERVER_TOKEN="mytoken" 50 | ``` 51 | 52 | or you can add token to your command: 53 | 54 | ```bash 55 | cat file | HASTE_SERVER_TOKEN=mytoken haste` 56 | ``` 57 | 58 | If your `Haste` installation requires http authentication, add the following to your ~.bash_profile: 59 | 60 | ```bash 61 | export HASTE_USER="myusername" 62 | export HASTE_PASS="mypassword" 63 | ``` 64 | 65 | if you are using SSL, you will need to supply your certs path 66 | 67 | ```bash 68 | export HASTE_SSL_CERTS="/System/Library/OpenSSL/certs" 69 | ``` 70 | 71 | ## Usage 72 | 73 | If you supply a valid file path as an argument to the client, it will be uploaded: 74 | 75 | ```bash 76 | # equivalent 77 | cat file | haste 78 | haste file 79 | ``` 80 | 81 | ### Different Haste server 82 | 83 | By default, `Haste` share page will point at `https://hastebin.com`. 84 | If you have haste-server configured to serve bins on a separate domain, you also need to set ENV['HASTE_SHARE_SERVER'] value to that domain. 85 | 86 | To set the value of share server, you can add the following to your ~.bash_profile: 87 | 88 | ```bash 89 | export HASTE_SHARE_SERVER="myshareserver" 90 | ``` 91 | 92 | ### Different Haste server 93 | 94 | By default, haste server will point at `https://hastebin.com`. 95 | You can change this by setting the value of `ENV['HASTE_SERVER']` to the URL of your `Haste` server. 96 | 97 | To set the value of server, you can add the following to your ~.bash_profile: 98 | 99 | ```bash 100 | export HASTE_SERVER="myserver" 101 | ``` 102 | 103 | ### Use with alias 104 | 105 | You can also use `alias` to make easy shortcuts if you commonly use a few hastes intermingled with each other. 106 | To do that, you'd put something like this into ~.bash_profile: 107 | 108 | ``` bash 109 | alias work_haste="HASTE_SERVER=https://something.com HASTE_SERVER_TOKEN=mytoken haste" 110 | ``` 111 | 112 | or 113 | 114 | ``` bash 115 | alias work_haste="HASTE_SERVER_TOKEN=mytoken haste" 116 | ``` 117 | 118 | After which you can use `work_haste` to send hastes to that server or with different tokens instead. 119 | 120 | 121 | ### Use as a library 122 | 123 | You can also use `Haste` as a library to upload hastes: 124 | 125 | ``` ruby 126 | require 'haste' 127 | uploader = Haste::Uploader.new 128 | uploader.upload_raw 'this is my data' # key 129 | uploader.upload_path '/tmp/whaaaa' # key 130 | ``` 131 | 132 | ## Contributor License Agreement 133 | 134 | Licensed under the [MIT](https://github.com/toptal/haste-client/blob/master/LICENSE.txt 'https://github.com/toptal/haste-client/blob/main/LICENSE.txt') license. 135 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require File.dirname(__FILE__) + '/lib/haste/version' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :build => :spec do 7 | system "gem build haste.gemspec" 8 | end 9 | 10 | task :release => :build do 11 | # tag and push 12 | system "git tag v#{Haste::VERSION}" 13 | system "git push origin --tags" 14 | # push the gem 15 | system "gem push haste-#{Haste::VERSION}.gem" 16 | end 17 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * make streaming 2 | * display feedback from errors in console 3 | -------------------------------------------------------------------------------- /bin/haste: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pathname' 4 | path = Pathname.new(__FILE__) 5 | require File.expand_path(File.dirname(path.realpath) + '/../lib/haste') 6 | 7 | cli = Haste::CLI.new 8 | cli.start 9 | -------------------------------------------------------------------------------- /haste.gemspec: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require File.dirname(__FILE__) + '/lib/haste/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'haste' 6 | s.author = 'Toptal' 7 | s.add_development_dependency('rspec') 8 | s.add_dependency('json') 9 | s.add_dependency('faraday', '~> 0.9') 10 | s.description = 'CLI Haste Client' 11 | s.license = 'MIT License' 12 | s.homepage = 'https://github.com/toptal/haste-client' 13 | s.email = 'open-source@toptal.com' 14 | s.executables = 'haste' 15 | s.files = Dir['lib/**/*.rb', 'haste'] 16 | s.platform = Gem::Platform::RUBY 17 | s.require_paths = ['lib'] 18 | s.summary = 'Haste Client' 19 | s.test_files = Dir.glob('spec/*.rb') 20 | s.version = Haste::VERSION 21 | end 22 | -------------------------------------------------------------------------------- /lib/haste.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/haste/uploader' 2 | require File.dirname(__FILE__) + '/haste/exception' 3 | require File.dirname(__FILE__) + '/haste/cli' 4 | 5 | module Haste 6 | end 7 | -------------------------------------------------------------------------------- /lib/haste/cli.rb: -------------------------------------------------------------------------------- 1 | module Haste 2 | 3 | class CLI 4 | 5 | # Create a new uploader 6 | def initialize 7 | @uploader = Uploader.new( 8 | ENV['HASTE_SERVER'], 9 | ENV['HASTE_SERVER_TOKEN'], 10 | ENV['HASTE_SHARE_SERVER'], 11 | ENV['HASTE_USER'], 12 | ENV['HASTE_PASS'], 13 | ENV['HASTE_SSL_CERTS']) 14 | end 15 | 16 | # And then handle the basic usage 17 | def start 18 | # Take data in 19 | if STDIN.tty? 20 | key = @uploader.upload_path ARGV.first 21 | else 22 | key = @uploader.upload_raw STDIN.readlines.join 23 | end 24 | # Put together a URL 25 | url = "#{@uploader.share_server_url}/share/#{key}" 26 | # And write data out 27 | if STDOUT.tty? 28 | STDOUT.puts url 29 | else 30 | STDOUT.print url 31 | end 32 | rescue Exception => e 33 | abort e.message 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/haste/exception.rb: -------------------------------------------------------------------------------- 1 | module Haste 2 | class Exception < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/haste/uploader.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'faraday' 3 | require 'uri' 4 | 5 | module Haste 6 | 7 | DEFAULT_SERVER_URL = 'https://hastebin.com' 8 | DEFAULT_SHARE_SERVER_URL = 'https://hastebin.com' 9 | 10 | class Uploader 11 | 12 | attr_reader :server_url, :server_token, :share_server_url, :server_user, :server_pass, :ssl_certs 13 | 14 | def initialize(server_url = nil, server_token = nil, share_server_url = nil, server_user = nil, server_pass = nil, ssl_certs = nil) 15 | @server_url = generate_url(server_url || Haste::DEFAULT_SERVER_URL) 16 | @share_server_url = generate_url(share_server_url || Haste::DEFAULT_SHARE_SERVER_URL) 17 | 18 | @server_user = server_user 19 | @server_token = server_token 20 | @server_pass = server_pass 21 | @ssl_certs = ssl_certs 22 | end 23 | 24 | def generate_url(url) 25 | url = url.dup 26 | url = url.chop if url.end_with?('/') 27 | return url 28 | end 29 | 30 | # Take in a path and return a key 31 | def upload_path(path) 32 | fail_with 'No input file given' unless path 33 | fail_with "#{path}: No such path" unless File.exists?(path) 34 | upload_raw open(path).read 35 | end 36 | 37 | # Take in data and return a key 38 | def upload_raw(data) 39 | data.rstrip! 40 | response = do_post data 41 | if response.status == 200 42 | data = JSON.parse(response.body) 43 | data['key'] 44 | else 45 | fail_with "failure uploading: #{response.body}" 46 | end 47 | rescue JSON::ParserError => e 48 | fail_with "failure parsing response: #{e.message}" 49 | rescue Errno::ECONNREFUSED => e 50 | fail_with "failure connecting: #{e.message}" 51 | end 52 | 53 | private 54 | 55 | def post_path 56 | parsed_uri = URI.parse(server_url) 57 | "#{parsed_uri.path}/documents" 58 | end 59 | 60 | def do_post(data) 61 | connection.post(post_path, data) 62 | end 63 | 64 | def connection 65 | @connection ||= connection_set 66 | end 67 | 68 | def connection_set 69 | return connection_https if @ssl_certs 70 | connection_http 71 | end 72 | 73 | def connection_http 74 | Faraday.new(:url => server_url) do |c| 75 | connection_config(c) 76 | end 77 | end 78 | 79 | def connection_https 80 | Faraday.new(:url => server_url, :ssl => { :ca_path => @ssl_certs }) do |c| 81 | connection_config(c) 82 | end 83 | end 84 | 85 | def connection_config(config) 86 | config.request :authorization, 'Bearer', @server_token if @server_token 87 | config.basic_auth(@server_user, @server_pass) if @server_user 88 | config.adapter Faraday.default_adapter 89 | end 90 | 91 | def fail_with(msg) 92 | raise Exception.new(msg) 93 | end 94 | 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /lib/haste/version.rb: -------------------------------------------------------------------------------- 1 | module Haste 2 | VERSION = '0.3.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/examples/uploader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Haste::Uploader do 4 | 5 | let(:uploader) { Haste::Uploader.new(base) } 6 | 7 | describe :upload_raw do 8 | 9 | let(:data) { 'hello world' } 10 | let(:base) { nil } 11 | let(:error_message) do 12 | begin 13 | @key = uploader.upload_raw data 14 | nil # nil otherwise 15 | rescue Haste::Exception => e 16 | e.message 17 | end 18 | end 19 | 20 | context 'with a good response' do 21 | 22 | let(:json) { '{"key":"hello"}' } 23 | 24 | before do 25 | ostruct = OpenStruct.new(:status => 200, :body => json) 26 | expect(uploader.send(:connection)).to receive(:post).with('/documents', data).and_return(ostruct) 27 | end 28 | 29 | it 'should get the key' do 30 | expect(error_message).to be_nil # no error 31 | expect(@key).to eq('hello') 32 | end 33 | 34 | end 35 | 36 | context 'with a bad json response' do 37 | 38 | let(:json) { '{that:not_even_json}' } 39 | 40 | before do 41 | ostruct = OpenStruct.new(:status => 200, :body => json) 42 | expect(uploader.send(:connection)).to receive(:post).with('/documents', data).and_return(ostruct) 43 | end 44 | 45 | it 'should get an error' do 46 | expect(error_message).to start_with('failure parsing response: ') 47 | end 48 | 49 | end 50 | 51 | context 'with a 404 response' do 52 | 53 | before do 54 | ostruct = OpenStruct.new(:status => 404, :body => 'ohno') 55 | expect(uploader.send(:connection)).to receive(:post).with('/documents', data).and_return(ostruct) 56 | end 57 | 58 | it 'should get an error' do 59 | expect(error_message).to eq('failure uploading: ohno') 60 | end 61 | 62 | end 63 | 64 | context 'with a non-existent server' do 65 | 66 | before do 67 | error = Errno::ECONNREFUSED 68 | expect(uploader.send(:connection)).to receive(:post).with('/documents', data).and_raise(error) 69 | end 70 | 71 | it 'should get the key' do 72 | expect(error_message).to eq('failure connecting: Connection refused') 73 | end 74 | 75 | end 76 | 77 | end 78 | 79 | describe :upload_path do 80 | 81 | let(:base) { nil } 82 | let(:error_message) do 83 | begin 84 | uploader.upload_path path 85 | nil # nil otherwise 86 | rescue Haste::Exception => e 87 | e.message 88 | end 89 | end 90 | 91 | context 'with no path given' do 92 | 93 | let(:path) { nil } 94 | 95 | it 'should have an error' do 96 | expect(error_message).to eq('No input file given') 97 | end 98 | 99 | end 100 | 101 | context 'with an invalid path given' do 102 | 103 | let(:path) { '/tmp/why-do-you-have-a-file-called-john' } 104 | 105 | it 'should have an error' do 106 | expect(error_message).to eq("#{path}: No such path") 107 | end 108 | 109 | end 110 | 111 | context 'with a valid path' do 112 | 113 | let(:data) { 'hello world' } 114 | let(:path) { '/tmp/real' } 115 | before { File.open(path, 'w') { |f| f.write(data) } } 116 | 117 | before do 118 | expect(uploader).to receive(:upload_raw).with(data) # check 119 | end 120 | 121 | it 'should not receive an error' do 122 | expect(error_message).to be_nil 123 | end 124 | 125 | end 126 | 127 | end 128 | 129 | describe :post_path do 130 | 131 | let(:post_path) { uploader.send(:post_path) } 132 | 133 | context "when the server URL doesn't have a path" do 134 | 135 | let(:base) { 'http://example.com/' } 136 | 137 | it 'should return /documents' do 138 | expect(post_path).to eq('/documents') 139 | end 140 | 141 | end 142 | 143 | context "when the server URL has a path" do 144 | 145 | let(:base) { 'http://example.com/friend' } 146 | 147 | it 'should return /documents' do 148 | expect(post_path).to eq('/friend/documents') 149 | end 150 | 151 | end 152 | 153 | context "when the server URL has a path that ends with slash" do 154 | 155 | let(:base) { 'http://example.com/friend/' } 156 | 157 | it 'should return /documents appended to the path without a duplicate slash' do 158 | expect(post_path).to eq('/friend/documents') 159 | end 160 | 161 | end 162 | 163 | end 164 | 165 | describe :server_url do 166 | 167 | let(:server_url) { uploader.server_url } 168 | let(:share_server_url) { uploader.share_server_url } 169 | 170 | context 'with default constructor' do 171 | 172 | let(:base) { nil } 173 | 174 | it 'should use the default urls' do 175 | expect(server_url).to eq(Haste::DEFAULT_SERVER_URL) 176 | expect(share_server_url).to eq(Haste::DEFAULT_SHARE_SERVER_URL) 177 | end 178 | 179 | end 180 | 181 | context 'with server url passed in constructor' do 182 | 183 | context 'with a trailing slash' do 184 | 185 | before { @string = 'hello/' } 186 | let(:base) { @string } 187 | 188 | it 'should remove the slash' do 189 | expect(server_url).to eq(@string.chop) 190 | end 191 | 192 | it 'should not modify the original' do 193 | expect(@string).to eq('hello/') 194 | end 195 | 196 | end 197 | 198 | context 'with no trailing slash' do 199 | 200 | let(:base) { 'hello' } 201 | 202 | it 'should not chop the url' do 203 | expect(server_url).to eq(base) 204 | end 205 | 206 | end 207 | 208 | end 209 | 210 | end 211 | 212 | end 213 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | require File.dirname(__FILE__) + '/../lib/haste' 4 | --------------------------------------------------------------------------------