ruby-bepasty-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e832e4200ea4decc2a800db7a43ad8deb8c0049000c29c0cb45679e9f62698d8
4
+ data.tar.gz: 1e0bcf99dc68452e83d4daec6913190dae4ecb715046c10318ffbee4c61600e9
5
+ SHA512:
6
+ metadata.gz: 3ea2c4da75e919b9a353b2084b5558f8916fc99b070ba9d904fabd4f39fdec682a783f78831b48dff83d10fdf56d82486818d9aa29ae6c78d6a7f0db3cb1ca1b
7
+ data.tar.gz: ae41da0e6fb739c7aec2b04fc848f3e64583495acb4051ffa7458719f4c03703a2bb3d4554d139037a231cefacd04d5bc8005e3dfcf9444417d460f80ed720e2
data/.editorconfig ADDED
@@ -0,0 +1,11 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ trim_trailing_whitespace = true
8
+
9
+ [*.rb]
10
+ indent_size = 2
11
+ indent_style = space
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ Gemfile.lock
2
+ pkg
3
+ man/style.css
4
+ man/*.html
5
+ man/**/*.html
6
+ man/man?/*.?
7
+ html_doc
8
+ .gems
9
+ .yardoc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Jakub Skokan
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # ruby-bepasty-client
2
+ Ruby client and CLI for [bepasty-server](https://bepasty-server.readthedocs.org/).
3
+
4
+ ## Usage
5
+ See [bepastyrb.1.md](./man/man1/bepastyrb.1.md).
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'md2man/rakefile'
3
+ require 'md2man/roff/engine'
4
+ require 'md2man/html/engine'
5
+
6
+ # Override markdown engine to add extra parameter
7
+ [Md2Man::Roff, Md2Man::HTML].each do |mod|
8
+ mod.send(:remove_const, :ENGINE)
9
+ mod.send(:const_set, :ENGINE, Redcarpet::Markdown.new(mod.const_get(:Engine),
10
+ tables: true,
11
+ autolink: true,
12
+ superscript: true,
13
+ strikethrough: true,
14
+ no_intra_emphasis: false,
15
+ fenced_code_blocks: true,
16
+ ))
17
+ end
data/bin/bepastyrb ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bepasty-client'
3
+ require 'bepasty-client/cli'
4
+ BepastyClient::Cli.run
@@ -0,0 +1,162 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+
4
+ module BepastyClient
5
+ class Cli
6
+ def self.run
7
+ cli = new
8
+ cli.run
9
+ end
10
+
11
+ def run
12
+ parse
13
+ load_config
14
+
15
+ if @server.nil?
16
+ warn 'Specify server URL using option --server or via config file'
17
+ exit(false)
18
+ end
19
+
20
+ client = Client.new(@server, password: @password, verbose: @verbose)
21
+ client.setup
22
+
23
+ if @files.empty?
24
+ puts client.upload_io(STDIN, **@upload_opts)
25
+ else
26
+ @files.each do |path|
27
+ file_loc = File.open(path, 'r') do |f|
28
+ client.upload_io(f, **@upload_opts)
29
+ end
30
+
31
+ if @files.length > 1
32
+ puts "#{path}: #{file_loc}"
33
+ else
34
+ puts file_loc
35
+ end
36
+ end
37
+ end
38
+
39
+ rescue Error => e
40
+ warn "Error occurred: #{e.message}"
41
+ exit(false)
42
+ end
43
+
44
+ protected
45
+ def parse
46
+ @upload_opts = {}
47
+
48
+ OptionParser.new do |parser|
49
+ parser.banner = "Usage: #{$0} [options] FILE..."
50
+
51
+ parser.on('-h', '--help', 'Show help message and exit') do |v|
52
+ puts parser
53
+ exit
54
+ end
55
+
56
+ parser.on('-v', '--verbose', 'Enable verbose output') do |v|
57
+ @verbose = true
58
+ end
59
+
60
+ parser.on('-s', '--server=SERVER', 'bepasty server URL') do |v|
61
+ @server = v
62
+ end
63
+
64
+ parser.on('-p', '--password=PASSWORD', 'bepasty server password') do |v|
65
+ @password = v
66
+ end
67
+
68
+ parser.on('--password-file=FILE', 'Read bepasty server password from a file') do |v|
69
+ @password = File.read(v).strip
70
+ end
71
+
72
+ parser.on('-f', '--filename=NAME', 'File name including extension') do |v|
73
+ @upload_opts[:filename] = v
74
+ end
75
+
76
+ parser.on('-t', '--content-type=TYPE', 'Content mime type') do |v|
77
+ @upload_opts[:content_type] = v
78
+ end
79
+
80
+ parser.on(
81
+ '--minute=[N]',
82
+ OptionParser::DecimalInteger,
83
+ 'Keep the file for N minutes, defaults to 30 minutes',
84
+ ) do |v|
85
+ @upload_opts[:max_life] = {unit: :minutes, value: v || 30}
86
+ end
87
+
88
+ parser.on(
89
+ '--hour=[N]',
90
+ OptionParser::DecimalInteger,
91
+ 'Keep the file for N hours, defaults to 1 hour',
92
+ ) do |v|
93
+ @upload_opts[:max_life] = {unit: :hours, value: v || 1}
94
+ end
95
+
96
+ parser.on(
97
+ '--day=[N]',
98
+ OptionParser::DecimalInteger,
99
+ 'Keep the file for N days, defaults to 1 day',
100
+ ) do |v|
101
+ @upload_opts[:max_life] = {unit: :days, value: v || 1}
102
+ end
103
+
104
+ parser.on(
105
+ '--week=[N]',
106
+ OptionParser::DecimalInteger,
107
+ 'Keep the file for N weeks, defaults to 1 week',
108
+ ) do |v|
109
+ @upload_opts[:max_life] = {unit: :weeks, value: v || 1}
110
+ end
111
+
112
+ parser.on(
113
+ '--month=[N]',
114
+ OptionParser::DecimalInteger,
115
+ 'Keep the file for N months, defaults to 1 month',
116
+ ) do |v|
117
+ @upload_opts[:max_life] = {unit: :months, value: v || 1}
118
+ end
119
+
120
+ parser.on(
121
+ '--forever',
122
+ 'Keep the file as long as possible',
123
+ ) do |v|
124
+ @upload_opts[:max_life] = {unit: :forever, value: 1}
125
+ end
126
+ end.parse!
127
+
128
+ @files = ARGV.empty? ? [] : ARGV
129
+ end
130
+
131
+ def load_config
132
+ cfg = load_config_file('/etc/bepastyrb.yml')
133
+
134
+ conf_dir = ENV.fetch('XDG_CONFIG_HOME', '')
135
+ conf_dir = File.join(Dir.home, '.config') if conf_dir.empty?
136
+
137
+ cfg.update(load_config_file(File.join(conf_dir, 'bepastyrb.yml')))
138
+
139
+ @verbose = true if cfg[:verbose]
140
+ @server ||= cfg[:server]
141
+ @password ||= cfg[:password]
142
+ @max_life ||= cfg[:max_life]
143
+ end
144
+
145
+ def load_config_file(path)
146
+ cfg = YAML.load_file(path)
147
+ puts "Reading config at #{path}" if @verbose
148
+
149
+ {
150
+ verbose: cfg.fetch('verbose', false),
151
+ server: cfg['server'],
152
+ password: cfg['password_file'] ? File.read(cfg['password_file']).strip : cfg['password'],
153
+ max_life: {
154
+ unit: cfg.fetch('max_life', {}).fetch('unit', 'days').to_sym,
155
+ value: cfg.fetch('max_life', {}).fetch('value', 1),
156
+ },
157
+ }
158
+ rescue Errno::ENOENT
159
+ {}
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,131 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'stringio'
5
+
6
+ module BepastyClient
7
+ class Error < StandardError ; end
8
+
9
+ class Client
10
+ BODY_SIZE = 128*1024
11
+
12
+ # @param server [String] bepasty server URL
13
+ # @param password [String, nil]
14
+ # @param verbose [Boolean]
15
+ def initialize(server, password: nil, verbose: false)
16
+ @server = server
17
+ @password = password
18
+ @verbose = verbose
19
+ end
20
+
21
+ # Fetch server settings
22
+ def setup
23
+ http_start('/apis/rest') do |uri, http|
24
+ res = http.get(uri)
25
+
26
+ case res
27
+ when Net::HTTPSuccess
28
+ settings = JSON.parse(res.body)
29
+
30
+ @max_file_size = settings['MAX_ALLOWED_FILE_SIZE']
31
+ @max_body_size = settings.fetch('MAX_BODY_SIZE', BODY_SIZE)
32
+
33
+ if @verbose
34
+ puts "Max file size = #{@max_file_size}"
35
+ puts "Max body size = #{@max_body_size}"
36
+ end
37
+ else
38
+ raise Error, "Failed to query server settings: HTTP #{res.code}, #{res.message}"
39
+ end
40
+ end
41
+ end
42
+
43
+ # Upload file
44
+ # @param io [IO]
45
+ # @param filename [String, nil]
46
+ # @param content_type [String, nil]
47
+ # @param max_life [Hash, nil]
48
+ # @raise [Error]
49
+ # @return [String] file URL
50
+ def upload_io(io, filename: nil, content_type: nil, max_life: nil)
51
+ if filename.nil? && io.respond_to?(:path)
52
+ filename = File.basename(io.path)
53
+ end
54
+
55
+ max_life ||= {unit: :days, value: 1}
56
+
57
+ sent_bytes = 0
58
+ transaction_id = nil
59
+ file_location = nil
60
+
61
+ if io.respond_to?(:size)
62
+ send_io = io
63
+ else
64
+ send_io = StringIO.new(io.read)
65
+ end
66
+
67
+ if @max_file_size && send_io.size > @max_file_size
68
+ raise Error, "File size #{send_io.size} exceeds server max file size of #{@max_file_size} bytes"
69
+ end
70
+
71
+ http_start('/apis/rest/items') do |uri, http|
72
+ until send_io.eof?
73
+ chunk = send_io.read(@max_body_size)
74
+
75
+ req = Net::HTTP::Post.new(uri)
76
+ req.basic_auth('bepasty', @password) if @password
77
+ req['Content-Range'] = "bytes #{sent_bytes}-#{sent_bytes + chunk.size - 1}/#{send_io.size}"
78
+ req['Transaction-ID'] = transaction_id if transaction_id
79
+ req['Content-Type'] = content_type || ''
80
+ req['Content-Filename'] = filename || ''
81
+ req['Content-Length'] = send_io.size
82
+ req['Maxlife-Unit'] = max_life[:unit].to_s.upcase
83
+ req['Maxlife-Value'] = max_life[:value]
84
+
85
+ req.body = Base64.encode64(chunk)
86
+
87
+ if @verbose
88
+ puts "Uploading chunk, Content-Range=#{req['Content-Range']}, Transaction-ID=#{transaction_id}"
89
+ end
90
+
91
+ res = http.request(req)
92
+
93
+ case res
94
+ when Net::HTTPSuccess
95
+ if transaction_id.nil? && res['Transaction-ID']
96
+ transaction_id = res['Transaction-ID']
97
+ end
98
+
99
+ file_location = res['Content-Location'] if res['Content-Location']
100
+ else
101
+ if res['Content-Type'] == 'application/json'
102
+ err = JSON.parse(res.body)['error']
103
+ raise Error, "bepasty error: code=#{err['code']}, message=#{err['message']}"
104
+ else
105
+ raise Error, "HTTP #{res.code}: #{res.message}"
106
+ end
107
+ end
108
+
109
+ sent_bytes += chunk.size
110
+ end
111
+ end
112
+
113
+ if file_location.nil?
114
+ raise Error, 'Server did not disclose file location'
115
+ end
116
+
117
+ # bepasty returns REST item URL, we want web URL instead
118
+ File.join(@server, file_location.split('/').last)
119
+ end
120
+
121
+ protected
122
+ def http_start(path)
123
+ uri = URI(File.join(@server, path))
124
+ args = [uri.hostname, uri.port] + Array.new(5, nil) + [{use_ssl: uri.scheme == 'https'}]
125
+
126
+ Net::HTTP.start(*args) do |http|
127
+ yield(uri, http)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ module BepastyClient
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ module BepastyClient ; end
2
+
3
+ require_relative 'bepasty-client/client'
4
+ require_relative 'bepasty-client/version'
@@ -0,0 +1,83 @@
1
+ # bepastyrb 8 2023-05-14 1.0.0
2
+
3
+ ## NAME
4
+ `bepastyrb` - upload files to bepasty servers
5
+
6
+ ## SYNOPSIS
7
+ `bepastyrb` [*options*] [*file*...]
8
+
9
+ ## DESCRIPTION
10
+ `bepastyrb` uploads one or more files to bepasty servers. If no *file* is given
11
+ as an argument, `bepastyrb` will read from standard input.
12
+
13
+ bepasty server URL needs to be given using option `-s`, `--server` or can be
14
+ set in a config file, see `CONFIG FILES`.
15
+
16
+ If no life time is set, it defaults to one day.
17
+
18
+ ## OPTIONS
19
+ `-v`, `--verbose`
20
+ Enable verbose output.
21
+
22
+ `-s`, `--server`=*SERVER*
23
+ bepasty server URL.
24
+
25
+ `-p`, `--password`=*PASSWORD*
26
+ bepasty server password.
27
+
28
+ `--password-file`=*FILE*
29
+ Read bepasty server password from a file.
30
+
31
+ `-f`, `--filename`=*NAME*
32
+ File name including extension. Can be used to provide name for files read
33
+ from standard input or to override given file name.
34
+
35
+ `-t`, `--content-type`=*TYPE*
36
+ Content mime type. If not given, the mime type is guessed by the bepasty server
37
+ based on file name.
38
+
39
+ `--minute`=[*N*]
40
+ Keep the file for *N* minutes, defaults to 30 minutes.
41
+
42
+ `--hour`=[*N*]
43
+ Keep the file for *N* hours, defaults to 1 hour.
44
+
45
+ `--day`=[*N*]
46
+ Keep the file for *N* days, defaults to 1 day.
47
+
48
+ `--week`=[*N*]
49
+ Keep the file for *N* weeks, defaults to 1 week.
50
+
51
+ `--month`=[*N*]
52
+ Keep the file for *N* months, defaults to 1 month.
53
+
54
+ `--forever`
55
+ Keep the file as long as possible.
56
+
57
+ ## CONFIG FILES
58
+ Config files can be used to provide default values for certain options.
59
+ Command-line options override settings found in config files. Config files are
60
+ read from the following locations:
61
+
62
+ - `/etc/bepastyrb.yml`
63
+ - `$XDG_CONFIG_HOME/bepastyrb.yml` or `~/.config/bepastyrb.yml`
64
+
65
+ Settings from user's config will override settings in `/etc`.
66
+
67
+ Example configuration:
68
+
69
+ ```
70
+ # bepasty config file in YAML
71
+ server: https://bepasty.yourserver.tld
72
+ # password: optional password if the server uses it
73
+ # password_file: read password from a file
74
+ # max_life:
75
+ # unit: minutes|hours|days|weeks|months|forever
76
+ # value: 14
77
+ ```
78
+
79
+ ## SEE ALSO
80
+ bepasty server: https://bepasty-server.readthedocs.org/
81
+
82
+ ## BUGS
83
+ Report bugs to https://github.com/aither64/ruby-bepasty-client.
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bepasty-client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ruby-bepasty-client'
8
+ spec.version = BepastyClient::VERSION
9
+ spec.authors = ['Jakub Skokan']
10
+ spec.email = ['jakub.skokan@vpsfree.cz']
11
+ spec.summary =
12
+ spec.description = 'Ruby client and CLI for bepasty'
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler'
22
+ spec.add_development_dependency 'md2man'
23
+ spec.add_development_dependency 'rake'
24
+ end
data/shell.nix ADDED
@@ -0,0 +1,21 @@
1
+ let
2
+ pkgs = import <nixpkgs> {};
3
+ stdenv = pkgs.stdenv;
4
+
5
+ in stdenv.mkDerivation rec {
6
+ name = "ruby-bepasty-client";
7
+
8
+ buildInputs = with pkgs;[
9
+ ruby
10
+ git
11
+ openssl
12
+ ];
13
+
14
+ shellHook = ''
15
+ export GEM_HOME=$(pwd)/.gems
16
+ export PATH="$GEM_HOME/bin:$PATH"
17
+ export MANPATH="$(pwd)/man:$(man --path)"
18
+ gem install bundler
19
+ bundle install
20
+ '';
21
+ }
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-bepasty-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jakub Skokan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: md2man
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Ruby client and CLI for bepasty
56
+ email:
57
+ - jakub.skokan@vpsfree.cz
58
+ executables:
59
+ - bepastyrb
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".editorconfig"
64
+ - ".gitignore"
65
+ - Gemfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - bin/bepastyrb
70
+ - lib/bepasty-client.rb
71
+ - lib/bepasty-client/cli.rb
72
+ - lib/bepasty-client/client.rb
73
+ - lib/bepasty-client/version.rb
74
+ - man/man1/bepastyrb.1.md
75
+ - ruby-bepasty-client.gemspec
76
+ - shell.nix
77
+ homepage: ''
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.3.20
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Ruby client and CLI for bepasty
100
+ test_files: []