bkblz 0.1.9 → 0.1.14

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