fog-backblaze 0.1.1 → 0.1.2

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
  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
-