b2-client 1.0.5 → 1.0.6
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 +4 -4
- data/lib/b2.rb +20 -11
- data/lib/b2/api_connection.rb +101 -0
- data/lib/b2/bucket.rb +14 -4
- data/lib/b2/connection.rb +56 -81
- data/lib/b2/errors.rb +3 -0
- data/lib/b2/file.rb +1 -1
- data/lib/b2/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 405cc51a4b94788687e8ccfa7aa774df40c29135b9ac89251405f56c32d73835
|
4
|
+
data.tar.gz: 820653192390b83efbb713f4104d739b496e75d3ae9c39dec6b99b3ac2e1af77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 73a212dbb80684eca0097f9e784ab8815a559e3a810cdb448eaf74fb9ec49a594c5f3f78790870fc05ce3fe47dc0accfb62f931dce6811178b771f63884fcdd1
|
7
|
+
data.tar.gz: 7f6a23851372cd9fa317faab23fa09fcca35344e024fe8c1496c3204bffdb258e75197f1e115b45c5b5f400b0f872b738827973bb5469b2d2b11d7a1bc9c6892
|
data/lib/b2.rb
CHANGED
@@ -5,37 +5,46 @@ require 'net/http'
|
|
5
5
|
require File.expand_path('../b2/errors', __FILE__)
|
6
6
|
require File.expand_path('../b2/file', __FILE__)
|
7
7
|
require File.expand_path('../b2/bucket', __FILE__)
|
8
|
+
require File.expand_path('../b2/api_connection', __FILE__)
|
8
9
|
require File.expand_path('../b2/connection', __FILE__)
|
9
10
|
require File.expand_path('../b2/upload_chunker', __FILE__)
|
10
11
|
|
11
12
|
class B2
|
12
13
|
|
13
|
-
def initialize(
|
14
|
-
@
|
15
|
-
|
14
|
+
def initialize(key_id: , secret: )
|
15
|
+
@connection = B2::Connection.new(key_id, secret)
|
16
|
+
end
|
17
|
+
|
18
|
+
def account_id
|
19
|
+
@connection.account_id
|
16
20
|
end
|
17
21
|
|
18
22
|
def buckets
|
19
23
|
@connection.buckets
|
20
24
|
end
|
21
25
|
|
26
|
+
def bucket(name)
|
27
|
+
bs = @connection.post('/b2api/v2/b2_list_buckets', {accountId: account_id, bucketName: name})['buckets']
|
28
|
+
B2::Bucket.new(bs.first, @connection)
|
29
|
+
end
|
30
|
+
|
22
31
|
def file(bucket, key)
|
23
32
|
bucket_id = @connection.lookup_bucket_id(bucket)
|
24
33
|
|
25
|
-
file = @connection.post('/b2api/
|
34
|
+
file = @connection.post('/b2api/v2/b2_list_file_names', {
|
26
35
|
bucketId: bucket_id,
|
27
36
|
startFileName: key
|
28
37
|
})['files'].find {|f| f['fileName'] == key }
|
29
38
|
|
30
|
-
file ? B2::File.new(file.merge({'bucketId' => bucket_id})) : nil
|
39
|
+
file ? B2::File.new(file.merge({'bucketId' => bucket_id}), @connection) : nil
|
31
40
|
end
|
32
41
|
|
33
42
|
def delete(bucket, key)
|
34
43
|
object = file(bucket, key)
|
35
44
|
if object
|
36
|
-
@connection.post('/b2api/
|
37
|
-
fileName:
|
38
|
-
fileId:
|
45
|
+
@connection.post('/b2api/v2/b2_delete_file_version', {
|
46
|
+
fileName: object.name,
|
47
|
+
fileId: object.id
|
39
48
|
})
|
40
49
|
else
|
41
50
|
false
|
@@ -43,7 +52,7 @@ class B2
|
|
43
52
|
end
|
44
53
|
|
45
54
|
def get_upload_token(bucket)
|
46
|
-
@connection.post("/b2api/
|
55
|
+
@connection.post("/b2api/v2/b2_get_upload_url", {
|
47
56
|
bucketId: @connection.lookup_bucket_id(bucket)
|
48
57
|
})
|
49
58
|
end
|
@@ -85,11 +94,11 @@ class B2
|
|
85
94
|
|
86
95
|
|
87
96
|
def download_to_file(bucket, key, filename)
|
88
|
-
file = File.open(filename, '
|
97
|
+
file = ::File.open(filename, 'wb')
|
89
98
|
download(bucket, key) do |chunk|
|
90
99
|
file << chunk
|
91
100
|
end
|
92
101
|
file.close
|
93
102
|
end
|
94
103
|
|
95
|
-
end
|
104
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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
|
+
@buckets_cache = []
|
38
|
+
end
|
39
|
+
|
40
|
+
def account_id
|
41
|
+
return @account_id if !@account_id.nil?
|
42
|
+
|
43
|
+
connect!
|
44
|
+
@account_id
|
45
|
+
end
|
46
|
+
|
47
|
+
def disconnect!
|
48
|
+
if @connection
|
49
|
+
@connection.finish if @connection.active?
|
50
|
+
@connection = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def reconnect!
|
55
|
+
disconnect!
|
56
|
+
connect!
|
57
|
+
end
|
58
|
+
|
59
|
+
def authorization_token
|
60
|
+
if @auth_token_expires_at.nil? || @auth_token_expires_at <= Time.now.to_i
|
61
|
+
reconnect!
|
62
|
+
end
|
63
|
+
@auth_token
|
64
|
+
end
|
65
|
+
|
66
|
+
def active?
|
67
|
+
!@connection.nil? && @connection.active?
|
68
|
+
end
|
69
|
+
|
70
|
+
def connection
|
71
|
+
reconnect! if !active?
|
72
|
+
@connection
|
73
|
+
end
|
74
|
+
|
75
|
+
def send_request(request, body=nil, &block)
|
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
|
+
connection.request(request) do |response|
|
82
|
+
close_connection = response['Connection'] == 'close'
|
83
|
+
|
84
|
+
case response
|
85
|
+
when Net::HTTPSuccess
|
86
|
+
if block_given?
|
87
|
+
return_value = yield(response)
|
88
|
+
else
|
89
|
+
return_value = JSON.parse(response.body)
|
90
|
+
end
|
91
|
+
else
|
92
|
+
raise "Error connecting to B2 API #{response.body}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
disconnect! if close_connection
|
96
|
+
|
97
|
+
return_value
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
data/lib/b2/bucket.rb
CHANGED
@@ -14,7 +14,7 @@ class B2
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def get_upload_token
|
17
|
-
@connection.post("/b2api/
|
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: {})
|
@@ -46,9 +46,19 @@ class B2
|
|
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/
|
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/
|
70
|
+
file = @connection.post('/b2api/v2/b2_list_file_names', {
|
61
71
|
bucketId: @id,
|
62
72
|
startFileName: key,
|
63
73
|
maxFileCount: 1,
|
@@ -80,4 +90,4 @@ class B2
|
|
80
90
|
end
|
81
91
|
|
82
92
|
end
|
83
|
-
end
|
93
|
+
end
|
data/lib/b2/connection.rb
CHANGED
@@ -1,93 +1,67 @@
|
|
1
1
|
require 'cgi'
|
2
|
+
require 'thread'
|
2
3
|
|
3
4
|
class B2
|
4
5
|
class Connection
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
@
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
def connect!
|
14
|
-
conn = Net::HTTP.new('api.backblazeb2.com', 443)
|
15
|
-
conn.use_ssl = true
|
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 = []
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
key_expiration = Time.now.to_i + 86_400 #24hr expiry
|
21
|
-
resp = conn.start { |http| http.request(req) }
|
22
|
-
if resp.is_a?(Net::HTTPSuccess)
|
23
|
-
resp = JSON.parse(resp.body)
|
24
|
-
else
|
25
|
-
raise "Error connecting to B2 API"
|
26
|
-
end
|
27
|
-
|
28
|
-
uri = URI.parse(resp['apiUrl'])
|
29
|
-
@connection = Net::HTTP.new(uri.host, uri.port)
|
30
|
-
@connection.use_ssl = uri.scheme == 'https'
|
31
|
-
@connection.start
|
15
|
+
@key_id = key_id
|
16
|
+
@key_secret = secret
|
32
17
|
|
33
|
-
@auth_token_expires_at = key_expiration
|
34
|
-
@minimum_part_size = resp['absoluteMinimumPartSize']
|
35
|
-
@recommended_part_size = resp['recommendedPartSize']
|
36
|
-
@auth_token = resp['authorizationToken']
|
37
|
-
@download_url = resp['downloadUrl']
|
38
18
|
@buckets_cache = []
|
39
19
|
end
|
40
20
|
|
41
|
-
def
|
42
|
-
if
|
43
|
-
|
44
|
-
|
45
|
-
end
|
21
|
+
def account_id
|
22
|
+
return @account_id if !@account_id.nil?
|
23
|
+
|
24
|
+
@account_id = with_connection { |conn| conn.account_id }
|
46
25
|
end
|
47
|
-
|
48
|
-
def
|
49
|
-
|
50
|
-
|
26
|
+
|
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()
|
48
|
+
end
|
51
49
|
end
|
52
50
|
|
53
51
|
def authorization_token
|
54
|
-
|
55
|
-
reconnect!
|
56
|
-
end
|
57
|
-
@auth_token
|
52
|
+
with_connection { |conn| conn.authorization_token }
|
58
53
|
end
|
59
54
|
|
60
|
-
def active?
|
61
|
-
!@connection.nil? && @connection.active?
|
62
|
-
end
|
63
|
-
|
64
55
|
def send_request(request, body=nil, &block)
|
65
|
-
request
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
@connection.request(request) do |response|
|
71
|
-
close_connection = response['Connection'] == 'close'
|
72
|
-
|
73
|
-
case response
|
74
|
-
when Net::HTTPSuccess
|
75
|
-
if block_given?
|
76
|
-
return_value = yield(response)
|
77
|
-
else
|
78
|
-
return_value = JSON.parse(response.body)
|
79
|
-
end
|
80
|
-
else
|
81
|
-
raise "Error connecting to B2 API #{response.body}"
|
82
|
-
end
|
83
|
-
end
|
84
|
-
@connection.finish if close_connection
|
85
|
-
|
86
|
-
return_value
|
56
|
+
with_connection { |conn| conn.send_request(request, body, &block) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def download_url
|
60
|
+
with_connection { |conn| conn.download_url }
|
87
61
|
end
|
88
62
|
|
89
63
|
def buckets
|
90
|
-
post('/b2api/
|
64
|
+
post('/b2api/v2/b2_list_buckets', {accountId: account_id})['buckets'].map do |b|
|
91
65
|
B2::Bucket.new(b, self)
|
92
66
|
end
|
93
67
|
end
|
@@ -95,30 +69,30 @@ class B2
|
|
95
69
|
def lookup_bucket_id(name)
|
96
70
|
bucket = @buckets_cache.find{ |b| b.name == name }
|
97
71
|
return bucket.id if bucket
|
98
|
-
|
72
|
+
|
99
73
|
@buckets_cache = buckets
|
100
74
|
@buckets_cache.find{ |b| b.name == name }&.id
|
101
75
|
end
|
102
76
|
|
103
77
|
def get_download_url(bucket, filename, expires_in: 3_600, disposition: nil)
|
104
|
-
response = post("/b2api/
|
78
|
+
response = post("/b2api/v2/b2_get_download_authorization", {
|
105
79
|
bucketId: lookup_bucket_id(bucket),
|
106
80
|
fileNamePrefix: filename,
|
107
81
|
validDurationInSeconds: expires_in,
|
108
82
|
b2ContentDisposition: disposition
|
109
83
|
})
|
110
|
-
url =
|
84
|
+
url = download_url + '/file/' + bucket + '/' + filename + "?Authorization=" + response['authorizationToken']
|
111
85
|
url += "&b2ContentDisposition=#{CGI.escape(disposition)}" if disposition
|
112
86
|
url
|
113
87
|
end
|
114
88
|
|
115
|
-
def download(bucket, key, to=nil
|
89
|
+
def download(bucket, key, to=nil)
|
116
90
|
opened_file = (to && to.is_a?(String))
|
117
91
|
to = ::File.open(to, 'wb') if to.is_a?(String)
|
118
92
|
digestor = Digest::SHA1.new
|
119
93
|
data = ""
|
120
94
|
|
121
|
-
uri = URI.parse(
|
95
|
+
uri = URI.parse(download_url)
|
122
96
|
conn = Net::HTTP.new(uri.host, uri.port)
|
123
97
|
conn.use_ssl = uri.scheme == 'https'
|
124
98
|
|
@@ -132,15 +106,15 @@ class B2
|
|
132
106
|
digestor << chunk
|
133
107
|
if to
|
134
108
|
to << chunk
|
135
|
-
elsif
|
136
|
-
|
109
|
+
elsif block_given?
|
110
|
+
yield(chunk)
|
137
111
|
else
|
138
112
|
data << chunk
|
139
113
|
end
|
140
114
|
end
|
141
115
|
|
142
|
-
if digestor.hexdigest != response['X-Bz-Content-Sha1']
|
143
|
-
|
116
|
+
if response['X-Bz-Content-Sha1'] != 'none' && digestor.hexdigest != response['X-Bz-Content-Sha1']
|
117
|
+
rase B2::FileIntegrityError.new("SHA1 Mismatch, expected: \"#{response['X-Bz-Content-Sha1']}\", actual: \"#{digestor.hexdigest}\"")
|
144
118
|
end
|
145
119
|
when Net::HTTPNotFound
|
146
120
|
raise B2::NotFound.new(JSON.parse(response.body)['message'])
|
@@ -164,7 +138,7 @@ class B2
|
|
164
138
|
elsif to
|
165
139
|
to.flush
|
166
140
|
end
|
167
|
-
|
141
|
+
!block_given? && to.nil? ? data : nil
|
168
142
|
end
|
169
143
|
|
170
144
|
def get(path, body=nil, &block)
|
@@ -180,4 +154,5 @@ class B2
|
|
180
154
|
end
|
181
155
|
|
182
156
|
end
|
183
|
-
end
|
157
|
+
end
|
158
|
+
|
data/lib/b2/errors.rb
CHANGED
data/lib/b2/file.rb
CHANGED
data/lib/b2/version.rb
CHANGED
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.
|
4
|
+
version: 1.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Bracy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-04-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -36,6 +36,7 @@ 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
|
@@ -62,8 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
63
|
- !ruby/object:Gem::Version
|
63
64
|
version: '0'
|
64
65
|
requirements: []
|
65
|
-
|
66
|
-
rubygems_version: 2.7.4
|
66
|
+
rubygems_version: 3.0.6
|
67
67
|
signing_key:
|
68
68
|
specification_version: 4
|
69
69
|
summary: Backblaze B2 Client
|