bkblz 0.1.9 → 0.1.14

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
- SHA1:
3
- metadata.gz: 276cd7b864f9a13fd8a32ea5839514f041afbd02
4
- data.tar.gz: c87c7242a87241a7b6e48f3bef5aab55117ef37b
2
+ SHA256:
3
+ metadata.gz: 47f032205c5b3348111f4cf170501fa08952ba7817cdc0c3ae06fba4c68c2a7d
4
+ data.tar.gz: d5a9dbae5c9fef54cbdc0e03520d283fe7634fc1b01cb537c81731ee0113d338
5
5
  SHA512:
6
- metadata.gz: 5ac0e9aa47318c48169be640350dede4d116d83b15980520b99289e19ea9b1ae7a2ae79c5a24a8e3c0e92f3dc7733e311888630b099ff150705a293a77715d10
7
- data.tar.gz: 53ac8463bd04b6a747f853837f825a6cb1908c1c55cefa837c472e0a83f575f12933969ed540ffb40fa9aad7c7d6a8f64f9553eca91ae4e3330c4366087c9956
6
+ metadata.gz: 3ac101a0d32a08d63ec18a156cc087b9401267ddaa37554fb4eadad5700f2a042b498953493d2c2c44127474665d6da688913814ab2620af20f17906d3fe7d50
7
+ data.tar.gz: 10bcde19f56e2488cb19879af0cbf46caa1a2221c987fd4cebf6237ad9c34d5b15671b5fa3142d21125325b5d36162969ab74d8e8bb0462c74362eda34a707ca
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ bkblz-*\.gem
data/README.rb CHANGED
@@ -1,6 +1,12 @@
1
1
  =begin
2
2
 
3
- This is the bkblz ruby gem, a library for the Backblaze B2 cloud
3
+ tldr:
4
+ $ gem install bkblz
5
+ $ bkblz help
6
+
7
+ More...
8
+
9
+ This is the bkblz Ruby gem, a library for the Backblaze B2 cloud
4
10
  storage API: https://www.backblaze.com/b2/docs/
5
11
 
6
12
  Currently the gem supports the following V1 API calls:
@@ -10,10 +16,14 @@ Currently the gem supports the following V1 API calls:
10
16
  * b2_delete_bucket
11
17
  * b2_delete_file_version
12
18
  * b2_get_file_info
19
+ * b2_get_upload_part_url
20
+ * b2_finish_large_file
13
21
  * b2_list_buckets
14
22
  * b2_list_file_names
15
23
  * b2_list_file_versions
24
+ * b2_start_large_file
16
25
  * b2_upload_file
26
+ * b2_upload_part
17
27
  * b2_download_file_by_id
18
28
  * b2_download_file_by_name
19
29
 
@@ -187,6 +197,8 @@ def run_readme
187
197
  byte_range_download = session.send(
188
198
  Bkblz::V1::DownloadFileByNameRequest.new bucket, file_name, bytes).to_model
189
199
  Bkblz.log.info "file bytes: #{byte_range_download.body}"
200
+
201
+ # TODO: add examples for uploading large files by parts... see lib/bkblz/task/upload_file
190
202
  rescue => e
191
203
  Bkblz.log.error "there was an error: #{e}"
192
204
  Bkblz.log.error e.backtrace.join "\n"
data/bin/bkblz CHANGED
@@ -109,18 +109,28 @@ module Bkblz
109
109
  class File < Thor
110
110
  include Helper
111
111
 
112
- desc "upload <bucket_name> <local_file_path>", "upload a local file to B2"
112
+ desc "upload <bucket_name> <local_file_path>", "upload a local file to B2" +
113
+ " if <local_file_path> is \"-\" (0x2d), then upload will read from STDIN."
113
114
  option :file_name, :desc => "remote file name, if not given the basename" +
114
- "of the local file is used"
115
+ " of the local file is used"
116
+ option :gzip, :type => :boolean, :desc => "automatically gzip files"
115
117
  def upload(bucket_name, local_file_path)
116
118
  parse_opts
117
119
 
120
+ params = {
121
+ :bucket_name => bucket_name,
122
+ :file_name => options[:file_name],
123
+ :gzip => options[:gzip]
124
+ }
125
+
126
+ if local_file_path == "-"
127
+ params[:file_body] = STDIN.read
128
+ else
129
+ params[:file_path] = local_file_path
130
+ end
131
+
118
132
  file_info = run_task do
119
- Bkblz::Task::UploadFile.run Bkblz.config, {
120
- :bucket_name => bucket_name,
121
- :file_path => local_file_path,
122
- :file_name => options[:file_name]
123
- }
133
+ Bkblz::Task::UploadFile.run Bkblz.config, params
124
134
  end
125
135
 
126
136
  print_header "File Info"
@@ -129,6 +139,7 @@ module Bkblz
129
139
 
130
140
  desc "downloadbyname <bucket> <name> <dir>", "downloads file by <bucket> " +
131
141
  "and <name> to directory <dir>"
142
+ option :strip_prefix, :desc => "prefix to strip from filename when writing locally"
132
143
  def downloadbyname(bucket_name, file_name, dir_path)
133
144
  parse_opts
134
145
 
@@ -137,7 +148,8 @@ module Bkblz
137
148
  :file_name => file_name,
138
149
  :bucket_name => bucket_name,
139
150
  :dir_path => dir_path,
140
- :use_filename => true
151
+ :use_filename => true,
152
+ :strip_prefix => options[:strip_prefix]
141
153
  }
142
154
  end
143
155
 
@@ -147,6 +159,7 @@ module Bkblz
147
159
 
148
160
  # TODO(erick): Rename to downloadbyid
149
161
  desc "download <id> <dir>", "downloads file <id> to directory <dir>"
162
+ option :strip_prefix, :desc => "prefix to strip from filename when writing locally"
150
163
  def download(file_id, dir_path)
151
164
  parse_opts
152
165
 
@@ -154,7 +167,8 @@ module Bkblz
154
167
  Bkblz::Task::DownloadFile.run Bkblz.config, {
155
168
  :file_id => file_id,
156
169
  :dir_path => dir_path,
157
- :use_filename => options[:by_name]
170
+ :use_filename => false,
171
+ :strip_prefix => options[:strip_prefix]
158
172
  }
159
173
  end
160
174
 
@@ -218,7 +232,8 @@ module Bkblz
218
232
  class Cli < Thor
219
233
  include Helper
220
234
 
221
- class_option :config, :desc => "path to a yml config file", :aliases => ["-c"]
235
+ class_option :config, :desc => "path to a yml config file", :aliases => ["-c"],
236
+ :default => ::File.expand_path('~/etc/bkblz/bkblz.yml')
222
237
 
223
238
  desc "bucket SUBCOMMAND ...ARGS", "manage backblaze buckets"
224
239
  subcommand "bucket", Bucket
@@ -9,7 +9,9 @@ module Bkblz
9
9
 
10
10
  :log_device => :stderr, # [:stdout, :stderr, :devnull, path, fd]
11
11
  :log_level => :warn, # [:debug, :info, :warn, :error, :fatal, (-6..-1)]
12
- :log_colorize => true
12
+ :log_colorize => true,
13
+
14
+ :large_file_max_chunk_size => 1e8, # 100MB
13
15
  }.freeze
14
16
 
15
17
  attr_reader *CONFIG_VARS.keys, :config_map
@@ -5,6 +5,7 @@ module Bkblz
5
5
  class DownloadFile < BaseTask
6
6
 
7
7
  task_param :dir_path, :required => true
8
+ task_param :strip_prefix
8
9
 
9
10
  # for downalod by id
10
11
  task_param :file_id
@@ -33,7 +34,12 @@ module Bkblz
33
34
  raise "unable to write to directory %s" % dir_path
34
35
  end
35
36
 
36
- f_path = ::File.join dir_path, download_file_info.file_name
37
+ out_file_name = download_file_info.file_name
38
+ unless params[:strip_prefix].nil?
39
+ out_file_name.gsub! params[:strip_prefix], ""
40
+ end
41
+
42
+ f_path = ::File.join dir_path, out_file_name
37
43
  if ::File.exists?(f_path) && !::File.writable?(f_path)
38
44
  raise "unable to write to existing file: %s" % f_path
39
45
  end
@@ -1,27 +1,112 @@
1
+ require 'zlib'
2
+
1
3
  module Bkblz
2
4
  module Task
3
5
  class UploadFile < BaseTask
4
6
 
5
7
  task_param :bucket_name, :required => true
6
- task_param :file_path, :required => true
7
- task_param :file_name # Overrides local file_path if given
8
8
 
9
- # TODO(erick): Change file_path to a byte string, let the task be agnostic
10
- # to where the data comes from. (Probably v0.2).
9
+ # Either file_body or file_path is required
10
+ task_param :file_body
11
+
12
+ task_param :file_path
13
+
14
+ # Overrides local file_path if given
15
+ task_param :file_name
16
+
17
+ # Use gzip default compression before writing to b2 the file name will be
18
+ # updated by appending ".gz"
19
+ task_param :gzip
20
+
11
21
  def run_internal(session, params)
12
- f = ::File.new(params[:file_path], "r")
22
+ file_name = if params[:file_name]
23
+ params[:file_name]
24
+ elsif params[:file_path]
25
+ ::File.basename(params[:file_path])
26
+ else
27
+ raise 'missing either :file_name or :file_path param'
28
+ end
29
+
30
+ file_mtime = if params[:file_path]
31
+ ::File.mtime(params[:file_path])
32
+ else
33
+ Time.now
34
+ end.to_i * 1000
13
35
 
14
36
  bucket = find_bucket_by_name session, params[:bucket_name]
37
+ size = file_size(params)
38
+
39
+ file_data = {
40
+ :file_name => file_name,
41
+ :file_mtime => file_mtime,
42
+ :file_size => size,
43
+ :bucket => bucket,
44
+ }
45
+
46
+ if size > config.large_file_max_chunk_size
47
+ upload_large_file session, params, **file_data
48
+ else
49
+ upload_file session, params, **file_data
50
+ end
51
+ end
52
+
53
+ private
54
+ def file_size(params)
55
+ if params[:file_path]
56
+ ::File.new(params[:file_path]).size
57
+ elsif params[:file_body]
58
+ params[:file_body].size
59
+ end
60
+ end
61
+
62
+ def upload_large_file(session, params, bucket:, file_name:, file_mtime:, **file_data)
63
+ start_large_file_info = session.send(Bkblz::V1::StartLargeFileRequest.new(
64
+ bucket.bucket_id, file_name, file_mtime)).to_model
65
+ file_id = start_large_file_info.file_id
66
+
67
+ upload_part_auth = session.send(Bkblz::V1::GetUploadPartUrlRequest.new(file_id)).to_model
68
+
69
+ actual_size = file_data[:file_size]
70
+ chunk_size = config.large_file_max_chunk_size
71
+ num_chunks = (actual_size / chunk_size.to_f).ceil
72
+
73
+ file_io = if params[:file_path]
74
+ ::File.new(params[:file_path], "rb")
75
+ else
76
+ raise 'only file_path is supported for large file uploads'
77
+ end
78
+
79
+ upload_part_infos = (0..num_chunks - 1).map do |chunk_i|
80
+ session.send(Bkblz::V1::UploadPartRequest.new(
81
+ upload_part_auth, file_io, chunk_i, chunk_size)).to_model
82
+ end
83
+
84
+ file_info = session.send(
85
+ Bkblz::V1::FinishLargeFileRequest.new(file_id, upload_part_infos)).to_model
86
+ end
87
+
88
+ def upload_file(session, params, bucket:, file_name:, file_mtime:, **file_data)
89
+ file_body = if params[:file_path]
90
+ f = ::File.new(params[:file_path], "r")
91
+ f.read
92
+ elsif params[:file_body]
93
+ params[:file_body]
94
+ else
95
+ raise 'missing either :file_body or :file_path param'
96
+ end
97
+
98
+ if params[:gzip]
99
+ # https://ruby-doc.org/stdlib-2.4.2/libdoc/zlib/rdoc/Zlib.html#method-c-gzip
100
+ file_body = Zlib.gzip file_body, level: Zlib::DEFAULT_COMPRESSION
101
+ file_name << ".gz" unless file_name =~ /\.gz$/
102
+ end
103
+
15
104
  upload_auth = session.send(
16
105
  Bkblz::V1::GetUploadUrlRequest.new bucket.bucket_id).to_model
17
106
 
18
- file_name = params[:file_name] || ::File.basename(f.path)
19
- file_body = f.read
20
- mtime_millis = f.mtime.to_i * 1000
21
-
22
107
  upload_file_info = session.send(
23
- Bkblz::V1::UploadFileRequest.new upload_auth, file_body, file_name,
24
- nil, mtime_millis).to_model
108
+ Bkblz::V1::UploadFileRequest.new(
109
+ upload_auth, file_body, file_name, nil, file_mtime)).to_model
25
110
  end
26
111
  end
27
112
  end
@@ -7,13 +7,17 @@ require_relative "request"
7
7
  require_relative "error_response"
8
8
 
9
9
  require_relative "authorize_account"
10
- require_relative "list_buckets"
11
10
  require_relative "create_bucket"
12
11
  require_relative "delete_bucket"
12
+ require_relative "delete_file_version"
13
+ require_relative "download_file"
14
+ require_relative "finish_large_file"
13
15
  require_relative "get_file_info"
16
+ require_relative "get_upload_part_url"
14
17
  require_relative "get_upload_url"
15
- require_relative "upload_file"
18
+ require_relative "list_buckets"
16
19
  require_relative "list_file_names"
17
20
  require_relative "list_file_versions"
18
- require_relative "delete_file_version"
19
- require_relative "download_file"
21
+ require_relative "start_large_file"
22
+ require_relative "upload_file"
23
+ require_relative "upload_part"
@@ -0,0 +1,30 @@
1
+ require "digest/sha1"
2
+
3
+ module Bkblz
4
+ module V1
5
+
6
+ class FinishLargeFileResponse < Response
7
+ response_model Model::FileInfo
8
+ end
9
+
10
+ class FinishLargeFileRequest < Request
11
+
12
+ response_class FinishLargeFileResponse
13
+ url_suffix "/b2api/v1/b2_finish_large_file"
14
+
15
+ def initialize(file_id, file_part_infos)
16
+ sha1_sums = file_part_infos.sort { |a, b| a.part_number <=> b.part_number }.map do |info|
17
+ info.content_sha1
18
+ end
19
+ @body = {
20
+ :file_id => file_id,
21
+ :part_sha1_array => sha1_sums,
22
+ }
23
+ end
24
+
25
+ def build_request(session)
26
+ session.create_post url(session), @body
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ module Bkblz
2
+ module V1
3
+
4
+ class GetUploadPartUrlResponse < Response
5
+ response_model Model::UploadPartAuth
6
+ end
7
+
8
+ class GetUploadPartUrlRequest < Request
9
+
10
+ response_class GetUploadPartUrlResponse
11
+ url_suffix "/b2api/v1/b2_get_upload_part_url"
12
+
13
+ def initialize(file_id)
14
+ @body = {:file_id => file_id}
15
+ end
16
+
17
+ def build_request(session)
18
+ session.create_post url(session), @body
19
+ end
20
+ end
21
+ end
22
+ end
@@ -15,18 +15,26 @@ module Bkblz
15
15
  :body, :content_length, :content_type, :file_id, :file_name, :sha1, :x_bz_info
16
16
  ]
17
17
 
18
- # Returned by upload_file
18
+ # Returned by upload_file, finish_large_file, start_large_file
19
19
  FileInfo = Model.define *[
20
- :account_id, :bucket_id, :content_length, :content_sha1, :content_type,
20
+ :account_id, :action, :bucket_id, :content_length, :content_sha1, :content_type,
21
21
  :file_id, :file_info, :file_name
22
22
  ]
23
23
 
24
+ # Returned by upload_file
25
+ FilePartInfo = Model.define *[
26
+ :file_id, :part_number, :content_length, :content_sha1, :upload_timestamp,
27
+ ]
28
+
24
29
  # Returned by delete_file_version
25
30
  PartialFileInfo = Model.define :file_id, :file_name
26
31
 
27
32
  # Returned by get_upload_url
28
33
  UploadAuth = Model.define :bucket_id, :upload_url, :authorization_token
29
34
 
35
+ # Returned by get_upload_part_url
36
+ UploadPartAuth = Model.define :file_id, :upload_url, :authorization_token
37
+
30
38
  # Possibly returned by any request
31
39
  Error = Model.define :status, :code, :message
32
40
  end
@@ -0,0 +1,40 @@
1
+ require "digest/sha1"
2
+
3
+ module Bkblz
4
+ module V1
5
+
6
+ class StartLargeFileResponse < Response
7
+ response_model Model::FileInfo
8
+ end
9
+
10
+ class StartLargeFileRequest < Request
11
+
12
+ response_class StartLargeFileResponse
13
+ url_suffix "/b2api/v1/b2_start_large_file"
14
+
15
+ def initialize(bucket_id, file_name, last_modified_millis=nil,
16
+ file_sha=nil, content_type='b2/x-auto', **file_info)
17
+ # Both of the following are recommended here:
18
+ # https://www.backblaze.com/b2/docs/b2_start_large_file.html
19
+ if last_modified_millis
20
+ # must be a string
21
+ file_info[:src_last_modified_millis] = last_modified_millis.to_s
22
+ end
23
+ if file_sha
24
+ file_info[:large_file_sha1] = file_sha
25
+ end
26
+
27
+ @body = {
28
+ :bucket_id => bucket_id,
29
+ :file_name => file_name,
30
+ :content_type => content_type,
31
+ :file_info => file_info
32
+ }
33
+ end
34
+
35
+ def build_request(session)
36
+ session.create_post url(session), @body
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,53 @@
1
+ require "digest/sha1"
2
+
3
+ module Bkblz
4
+ module V1
5
+
6
+ class UploadPartResponse < Response
7
+ response_model Model::FilePartInfo
8
+ end
9
+
10
+ class UploadPartRequest < Request
11
+
12
+ response_class UploadPartResponse
13
+
14
+ REQUIRED_HEADERS = {
15
+ :"Authorization" => nil,
16
+ :"Content-Length" => nil,
17
+ :"Content-Type" => 'application/octet-stream',
18
+ :"X-Bz-Part-Number" => nil, # a value in [1..10000]
19
+ :"X-Bz-Content-Sha1" => nil
20
+ }
21
+
22
+ ##
23
+ # @param {chunk_number} is a value in [0...9999]
24
+ def initialize(upload_part_auth, io, chunk_number, chunk_size)
25
+ @upload_url = upload_part_auth.upload_url
26
+ @body_chunk = read_chunk(io, chunk_number, chunk_size)
27
+ @headers = REQUIRED_HEADERS.dup
28
+
29
+ part_number = chunk_number + 1
30
+ @headers[:"Authorization"] = upload_part_auth.authorization_token
31
+ @headers[:"Content-Length"] = @body_chunk.size
32
+ @headers[:"X-Bz-Part-Number"] = part_number
33
+ @headers[:"X-Bz-Content-Sha1"] = Digest::SHA1.hexdigest @body_chunk
34
+ end
35
+
36
+ def build_request(session)
37
+ session.create_post @upload_url, @body_chunk, @headers
38
+ end
39
+
40
+ private
41
+ def read_chunk(io, chunk_number, chunk_size)
42
+ unless io.is_a?(IO)
43
+ raise 'only IO type is supported for upload_part'
44
+ end
45
+
46
+ byte = chunk_number * chunk_size
47
+ # https://ruby-doc.org/core-2.5/IO.html#method-i-seek
48
+ io.seek byte, IO::SEEK_SET
49
+ io.read(chunk_size)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,3 @@
1
1
  module Bkblz
2
- VERSION = "0.1.9"
2
+ VERSION = "0.1.14"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bkblz
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erick Johnson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-27 00:00:00.000000000 Z
11
+ date: 2020-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -105,7 +105,9 @@ files:
105
105
  - lib/bkblz/v1/delete_file_version.rb
106
106
  - lib/bkblz/v1/download_file.rb
107
107
  - lib/bkblz/v1/error_response.rb
108
+ - lib/bkblz/v1/finish_large_file.rb
108
109
  - lib/bkblz/v1/get_file_info.rb
110
+ - lib/bkblz/v1/get_upload_part_url.rb
109
111
  - lib/bkblz/v1/get_upload_url.rb
110
112
  - lib/bkblz/v1/list_buckets.rb
111
113
  - lib/bkblz/v1/list_file_names.rb
@@ -115,7 +117,9 @@ files:
115
117
  - lib/bkblz/v1/request.rb
116
118
  - lib/bkblz/v1/response.rb
117
119
  - lib/bkblz/v1/session.rb
120
+ - lib/bkblz/v1/start_large_file.rb
118
121
  - lib/bkblz/v1/upload_file.rb
122
+ - lib/bkblz/v1/upload_part.rb
119
123
  - lib/bkblz/version.rb
120
124
  homepage: https://github.com/erickj/bkblz
121
125
  licenses:
@@ -136,8 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
140
  - !ruby/object:Gem::Version
137
141
  version: '0'
138
142
  requirements: []
139
- rubyforge_project:
140
- rubygems_version: 2.6.13
143
+ rubygems_version: 3.1.4
141
144
  signing_key:
142
145
  specification_version: 4
143
146
  summary: Bkblz GEM for the Backblaze B2 API. https://www.backblaze.com/b2/docs/