bunbun 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +4 -0
  3. data/README.md +76 -0
  4. data/bin/bunny +14 -0
  5. data/bunbun.gemspec +26 -0
  6. data/lib/bunbun/body.rb +53 -0
  7. data/lib/bunbun/cli/command.rb +97 -0
  8. data/lib/bunbun/cli/countries.rb +12 -0
  9. data/lib/bunbun/cli/keys.rb +13 -0
  10. data/lib/bunbun/cli/purge.rb +5 -0
  11. data/lib/bunbun/cli/regions.rb +16 -0
  12. data/lib/bunbun/cli/storage_command.rb +21 -0
  13. data/lib/bunbun/cli/storage_delete.rb +9 -0
  14. data/lib/bunbun/cli/storage_download.rb +9 -0
  15. data/lib/bunbun/cli/storage_edit.rb +29 -0
  16. data/lib/bunbun/cli/storage_files.rb +22 -0
  17. data/lib/bunbun/cli/storage_upload.rb +20 -0
  18. data/lib/bunbun/cli/storage_zone.rb +5 -0
  19. data/lib/bunbun/cli/storage_zone_create.rb +5 -0
  20. data/lib/bunbun/cli/storage_zones.rb +17 -0
  21. data/lib/bunbun/cli/zone.rb +5 -0
  22. data/lib/bunbun/cli/zone_create.rb +5 -0
  23. data/lib/bunbun/cli/zone_purge.rb +5 -0
  24. data/lib/bunbun/cli/zone_rules_delete.rb +5 -0
  25. data/lib/bunbun/cli/zone_rules_disenable.rb +5 -0
  26. data/lib/bunbun/cli/zone_rules_enable.rb +5 -0
  27. data/lib/bunbun/cli/zone_rules_post.rb +5 -0
  28. data/lib/bunbun/cli/zones.rb +18 -0
  29. data/lib/bunbun/cli.rb +123 -0
  30. data/lib/bunbun/client/api_key.rb +7 -0
  31. data/lib/bunbun/client/country.rb +7 -0
  32. data/lib/bunbun/client/namespace.rb +9 -0
  33. data/lib/bunbun/client/pull_zone/edge_rules.rb +33 -0
  34. data/lib/bunbun/client/pull_zone.rb +37 -0
  35. data/lib/bunbun/client/region.rb +7 -0
  36. data/lib/bunbun/client/statistics.rb +16 -0
  37. data/lib/bunbun/client/storage_zone/statistics.rb +10 -0
  38. data/lib/bunbun/client/storage_zone.rb +40 -0
  39. data/lib/bunbun/client.rb +140 -0
  40. data/lib/bunbun/response.rb +39 -0
  41. data/lib/bunbun/uri.rb +32 -0
  42. data/lib/bunbun.rb +15 -0
  43. metadata +141 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2baeb841135209850e6ce3f030cb5751024fae83144edb11ada150534f86f3e0
4
+ data.tar.gz: af7fe28e6bd0dccb06d43ab23e85514b9a1e94975af9951d0b474492dca19615
5
+ SHA512:
6
+ metadata.gz: b1d0081d994305b6db4882f03d8d13e8e8d6401a07cb167ed5d174ba18ac805e7abbd6a4bc5c8aea74a90b0720533a2edc9ccae5014c82dbc4a1a38f066b0c67
7
+ data.tar.gz: 10bbc9a3d6d1918c16a902d30f1254b868d4658d493c565f904de1530369988f7916c913394f4aca1d38f811bb2d7d5b29e676bd036b0b42c1e61eb5bc0f7056
data/LICENSE.txt ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) 2025 TIMCRAFT
2
+
3
+ This is an Open Source project licensed under the terms of the LGPLv3 license.
4
+ Please see <http://www.gnu.org/licenses/lgpl-3.0.html> for license text.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # bunbun
2
+
3
+ Ruby client for the [bunny.net](https://bunny.net) [API](https://docs.bunny.net/reference/bunnynet-api-overview).
4
+
5
+
6
+ ## Installation
7
+
8
+ Using Bundler:
9
+
10
+ $ bundle add bunbun
11
+
12
+ Using RubyGems:
13
+
14
+ $ gem install bunbun
15
+
16
+
17
+ ## Usage
18
+
19
+ For accessing the bunny.net API:
20
+
21
+ ```ruby
22
+ require 'bunbun'
23
+
24
+ client = BunBun::Client.new(access_key: access_key)
25
+
26
+ client.storage_zone.list.each do |zone|
27
+ puts zone.values_at('Id', 'Name', 'FilesStored').join(' )
28
+ end
29
+ ```
30
+
31
+ For accessing files on edge storage:
32
+
33
+ ```ruby
34
+ require 'bunbun'
35
+
36
+ file = 'image.jpg'
37
+
38
+ zone = 'zone-name'
39
+
40
+ host = 'storage.bunnycdn.com'
41
+
42
+ client = BunBun::Client.new(access_key: access_key, host: host)
43
+
44
+ client.download("/#{zone}/#{file}", file)
45
+ ```
46
+
47
+ You can store your access key in a `BUNNY_ACCESS_KEY` environment variable,
48
+ and the storage host in a `BUNNY_HOST` environment variable.
49
+
50
+
51
+ ## CLI
52
+
53
+ The gem also includes a command line tool.
54
+
55
+ Store your access keys in your .netrc file, for example:
56
+
57
+ ```
58
+ machine api.bunny.net
59
+ login user
60
+ password xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
61
+ machine storage.bunnycdn.com
62
+ login zone-name
63
+ password xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx-xxxx-xxxx
64
+ ```
65
+
66
+ Specify the access key for the bunny.net API, and any additional keys for
67
+ accessing edge storage. The login for the api.bunny.net entry can be anything,
68
+ the login for each storage entry should be the name of the storage zone.
69
+
70
+ You can then use the `bunny` command line tool like this:
71
+
72
+ ```
73
+ $ bunny storage zones
74
+ ```
75
+
76
+ Run the tool without any arguments to see a list of available commands.
data/bin/bunny ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+
8
+ gem 'bunbun', '~> 1'
9
+ gem 'netrc'
10
+ gem 'tabulo', '~> 3'
11
+ end
12
+
13
+ cli = BunBun::CLI.new
14
+ cli.call(ARGV)
data/bunbun.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ require_relative './lib/bunbun'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'bunbun'
5
+ s.version = BunBun::VERSION
6
+ s.license = 'LGPL-3.0'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Tim Craft']
9
+ s.email = ['email@timcraft.com']
10
+ s.homepage = 'https://github.com/readysteady/bunbun'
11
+ s.description = 'Ruby client for bunny.net'
12
+ s.summary = 'Ruby client for bunny.net'
13
+ s.files = Dir.glob('lib/**/*.rb') + %w[LICENSE.txt README.md bunbun.gemspec]
14
+ s.required_ruby_version = '>= 3.3.0'
15
+ s.require_path = 'lib'
16
+ s.executables = ['bunny']
17
+ s.metadata = {
18
+ 'homepage' => 'https://github.com/readysteady/bunbun',
19
+ 'source_code_uri' => 'https://github.com/readysteady/bunbun',
20
+ 'bug_tracker_uri' => 'https://github.com/readysteady/bunbun/issues'
21
+ }
22
+ s.add_dependency 'base64'
23
+ s.add_dependency 'digest', '~> 3'
24
+ s.add_dependency 'net-http'
25
+ s.add_dependency 'json', '~> 2'
26
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BunBun::Body
4
+ def self.transform_keys(object)
5
+ if object.is_a?(Hash)
6
+ hash = {}
7
+ object.each { |key, value| hash[transform(key)] = transform_keys(value) }
8
+ hash
9
+ elsif object.is_a?(Array)
10
+ object.map { transform_keys(_1) }
11
+ else
12
+ object
13
+ end
14
+ end
15
+
16
+ def self.transform(key)
17
+ key = key.to_s
18
+
19
+ return key if key.chars.first.between?('A', 'Z')
20
+
21
+ transforms[key] ||= key.split('_').map(&:capitalize).join
22
+ end
23
+
24
+ def self.transforms
25
+ @transforms ||= {}
26
+ end
27
+
28
+ transforms['aws_signing_enabled'] = 'AWSSigningEnabled'
29
+ transforms['aws_signing_key'] = 'AWSSigningKey'
30
+ transforms['aws_signing_region_name'] = 'AWSSigningRegionName'
31
+ transforms['aws_signing_secret'] = 'AWSSigningSecret'
32
+ transforms['connection_limit_per_ip_count'] = 'ConnectionLimitPerIPCount'
33
+ transforms['enable_auto_ssl'] = 'EnableAutoSSL'
34
+ transforms['enable_geo_zone_af'] = 'EnableGeoZoneAF'
35
+ transforms['enable_geo_zone_asia'] = 'EnableGeoZoneASIA'
36
+ transforms['enable_geo_zone_eu'] = 'EnableGeoZoneEU'
37
+ transforms['enable_geo_zone_sa'] = 'EnableGeoZoneSA'
38
+ transforms['enable_geo_zone_us'] = 'EnableGeoZoneUS'
39
+ transforms['enable_tls1'] = 'EnableTLS1'
40
+ transforms['enable_tls1_1'] = 'EnableTLS1_1'
41
+ transforms['logging_ip_anonymization_enabled'] = 'LoggingIPAnonymizationEnabled'
42
+ transforms['optimizer_enable_webp'] = 'OptimizerEnableWebP'
43
+ transforms['optimizer_minify_css'] = 'OptimizerMinifyCSS'
44
+ transforms['origin_retry_5xx_responses'] = 'OriginRetry5XXResponses'
45
+ transforms['shield_ddos_protection_enabled'] = 'ShieldDDosProtectionEnabled'
46
+ transforms['shield_ddos_protection_type'] = 'ShieldDDosProtectionType'
47
+ transforms['verify_origin_ssl'] = 'VerifyOriginSSL'
48
+ transforms['waf_disabled_rules'] = 'WAFDisabledRules'
49
+ transforms['waf_disabled_rule_groups'] = 'WAFDisabledRuleGroups'
50
+ transforms['waf_enabled'] = 'WAFEnabled'
51
+ transforms['waf_enable_request_header_logging'] = 'WAFEnableRequestHeaderLogging'
52
+ transforms['waf_request_header_ignores'] = 'WAFRequestHeaderIgnores'
53
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+ require 'optparse'
3
+ require 'tabulo'
4
+ require 'json'
5
+
6
+ class BunBun::CLI::Command
7
+ def self.options
8
+ @options ||= []
9
+ end
10
+
11
+ def self.option(name)
12
+ options << name
13
+ end
14
+
15
+ def self.argument_names
16
+ @argument_names ||= instance_method(:call).parameters.select { _1[0] == :req || _1[0] == :opt }.map { _1[1] }
17
+ end
18
+
19
+ def self.argument_count
20
+ argument_names.length
21
+ end
22
+
23
+ def option_parser
24
+ OptionParser.new do |opts|
25
+ self.class.options.each do |name|
26
+ opts.on("--#{name}=VALUE")
27
+ end
28
+ end
29
+ end
30
+
31
+ attr_accessor :options
32
+
33
+ def call
34
+ end
35
+
36
+ private
37
+
38
+ NETRC = Netrc.read
39
+
40
+ def get_access_key(host, zone_name = nil)
41
+ if zone_name.nil?
42
+ netrc_entry = NETRC[host]
43
+ raise BunBun::CLI::Error, "no netrc entry for #{host}" if netrc_entry.nil?
44
+ netrc_entry.password
45
+ else
46
+ netrc_entry = NETRC.each.find { _1[1] == host && _1[3] == zone_name }
47
+ raise BunBun::CLI::Error, "no netrc entry for #{host} (#{zone_name})" if netrc_entry.nil?
48
+ netrc_entry[5]
49
+ end
50
+ end
51
+
52
+ def read_body_params(file)
53
+ JSON.parse(File.read(file))
54
+ rescue Errno::ENOENT
55
+ raise BunBun::CLI::Error, 'file does not exist'
56
+ rescue JSON::ParserError
57
+ raise BunBun::CLI::Error, 'file must be valid json'
58
+ end
59
+
60
+ def client
61
+ @client ||= BunBun::Client.new(access_key: get_access_key('api.bunny.net'))
62
+ end
63
+
64
+ BYTESIZE_UNITS = {
65
+ GB: 1_000_000_000,
66
+ MB: 1_000_000,
67
+ KB: 1_000
68
+ }
69
+
70
+ def format_bytesize(integer)
71
+ BYTESIZE_UNITS.each do |suffix, size|
72
+ if integer >= size
73
+ return "#{(integer.to_f / size).round(2)} #{suffix}"
74
+ end
75
+ end
76
+
77
+ "#{integer} bytes"
78
+ end
79
+
80
+ def format_timestamp(string)
81
+ string.split('.').first.tr('T', ' ')
82
+ end
83
+
84
+ def print_json(object, io: STDOUT)
85
+ io.puts JSON.pretty_generate(object)
86
+ end
87
+
88
+ def print_table(array, io: STDOUT)
89
+ return if array.empty?
90
+
91
+ table = Tabulo::Table.new(array, border: :modern)
92
+
93
+ yield table
94
+
95
+ io.puts table.pack
96
+ end
97
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::Countries < BunBun::CLI::Command
4
+ def call
5
+ items = client.country.list
6
+
7
+ print_table(items) do |t|
8
+ t.add_column('Name', align_header: :left) { _1['Name'] }
9
+ t.add_column('Code', align_header: :left) { _1['IsoCode'] }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::Keys < BunBun::CLI::Command
4
+ def call
5
+ items = client.api_key.list
6
+
7
+ print_table(items.fetch('Items')) do |t|
8
+ t.add_column('ID', align_header: :left) { _1['Id'] }
9
+ t.add_column('Key', align_header: :left) { _1['Key'] }
10
+ t.add_column('Roles', align_header: :left) { _1['Roles'].join(', ') }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::Purge < BunBun::CLI::Command
2
+ def call(url)
3
+ client.purge(url)
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::Regions < BunBun::CLI::Command
4
+ def call
5
+ items = client.region.list
6
+
7
+ print_table(items) do |t|
8
+ t.add_column('ID', align_header: :right) { _1['Id'] }
9
+ t.add_column('Name', align_header: :left) { _1['Name'] }
10
+ t.add_column('Region', align_header: :left) { _1['RegionCode'] }
11
+ t.add_column('Country', align_header: :left) { _1['CountryCode'] }
12
+ t.add_column('Latitude', align_header: :right) { _1['Latitude'] }
13
+ t.add_column('Longitude', align_header: :right) { _1['Longitude'] }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::StorageCommand < BunBun::CLI::Command
4
+ attr_accessor :storage_zone
5
+ attr_accessor :storage_host
6
+ attr_accessor :storage_access_key
7
+
8
+ def call(path)
9
+ unless path =~ /\A\/([\w\-]+)\//
10
+ raise BunBun::CLI::Error, 'path must start with a zone name'
11
+ end
12
+
13
+ self.storage_zone = $1
14
+
15
+ self.storage_host = options[:host] || 'storage.bunnycdn.com'
16
+
17
+ self.storage_access_key = get_access_key(storage_host, storage_zone)
18
+
19
+ @client = BunBun::Client.new(host: storage_host, access_key: storage_access_key)
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ class BunBun::CLI::StorageDelete < BunBun::CLI::StorageCommand
2
+ option :host
3
+
4
+ def call(path)
5
+ super
6
+
7
+ client.delete(path)
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class BunBun::CLI::StorageDownload < BunBun::CLI::StorageCommand
2
+ option :host
3
+
4
+ def call(path)
5
+ super
6
+
7
+ client.download(path, File.basename(path))
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::StorageEdit < BunBun::CLI::StorageCommand
4
+ option :host
5
+
6
+ def call(path)
7
+ super
8
+
9
+ response = client.download(path)
10
+
11
+ unless response.content_type.start_with?('text/') || response.content_type == 'application/json'
12
+ raise BunBun::CLI::Error, "cannot edit #{response.content_type} files"
13
+ end
14
+
15
+ text = response.body
16
+
17
+ pipe = IO.popen(ENV.fetch('EDITOR'), 'w+')
18
+ pipe.puts(text)
19
+ pipe.close_write
20
+
21
+ edited_text = pipe.read
22
+
23
+ pipe.close
24
+
25
+ unless edited_text.empty? || edited_text == text
26
+ client.upload(path, edited_text, content_type: response.content_type)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::StorageFiles < BunBun::CLI::StorageCommand
4
+ option :host
5
+
6
+ def call(path)
7
+ unless path.end_with?('/')
8
+ raise BunBun::CLI::Error, 'path must end with a trailing slash'
9
+ end
10
+
11
+ super
12
+
13
+ items = client.get(path)
14
+
15
+ print_table(items) do |t|
16
+ t.add_column('Name', align_header: :left) { _1['ObjectName'] }
17
+ t.add_column('Size') { format_bytesize(_1['Length']) unless _1['Length'].zero? }
18
+ t.add_column('Created', align_header: :right) { format_timestamp(_1['DateCreated']) }
19
+ t.add_column('Modified', align_header: :right) { format_timestamp(_1['LastChanged']) }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require 'shellwords'
3
+
4
+ class BunBun::CLI::StorageUpload < BunBun::CLI::StorageCommand
5
+ option :host
6
+
7
+ def call(path, file)
8
+ unless path.end_with?('/')
9
+ raise BunBun::CLI::Error, 'path must end with a trailing slash'
10
+ end
11
+
12
+ super
13
+
14
+ content_type = `file -b --mime-type #{Shellwords.escape(file)}`.chomp
15
+
16
+ File.open(file, 'rb') do |io|
17
+ client.upload(path + File.basename(file), io, content_type: content_type)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::StorageZone < BunBun::CLI::Command
2
+ def call(id)
3
+ print_json client.storage_zone.get(id)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::StorageZoneCreate < BunBun::CLI::Command
2
+ def call(file)
3
+ client.storage_zone.create(read_body_params(file))
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::StorageZones < BunBun::CLI::Command
4
+ def call
5
+ items = client.storage_zone.list
6
+
7
+ print_table(items.sort_by { _1['Name'] }) do |t|
8
+ t.add_column('ID', align_header: :right) { _1['Id'] }
9
+ t.add_column('Name', align_header: :left) { _1['Name'] }
10
+ t.add_column('Region', align_header: :left) { _1['Region'] }
11
+ t.add_column('Hostname', align_header: :left) { _1['StorageHostname'] }
12
+ t.add_column('Size', align_header: :left) { format_bytesize(_1['StorageUsed']) }
13
+ t.add_column('Files', align_header: :right) { _1['FilesStored'] }
14
+ t.add_column('Modified', align_header: :right) { format_timestamp(_1['DateModified']) }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::Zone < BunBun::CLI::Command
2
+ def call(id)
3
+ print_json client.pull_zone.get(id)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::ZoneCreate < BunBun::CLI::Command
2
+ def call(file)
3
+ client.pull_zone.create(read_body_params(file))
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::ZonePurge < BunBun::CLI::Command
2
+ def call(id)
3
+ client.pull_zone.purge(id)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::ZoneRulesDelete < BunBun::CLI::Command
2
+ def call(zone_id, rule_id)
3
+ client.pull_zone.edge_rules.delete(zone_id, rule_id)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::ZoneRulesDisenable < BunBun::CLI::Command
2
+ def call(zone_id, rule_id)
3
+ client.pull_zone.edge_rules.disenable(zone_id, rule_id)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::ZoneRulesEnable < BunBun::CLI::Command
2
+ def call(zone_id, rule_id)
3
+ client.pull_zone.edge_rules.enable(zone_id, rule_id)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class BunBun::CLI::ZoneRulesPost < BunBun::CLI::Command
2
+ def call(zone_id, file)
3
+ client.pull_zone.edge_rules.post(zone_id, read_body_params(file))
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::CLI::Zones < BunBun::CLI::Command
4
+ def call
5
+ items = client.pull_zone.list
6
+
7
+ print_table(items) do |t|
8
+ t.add_column('ID', align_header: :right) { _1['Id'] }
9
+ t.add_column('Name', align_header: :left) { _1['Name'] }
10
+ t.add_column('Storage Zone', align_header: :right) { _1['StorageZoneId'] }
11
+ t.add_column('Bandwidth') { format_bytesize(_1['MonthlyBandwidthUsed']) }
12
+ t.add_column('Domain', align_header: :left) { |zone|
13
+ hostnames = zone['Hostnames'].reject { _1['IsSystemHostname'] || _1['IsManagedHostname'] }
14
+ hostnames.map { _1['Value'] }.first
15
+ }
16
+ end
17
+ end
18
+ end
data/lib/bunbun/cli.rb ADDED
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+ require 'netrc'
3
+ require 'optparse'
4
+
5
+ class BunBun::CLI
6
+ Error = Class.new(StandardError)
7
+
8
+ autoload :Command, 'bunbun/cli/command'
9
+ autoload :StorageCommand, 'bunbun/cli/storage_command'
10
+
11
+ def self.commands
12
+ @commands ||= []
13
+ end
14
+
15
+ def self.command(key)
16
+ autoload_path = 'bunbun/cli/' + key.join('_')
17
+
18
+ autoload command_class_name(key), autoload_path
19
+
20
+ commands << key
21
+ end
22
+
23
+ def self.command_class_name(key)
24
+ key.map(&:capitalize).join.to_sym
25
+ end
26
+
27
+ command %w[countries]
28
+ command %w[keys]
29
+ command %w[purge]
30
+ command %w[regions]
31
+ command %w[storage delete]
32
+ command %w[storage download]
33
+ command %w[storage edit]
34
+ command %w[storage files]
35
+ command %w[storage upload]
36
+ command %w[storage zone create]
37
+ command %w[storage zone]
38
+ command %w[storage zones]
39
+ command %w[zone create]
40
+ command %w[zone purge]
41
+ command %w[zone rules delete]
42
+ command %w[zone rules disenable]
43
+ command %w[zone rules enable]
44
+ command %w[zone rules post]
45
+ command %w[zone]
46
+ command %w[zones]
47
+
48
+ def call(args)
49
+ if args.empty? || args.size == 1 && args.first == '--help'
50
+ print_usage
51
+
52
+ return
53
+ end
54
+
55
+ option_args, command_args = args.partition { _1.start_with?('-') }
56
+
57
+ key = self.class.commands.find { command_args.first(_1.length) == _1 }
58
+
59
+ if key.nil?
60
+ error "#{command_args.join(' ')} is not a valid command"
61
+ end
62
+
63
+ command_class = get_command_class(key)
64
+
65
+ command = command_class.new
66
+
67
+ if option_parser = command.option_parser
68
+ command.options = {}
69
+
70
+ option_parser.parse(option_args, into: command.options)
71
+ end
72
+
73
+ command_args = command_args.drop(key.length)
74
+
75
+ if command_args.length < command_class.argument_count
76
+ error "not enough arguments (expected #{command_class.argument_count})"
77
+ elsif command_args.length > command_class.argument_count
78
+ error "too many arguments (expected #{command_class.argument_count})"
79
+ end
80
+
81
+ command.call(*command_args)
82
+ rescue OptionParser::InvalidOption
83
+ error 'invalid option'
84
+ rescue OptionParser::MissingArgument
85
+ error 'missing option argument'
86
+ rescue BunBun::Error, BunBun::CLI::Error => exception
87
+ error exception.message
88
+ end
89
+
90
+ private
91
+
92
+ def get_command_class(key)
93
+ BunBun::CLI.const_get(self.class.command_class_name(key))
94
+ end
95
+
96
+ def print_usage(io: STDOUT, program_name: File.basename($0))
97
+ io.puts "Usage: #{program_name} COMMAND"
98
+ io.puts
99
+
100
+ self.class.commands.each do |key|
101
+ command_class = get_command_class(key)
102
+
103
+ arguments = command_class.argument_names.map { ' ' + _1.to_s.upcase }.join
104
+
105
+ options = command_class.options.map { " [--#{_1}=#{_1.upcase}]" }.join
106
+
107
+ io.puts " #{program_name} #{key.join(' ')}#{arguments}#{options}"
108
+ end
109
+
110
+ io.puts
111
+ end
112
+
113
+ def error(message)
114
+ message = 'ERROR: ' + message
115
+ message = color_red(message) if STDERR.isatty
116
+
117
+ Kernel::abort(message)
118
+ end
119
+
120
+ def color_red(text)
121
+ "\e[#{31}m#{text}\e[m"
122
+ end
123
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::Client::ApiKey < BunBun::Client::Namespace
4
+ def list
5
+ @client.get('/apikey')
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::Client::Country < BunBun::Client::Namespace
4
+ def list
5
+ @client.get('/country')
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class BunBun::Client::Namespace
2
+ def initialize(client)
3
+ @client = client
4
+ end
5
+
6
+ def inspect
7
+ "<#{self.class.name}>"
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::Client::PullZone::EdgeRules < BunBun::Client::Namespace
4
+ def post(zone_id, body_params)
5
+ @client.post("/pullzone/#{zone_id}/edgerules/addOrUpdate", BunBun::Body.transform_keys(body_params))
6
+ end
7
+
8
+ alias_method :create, :post
9
+
10
+ def delete(zone_id, rule_id)
11
+ @client.delete("/pullzone/#{zone_id}/edgerules/#{rule_id}")
12
+ end
13
+
14
+ def disenable(zone_id, rule_id)
15
+ body_params = {
16
+ Id: Integer(zone_id),
17
+ Value: false
18
+ }
19
+
20
+ @client.post("/pullzone/#{zone_id}/edgerules/#{rule_id}/setEdgeRuleEnabled", body_params)
21
+ end
22
+
23
+ def enable(zone_id, rule_id)
24
+ body_params = {
25
+ Id: Integer(zone_id),
26
+ Value: true
27
+ }
28
+
29
+ @client.post("/pullzone/#{zone_id}/edgerules/#{rule_id}/setEdgeRuleEnabled", body_params)
30
+ end
31
+
32
+ alias_method :update, :post
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::Client::PullZone < BunBun::Client::Namespace
4
+ autoload :EdgeRules, 'bunbun/client/pull_zone/edge_rules'
5
+
6
+ def initialize(client)
7
+ super
8
+
9
+ @edge_rules = EdgeRules.new(client)
10
+ end
11
+
12
+ def create(body_params)
13
+ @client.post('/pullzone', BunBun::Body.transform_keys(body_params))
14
+ end
15
+
16
+ def delete(id)
17
+ @client.delete("/pullzone/#{id}")
18
+ end
19
+
20
+ attr_reader :edge_rules
21
+
22
+ def get(id)
23
+ @client.get("/pullzone/#{id}")
24
+ end
25
+
26
+ def list
27
+ @client.get('/pullzone')
28
+ end
29
+
30
+ def purge(id)
31
+ @client.post("/pullzone/#{id}/purgeCache")
32
+ end
33
+
34
+ def update(id, body_params)
35
+ @client.post("/pullzone/#{id}", BunBun::Body.transform_keys(body_params))
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::Client::Region < BunBun::Client::Namespace
4
+ def list
5
+ @client.get('/region')
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::Client::Statistics < BunBun::Client::Namespace
4
+ def get(date_from: nil, date_to: nil, pull_zone: nil, server_zone_id: nil, load_errors: nil, hourly: nil)
5
+ params = {
6
+ dateFrom: date_from,
7
+ dateTo: date_to,
8
+ pullZone: pull_zone,
9
+ serverZoneId: server_zone_id,
10
+ loadErrors: load_errors,
11
+ hourly: hourly
12
+ }
13
+
14
+ @client.get(BunBun::URI.join('/statistics', params))
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ class BunBun::Client::StorageZone::Statistics < BunBun::Client::Namespace
2
+ def get(id, date_from: nil, date_to: nil)
3
+ params = {
4
+ dateFrom: date_from,
5
+ dateTo: date_to
6
+ }
7
+
8
+ @client.get(BunBun::URI.join("/storagezone/#{id}/statistics", params))
9
+ end
10
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BunBun::Client::StorageZone < BunBun::Client::Namespace
4
+ autoload :Statistics, 'bunbun/client/storage_zone/statistics'
5
+
6
+ def initialize(client)
7
+ super
8
+
9
+ @statistics = Statistics.new(client)
10
+ end
11
+
12
+ def create(body_params)
13
+ @client.post('/storagezone', BunBun::Body.transform_keys(body_params))
14
+ end
15
+
16
+ def delete(id)
17
+ @client.delete("/storagezone/#{id}")
18
+ end
19
+
20
+ def get(id)
21
+ @client.get("/storagezone/#{id}")
22
+ end
23
+
24
+ def list(page: nil, per_page: nil, include_deleted: nil, search: nil)
25
+ params = {
26
+ page: page,
27
+ perPage: per_page,
28
+ includeDeleted: include_deleted,
29
+ search: search
30
+ }
31
+
32
+ @client.get(BunBun::URI.join('/storagezone', params))
33
+ end
34
+
35
+ attr_reader :statistics
36
+
37
+ def update(id, body_params)
38
+ @client.post("/storagezone/#{id}", BunBun::Body.transform_keys(body_params))
39
+ end
40
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ class BunBun::Client
6
+ autoload :ApiKey, 'bunbun/client/api_key'
7
+ autoload :Country, 'bunbun/client/country'
8
+ autoload :Namespace, 'bunbun/client/namespace'
9
+ autoload :PullZone, 'bunbun/client/pull_zone'
10
+ autoload :Region, 'bunbun/client/region'
11
+ autoload :Statistics, 'bunbun/client/statistics'
12
+ autoload :StorageZone, 'bunbun/client/storage_zone'
13
+
14
+ def initialize(access_key: nil, host: nil, open_timeout: 2, read_timeout: 2)
15
+ @access_key = access_key || ENV.fetch('BUNNY_ACCESS_KEY')
16
+ @host = host || ENV['BUNNY_HOST'] || 'api.bunny.net'
17
+ @port = Net::HTTP.https_default_port
18
+ @opts = {
19
+ use_ssl: true,
20
+ open_timeout: open_timeout,
21
+ read_timeout: read_timeout
22
+ }
23
+ @user_agent = "ruby/#{RUBY_VERSION} bunbun/#{BunBun::VERSION}"
24
+ @api_key = BunBun::Client::ApiKey.new(self)
25
+ @country = BunBun::Client::Country.new(self)
26
+ @pull_zone = BunBun::Client::PullZone.new(self)
27
+ @region = BunBun::Client::Region.new(self)
28
+ @statistics = BunBun::Client::Statistics.new(self)
29
+ @storage_zone = BunBun::Client::StorageZone.new(self)
30
+ end
31
+
32
+ attr_reader :api_key
33
+
34
+ attr_reader :country
35
+
36
+ def delete(path)
37
+ start do |http|
38
+ message = Net::HTTP::Delete.new(path)
39
+ message['AccessKey'] = @access_key
40
+ message['User-Agent'] = @user_agent
41
+
42
+ BunBun::Response.parse(http.request(message))
43
+ end
44
+ end
45
+
46
+ def download(path, destination = nil)
47
+ start do |http|
48
+ message = Net::HTTP::Get.new(path)
49
+ message['Accept'] = '*/*'
50
+ message['AccessKey'] = @access_key
51
+ message['User-Agent'] = @user_agent
52
+
53
+ http.request(message) do |response|
54
+ unless response.is_a?(Net::HTTPSuccess)
55
+ raise BunBun::Response.error(response)
56
+ end
57
+
58
+ if destination
59
+ File.open(destination, 'wb') do |file|
60
+ response.read_body do |chunk|
61
+ file.write(chunk)
62
+ end
63
+ end
64
+ end
65
+
66
+ response
67
+ end
68
+ end
69
+ end
70
+
71
+ def get(path)
72
+ start do |http|
73
+ message = Net::HTTP::Get.new(path)
74
+ message['Accept'] = 'application/json'
75
+ message['AccessKey'] = @access_key
76
+ message['User-Agent'] = @user_agent
77
+
78
+ BunBun::Response.parse(http.request(message))
79
+ end
80
+ end
81
+
82
+ def inspect
83
+ "<#{self.class.name}: host=#{@host}>"
84
+ end
85
+
86
+ def post(path, params = nil)
87
+ start do |http|
88
+ message = Net::HTTP::Post.new(path)
89
+ message['AccessKey'] = @access_key
90
+ message['User-Agent'] = @user_agent
91
+
92
+ unless params.nil?
93
+ message['Content-Type'] = 'application/json'
94
+ message.body = JSON.generate(params)
95
+ end
96
+
97
+ BunBun::Response.parse(http.request(message))
98
+ end
99
+ end
100
+
101
+ attr_reader :pull_zone
102
+
103
+ def purge(url:, async: nil)
104
+ params = {url: url, async: async}
105
+
106
+ post(BunBun::URI.join('/purge', params))
107
+ end
108
+
109
+ attr_reader :region
110
+
111
+ attr_reader :statistics
112
+
113
+ attr_reader :storage_zone
114
+
115
+ def upload(path, content, content_type: 'application/octet-stream')
116
+ start do |http|
117
+ message = Net::HTTP::Put.new(path)
118
+ message['AccessKey'] = @access_key
119
+ message['Content-Type'] = content_type
120
+ message['User-Agent'] = @user_agent
121
+
122
+ if content.respond_to?(:read)
123
+ size = content.respond_to?(:path) ? File.size(content.path) : content.size
124
+
125
+ message['Content-Length'] = size
126
+ message.body_stream = content
127
+ else
128
+ message.body = content
129
+ end
130
+
131
+ http.request(message)
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def start(&block)
138
+ Net::HTTP.start(@host, @port, @opts, &block)
139
+ end
140
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ module BunBun::Response
5
+ def self.parse(response)
6
+ unless response.is_a?(Net::HTTPSuccess)
7
+ raise error(response)
8
+ end
9
+
10
+ body = response.body
11
+
12
+ unless response.content_type == 'application/json'
13
+ return body
14
+ end
15
+
16
+ JSON.parse(body)
17
+ end
18
+
19
+ def self.error(response)
20
+ error_class(response).new(error_message(response))
21
+ end
22
+
23
+ def self.error_class(response)
24
+ case response
25
+ when Net::HTTPClientError then BunBun::ClientError
26
+ when Net::HTTPServerError then BunBun::ServerError
27
+ else
28
+ BunBun::Error
29
+ end
30
+ end
31
+
32
+ def self.error_message(response)
33
+ if response.content_type == 'application/json'
34
+ JSON.parse(response.body).fetch('Message')
35
+ else
36
+ "unexpected #{response.code} #{response.content_type} response"
37
+ end
38
+ end
39
+ end
data/lib/bunbun/uri.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ require 'base64'
3
+ require 'digest'
4
+ require 'uri'
5
+
6
+ module BunBun::URI
7
+ def self.join(path, params = nil)
8
+ return path if params.nil? || params.empty? || params.all? { |_, value| value.nil? }
9
+
10
+ encoded = []
11
+
12
+ params.each do |name, value|
13
+ next if value.nil?
14
+
15
+ encoded << ::URI.encode_uri_component(name) + '=' + ::URI.encode_uri_component(value)
16
+ end
17
+
18
+ path + '?' + encoded.join('&')
19
+ end
20
+
21
+ def self.sign(path, expires: Time.now + 3600, host: ENV.fetch('BUNNY_CDN_HOST'), security_key: ENV.fetch('BUNNY_CDN_KEY'))
22
+ uri = 'https://' + host + path
23
+
24
+ expires = expires.to_i.to_s
25
+
26
+ string = security_key + path + expires
27
+
28
+ token = Base64.urlsafe_encode64(Digest::SHA256.digest(string)).delete("=\n")
29
+
30
+ uri + "?token=#{token}&expires=#{expires}"
31
+ end
32
+ end
data/lib/bunbun.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BunBun
4
+ VERSION = '1.0.0'
5
+
6
+ Error = Class.new(StandardError)
7
+ ClientError = Class.new(Error)
8
+ ServerError = Class.new(Error)
9
+
10
+ autoload :Body, 'bunbun/body'
11
+ autoload :CLI, 'bunbun/cli'
12
+ autoload :Client, 'bunbun/client'
13
+ autoload :Response, 'bunbun/response'
14
+ autoload :URI, 'bunbun/uri'
15
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bunbun
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim Craft
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: digest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: net-http
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: json
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2'
68
+ description: Ruby client for bunny.net
69
+ email:
70
+ - email@timcraft.com
71
+ executables:
72
+ - bunny
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE.txt
77
+ - README.md
78
+ - bin/bunny
79
+ - bunbun.gemspec
80
+ - lib/bunbun.rb
81
+ - lib/bunbun/body.rb
82
+ - lib/bunbun/cli.rb
83
+ - lib/bunbun/cli/command.rb
84
+ - lib/bunbun/cli/countries.rb
85
+ - lib/bunbun/cli/keys.rb
86
+ - lib/bunbun/cli/purge.rb
87
+ - lib/bunbun/cli/regions.rb
88
+ - lib/bunbun/cli/storage_command.rb
89
+ - lib/bunbun/cli/storage_delete.rb
90
+ - lib/bunbun/cli/storage_download.rb
91
+ - lib/bunbun/cli/storage_edit.rb
92
+ - lib/bunbun/cli/storage_files.rb
93
+ - lib/bunbun/cli/storage_upload.rb
94
+ - lib/bunbun/cli/storage_zone.rb
95
+ - lib/bunbun/cli/storage_zone_create.rb
96
+ - lib/bunbun/cli/storage_zones.rb
97
+ - lib/bunbun/cli/zone.rb
98
+ - lib/bunbun/cli/zone_create.rb
99
+ - lib/bunbun/cli/zone_purge.rb
100
+ - lib/bunbun/cli/zone_rules_delete.rb
101
+ - lib/bunbun/cli/zone_rules_disenable.rb
102
+ - lib/bunbun/cli/zone_rules_enable.rb
103
+ - lib/bunbun/cli/zone_rules_post.rb
104
+ - lib/bunbun/cli/zones.rb
105
+ - lib/bunbun/client.rb
106
+ - lib/bunbun/client/api_key.rb
107
+ - lib/bunbun/client/country.rb
108
+ - lib/bunbun/client/namespace.rb
109
+ - lib/bunbun/client/pull_zone.rb
110
+ - lib/bunbun/client/pull_zone/edge_rules.rb
111
+ - lib/bunbun/client/region.rb
112
+ - lib/bunbun/client/statistics.rb
113
+ - lib/bunbun/client/storage_zone.rb
114
+ - lib/bunbun/client/storage_zone/statistics.rb
115
+ - lib/bunbun/response.rb
116
+ - lib/bunbun/uri.rb
117
+ homepage: https://github.com/readysteady/bunbun
118
+ licenses:
119
+ - LGPL-3.0
120
+ metadata:
121
+ homepage: https://github.com/readysteady/bunbun
122
+ source_code_uri: https://github.com/readysteady/bunbun
123
+ bug_tracker_uri: https://github.com/readysteady/bunbun/issues
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: 3.3.0
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubygems_version: 3.6.7
139
+ specification_version: 4
140
+ summary: Ruby client for bunny.net
141
+ test_files: []