b2-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c46280fcd18ce9df082fc3edfcddf837786817a52d70cd0e485584027807e4b8
4
+ data.tar.gz: 58e53bc7bec6095f7e4a541a8e6ec4fa28994cabe933b9f397fc9c0d820b9470
5
+ SHA512:
6
+ metadata.gz: 9139847cc212553bc2d214711c1ae79db7065c0e7a456c62ae15e2908b25f6dafa2effaa72df915d3f6afb72a62f07c8b1c194bcaeabf665808000f472fa94e0
7
+ data.tar.gz: 6c093acb1482a0bd61c949f68d02f11782b5c4bd5fdf816d7686074c3a30499f8e95318ffe77d011a1d7b6d500c085c2de070bc88680b3efbdbd38f1f76ee0fe
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ .DS_Store
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Jon Bracy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ # B2
2
+ A Backblaze B2 Client
3
+
4
+ # Usage
5
+
6
+ ```ruby
7
+ b2 = B2.new(account_id: B2_ACCOUNT_ID, application_key: B2_APPLICATION_KEY)
8
+
9
+ b2.upload_file('bucket_name', 'key', io_or_string)
10
+
11
+ b2.download('bucket_name', 'key') # => binary_string
12
+
13
+ b2.download('bucket_name', 'key') do |chunk|
14
+ # ... process the file as it streams ...
15
+ end
16
+
17
+ b2.download_to_file('bucket_name', 'key', '/path/to/file')
18
+
19
+ b2.file('bucket_name', 'key') # => #<B2::File>
20
+
21
+ b2.delete('bucket_name', 'key') # => bool
22
+ ```
@@ -0,0 +1,20 @@
1
+ require File.expand_path("../lib/b2/version", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "b2-client"
5
+ s.version = B2::VERSION
6
+ s.authors = ["Jon Bracy"]
7
+ s.email = ["jonbracy@gmail.com"]
8
+ s.homepage = "https://github.com/malomalo/b2"
9
+ s.summary = %q{Backblaze B2 Client}
10
+ s.description = %q{Backblaze B2 Client}
11
+ s.licenses = ['MIT']
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ # Developoment
19
+ s.add_development_dependency 'rake'
20
+ end
@@ -0,0 +1,121 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'net/http'
4
+
5
+ require File.expand_path('../b2/file', __FILE__)
6
+ require File.expand_path('../b2/bucket', __FILE__)
7
+ require File.expand_path('../b2/connection', __FILE__)
8
+ require File.expand_path('../b2/upload_chunker', __FILE__)
9
+
10
+ class B2
11
+
12
+ def initialize(account_id:, application_key:)
13
+ @account_id = account_id
14
+ @connection = B2::Connection.new(account_id, application_key)
15
+ @buckets_cache = []
16
+ end
17
+
18
+ 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
22
+ 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
30
+ end
31
+
32
+ def file(bucket, key)
33
+ bucket_id = lookup_bucket_id(bucket)
34
+
35
+ file = @connection.post('/b2api/v1/b2_list_file_names', {
36
+ bucketId: bucket_id,
37
+ startFileName: key
38
+ })['files'].find {|f| f['fileName'] == key }
39
+
40
+ file ? B2::File.new(file.merge({'bucketId' => bucket_id})) : nil
41
+ end
42
+
43
+ def delete(bucket, key)
44
+ object = file(bucket, key)
45
+ if object
46
+ @connection.post('/b2api/v1/b2_delete_file_version', {
47
+ fileName: file.name,
48
+ fileId: file.id
49
+ })
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ def get_upload_token(bucket)
56
+ @connection.post("/b2api/v1/b2_get_upload_url", {
57
+ bucketId: lookup_bucket_id(bucket)
58
+ })
59
+ end
60
+
61
+ def upload_file(bucket, key, io_or_string, mime_type: nil, info: {})
62
+ upload = get_upload_token(bucket)
63
+
64
+ uri = URI.parse(upload['uploadUrl'])
65
+ conn = Net::HTTP.new(uri.host, uri.port)
66
+ conn.use_ssl = uri.scheme == 'https'
67
+
68
+ chunker = B2::UploadChunker.new(io_or_string)
69
+ req = Net::HTTP::Post.new(uri.path)
70
+ req['Authorization'] = upload['authorizationToken']
71
+ req['X-Bz-File-Name'] = B2::File.encode_filename(key)
72
+ req['Content-Type'] = mime_type || 'b2/x-auto'
73
+ req['X-Bz-Content-Sha1'] = 'hex_digits_at_end'
74
+ info.each do |key, value|
75
+ req["X-Bz-Info-#{key}"] = value
76
+ end
77
+ req['Content-Length'] = chunker.size
78
+ req.body_stream = chunker
79
+
80
+ resp = conn.start { |http| http.request(req) }
81
+ if resp.is_a?(Net::HTTPSuccess)
82
+ JSON.parse(resp.body)
83
+ else
84
+ raise "Error connecting to B2 API"
85
+ end
86
+ end
87
+
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
+ end
96
+
97
+ def download(bucket, filename, &block)
98
+ digestor = Digest::SHA1.new
99
+ data = ""
100
+
101
+ @connection.get("/file/#{bucket}/#{filename}") do |response|
102
+ response.read_body do |chunk|
103
+ digestor << chunk
104
+ block.nil? ? (data << chunk) : block(chunk)
105
+ end
106
+ if digestor.hexdigest != resp['X-Bz-Content-Sha1']
107
+ raise 'file error'
108
+ end
109
+ end
110
+ block.nil? ? data : nil
111
+ end
112
+
113
+ def download_to_file(bucket, key, filename)
114
+ file = File.open(filename, 'w')
115
+ download(bucket, key) do |chunk|
116
+ file << chunk
117
+ end
118
+ file.close
119
+ end
120
+
121
+ end
@@ -0,0 +1,105 @@
1
+ class B2
2
+ class Bucket
3
+
4
+ attr_reader :id, :name, :account_id, :revision
5
+
6
+ def initialize(attrs, connection)
7
+ @id = attrs['bucketId']
8
+ @name = attrs['bucketName']
9
+
10
+ @account_id = attrs['accountId']
11
+ @revision = attrs['revision']
12
+
13
+ @connection = connection
14
+ end
15
+
16
+ def get_upload_token
17
+ @connection.post("/b2api/v1/b2_get_upload_url", { bucketId: @id })
18
+ end
19
+
20
+ def upload_file(key, io_or_string, mime_type: nil, sha1: nil, content_disposition: nil, info: {})
21
+ upload = get_upload_token
22
+
23
+ uri = URI.parse(upload['uploadUrl'])
24
+ conn = Net::HTTP.new(uri.host, uri.port)
25
+ conn.use_ssl = uri.scheme == 'https'
26
+
27
+ chunker = sha1 ? io_or_string : B2::UploadChunker.new(io_or_string)
28
+ req = Net::HTTP::Post.new(uri.path)
29
+ req['Authorization'] = upload['authorizationToken']
30
+ req['X-Bz-File-Name'] = B2::File.encode_filename(key)
31
+ req['Content-Type'] = mime_type || 'b2/x-auto'
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
34
+ info.each do |key, value|
35
+ req["X-Bz-Info-#{key}"] = value
36
+ end
37
+ req['Content-Length'] = chunker.size
38
+ req.body_stream = chunker
39
+
40
+ resp = conn.start { |http| http.request(req) }
41
+ result = if resp.is_a?(Net::HTTPSuccess)
42
+ JSON.parse(resp.body)
43
+ else
44
+ raise "Error connecting to B2 API"
45
+ end
46
+
47
+ B2::File.new(result, @connection)
48
+ end
49
+
50
+ def has_key?(key)
51
+ !@connection.post('/b2api/v1/b2_list_file_names', {
52
+ bucketId: @id,
53
+ startFileName: key,
54
+ maxFileCount: 1,
55
+ prefix: key
56
+ })['files'].empty?
57
+ end
58
+
59
+ def file(key)
60
+ file = @connection.post('/b2api/v1/b2_list_file_names', {
61
+ bucketId: @id,
62
+ startFileName: key,
63
+ maxFileCount: 1,
64
+ prefix: key
65
+ })['files'].first
66
+
67
+ file ? B2::File.new(file.merge({'bucketId' => @id}), @connection) : nil
68
+ end
69
+
70
+ def download(key, to=nil, &block)
71
+ to = File.open(to, 'w') if to.is_a?(String)
72
+ data = ""
73
+ digestor = Digest::SHA1.new
74
+
75
+ uri = URI.parse(@connection.download_url)
76
+ conn = Net::HTTP.new(uri.host, uri.port)
77
+ conn.use_ssl = uri.scheme == 'https'
78
+
79
+ conn.get("/file/#{@name}/#{key}") do |response|
80
+
81
+ response.read_body do |chunk|
82
+ digestor << chunk
83
+ if to
84
+ to << chunk
85
+ elsif block
86
+ block(chunk)
87
+ else
88
+ data << chunk
89
+ end
90
+ end
91
+
92
+ if digestor.hexdigest != resp['X-Bz-Content-Sha1']
93
+ raise 'file error'
94
+ end
95
+
96
+ end
97
+ block.nil? && to.nil? ? data : nil
98
+ end
99
+
100
+ def delete!(key)
101
+ file(key).delete!
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path('../../b2', __FILE__)
@@ -0,0 +1,99 @@
1
+ class B2
2
+ class Connection
3
+
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
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/v1/b2_authorize_account')
16
+ req.basic_auth(account_id, application_key)
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
+ @minimum_part_size = resp['absoluteMinimumPartSize']
33
+ @recommended_part_size = resp['recommendedPartSize']
34
+ @auth_token = resp['authorizationToken']
35
+ @download_url = resp['downloadUrl']
36
+ end
37
+
38
+ def disconnect!
39
+ if @connection
40
+ @connection.finish if @connection.active?
41
+ @connection = nil
42
+ end
43
+ end
44
+
45
+ def reconnect!
46
+ disconnect!
47
+ connect!
48
+ end
49
+
50
+ def authorization_token
51
+ if @auth_token_expires_at.nil? || @auth_token_expires_at <= Time.now.to_i
52
+ reconnect!
53
+ end
54
+ @auth_token
55
+ end
56
+
57
+ def active?
58
+ !@connection.nil? && @connection.active?
59
+ end
60
+
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
82
+
83
+ return_value
84
+ end
85
+
86
+ def get(path, body=nil, &block)
87
+ request = Net::HTTP::Get.new(path)
88
+
89
+ send_request(request, body, &block)
90
+ end
91
+
92
+ def post(path, body=nil, &block)
93
+ request = Net::HTTP::Post.new(path)
94
+
95
+ send_request(request, body, &block)
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,36 @@
1
+ class B2
2
+ class File
3
+
4
+ attr_reader :id, :name, :account_id, :bucket_id, :size, :sha1, :mime_type, :uploaded_at, :metadata
5
+
6
+ def initialize(attrs, connection)
7
+ @id = attrs['fileId']
8
+ @name = B2::File.decode_filename(attrs['fileName'])
9
+ @account_id = attrs['accountId']
10
+ @bucket_id = attrs['bucketId']
11
+ @size = attrs['contentLength']
12
+ @sha1 = attrs['contentSha1']
13
+ @mime_type = attrs['contentType']
14
+ @uploaded_at = attrs['uploadTimestamp']
15
+ @metadata = attrs['fileInfo']
16
+
17
+ @connection = connection
18
+ end
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
+ def delete!
29
+ @connection.post('/b2api/v1/b2_delete_file_version', {
30
+ fileId: @id,
31
+ fileName: @name
32
+ })
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ class B2
2
+ class UploadChunker
3
+ attr_reader :size, :sha1
4
+
5
+ def initialize(data)
6
+ @data = data
7
+ @sha_appended = false
8
+ @digestor = Digest::SHA1.new
9
+ @size = if data.is_a?(::File)
10
+ data.size + 40
11
+ elsif data.is_a?(String)
12
+ data.bytesize + 40
13
+ end
14
+ end
15
+
16
+ def read(length=nil, outbuf=nil)
17
+ return_value = @data.read(length, outbuf)
18
+
19
+ if outbuf.nil?
20
+ if return_value.nil? && !@sha_appended
21
+ @sha_appended = true
22
+ @digestor.hexdigest
23
+ else
24
+ @digestor << return_value
25
+ return_value
26
+ end
27
+ else
28
+ if outbuf.empty? && !@sha_appended
29
+ @sha_appended = true
30
+ outbuf.replace(@digestor.hexdigest)
31
+ else
32
+ @digestor << outbuf
33
+ end
34
+ outbuf
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module B2
2
+ VERSION = '1.0.0'
3
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: b2-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jon Bracy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Backblaze B2 Client
28
+ email:
29
+ - jonbracy@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - LICENSE
36
+ - README.md
37
+ - b2-client.gemspec
38
+ - lib/b2.rb
39
+ - lib/b2/bucket.rb
40
+ - lib/b2/client.rb
41
+ - lib/b2/connection.rb
42
+ - lib/b2/file.rb
43
+ - lib/b2/upload_chunker.rb
44
+ - lib/b2/version.rb
45
+ homepage: https://github.com/malomalo/b2
46
+ licenses:
47
+ - MIT
48
+ metadata: {}
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 2.7.4
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Backblaze B2 Client
69
+ test_files: []