b2-client 1.0.2 → 1.0.7

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.
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: []