fog-backblaze 0.1.1 → 0.1.2

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
  SHA1:
3
- metadata.gz: e72a3f8088a51fed32e1b1efc3d18772ff03348b
4
- data.tar.gz: 635bf11d1a64069912521d3c179fb5e46e67e3ec
3
+ metadata.gz: c206d62c779b21fd9771105907c940dec19a1ea0
4
+ data.tar.gz: 35c054e6d149c3b09ab1f3521643bc255187f5c5
5
5
  SHA512:
6
- metadata.gz: d3af95365d4d87cd941c016e18ebf326c9bcdbebb3d7440b29e1f8bc0557cc416c1fd9d973bc5e9cdf3a0091123bdff9b8c933d7c41e53ec25b682a421585e3c
7
- data.tar.gz: 86a1546b2f45cf95bb0f79a93c97b3eaa3201b3d3f9655bd264cf13cc539d0c9e882d892392ab6d8f45e7cde3041bcaec7aa2bfe34d142ab7877e0a99b3bed6b
6
+ metadata.gz: a1858a028b04d845479b515cf3a9b30cf44ca3cde4265095fb40b64e4154593260615bbb6685c20b4fdc0bdcd4096188639c4a858e2791d0cded17453df78886
7
+ data.tar.gz: 9655f45406f1bd4ff75c35155f46d294bb6ed26b7e6b7f10d7291218107bad1f59f2cfd3f1866bfe9b60535c7ee6edc68697719115060e64fa05e76369574360
data/.gitignore CHANGED
@@ -1,8 +1,23 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ gemfiles/*.lock
data/.travis.yml CHANGED
@@ -2,4 +2,5 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.4.0
5
+ - 2.5.0
5
6
  before_install: gem install bundler -v 1.15.1
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require "rake/testtask"
4
4
  Rake::TestTask.new(:test) do |t|
5
5
  t.libs << "test"
6
6
  t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
7
+ t.test_files = FileList["test/**/*_spec.rb"]
8
8
  end
9
9
 
10
10
  task :default => :test
data/examples/example.rb CHANGED
@@ -23,8 +23,7 @@ connection = Fog::Storage.new(
23
23
  #b2_bucket_name: ENV['B2_BUCKET'],
24
24
  #b2_bucket_id: '111222333444',
25
25
 
26
- logger: ENV['DEBUG'] ? Logger.new(STDOUT) : nil,
27
- token_cache: :memory
26
+ logger: ENV['DEBUG'] ? Logger.new(STDOUT) : nil
28
27
  )
29
28
 
30
29
  connection.delete_bucket("fog-smoke-test") rescue nil
@@ -1,6 +1,6 @@
1
1
  module Fog
2
2
  module Backblaze
3
- module JSONReponse
3
+ module JSONResponse
4
4
 
5
5
  attr_writer :json
6
6
 
@@ -9,7 +9,7 @@ module Fog
9
9
  end
10
10
 
11
11
  def json
12
- @json ||= JSON.parse(raw_body)
12
+ @json ||= ::JSON.parse(raw_body)
13
13
  end
14
14
 
15
15
  def assign_json_body!
@@ -12,7 +12,7 @@
12
12
  # end
13
13
  # def load_data
14
14
  # raw_data = @redis.get("b2_token_cache")
15
- # raw_data ? JSON.parse(raw_data) : {}
15
+ # raw_data ? ::JSON.parse(raw_data) : {}
16
16
  # end
17
17
  # def save_data
18
18
  # @redis.set("b2_token_cache", JSON.pretty_generate(@data))
@@ -22,11 +22,6 @@
22
22
  # Fog::Storage.new(provider: 'backblaze', ..., token_cache: RedisTokenCache.new)
23
23
  #
24
24
 
25
- module Fog
26
- module Backblaze
27
- end
28
- end
29
-
30
25
  class Fog::Backblaze::TokenCache
31
26
 
32
27
  def initialize
@@ -113,7 +108,7 @@ class Fog::Backblaze::TokenCache
113
108
 
114
109
  def load_data
115
110
  if File.exist?(@file)
116
- JSON.parse(File.open(@file, 'rb', &:read))
111
+ ::JSON.parse(File.open(@file, 'rb', &:read))
117
112
  else
118
113
  {}
119
114
  end
@@ -1,5 +1,5 @@
1
1
  module Fog
2
2
  module Backblaze
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
data/lib/fog/backblaze.rb CHANGED
@@ -1,16 +1,16 @@
1
- require_relative 'backblaze/version'
2
- require_relative 'backblaze/token_cache'
3
- require_relative 'backblaze/json_response'
4
-
5
1
  require 'fog/core'
6
2
  require 'json'
7
3
 
8
- #require_relative 'backblaze/storage'
9
-
10
4
  module Fog
11
5
  module Backblaze
6
+ autoload :JSONResponse, File.expand_path("../backblaze/json_response", __FILE__)
7
+ autoload :TokenCache, File.expand_path("../backblaze/token_cache", __FILE__)
8
+
12
9
  extend Fog::Provider
13
10
  service(:storage, "Storage")
11
+ end
14
12
 
13
+ module Storage
14
+ autoload :Backblaze, File.expand_path("../storage/backblaze", __FILE__)
15
15
  end
16
16
  end
@@ -0,0 +1,36 @@
1
+ class Fog::Storage::Backblaze::Mock
2
+ def self.data
3
+ @data ||= Hash.new do |hash, key|
4
+ hash[key] = {}
5
+ end
6
+ end
7
+
8
+ def self.reset
9
+ @data = nil
10
+ end
11
+
12
+ def initialize(options={})
13
+ @b2_account_id = options[:b2_account_id]
14
+ @b2_account_token = options[:b2_account_token]
15
+ @path = '/v1/AUTH_1234'
16
+ @containers = {}
17
+ end
18
+
19
+ def data
20
+ self.class.data[@softlayer_username]
21
+ end
22
+
23
+ def reset_data
24
+ self.class.data.delete(@softlayer_username)
25
+ end
26
+
27
+ def change_account(account)
28
+ @original_path ||= @path
29
+ version_string = @original_path.split('/')[1]
30
+ @path = "/#{version_string}/#{account}"
31
+ end
32
+
33
+ def reset_account_name
34
+ @path = @original_path
35
+ end
36
+ end
@@ -3,7 +3,7 @@ class Fog::Storage::Backblaze::Directories < Fog::Collection
3
3
 
4
4
  def all
5
5
  data = service.list_buckets
6
- load(data)
6
+ load(data.body['buckets'])
7
7
  end
8
8
 
9
9
  def get(name)
@@ -0,0 +1,61 @@
1
+ class Fog::Storage::Backblaze::Directory < Fog::Model
2
+ identity :key, aliases: %w(bucketName)
3
+
4
+ attribute :bucket_id#, aliases: 'bucketId'
5
+ attribute :bucket_info#, aliases: 'bucketInfo'
6
+ attribute :bucket_type#, aliases: 'bucketType'
7
+ attribute :cors_rules#, aliases: 'corsRules'
8
+ attribute :lifecycle_rules#, aliases: 'lifecycleRules'
9
+ attribute :revision
10
+
11
+ alias_method :name, :key
12
+
13
+ def destroy
14
+ requires :key
15
+ response = service.delete_bucket(key)
16
+ return response.status < 400
17
+ #rescue Fog::Errors::NotFound
18
+ # false
19
+ end
20
+
21
+ def save
22
+ requires :key
23
+ options = {}
24
+
25
+ options[:bucketInfo] = bucket_info if bucket_info
26
+ options[:bucketType] = bucket_type if bucket_type
27
+ options[:lifecycleRules] = lifecycle_rules if lifecycle_rules
28
+ options[:corsRules] = cors_rules if cors_rules
29
+
30
+ if attributes[:bucket_id]
31
+ options[:bucketId] = attributes[:bucket_id]
32
+ response = service.update_bucket(key, options)
33
+ else
34
+ response = service.put_bucket(key, options)
35
+ end
36
+
37
+ attributes[:bucket_id] = response.json['bucketId']
38
+ attributes[:bucket_type] = response.json['bucketType']
39
+ attributes[:bucket_info] = response.json['bucketInfo']
40
+ attributes[:revision] = response.json['revision']
41
+ attributes[:lifecycle_rules] = response.json['lifecycleRules']
42
+ attributes[:cors_rules] = response.json['corsRules']
43
+
44
+ true
45
+ end
46
+
47
+ def files
48
+ @files ||= Fog::Storage::Backblaze::Files.new(directory: self, service: service)
49
+ end
50
+
51
+ def public?
52
+ attributes[:bucket_type] == "allPublic"
53
+ end
54
+
55
+ alias_method :public, :public?
56
+
57
+ def public=(value)
58
+ self.bucket_type = value ? 'allPublic' : 'allPrivate'
59
+ end
60
+
61
+ end
@@ -1,6 +1,6 @@
1
1
  class Fog::Storage::Backblaze::File < Fog::Model
2
2
 
3
- identity :file_name, aliases: %w{fileName key}
3
+ identity :file_name, aliases: %w{fileName key name}
4
4
 
5
5
  attribute :content_length, aliases: 'contentLength'
6
6
  attribute :content_type, aliases: 'contentType'
@@ -10,6 +10,8 @@ class Fog::Storage::Backblaze::File < Fog::Model
10
10
 
11
11
  attr_accessor :directory
12
12
 
13
+ alias_method :name, :file_name
14
+
13
15
  # TODO: read content from cloud on demand
14
16
  def body
15
17
  attributes[:body] #||= file_id && (file = collection.get(identity)) ? file.body : ""
@@ -37,6 +39,14 @@ class Fog::Storage::Backblaze::File < Fog::Model
37
39
  true
38
40
  end
39
41
 
42
+ def destroy
43
+ requires :key
44
+ response = service.delete_object(directory.key, key)
45
+ return response.status < 400
46
+ #rescue Fog::Errors::NotFound
47
+ # false
48
+ end
49
+
40
50
  def public_url
41
51
  requires :directory, :key
42
52
 
@@ -0,0 +1,433 @@
1
+ class Fog::Storage::Backblaze::Real
2
+ attr_reader :token_cache, :options
3
+
4
+ def initialize(options = {})
5
+ @options = options
6
+ @logger = @options[:logger] || begin
7
+ require 'logger'
8
+ Logger.new("/dev/null")
9
+ end
10
+
11
+ @token_cache = if options[:token_cache].nil? || options[:token_cache] == :memory
12
+ Fog::Backblaze::TokenCache.new
13
+ elsif options[:token_cache] === false
14
+ Fog::Backblaze::TokenCache::NullTokenCache.new
15
+ elsif token_cache.is_a?(Fog::Backblaze::TokenCache)
16
+ token_cache
17
+ else
18
+ Fog::Backblaze::TokenCache::FileTokenCache.new(options[:token_cache])
19
+ end
20
+ end
21
+
22
+ def logger
23
+ @logger
24
+ end
25
+
26
+ ## Buckets
27
+
28
+ # call b2_create_bucket
29
+ def put_bucket(bucket_name, extra_options = {})
30
+ options = {
31
+ accountId: @options[:b2_account_id],
32
+ bucketType: extra_options.delete(:public) ? 'allPublic' : 'allPrivate',
33
+ bucketName: bucket_name,
34
+ }.merge(extra_options)
35
+
36
+ response = b2_command(:b2_create_bucket, body: options)
37
+
38
+ if response.status >= 400
39
+ raise Fog::Errors::Error, "Failed put_bucket, status = #{response.status} #{response.body}"
40
+ end
41
+
42
+ if cached = @token_cache.buckets
43
+ @token_cache.buckets = cached.merge(bucket_name => response.json)
44
+ else
45
+ @token_cache.buckets = {bucket_name => response.json}
46
+ end
47
+
48
+ response
49
+ end
50
+
51
+ # call b2_update_bucket
52
+ # if options[:bucketId] presents, then bucket_name is option
53
+ def update_bucket(bucket_name, extra_options)
54
+ options = {
55
+ accountId: @options[:b2_account_id],
56
+ bucketId: extra_options[:bucketId] || _get_bucket_id!(bucket_name),
57
+ }
58
+ if extra_options.has_key?(:public)
59
+ options[:bucketType] = extra_options.delete(:public) ? 'allPublic' : 'allPrivate'
60
+ end
61
+ options.merge!(extra_options)
62
+
63
+ response = b2_command(:b2_update_bucket, body: options)
64
+
65
+ if response.status >= 400
66
+ raise Fog::Errors::Error, "Failed update_bucket, status = #{response.status} #{response.body}"
67
+ end
68
+
69
+ if cached = @token_cache.buckets
70
+ @token_cache.buckets = cached.merge(bucket_name => response.json)
71
+ else
72
+ @token_cache.buckets = {bucket_name => response.json}
73
+ end
74
+
75
+ response
76
+ end
77
+
78
+ # call b2_list_buckets
79
+ def list_buckets
80
+ response = b2_command(:b2_list_buckets, body: {accountId: @options[:b2_account_id]})
81
+
82
+ response
83
+ end
84
+
85
+ # call b2_list_buckets
86
+ def get_bucket(bucket_name)
87
+ response = list_buckets
88
+ bucket = response.json['buckets'].detect do |bucket|
89
+ bucket['bucketName'] == bucket_name
90
+ end
91
+
92
+ unless bucket
93
+ raise Fog::Errors::NotFound, "No bucket with name: #{bucket_name}, " +
94
+ "found buckets: #{response.json['buckets'].map {|b| b['bucketName']}.join(", ")}"
95
+ end
96
+
97
+ response.body = bucket
98
+ response.json = bucket
99
+ return response
100
+ end
101
+
102
+ # call b2_delete_bucket
103
+ def delete_bucket(bucket_name, options = {})
104
+ bucket_id = _get_bucket_id!(bucket_name)
105
+
106
+ response = b2_command(:b2_delete_bucket,
107
+ body: {
108
+ bucketId: bucket_id,
109
+ accountId: @options[:b2_account_id]
110
+ }
111
+ )
112
+
113
+ if !options[:is_retrying]
114
+ if response.status == 400 && response.json['message'] =~ /Bucket .+ does not exist/
115
+ logger.info("Try drop cache and try again")
116
+ @token_cache.buckets = nil
117
+ return delete_bucket(bucket_name, is_retrying: true)
118
+ end
119
+ end
120
+
121
+ if response.status >= 400
122
+ raise Fog::Errors::Error, "Failed delete_bucket, status = #{response.status} #{response.body}"
123
+ end
124
+
125
+ if cached = @token_cache.buckets
126
+ #cached.delete(bucket_name)
127
+ #@token_cache.buckets = cached
128
+ @token_cache.buckets = nil
129
+ end
130
+
131
+ response
132
+ end
133
+
134
+ ## Objects
135
+
136
+ # call b2_list_file_names
137
+ def list_objects(bucket_name, options = {})
138
+ bucket_id = _get_bucket_id!(bucket_name)
139
+
140
+ b2_command(:b2_list_file_names, body: {
141
+ bucketId: bucket_id,
142
+ maxFileCount: 10_000
143
+ }.merge(options))
144
+ end
145
+
146
+ def head_object(bucket_name, file_path)
147
+ file_url = get_object_url(bucket_name, file_path)
148
+
149
+ result = b2_command(nil,
150
+ method: :head,
151
+ url: file_url
152
+ )
153
+
154
+ if result.status == 404
155
+ raise Fog::Errors::NotFound, "Can not find #{file_path.inspect} in bucket #{bucket_name}"
156
+ end
157
+
158
+ if result.status >= 400
159
+ raise Fog::Errors::NotFound, "Backblaze respond with status = #{result.status} - #{result.reason_phrase}"
160
+ end
161
+
162
+ result
163
+ end
164
+
165
+ # call b2_get_upload_url
166
+ #
167
+ # connection.put_object("a-bucket", "/some_file.txt", string_or_io, options)
168
+ #
169
+ # Possible options:
170
+ # * content_type
171
+ # * last_modified - time object or number of miliseconds
172
+ # * content_disposition
173
+ # * extra_headers - hash, list of custom headers
174
+ def put_object(bucket_name, file_path, content, options = {})
175
+ upload_url = @token_cache.fetch("upload_url/#{bucket_name}") do
176
+ bucket_id = _get_bucket_id!(bucket_name)
177
+ result = b2_command(:b2_get_upload_url, body: {bucketId: bucket_id})
178
+ result.json
179
+ end
180
+
181
+ if content.is_a?(IO)
182
+ content = content.read
183
+ end
184
+
185
+ extra_headers = {}
186
+ if options[:content_type]
187
+ extra_headers['Content-Type'] = options[:content_type]
188
+ end
189
+
190
+ if options[:last_modified]
191
+ value = if options[:last_modified].is_a?(::Time)
192
+ (options[:last_modified].to_f * 1000).round
193
+ else
194
+ value
195
+ end
196
+ extra_headers['X-Bz-Info-src_last_modified_millis'] = value
197
+ end
198
+
199
+ if options[:content_disposition]
200
+ extra_headers['X-Bz-Info-b2-content-disposition'] = options[:content_disposition]
201
+ end
202
+
203
+ if options[:extra_headers]
204
+ options[:extra_headers].each do |key, value|
205
+ extra_headers["X-Bz-Info-#{key}"] = value
206
+ end
207
+ end
208
+
209
+ response = b2_command(nil,
210
+ url: upload_url['uploadUrl'],
211
+ body: content,
212
+ headers: {
213
+ 'Authorization': upload_url['authorizationToken'],
214
+ 'Content-Type': 'b2/x-auto',
215
+ 'X-Bz-File-Name': "#{_esc_file(file_path)}",
216
+ 'X-Bz-Content-Sha1': Digest::SHA1.hexdigest(content)
217
+ }.merge(extra_headers)
218
+ )
219
+
220
+ if response.json['fileId'] == nil
221
+ raise Fog::Errors::Error, "Failed put_object, status = #{response.status} #{response.body}"
222
+ end
223
+
224
+ response
225
+ end
226
+
227
+ # generates url regardless if bucket is private or not
228
+ def get_object_url(bucket_name, file_path)
229
+ "#{auth_response['downloadUrl']}/file/#{CGI.escape(bucket_name)}/#{_esc_file(file_path)}"
230
+ end
231
+
232
+ alias_method :get_object_https_url, :get_object_url
233
+
234
+ # call b2_get_download_authorization
235
+ def get_public_object_url(bucket_name, file_path, options = {})
236
+ bucket_id = _get_bucket_id!(bucket_name)
237
+
238
+ result = b2_command(:b2_get_download_authorization, body: {
239
+ bucketId: bucket_id,
240
+ fileNamePrefix: file_path,
241
+ validDurationInSeconds: 604800
242
+ }.merge(options))
243
+
244
+ if result.status == 404
245
+ raise Fog::Errors::NotFound, "Can not find #{file_path.inspect} in bucket #{bucket_name}"
246
+ end
247
+
248
+ if result.status >= 400
249
+ raise Fog::Errors::NotFound, "Backblaze respond with status = #{result.status} - #{result.reason_phrase}"
250
+ end
251
+
252
+ "#{get_object_url(bucket_name, file_path)}?Authorization=#{result.json['authorizationToken']}"
253
+ end
254
+
255
+ def get_object(bucket_name, file_name)
256
+ file_url = get_object_url(bucket_name, file_name)
257
+
258
+ response = b2_command(nil,
259
+ method: :get,
260
+ url: file_url
261
+ )
262
+
263
+ if response.status == 404
264
+ raise Fog::Errors::NotFound, "Can not find #{file_name.inspect} in bucket #{bucket_name}"
265
+ end
266
+
267
+ if response.status > 400
268
+ raise Fog::Errors::Error, "Failed get_object, status = #{response.status} #{response.body}"
269
+ end
270
+
271
+ return response
272
+ end
273
+
274
+ # call b2_delete_file_version
275
+ def delete_object(bucket_name, file_name)
276
+ version_ids = _get_object_version_ids(bucket_name, file_name)
277
+
278
+ if version_ids.size == 0
279
+ raise Fog::Errors::NotFound, "Can not find #{file_name} in in bucket #{bucket_name}"
280
+ end
281
+
282
+ logger.info("Deleting #{version_ids.size} versions of #{file_name}")
283
+
284
+ last_response = nil
285
+ version_ids.each do |version_id|
286
+ last_response = b2_command(:b2_delete_file_version, body: {
287
+ fileName: file_name,
288
+ fileId: version_id
289
+ })
290
+ end
291
+
292
+ last_response
293
+ end
294
+
295
+ def _get_object_version_ids(bucket_name, file_name)
296
+ response = b2_command(:b2_list_file_versions,
297
+ body: {
298
+ startFileName: file_name,
299
+ prefix: file_name,
300
+ bucketId: _get_bucket_id!(bucket_name),
301
+ maxFileCount: 1000
302
+ }
303
+ )
304
+
305
+ if response.status >= 400
306
+ raise Fog::Errors::Error, "Fetch error: #{response.json['message']} (status = #{response.status})"
307
+ end
308
+
309
+ if response.json['files']
310
+ version_ids = []
311
+ response.json['files'].map do |file_version|
312
+ version_ids << file_version['fileId'] if file_version['fileName'] == file_name
313
+ end
314
+ version_ids
315
+ else
316
+ []
317
+ end
318
+ end
319
+
320
+ def _get_bucket_id(bucket_name)
321
+ if @options[:b2_bucket_name] == bucket_name && @options[:b2_bucket_id]
322
+ return @options[:b2_bucket_id]
323
+ else
324
+ cached = @token_cache && @token_cache.buckets
325
+
326
+ if cached && cached[bucket_name]
327
+ return cached[bucket_name]['bucketId']
328
+ else
329
+ fetched = _cached_buchets_hash(force_fetch: !!cached)
330
+ return fetched[bucket_name] && fetched[bucket_name]['bucketId']
331
+ end
332
+ end
333
+ end
334
+
335
+ def _get_bucket_id!(bucket_name)
336
+ bucket_id = _get_bucket_id(bucket_name)
337
+ unless bucket_id
338
+ raise Fog::Errors::NotFound, "Can not find bucket #{bucket_name}"
339
+ end
340
+
341
+ return bucket_id
342
+ end
343
+
344
+ def _cached_buchets_hash(force_fetch: false)
345
+
346
+ if !force_fetch && cached = @token_cache.buckets
347
+ cached
348
+ end
349
+
350
+ buckets_hash = {}
351
+ list_buckets.json['buckets'].each do |bucket|
352
+ buckets_hash[bucket['bucketName']] = bucket
353
+ end
354
+
355
+ @token_cache.buckets = buckets_hash
356
+
357
+ buckets_hash
358
+ end
359
+
360
+ def auth_response
361
+ #return @auth_response.json if @auth_response
362
+
363
+ if cached = @token_cache.auth_response
364
+ logger.info("get token from cache")
365
+ return cached
366
+ end
367
+
368
+ @auth_response = json_req(:get, "https://api.backblazeb2.com/b2api/v1/b2_authorize_account",
369
+ headers: {
370
+ "Authorization" => "Basic " + Base64.strict_encode64("#{@options[:b2_account_id]}:#{@options[:b2_account_token]}")
371
+ },
372
+ persistent: false
373
+ )
374
+
375
+ if @auth_response.status >= 400
376
+ raise Fog::Errors::Error, "Authentication error: #{@auth_response.json['message']} (status = #{@auth_response.status})\n#{@auth_response.body}"
377
+ end
378
+
379
+ @token_cache.auth_response = @auth_response.json
380
+
381
+ @auth_response.json
382
+ end
383
+
384
+ def b2_command(command, options = {})
385
+ auth_response = self.auth_response
386
+ options[:headers] ||= {}
387
+ options[:headers]['Authorization'] ||= auth_response['authorizationToken']
388
+
389
+ if options[:body] && !options[:body].is_a?(String)
390
+ options[:body] = JSON.generate(options[:body])
391
+ end
392
+
393
+ request_url = options.delete(:url) || "#{auth_response['apiUrl']}/b2api/v1/#{command}"
394
+
395
+ #pp [:b2_command, request_url, options]
396
+
397
+ json_req(options.delete(:method) || :post, request_url, options)
398
+ end
399
+
400
+ def json_req(method, url, options = {})
401
+ start_time = Time.now.to_f
402
+ logger.info("Req #{method.to_s.upcase} #{url}")
403
+ logger.debug(options.to_s)
404
+
405
+ if !options.has_key?(:persistent) || options[:persistent] == true
406
+ @connections ||= {}
407
+ full_path = [URI.parse(url).request_uri, URI.parse(url).fragment].compact.join("#")
408
+ host_url = url.sub(full_path, "")
409
+ connection = @connections[host_url] ||= Excon.new(host_url, persistent: true)
410
+ http_response = connection.send(method, options.merge(path: full_path, idempotent: true))
411
+ else
412
+ http_response = Excon.send(method, url, options)
413
+ end
414
+
415
+ http_response.extend(Fog::Backblaze::JSONResponse)
416
+ http_response.assign_json_body! if http_response.josn_response?
417
+
418
+ http_response
419
+ ensure
420
+ status = http_response && http_response.status
421
+ logger.info("Done #{method.to_s.upcase} #{url} = #{status} (#{(Time.now.to_f - start_time).round(3)} sec)")
422
+ logger.debug("Response Headers: #{http_response.headers}") if http_response
423
+ logger.debug("Response Body: #{http_response.body}") if http_response
424
+ end
425
+
426
+ def reset_token_cache
427
+ @token_cache.reset
428
+ end
429
+
430
+ def _esc_file(file_name)
431
+ CGI.escape(file_name).gsub('%2F', '/')
432
+ end
433
+ end
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+ require 'digest'
3
+ require 'cgi'
4
+
5
+ class Fog::Storage::Backblaze < Fog::Service
6
+ requires :b2_account_id, :b2_account_token
7
+ recognizes :b2_bucket_name, :b2_bucket_id, :token_cache, :logger
8
+
9
+ model_path 'fog/storage/backblaze/models'
10
+ model :directory
11
+ collection :directories
12
+ model :file
13
+ collection :files
14
+
15
+ autoload :Mock, File.expand_path("../backblaze/mock", __FILE__)
16
+ autoload :Real, File.expand_path("../backblaze/real", __FILE__)
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fog-backblaze
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Evstigneev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-27 00:00:00.000000000 Z
11
+ date: 2018-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fog-core
@@ -41,7 +41,6 @@ files:
41
41
  - ".gitignore"
42
42
  - ".travis.yml"
43
43
  - Gemfile
44
- - Gemfile.lock
45
44
  - LICENSE
46
45
  - README.md
47
46
  - Rakefile
@@ -51,13 +50,15 @@ files:
51
50
  - fog-backblaze.gemspec
52
51
  - lib/fog/backblaze.rb
53
52
  - lib/fog/backblaze/json_response.rb
54
- - lib/fog/backblaze/models/directories.rb
55
- - lib/fog/backblaze/models/directory.rb
56
- - lib/fog/backblaze/models/file.rb
57
- - lib/fog/backblaze/models/files.rb
58
- - lib/fog/backblaze/storage.rb
59
53
  - lib/fog/backblaze/token_cache.rb
60
54
  - lib/fog/backblaze/version.rb
55
+ - lib/fog/storage/backblaze.rb
56
+ - lib/fog/storage/backblaze/mock.rb
57
+ - lib/fog/storage/backblaze/models/directories.rb
58
+ - lib/fog/storage/backblaze/models/directory.rb
59
+ - lib/fog/storage/backblaze/models/file.rb
60
+ - lib/fog/storage/backblaze/models/files.rb
61
+ - lib/fog/storage/backblaze/real.rb
61
62
  homepage: https://github.com/fog/fog-backblaze
62
63
  licenses: []
63
64
  metadata: {}
data/Gemfile.lock DELETED
@@ -1,34 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- fog-backblaze (0.1.1)
5
- fog-core
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- builder (3.2.3)
11
- excon (0.61.0)
12
- fog-core (2.1.0)
13
- builder
14
- excon (~> 0.58)
15
- formatador (~> 0.2)
16
- mime-types
17
- formatador (0.2.5)
18
- mime-types (3.1)
19
- mime-types-data (~> 3.2015)
20
- mime-types-data (3.2016.0521)
21
- minitest (5.11.3)
22
- rake (12.3.1)
23
-
24
- PLATFORMS
25
- ruby
26
-
27
- DEPENDENCIES
28
- bundler (>= 1.15)
29
- fog-backblaze!
30
- minitest (>= 5.0)
31
- rake (~> 12.0)
32
-
33
- BUNDLED WITH
34
- 1.16.1
@@ -1,37 +0,0 @@
1
- class Fog::Storage::Backblaze::Directory < Fog::Model
2
- identity :key, aliases: %w(bucketName)
3
-
4
- attribute :bucket_id, aliases: 'bucketId'
5
- attribute :bucket_info, aliases: 'bucketInfo'
6
- attribute :bucket_type, aliases: 'bucketType'
7
-
8
- def destroy
9
- requires :key
10
- service.delete_bucket(key)
11
- true
12
- #rescue Fog::Errors::NotFound
13
- # false
14
- end
15
-
16
- def save
17
- requires :key
18
- options = {}
19
-
20
- response = service.put_bucket(key, options)
21
-
22
- attributes[:bucket_id] = response.json['bucketId']
23
- attributes[:bucket_type] = response.json['bucketType']
24
- attributes[:bucket_info] = response.json['bucketInfo']
25
-
26
- true
27
- end
28
-
29
- def files
30
- @files ||= Fog::Storage::Backblaze::Files.new(directory: self, service: service)
31
- end
32
-
33
- def public?
34
- attributes[:bucket_type] == "allPublic"
35
- end
36
-
37
- end
@@ -1,426 +0,0 @@
1
- require 'json'
2
- require 'digest'
3
- require 'cgi'
4
-
5
- class Fog::Storage::Backblaze < Fog::Service
6
- requires :b2_account_id, :b2_account_token
7
- recognizes :b2_bucket_name, :b2_bucket_id, :token_cache, :logger
8
-
9
- model_path 'fog/backblaze/models'
10
- model :directory
11
- collection :directories
12
- model :file
13
- collection :files
14
-
15
- class Mock
16
- #include Integrity
17
-
18
- def self.data
19
- @data ||= Hash.new do |hash, key|
20
- hash[key] = {}
21
- end
22
- end
23
-
24
- def self.reset
25
- @data = nil
26
- end
27
-
28
- def initialize(options={})
29
- @b2_account_id = options[:b2_account_id]
30
- @b2_account_token = options[:b2_account_token]
31
- @path = '/v1/AUTH_1234'
32
- @containers = {}
33
- end
34
-
35
- def data
36
- self.class.data[@softlayer_username]
37
- end
38
-
39
- def reset_data
40
- self.class.data.delete(@softlayer_username)
41
- end
42
-
43
- def change_account(account)
44
- @original_path ||= @path
45
- version_string = @original_path.split('/')[1]
46
- @path = "/#{version_string}/#{account}"
47
- end
48
-
49
- def reset_account_name
50
- @path = @original_path
51
- end
52
-
53
- end
54
-
55
- class Real
56
- attr_reader :token_cache, :options
57
-
58
- def initialize(options = {})
59
- @options = options
60
- @logger = @options[:logger] || begin
61
- require 'logger'
62
- Logger.new("/dev/null")
63
- end
64
-
65
- @token_cache = if options[:token_cache].nil? || options[:token_cache] == :memory
66
- Fog::Backblaze::TokenCache.new
67
- elsif options[:token_cache] === false
68
- Fog::Backblaze::TokenCache::NullTokenCache.new
69
- elsif token_cache.is_a?(Fog::Backblaze::TokenCache)
70
- token_cache
71
- else
72
- Fog::Backblaze::TokenCache::FileTokenCache.new(options[:token_cache])
73
- end
74
- end
75
-
76
- def logger
77
- @logger
78
- end
79
-
80
- ## Buckets
81
-
82
- def put_bucket(key, extra_options = {})
83
- options = {
84
- accountId: @options[:b2_account_id],
85
- bucketType: extra_options.delete(:public) ? 'allPublic' : 'allPrivate',
86
- bucketName: key,
87
- }.merge(extra_options)
88
-
89
- response = b2_command(:b2_create_bucket, body: options)
90
-
91
- if response.status >= 400
92
- raise Fog::Errors::Error, "Failed put_bucket, status = #{response.status} #{response.body}"
93
- end
94
-
95
- if cached = @token_cache.buckets
96
- @token_cache.buckets = cached.merge(key => response.json)
97
- else
98
- @token_cache.buckets = {key => response.json}
99
- end
100
-
101
- response
102
- end
103
-
104
- def list_buckets
105
- response = b2_command(:b2_list_buckets, body: {accountId: @options[:b2_account_id]})
106
-
107
- response
108
- end
109
-
110
- def get_bucket(bucket_name)
111
- reponse = list_buckets
112
- bucket = reponse.json['buckets'].detect do |bucket|
113
- bucket['bucketName'] == bucket_name
114
- end
115
-
116
- unless bucket
117
- raise Fog::Errors::NotFound, "No bucket with name: #{bucket_name}, " +
118
- "found: #{reponse.json['buckets'].map {|b| b['bucketName']}.join(", ")}"
119
- end
120
-
121
- reponse.body = bucket
122
- reponse.json = bucket
123
- return reponse
124
- end
125
-
126
- def delete_bucket(bucket_name, options = {})
127
- bucket_id = _get_bucket_id(bucket_name)
128
-
129
- unless bucket_id
130
- raise Fog::Errors::NotFound, "Can not bucket #{bucket_name}"
131
- end
132
-
133
- response = b2_command(:b2_delete_bucket,
134
- body: {
135
- bucketId: bucket_id,
136
- accountId: @options[:b2_account_id]
137
- }
138
- )
139
-
140
- if !options[:is_retrying]
141
- if response.status == 400 && response.json['message'] =~ /Bucket .+ does not exist/
142
- logger.info("Try drop cache and try again")
143
- @token_cache.buckets = nil
144
- return delete_bucket(bucket_name, is_retrying: true)
145
- end
146
- end
147
-
148
- if response.status >= 400
149
- raise Fog::Errors::Error, "Failed delete_bucket, status = #{response.status} #{response.body}"
150
- end
151
-
152
- if cached = @token_cache.buckets
153
- cached.delete(bucket_name)
154
- @token_cache.buckets = cached
155
- end
156
-
157
- response
158
- end
159
-
160
- ## Objects
161
-
162
- def list_objects(bucket_name, options = {})
163
- bucket_id = _get_bucket_id(bucket_name)
164
-
165
- unless bucket_id
166
- raise Fog::Errors::NotFound, "Can not bucket #{bucket_name}"
167
- end
168
-
169
- b2_command(:b2_list_file_names, body: {
170
- bucketId: bucket_id,
171
- maxFileCount: 10_000
172
- }.merge(options))
173
- end
174
-
175
- def head_object(bucket_name, file_path)
176
- file_url = get_object_url(bucket_name, file_path)
177
-
178
- result = b2_command(nil,
179
- method: :head,
180
- url: file_url
181
- )
182
-
183
- if result.status == 404
184
- raise Fog::Errors::NotFound, "Can not find #{file_path.inspect} in bucket #{bucket_name}"
185
- end
186
-
187
- if result.status >= 400
188
- raise Fog::Errors::NotFound, "Backblaze respond with status = #{result.status} - #{result.reason_phrase}"
189
- end
190
-
191
- result
192
- end
193
-
194
- # TODO: handle options
195
- def put_object(bucket_name, file_path, content, options = {})
196
- upload_url = @token_cache.fetch("upload_url/#{bucket_name}") do
197
- bucket_id = _get_bucket_id(bucket_name)
198
- unless bucket_id
199
- raise Fog::Errors::NotFound, "Can not find bucket #{bucket_name.inspect}"
200
- end
201
- result = b2_command(:b2_get_upload_url, body: {bucketId: _get_bucket_id(bucket_name)})
202
- result.json
203
- end
204
-
205
- extra_headers = {}
206
- if options[:content_type]
207
- extra_headers['Content-Type'] = options[:content_type]
208
- end
209
- if options[:last_modified]
210
- extra_headers['X-Bz-Info-src_last_modified_millis'] = options[:last_modified]
211
- end
212
- if options[:content_disposition]
213
- extra_headers['X-Bz-Info-b2-content-disposition'] = options[:content_disposition]
214
- end
215
-
216
- response = b2_command(nil,
217
- url: upload_url['uploadUrl'],
218
- body: content,
219
- headers: {
220
- 'Authorization': upload_url['authorizationToken'],
221
- 'Content-Type': 'b2/x-auto',
222
- 'X-Bz-File-Name': "#{_esc_file(file_path)}",
223
- 'X-Bz-Content-Sha1': Digest::SHA1.hexdigest(content)
224
- }.merge(extra_headers)
225
- )
226
-
227
- if response.json['fileId'] == nil
228
- raise Fog::Errors::Error, "Failed put_object, status = #{response.status} #{response.body}"
229
- end
230
-
231
- response
232
- end
233
-
234
- def get_object_url(bucket_name, file_path)
235
- "#{auth_response['downloadUrl']}/file/#{CGI.escape(bucket_name)}/#{_esc_file(file_path)}"
236
- end
237
-
238
- alias_method :get_object_https_url, :get_object_url
239
-
240
- def get_public_object_url(bucket_name, file_path, options = {})
241
- bucket_id = _get_bucket_id(bucket_name)
242
-
243
- unless bucket_id
244
- raise Fog::Errors::NotFound, "Can not bucket #{bucket_name}"
245
- end
246
-
247
- result = b2_command(:b2_get_download_authorization, body: {
248
- bucketId: bucket_id,
249
- fileNamePrefix: file_path,
250
- validDurationInSeconds: 604800
251
- }.merge(options))
252
-
253
- if result.status == 404
254
- raise Fog::Errors::NotFound, "Can not find #{file_path.inspect} in bucket #{bucket_name}"
255
- end
256
-
257
- if result.status >= 400
258
- raise Fog::Errors::NotFound, "Backblaze respond with status = #{result.status} - #{result.reason_phrase}"
259
- end
260
-
261
- "#{get_object_url(bucket_name, file_path)}?Authorization=#{result.json['authorizationToken']}"
262
- end
263
-
264
- def get_object(bucket_name, file_name)
265
- file_url = get_object_url(bucket_name, file_name)
266
-
267
- result = b2_command(nil,
268
- method: :get,
269
- url: file_url
270
- )
271
-
272
- if result.status == 404
273
- raise Fog::Errors::NotFound, "Can not find #{file_name.inspect} in bucket #{bucket_name}"
274
- end
275
-
276
- return result
277
- end
278
-
279
- def delete_object(bucket_name, file_name)
280
- version_ids = _get_object_version_ids(bucket_name, file_name)
281
-
282
- if version_ids.size == 0
283
- raise Fog::Errors::NotFound, "Can not find #{file_name} in in bucket #{bucket_name}"
284
- end
285
-
286
- logger.info("Deleting #{version_ids.size} versions of #{file_name}")
287
-
288
- last_response = nil
289
- version_ids.each do |version_id|
290
- last_response = b2_command(:b2_delete_file_version, body: {
291
- fileName: file_name,
292
- fileId: version_id
293
- })
294
- end
295
-
296
- last_response
297
- end
298
-
299
- def _get_object_version_ids(bucket_name, file_name)
300
- response = b2_command(:b2_list_file_versions,
301
- body: {
302
- startFileName: file_name,
303
- prefix: file_name,
304
- bucketId: _get_bucket_id(bucket_name),
305
- maxFileCount: 1000
306
- }
307
- )
308
-
309
- if response.json['files']
310
- version_ids = []
311
- response.json['files'].map do |file_version|
312
- version_ids << file_version['fileId'] if file_version['fileName'] == file_name
313
- end
314
- version_ids
315
- else
316
- []
317
- end
318
- end
319
-
320
- def _get_bucket_id(bucket_name)
321
- if @options[:b2_bucket_name] == bucket_name && @options[:b2_bucket_id]
322
- return @options[:b2_bucket_id]
323
- else
324
- cached = @token_cache && @token_cache.buckets
325
-
326
- if cached && cached[bucket_name]
327
- return cached[bucket_name]['bucketId']
328
- else
329
- fetched = _cached_buchets_hash(force_fetch: !!cached)
330
- return fetched[bucket_name] && fetched[bucket_name]['bucketId']
331
- end
332
- end
333
- end
334
-
335
- def _cached_buchets_hash(force_fetch: false)
336
-
337
- if !force_fetch && cached = @token_cache.buckets
338
- cached
339
- end
340
-
341
- buckets_hash = {}
342
- list_buckets.json['buckets'].each do |bucket|
343
- buckets_hash[bucket['bucketName']] = bucket
344
- end
345
-
346
- @token_cache.buckets = buckets_hash
347
-
348
- buckets_hash
349
- end
350
-
351
- def auth_response
352
- #return @auth_response.json if @auth_response
353
-
354
- if cached = @token_cache.auth_response
355
- logger.info("get token from cache")
356
- return cached
357
- end
358
-
359
- @auth_response = json_req(:get, "https://api.backblazeb2.com/b2api/v1/b2_authorize_account",
360
- headers: {
361
- "Authorization" => "Basic " + Base64.strict_encode64("#{@options[:b2_account_id]}:#{@options[:b2_account_token]}")
362
- },
363
- persistent: false
364
- )
365
-
366
- if @auth_response.status >= 400
367
- raise Fog::Errors::Error, "Authentication error: #{@auth_response.json['message']} (status = #{@auth_response.status})\n#{@auth_response.body}"
368
- end
369
-
370
- @token_cache.auth_response = @auth_response.json
371
-
372
- @auth_response.json
373
- end
374
-
375
- def b2_command(command, options = {})
376
- auth_response = self.auth_response
377
- options[:headers] ||= {}
378
- options[:headers]['Authorization'] ||= auth_response['authorizationToken']
379
-
380
- if options[:body] && !options[:body].is_a?(String)
381
- options[:body] = JSON.generate(options[:body])
382
- end
383
-
384
- request_url = options.delete(:url) || "#{auth_response['apiUrl']}/b2api/v1/#{command}"
385
-
386
- #pp [:b2_command, request_url, options]
387
-
388
- json_req(options.delete(:method) || :post, request_url, options)
389
- end
390
-
391
- def json_req(method, url, options = {})
392
- start_time = Time.now.to_f
393
- logger.info("Req #{method.to_s.upcase} #{url}")
394
- logger.debug(options.to_s)
395
-
396
- if !options.has_key?(:persistent) || options[:persistent] == true
397
- @connections ||= {}
398
- full_path = [URI.parse(url).request_uri, URI.parse(url).fragment].compact.join("#")
399
- host_url = url.sub(full_path, "")
400
- connection = @connections[host_url] ||= Excon.new(host_url, persistent: true)
401
- http_response = connection.send(method, options.merge(path: full_path, idempotent: true))
402
- else
403
- http_response = Excon.send(method, url, options)
404
- end
405
-
406
- http_response.extend(Fog::Backblaze::JSONReponse)
407
- http_response.assign_json_body! if http_response.josn_response?
408
-
409
- http_response
410
- ensure
411
- status = http_response && http_response.status
412
- logger.info("Done #{method.to_s.upcase} #{url} = #{status} (#{(Time.now.to_f - start_time).round(3)} sec)")
413
- logger.debug(http_response.headers) if http_response
414
- logger.debug(http_response.body) if http_response
415
- end
416
-
417
- def reset_token_cache
418
- @token_cache.reset
419
- end
420
-
421
- def _esc_file(file_name)
422
- CGI.escape(file_name).gsub('%2F', '/')
423
- end
424
- end
425
- end
426
-