b2-client 1.0.2 → 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0be79261fee4bcca3e1ba5a8dbe172bc505d38a35b79a0b3e25f81de26a900a
4
- data.tar.gz: 593efe787fb433de833a8acb964f470ee9f5bc27c71b6ada97af1284e9d955e7
3
+ metadata.gz: 70e5dc6cc5de5772f02f575cceefa1137ce7750cf2042067c6e06d1809e0758f
4
+ data.tar.gz: 96e70fb4bbd010ff178a3ac506a62cf5dd85c65d9c6477c78c89687aeda21b5c
5
5
  SHA512:
6
- metadata.gz: 8eb2b33f95494b125c0397de1355063b256c420a717ca4eb0ffd0772f815afd2e094d8dbb7ab22904ef8f7ac651a5e019aa19137e3127a46fa1b6e97982d60d7
7
- data.tar.gz: 5505cb044eba0400384f2d072fa079bd7adfa8a5a3826719825644ab819d8945f5966cbc69f6485214cddc81a9f8890fbc2b79c72f2e0c110f9418ef474f91bf
6
+ metadata.gz: 7908671a61289946d1330ffa0668d38c0952607ab07932520abb3791ff6a217c8ded4b97e66f2b98784368dac62a55aa412ed636ee386bade7d37c80b013c729
7
+ data.tar.gz: 5e38e319103709afaca046533501b82f30050b48db439ac927bfc67b0fd0ce7f222327c439092236658f821aa3e025d457280acbaf7b1f1faa57cffeb12f0e95
data/lib/b2.rb CHANGED
@@ -2,50 +2,57 @@ require 'uri'
2
2
  require 'json'
3
3
  require 'net/http'
4
4
 
5
+ require File.expand_path('../b2/errors', __FILE__)
5
6
  require File.expand_path('../b2/file', __FILE__)
6
7
  require File.expand_path('../b2/bucket', __FILE__)
8
+ require File.expand_path('../b2/api_connection', __FILE__)
7
9
  require File.expand_path('../b2/connection', __FILE__)
8
10
  require File.expand_path('../b2/upload_chunker', __FILE__)
9
11
 
10
12
  class B2
11
13
 
12
- def initialize(account_id:, application_key:)
13
- @account_id = account_id
14
- @connection = B2::Connection.new(account_id, application_key)
15
- @buckets_cache = []
14
+ def self.encode(value)
15
+ URI.encode_www_form_component(value.force_encoding(Encoding::UTF_8)).gsub("%2F", "/")
16
+ end
17
+
18
+ def self.decode(value)
19
+ URI.decode_www_form_component(value, Encoding::UTF_8)
20
+ end
21
+
22
+ def initialize(key_id: , secret: )
23
+ @connection = B2::Connection.new(key_id, secret)
24
+ end
25
+
26
+ def account_id
27
+ @connection.account_id
16
28
  end
17
29
 
18
30
  def buckets
19
- @connection.post('/b2api/v1/b2_list_buckets', {accountId: @account_id})['buckets'].map do |b|
20
- B2::Bucket.new(b, @connection)
21
- end
31
+ @connection.buckets
22
32
  end
23
-
24
- def lookup_bucket_id(name)
25
- bucket = @buckets_cache.find{ |b| b.name == name }
26
- return bucket.id if bucket
27
-
28
- @buckets_cache = buckets
29
- @buckets_cache.find{ |b| b.name == name }&.id
33
+
34
+ def bucket(name)
35
+ bs = @connection.post('/b2api/v2/b2_list_buckets', {accountId: account_id, bucketName: name})['buckets']
36
+ B2::Bucket.new(bs.first, @connection)
30
37
  end
31
38
 
32
39
  def file(bucket, key)
33
- bucket_id = lookup_bucket_id(bucket)
40
+ bucket_id = @connection.lookup_bucket_id(bucket)
34
41
 
35
- file = @connection.post('/b2api/v1/b2_list_file_names', {
42
+ file = @connection.post('/b2api/v2/b2_list_file_names', {
36
43
  bucketId: bucket_id,
37
44
  startFileName: key
38
45
  })['files'].find {|f| f['fileName'] == key }
39
46
 
40
- file ? B2::File.new(file.merge({'bucketId' => bucket_id})) : nil
47
+ file ? B2::File.new(file.merge({'bucketId' => bucket_id}), @connection) : nil
41
48
  end
42
49
 
43
50
  def delete(bucket, key)
44
51
  object = file(bucket, key)
45
52
  if object
46
- @connection.post('/b2api/v1/b2_delete_file_version', {
47
- fileName: file.name,
48
- fileId: file.id
53
+ @connection.post('/b2api/v2/b2_delete_file_version', {
54
+ fileName: object.name,
55
+ fileId: object.id
49
56
  })
50
57
  else
51
58
  false
@@ -53,8 +60,8 @@ class B2
53
60
  end
54
61
 
55
62
  def get_upload_token(bucket)
56
- @connection.post("/b2api/v1/b2_get_upload_url", {
57
- bucketId: lookup_bucket_id(bucket)
63
+ @connection.post("/b2api/v2/b2_get_upload_url", {
64
+ bucketId: @connection.lookup_bucket_id(bucket)
58
65
  })
59
66
  end
60
67
 
@@ -68,7 +75,7 @@ class B2
68
75
  chunker = B2::UploadChunker.new(io_or_string)
69
76
  req = Net::HTTP::Post.new(uri.path)
70
77
  req['Authorization'] = upload['authorizationToken']
71
- req['X-Bz-File-Name'] = B2::File.encode_filename(key)
78
+ req['X-Bz-File-Name'] = B2.encode(key)
72
79
  req['Content-Type'] = mime_type || 'b2/x-auto'
73
80
  req['X-Bz-Content-Sha1'] = 'hex_digits_at_end'
74
81
  info.each do |key, value|
@@ -85,13 +92,8 @@ class B2
85
92
  end
86
93
  end
87
94
 
88
- def get_download_url(bucket, filename, expires_in: 3_600)
89
- response = @connection.post("/b2api/v1/b2_get_download_authorization", {
90
- bucketId: lookup_bucket_id(bucket),
91
- fileNamePrefix: filename,
92
- validDurationInSeconds: expires_in
93
- })
94
- @connection.download_url + '/file/' + bucket + '/' + filename + "?Authorization=" + response['authorizationToken']
95
+ def get_download_url(bucket, filename, **options)
96
+ @connection.get_download_url(bucket, filename, **options)
95
97
  end
96
98
 
97
99
  def download(bucket, key, to=nil, &block)
@@ -100,11 +102,11 @@ class B2
100
102
 
101
103
 
102
104
  def download_to_file(bucket, key, filename)
103
- file = File.open(filename, 'w')
105
+ file = ::File.open(filename, 'wb')
104
106
  download(bucket, key) do |chunk|
105
107
  file << chunk
106
108
  end
107
109
  file.close
108
110
  end
109
111
 
110
- end
112
+ end
@@ -0,0 +1,118 @@
1
+ class B2
2
+ class APIConnection
3
+
4
+ attr_reader :key_id, :key_secret, :download_url
5
+
6
+ def initialize(key_id, secret)
7
+ @key_id = key_id
8
+ @key_secret = secret
9
+ end
10
+
11
+ def connect!
12
+ conn = Net::HTTP.new('api.backblazeb2.com', 443)
13
+ conn.use_ssl = true
14
+
15
+ req = Net::HTTP::Get.new('/b2api/v2/b2_authorize_account')
16
+ req.basic_auth(@key_id, @key_secret)
17
+
18
+ key_expiration = Time.now.to_i + 86_400 #24hr expiry
19
+ resp = conn.start { |http| http.request(req) }
20
+ if resp.is_a?(Net::HTTPSuccess)
21
+ resp = JSON.parse(resp.body)
22
+ else
23
+ raise "Error connecting to B2 API"
24
+ end
25
+
26
+ uri = URI.parse(resp['apiUrl'])
27
+ @connection = Net::HTTP.new(uri.host, uri.port)
28
+ @connection.use_ssl = uri.scheme == 'https'
29
+ @connection.start
30
+
31
+ @auth_token_expires_at = key_expiration
32
+ @account_id = resp['accountId']
33
+ @minimum_part_size = resp['absoluteMinimumPartSize']
34
+ @recommended_part_size = resp['recommendedPartSize']
35
+ @auth_token = resp['authorizationToken']
36
+ @download_url = resp['downloadUrl']
37
+ end
38
+
39
+ def account_id
40
+ return @account_id if !@account_id.nil?
41
+
42
+ connect!
43
+ @account_id
44
+ end
45
+
46
+ def disconnect!
47
+ if @connection
48
+ @connection.finish if @connection.active?
49
+ @connection = nil
50
+ end
51
+ end
52
+
53
+ def reconnect!
54
+ disconnect!
55
+ connect!
56
+ end
57
+
58
+ def authorization_token
59
+ if @auth_token_expires_at.nil? || @auth_token_expires_at <= Time.now.to_i
60
+ reconnect!
61
+ end
62
+ @auth_token
63
+ end
64
+
65
+ def active?
66
+ !@connection.nil? && @connection.active?
67
+ end
68
+
69
+ def connection
70
+ reconnect! if !active?
71
+ @connection
72
+ end
73
+
74
+ def send_request(request, body=nil, &block)
75
+ retries = 0
76
+ request['Authorization'] = authorization_token
77
+ request.body = (body.is_a?(String) ? body : JSON.generate(body)) if body
78
+
79
+ return_value = nil
80
+ close_connection = false
81
+ begin
82
+ connection.request(request) do |response|
83
+ close_connection = response['Connection'] == 'close'
84
+
85
+ case response
86
+ when Net::HTTPSuccess
87
+ if block_given?
88
+ return_value = yield(response)
89
+ else
90
+ return_value = JSON.parse(response.body)
91
+ end
92
+ else
93
+ body = JSON.parse(response.body)
94
+ case body['code']
95
+ when 'not_found'
96
+ raise B2::NotFound(body['message'])
97
+ when 'expired_auth_token'
98
+ raise B2::ExpiredAuthToken(body['message'])
99
+ else
100
+ raise "Error connecting to B2 API #{response.body}"
101
+ end
102
+ end
103
+ end
104
+
105
+ # Unexpected EOF (end of file) errors can occur when streaming from a
106
+ # remote because of Backblaze quota restrictions.
107
+ rescue B2::ExpiredAuthToken, EOFError
108
+ reconnect!
109
+ retries =+ 1
110
+ retry if retries < 2
111
+ end
112
+ disconnect! if close_connection
113
+
114
+ return_value
115
+ end
116
+
117
+ end
118
+ end
@@ -14,7 +14,7 @@ class B2
14
14
  end
15
15
 
16
16
  def get_upload_token
17
- @connection.post("/b2api/v1/b2_get_upload_url", { bucketId: @id })
17
+ @connection.post("/b2api/v2/b2_get_upload_url", { bucketId: @id })
18
18
  end
19
19
 
20
20
  def upload_file(key, io_or_string, mime_type: nil, sha1: nil, content_disposition: nil, info: {})
@@ -27,12 +27,12 @@ class B2
27
27
  chunker = sha1 ? io_or_string : B2::UploadChunker.new(io_or_string)
28
28
  req = Net::HTTP::Post.new(uri.path)
29
29
  req['Authorization'] = upload['authorizationToken']
30
- req['X-Bz-File-Name'] = B2::File.encode_filename(key)
30
+ req['X-Bz-File-Name'] = B2.encode(key)
31
31
  req['Content-Type'] = mime_type || 'b2/x-auto'
32
32
  req['X-Bz-Content-Sha1'] = sha1 ? sha1 : 'hex_digits_at_end'
33
- req['X-Bz-Info-b2-content-disposition'] = content_disposition if content_disposition
33
+ req['X-Bz-Info-b2-content-disposition'] = B2.encode(content_disposition) if content_disposition
34
34
  info.each do |key, value|
35
- req["X-Bz-Info-#{key}"] = value
35
+ req["X-Bz-Info-#{key}"] = B2.encode(value)
36
36
  end
37
37
  req['Content-Length'] = chunker.size
38
38
  req.body_stream = chunker
@@ -41,14 +41,24 @@ class B2
41
41
  result = if resp.is_a?(Net::HTTPSuccess)
42
42
  JSON.parse(resp.body)
43
43
  else
44
- raise "Error connecting to B2 API"
44
+ raise "Error connecting to B2 API #{resp.body}"
45
45
  end
46
46
 
47
47
  B2::File.new(result, @connection)
48
48
  end
49
+
50
+ def keys(prefix: nil, delimiter: nil)
51
+ #TODO: add abilty to get all names
52
+ @connection.post('/b2api/v2/b2_list_file_names', {
53
+ bucketId: @id,
54
+ maxFileCount: 1000,
55
+ prefix: prefix,
56
+ delimiter: delimiter
57
+ })['files'].map{ |f| f['fileName'] }
58
+ end
49
59
 
50
60
  def has_key?(key)
51
- !@connection.post('/b2api/v1/b2_list_file_names', {
61
+ !@connection.post('/b2api/v2/b2_list_file_names', {
52
62
  bucketId: @id,
53
63
  startFileName: key,
54
64
  maxFileCount: 1,
@@ -57,7 +67,7 @@ class B2
57
67
  end
58
68
 
59
69
  def file(key)
60
- file = @connection.post('/b2api/v1/b2_list_file_names', {
70
+ file = @connection.post('/b2api/v2/b2_list_file_names', {
61
71
  bucketId: @id,
62
72
  startFileName: key,
63
73
  maxFileCount: 1,
@@ -67,6 +77,10 @@ class B2
67
77
  file ? B2::File.new(file.merge({'bucketId' => @id}), @connection) : nil
68
78
  end
69
79
 
80
+ def get_download_url(key, **options)
81
+ @connection.get_download_url(@name, key, **options)
82
+ end
83
+
70
84
  def download(key, to=nil, &block)
71
85
  @connection.download(@name, key, to, &block)
72
86
  end
@@ -76,4 +90,4 @@ class B2
76
90
  end
77
91
 
78
92
  end
79
- end
93
+ end
@@ -1,122 +1,140 @@
1
+ require 'cgi'
2
+ require 'thread'
3
+
1
4
  class B2
2
5
  class Connection
3
6
 
4
- attr_reader :account_id, :application_key, :download_url
5
-
6
- def initialize(account_id, application_key)
7
- @account_id = account_id
8
- @application_key = application_key
7
+ def initialize(key_id, secret, pool: 5, timeout: 5)
8
+ @mutex = Mutex.new
9
+ @availability = ConditionVariable.new
10
+ @max = pool
11
+ @timeout = timeout
12
+ @free_pool = []
13
+ @used_pool = []
14
+
15
+ @key_id = key_id
16
+ @key_secret = secret
17
+
18
+ @buckets_cache = Hash.new { |hash, name| hash[name] = bucket(name) }
9
19
  end
10
20
 
11
- def connect!
12
- conn = Net::HTTP.new('api.backblazeb2.com', 443)
13
- conn.use_ssl = true
21
+ def account_id
22
+ return @account_id if !@account_id.nil?
14
23
 
15
- req = Net::HTTP::Get.new('/b2api/v1/b2_authorize_account')
16
- req.basic_auth(account_id, application_key)
24
+ @account_id = with_connection { |conn| conn.account_id }
25
+ end
17
26
 
18
- key_expiration = Time.now.to_i + 86_400 #24hr expiry
19
- resp = conn.start { |http| http.request(req) }
20
- if resp.is_a?(Net::HTTPSuccess)
21
- resp = JSON.parse(resp.body)
22
- else
23
- raise "Error connecting to B2 API"
27
+ def with_connection
28
+ conn = @mutex.synchronize do
29
+ cxn = if !@free_pool.empty?
30
+ @free_pool.shift
31
+ elsif @free_pool.size + @used_pool.size < @max
32
+ B2::APIConnection.new(@key_id, @key_secret)
33
+ else
34
+ @availability.wait(@mutex, @timeout)
35
+ @free_pool.shift || B2::APIConnection.new(@key_id, @key_secret)
36
+ end
37
+
38
+ @used_pool << cxn
39
+ cxn
40
+ end
41
+
42
+ yield conn
43
+ ensure
44
+ @mutex.synchronize do
45
+ @used_pool.delete(conn)
46
+ @free_pool << conn if conn.active?
47
+ @availability.signal()
24
48
  end
25
-
26
- uri = URI.parse(resp['apiUrl'])
27
- @connection = Net::HTTP.new(uri.host, uri.port)
28
- @connection.use_ssl = uri.scheme == 'https'
29
- @connection.start
30
-
31
- @auth_token_expires_at = key_expiration
32
- @minimum_part_size = resp['absoluteMinimumPartSize']
33
- @recommended_part_size = resp['recommendedPartSize']
34
- @auth_token = resp['authorizationToken']
35
- @download_url = resp['downloadUrl']
36
49
  end
37
50
 
38
- def disconnect!
39
- if @connection
40
- @connection.finish if @connection.active?
41
- @connection = nil
42
- end
51
+ def authorization_token
52
+ with_connection { |conn| conn.authorization_token }
43
53
  end
44
54
 
45
- def reconnect!
46
- disconnect!
47
- connect!
55
+ def send_request(request, body=nil, &block)
56
+ with_connection { |conn| conn.send_request(request, body, &block) }
48
57
  end
49
58
 
50
- def authorization_token
51
- if @auth_token_expires_at.nil? || @auth_token_expires_at <= Time.now.to_i
52
- reconnect!
59
+ def download_url
60
+ with_connection { |conn| conn.download_url }
61
+ end
62
+
63
+ def buckets
64
+ post('/b2api/v2/b2_list_buckets', {accountId: account_id})['buckets'].map do |b|
65
+ B2::Bucket.new(b, self)
53
66
  end
54
- @auth_token
55
67
  end
56
68
 
57
- def active?
58
- !@connection.nil? && @connection.active?
69
+ def bucket(name)
70
+ response = post('/b2api/v2/b2_list_buckets', {
71
+ accountId: account_id,
72
+ bucketName: name
73
+ })['buckets']
74
+ response.map { |b| B2::Bucket.new(b, self) }.first
59
75
  end
60
76
 
61
- def send_request(request, body=nil, &block)
62
- request['Authorization'] = authorization_token
63
- request.body = (body.is_a?(String) ? body : JSON.generate(body)) if body
64
-
65
- return_value = nil
66
- close_connection = false
67
- @connection.request(request) do |response|
68
- close_connection = response['Connection'] == 'close'
69
-
70
- case response
71
- when Net::HTTPSuccess
72
- if block_given?
73
- return_value = yield(response)
74
- else
75
- return_value = JSON.parse(response.body)
76
- end
77
- else
78
- raise "Error connecting to B2 API #{response.body}"
79
- end
80
- end
81
- @connection.finish if close_connection
77
+ def lookup_bucket_id(name)
78
+ @buckets_cache[name].id
79
+ end
82
80
 
83
- return_value
81
+ def get_download_url(bucket, filename, expires_in: 3_600, disposition: nil)
82
+ response = post("/b2api/v2/b2_get_download_authorization", {
83
+ bucketId: lookup_bucket_id(bucket),
84
+ fileNamePrefix: filename,
85
+ validDurationInSeconds: expires_in,
86
+ b2ContentDisposition: disposition
87
+ })
88
+ url = download_url + '/file/' + bucket + '/' + filename + "?Authorization=" + response['authorizationToken']
89
+ url += "&b2ContentDisposition=#{CGI.escape(disposition)}" if disposition
90
+ url
84
91
  end
85
-
86
- def download(bucket, key, to=nil, &block)
92
+
93
+ def download(bucket, key, to=nil)
87
94
  opened_file = (to && to.is_a?(String))
88
95
  to = ::File.open(to, 'wb') if to.is_a?(String)
89
96
  digestor = Digest::SHA1.new
90
97
  data = ""
91
98
 
92
- uri = URI.parse(@download_url)
99
+ uri = URI.parse(download_url)
93
100
  conn = Net::HTTP.new(uri.host, uri.port)
94
101
  conn.use_ssl = uri.scheme == 'https'
95
102
 
96
103
  req = Net::HTTP::Get.new("/file/#{bucket}/#{key}")
97
104
  req['Authorization'] = authorization_token
98
105
  conn.start do |http|
99
- http.request(req) do |response|
100
- case response
101
- when Net::HTTPSuccess
102
- response.read_body do |chunk|
103
- digestor << chunk
104
- if to
105
- to << chunk
106
- elsif block
107
- block(chunk)
108
- else
109
- data << chunk
106
+ http.request(req) do |response|
107
+ case response
108
+ when Net::HTTPSuccess
109
+ response.read_body do |chunk|
110
+ digestor << chunk
111
+ if to
112
+ to << chunk
113
+ elsif block_given?
114
+ yield(chunk)
115
+ else
116
+ data << chunk
117
+ end
110
118
  end
111
- end
112
119
 
113
- if digestor.hexdigest != response['X-Bz-Content-Sha1']
114
- raise 'file error'
120
+ if response['X-Bz-Content-Sha1'] != 'none' && digestor.hexdigest != response['X-Bz-Content-Sha1']
121
+ rase B2::FileIntegrityError.new("SHA1 Mismatch, expected: \"#{response['X-Bz-Content-Sha1']}\", actual: \"#{digestor.hexdigest}\"")
122
+ end
123
+ when Net::HTTPNotFound
124
+ raise B2::NotFound.new(JSON.parse(response.body)['message'])
125
+ else
126
+ begin
127
+ body = JSON.parse(response.body)
128
+ if body['code'] == 'not_found'
129
+ raise B2::NotFound(body['message'])
130
+ else
131
+ raise "#{body['code']} (#{body['message']})"
132
+ end
133
+ rescue
134
+ raise response.body
135
+ end
115
136
  end
116
- else
117
- raise response.body
118
137
  end
119
- end
120
138
  end
121
139
 
122
140
  if opened_file
@@ -124,7 +142,7 @@ class B2
124
142
  elsif to
125
143
  to.flush
126
144
  end
127
- block.nil? && to.nil? ? data : nil
145
+ !block_given? && to.nil? ? data : nil
128
146
  end
129
147
 
130
148
  def get(path, body=nil, &block)
@@ -140,4 +158,5 @@ class B2
140
158
  end
141
159
 
142
160
  end
143
- end
161
+ end
162
+
@@ -0,0 +1,15 @@
1
+ class B2
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ class NotFound < Error
7
+ end
8
+
9
+ class FileIntegrityError < Error
10
+ end
11
+
12
+ class ExpiredAuthToken < Error
13
+ end
14
+
15
+ end
@@ -5,7 +5,7 @@ class B2
5
5
 
6
6
  def initialize(attrs, connection)
7
7
  @id = attrs['fileId']
8
- @name = B2::File.decode_filename(attrs['fileName'])
8
+ @name = B2.decode(attrs['fileName'])
9
9
  @account_id = attrs['accountId']
10
10
  @bucket_id = attrs['bucketId']
11
11
  @size = attrs['contentLength']
@@ -17,16 +17,8 @@ class B2
17
17
  @connection = connection
18
18
  end
19
19
 
20
- def self.encode_filename(str)
21
- URI.encode_www_form_component(str.force_encoding(Encoding::UTF_8)).gsub("%2F", "/")
22
- end
23
-
24
- def self.decode_filename(str)
25
- URI.decode_www_form_component(str, Encoding::UTF_8)
26
- end
27
-
28
20
  def delete!
29
- @connection.post('/b2api/v1/b2_delete_file_version', {
21
+ @connection.post('/b2api/v2/b2_delete_file_version', {
30
22
  fileId: @id,
31
23
  fileName: @name
32
24
  })
@@ -1,3 +1,3 @@
1
1
  class B2
2
- VERSION = '1.0.2'
2
+ VERSION = '1.0.7'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: b2-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-20 00:00:00.000000000 Z
11
+ date: 2020-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -36,9 +36,11 @@ files:
36
36
  - README.md
37
37
  - b2-client.gemspec
38
38
  - lib/b2.rb
39
+ - lib/b2/api_connection.rb
39
40
  - lib/b2/bucket.rb
40
41
  - lib/b2/client.rb
41
42
  - lib/b2/connection.rb
43
+ - lib/b2/errors.rb
42
44
  - lib/b2/file.rb
43
45
  - lib/b2/upload_chunker.rb
44
46
  - lib/b2/version.rb
@@ -46,7 +48,7 @@ homepage: https://github.com/malomalo/b2
46
48
  licenses:
47
49
  - MIT
48
50
  metadata: {}
49
- post_install_message:
51
+ post_install_message:
50
52
  rdoc_options: []
51
53
  require_paths:
52
54
  - lib
@@ -61,9 +63,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
63
  - !ruby/object:Gem::Version
62
64
  version: '0'
63
65
  requirements: []
64
- rubyforge_project:
65
- rubygems_version: 2.7.4
66
- signing_key:
66
+ rubygems_version: 3.1.2
67
+ signing_key:
67
68
  specification_version: 4
68
69
  summary: Backblaze B2 Client
69
70
  test_files: []