wetransfer 0.1.0 → 0.2.0

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: a6bb6ae2971c42681f0f887d3bcd76f3e19041c5
4
- data.tar.gz: 02e73980a479f35655e0a775072a0a2728d3f023
3
+ metadata.gz: 3e0f327dda7a10e3f6b84555d9a3345b1542061c
4
+ data.tar.gz: 3800f908a5b774c44e297cdc1d5047fcdbb95ef7
5
5
  SHA512:
6
- metadata.gz: a9badb333a33924c63de6ba39f0c40f05ce3444618164a166a08a21ff3d16e46b5f7c0bbb2d046f2674a8948146fdec26b6386716172fdd81cc71bdf9d9e916c
7
- data.tar.gz: 8c6c6641be4a570ea300e75e24512896b0d55c4ea1efabb5f45c2277bd7378a19fc8196e6aab9d5079a42da8a7befbb5dfda8f7bc4dc56682c71746cc51b4db6
6
+ metadata.gz: 32550fa1fccece36ff52b75a95f7fc51e6d3d67bf90b6c63962b99671be5b6fa81b6576bbee71a786ee49dea7701450fc3a28007743edd30379bbea0917d3eb5
7
+ data.tar.gz: 4625e08a9cce1b5bf3f8df1ac209499cc488791b96bb2c18f9086f76ecb999261d3a8482ed2ac878af017f50813b4e6a14a20664f3521dfdaa1c37196634082e
data/.travis.yml CHANGED
@@ -1,8 +1,7 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.7
5
- - 2.4.4
4
+ - 2.1.0
6
5
  - 2.5.1
7
6
  - jruby-9.0
8
7
  before_install: gem install bundler -v 1.16.1
@@ -11,5 +10,5 @@ matrix:
11
10
  allow_failures:
12
11
  - rvm: jruby-9.0
13
12
  script:
14
- - bundle exec rubocop -c .rubocop.yml --force-exclusion
15
13
  - bundle exec rspec
14
+ - bundle exec rubocop -c .rubocop.yml --force-exclusion
data/README.md CHANGED
@@ -19,7 +19,7 @@ For your API key please visit our [developer portal](https://developers.wetransf
19
19
  Add this line to your application's Gemfile:
20
20
 
21
21
  ```ruby
22
- gem 'wetransfer'
22
+ gem 'we_transfer_client'
23
23
  ```
24
24
 
25
25
  And then execute:
@@ -32,56 +32,32 @@ Or install it yourself as:
32
32
 
33
33
  ## Usage
34
34
 
35
- ### Configuration
36
-
37
- The gem allows you to configure several settings using environment variables.
38
-
39
- - `WT_API_LOGGING_ON` can be set to (a string) "true" if you want to switch Faraday's default logging on.
40
-
41
- - `WT_API_URL` can be set to a staging or test URL (something we do not offer yet, but plan to in the future)
42
-
43
- - `WT_API_CONNECTION_PATH` can be set to prefix the paths passed to faraday - for example if you're testing against a test API or a different version.
44
-
45
35
  ### Super simple transfers
46
36
 
47
37
  You'll need to retrieve an API key from [our developer portal](https://developers.wetransfer.com).
48
38
 
49
- Be sure to not commit this key to github! If you do though, no worries, you can always revoke & create a new key from within the portal. You will most likely want to pass this to the client setter using an environment variable.
39
+ Be sure to not commit this key to Github! If you do though, no worries, you can always revoke & create a new key from within the portal. You will most likely want to pass this to the client setter using an environment variable.
50
40
 
51
41
  Now that you've got a wonderful WeTransfer API key, you can create a Client object like so:
52
42
 
53
43
  ```ruby
54
- # In a .env or other secret handling file, not checked in to version control:
55
- WT_API_KEY=<your API key>
56
-
57
44
  # In your project file:
58
- @client = WeTransfer::Client.new(api_key: ENV['WT_API_KEY'])
45
+ @client = WeTransferClient.new(api_key: ENV.fetch('WT_API_KEY'))
59
46
  ```
60
47
 
61
48
  Now that you've got the client set up you can use the `create_transfer` to, well, create a transfer!
62
49
 
63
- If you pass item paths to the method it will handle the upload process itself, otherwise you can omit them and
64
- use the `add_items` method once the transfer has been created.
65
-
66
50
  ```ruby
67
- transfer = @client.create_transfer(name: "My wonderful transfer", description: "I'm so excited to share this", items: ["/path/to/local/file_1.jpg", "/path/to/local/file_2.png", "/path/to/local/file_3.key"])
51
+ transfer = @client.create_transfer(name: "My wonderful transfer", description: "I'm so excited to share this") do |upload|
52
+ upload.add_file_at(path: '/path/to/local/file.jpg')
53
+ upload.add_file_at(path: '/path/to/another/local/file.jpg')
54
+ upload.add_file(name: 'README.txt', io: StringIO.new("This is the contents of the file"))
55
+ end
68
56
 
69
- transfer.shortened_url = "https://we.tl/SSBsb3ZlIHJ1Ynk="
57
+ transfer.shortened_url => "https://we.tl/SSBsb3ZlIHJ1Ynk="
70
58
  ```
71
59
 
72
- ## Item upload flow
73
-
74
- ### `add_items`
75
-
76
- If you want slightly more granular control over your transfer, create it without an `items` array, and then use `add_items` with the resulting transfer object.
77
-
78
- ```ruby
79
- transfer = @client.create_transfer(name: "My wonderful transfer", description: "I'm so excited to share this")
80
-
81
- @client.add_items(transfer: @transfer, items: ["/path/to/local/file_1.jpg", "/path/to/local/file_2.png", "/path/to/local/file_3.key"])
82
-
83
- transfer.shortened_url = "https://we.tl/d2V0cmFuc2Zlci5ob21lcnVuLmNv"
84
- ```
60
+ The upload will be performed at the end of the block.
85
61
 
86
62
  ## Development
87
63
 
@@ -99,4 +75,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
99
75
 
100
76
  ## Code of Conduct
101
77
 
102
- Everyone interacting in the WetransferRubySdk project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/wetransfer/wetransfer/blob/master/CODE_OF_CONDUCT.md).
78
+ Everyone interacting in the WetransferRubySdk project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/wetransfer/wetransfer/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,13 @@
1
+ require_relative 'we_transfer_client'
2
+ require 'dotenv'
3
+ Dotenv.load
4
+
5
+ client = WeTransferClient.new(api_key: ENV.fetch('WT_API_KEY')) # , logger: Logger.new($stderr))
6
+ transfer = client.create_transfer(title: 'My amazing board', message: 'Hi there!') do |builder|
7
+ builder.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb'))
8
+ builder.add_file(name: 'amazing.txt', io: StringIO.new('This is unbelievable'))
9
+ builder.add_file(name: 'huge.bin', io: File.open('/path/to/local/file.jpg', 'rb'))
10
+ builder.add_file_at(path: __FILE__)
11
+ end
12
+
13
+ puts transfer.shortened_url
@@ -0,0 +1,194 @@
1
+ require 'faraday'
2
+ require 'logger'
3
+ require 'ks'
4
+ require 'securerandom'
5
+ require 'json'
6
+
7
+ class WeTransferClient
8
+ require_relative 'we_transfer_client/version'
9
+
10
+ class Error < StandardError
11
+ end
12
+
13
+ NULL_LOGGER = Logger.new(nil)
14
+ MAGIC_PART_SIZE = 6 * 1024 * 1024
15
+ EXPOSED_COLLECTION_ATTRIBUTES = [:id, :version_identifier, :state, :shortened_url, :name, :description, :size, :items]
16
+ EXPOSED_ITEM_ATTRIBUTES = [:id, :local_identifier, :content_identifier, :name, :size, :mime_type]
17
+
18
+ class FutureFileItem < Ks.strict(:name, :io, :local_identifier)
19
+ def initialize(**kwargs)
20
+ super(local_identifier: SecureRandom.uuid, **kwargs)
21
+ end
22
+
23
+ def to_item_request_params
24
+ # Ideally the content identifier should stay the same throughout multiple
25
+ # calls if the file contents doesn't change.
26
+ {
27
+ content_identifier: 'file',
28
+ local_identifier: local_identifier,
29
+ filename: name,
30
+ filesize: io.size,
31
+ }
32
+ end
33
+ end
34
+
35
+ class TransferBuilder
36
+ attr_reader :items
37
+
38
+ def initialize
39
+ @items = []
40
+ end
41
+
42
+ def add_file(name:, io:)
43
+ ensure_io_compliant!(io)
44
+ @items << FutureFileItem.new(name: name, io: io)
45
+ true
46
+ end
47
+
48
+ def add_file_at(path:)
49
+ add_file(name: File.basename(path), io: File.open(path, 'rb'))
50
+ end
51
+
52
+ def ensure_io_compliant!(io)
53
+ io.seek(0)
54
+ io.read(1) # Will cause things like Errno::EACCESS to happen early, before the upload begins
55
+ io.seek(0) # Also rewinds the IO for later uploading action
56
+ size = io.size # Will cause a NoMethodError
57
+ raise Error, 'The IO object given to add_file has a size of 0' if size <= 0
58
+ rescue NoMethodError
59
+ raise Error, "The IO object given to add_file must respond to seek(), read() and size(), but #{io.inspect} did not"
60
+ end
61
+ end
62
+
63
+ class FutureTransfer < Ks.strict(:name, :description, :items)
64
+ def to_create_transfer_params
65
+ {
66
+ name: name,
67
+ description: description,
68
+ items: items.map(&:to_item_request_params),
69
+ }
70
+ end
71
+ end
72
+
73
+ class RemoteTransfer < Ks.strict(*EXPOSED_COLLECTION_ATTRIBUTES)
74
+ end
75
+
76
+ class RemoteItem < Ks.strict(*EXPOSED_ITEM_ATTRIBUTES)
77
+ end
78
+
79
+ def initialize(api_key:, logger: NULL_LOGGER)
80
+ @api_url_base = 'https://dev.wetransfer.com'
81
+ @api_key = api_key.to_str
82
+ @bearer_token = nil
83
+ @logger = logger
84
+ end
85
+
86
+ def create_transfer(title:, message:)
87
+ builder = TransferBuilder.new
88
+ yield(builder)
89
+ raise 'The transfer you have tried to create contains no items' if builder.items.empty?
90
+ future_transfer = FutureTransfer.new(name: title, description: message, items: builder.items)
91
+ create_and_upload(future_transfer)
92
+ end
93
+
94
+ def create_and_upload(xfer)
95
+ authorize_if_no_bearer_token!
96
+ response = faraday.post(
97
+ '/v1/transfers',
98
+ JSON.pretty_generate(xfer.to_create_transfer_params),
99
+ auth_headers.merge('Content-Type' => 'application/json')
100
+ )
101
+ ensure_ok_status!(response)
102
+ create_transfer_response = JSON.parse(response.body, symbolize_names: true)
103
+
104
+ remote_transfer = hash_to_struct(create_transfer_response, RemoteTransfer)
105
+ remote_transfer.items = remote_transfer.items.map do |remote_item_hash|
106
+ hash_to_struct(remote_item_hash, RemoteItem)
107
+ end
108
+
109
+ item_id_map = Hash[xfer.items.map(&:local_identifier).zip(xfer.items)]
110
+
111
+ create_transfer_response.fetch(:items).each do |remote_item|
112
+ local_item = item_id_map.fetch(remote_item.fetch(:local_identifier))
113
+ remote_item_id = remote_item.fetch(:id)
114
+ put_io_in_parts(
115
+ remote_item_id,
116
+ remote_item.fetch(:meta).fetch(:multipart_parts),
117
+ remote_item.fetch(:meta).fetch(:multipart_upload_id),
118
+ local_item.io
119
+ )
120
+
121
+ complete_response = faraday.post(
122
+ "/v1/files/#{remote_item_id}/uploads/complete",
123
+ '{}',
124
+ auth_headers.merge('Content-Type' => 'application/json')
125
+ )
126
+ ensure_ok_status!(complete_response)
127
+ end
128
+
129
+ remote_transfer
130
+ end
131
+
132
+ def hash_to_struct(hash, struct_class)
133
+ members = struct_class.members
134
+ struct_attrs = Hash[members.zip(hash.values_at(*members))]
135
+ struct_class.new(**struct_attrs)
136
+ end
137
+
138
+ def put_io_in_parts(item_id, n_parts, multipart_id, io)
139
+ chunk_size = MAGIC_PART_SIZE
140
+ (1..n_parts).each do |part_n_one_based|
141
+ response = faraday.get("/v1/files/#{item_id}/uploads/#{part_n_one_based}/#{multipart_id}", {}, auth_headers)
142
+ ensure_ok_status!(response)
143
+ response = JSON.parse(response.body, symbolize_names: true)
144
+
145
+ upload_url = response.fetch(:upload_url)
146
+ part_io = StringIO.new(io.read(chunk_size)) # needs a lens
147
+ part_io.rewind
148
+ response = faraday.put(upload_url, part_io, 'Content-Type' => 'binary/octet-stream', 'Content-Length' => part_io.size.to_s)
149
+ ensure_ok_status!(response)
150
+ end
151
+ end
152
+
153
+ def faraday
154
+ Faraday.new(@api_url_base) do |c|
155
+ c.response :logger, @logger
156
+ c.adapter Faraday.default_adapter
157
+ c.headers = { 'User-Agent' => "WetransferRubySdk/#{WeTransferClient::VERSION} Ruby #{RUBY_VERSION}"}
158
+ end
159
+ end
160
+
161
+ def authorize_if_no_bearer_token!
162
+ return if @bearer_token
163
+ response = faraday.post('/v1/authorize', '{}', 'Content-Type' => 'application/json', 'X-API-Key' => @api_key)
164
+ ensure_ok_status!(response)
165
+ @bearer_token = JSON.parse(response.body, symbolize_names: true)[:token]
166
+ if @bearer_token.nil? || @bearer_token.empty?
167
+ raise Error, "The authorization call returned #{response.body} and no usable :token key could be found there"
168
+ end
169
+ end
170
+
171
+ def auth_headers
172
+ raise 'No bearer token retrieved yet' unless @bearer_token
173
+ {
174
+ 'X-API-Key' => @api_key,
175
+ 'Authorization' => ('Bearer %s' % @bearer_token),
176
+ }
177
+ end
178
+
179
+ def ensure_ok_status!(response)
180
+ case response.status
181
+ when 200..299
182
+ nil
183
+ when 400..499
184
+ @logger.error { response.body }
185
+ raise Error, "Response had a #{response.status} code, the server will not accept this request even if retried"
186
+ when 500..504
187
+ @logger.error { response.body }
188
+ raise Error, "Response had a #{response.status} code, we could retry"
189
+ else
190
+ @logger.error { response.body }
191
+ raise Error, "Response had a #{response.status} code, no idea what to do with that"
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,3 @@
1
+ class WeTransferClient
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,98 @@
1
+ require 'tempfile'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'dotenv'
6
+ Dotenv.load
7
+
8
+ require_relative '../lib/we_transfer_client.rb'
9
+
10
+ describe WeTransferClient do
11
+ let :test_logger do
12
+ Logger.new($stderr).tap { |log| log.level = Logger::WARN }
13
+ end
14
+
15
+ let :very_large_file do
16
+ tf = Tempfile.new('test-upload')
17
+ 20.times { tf << Random.new.bytes(1024 * 1024) }
18
+ tf << Random.new.bytes(rand(1..512))
19
+ tf.rewind
20
+ tf
21
+ end
22
+
23
+ it 'exposes VERSION' do
24
+ expect(WeTransferClient::VERSION).to be_kind_of(String)
25
+ end
26
+
27
+ it 'is able to create a transfer start to finish, both with small and large files' do
28
+ client = WeTransferClient.new(api_key: ENV.fetch('WT_API_KEY'), logger: test_logger)
29
+ transfer = client.create_transfer(title: 'My amazing board', message: 'Hi there!') do |builder|
30
+ # Upload ourselves
31
+ add_result = builder.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb'))
32
+ expect(add_result).to eq(true)
33
+
34
+ # Upload ourselves again, but using add_file_at
35
+ add_result = builder.add_file_at(path: __FILE__) # Upload ourselves again, but this time via path
36
+ expect(add_result).to eq(true)
37
+
38
+ # Upload the large file
39
+ add_result = builder.add_file(name: 'large.bin', io: very_large_file)
40
+ expect(add_result).to eq(true)
41
+
42
+ expect(add_result).to eq(true)
43
+ end
44
+
45
+ expect(transfer).to be_kind_of(WeTransferClient::RemoteTransfer)
46
+ expect(transfer.id).to be_kind_of(String)
47
+
48
+ # expect(transfer.version_identifier).to be_kind_of(String)
49
+ expect(transfer.state).to be_kind_of(String)
50
+ expect(transfer.name).to eq('My amazing board')
51
+ expect(transfer.description).to eq('Hi there!')
52
+ expect(transfer.items).to be_kind_of(Array)
53
+ expect(transfer.items.length).to eq(3)
54
+
55
+ item = transfer.items.first
56
+ expect(item).to be_kind_of(WeTransferClient::RemoteItem)
57
+
58
+ expect(transfer.shortened_url).to be_kind_of(String)
59
+ response = Faraday.get(transfer.shortened_url)
60
+ expect(response.status).to eq(302)
61
+ expect(response['location']).to start_with('https://wetransfer')
62
+ end
63
+
64
+ it 'refuses to create a transfer with no items' do
65
+ client = WeTransferClient.new(api_key: ENV.fetch('WT_API_KEY'), logger: test_logger)
66
+ expect {
67
+ client.create_transfer(title: 'My amazing board', message: 'Hi there!') do |builder|
68
+ # ...do nothing
69
+ end
70
+ }.to raise_error(/no items/)
71
+ end
72
+
73
+ it 'refuses to create a transfer when reading an IO raises an error' do
74
+ broken = StringIO.new('hello')
75
+ def broken.read(*)
76
+ raise 'This failed somehow'
77
+ end
78
+
79
+ client = WeTransferClient.new(api_key: ENV.fetch('WT_API_KEY'), logger: test_logger)
80
+ expect(client).not_to receive(:faraday) # Since we will not be doing any requests - we fail earlier
81
+ expect {
82
+ client.create_transfer(title: 'My amazing board', message: 'Hi there!') do |builder|
83
+ builder.add_file(name: 'broken', io: broken)
84
+ end
85
+ }.to raise_error(/failed somehow/)
86
+ end
87
+
88
+ it 'refuses to create a transfer when given an IO of 0 size' do
89
+ broken = StringIO.new('')
90
+
91
+ client = WeTransferClient.new(api_key: ENV.fetch('WT_API_KEY'), logger: test_logger)
92
+ expect {
93
+ client.create_transfer(title: 'My amazing board', message: 'Hi there!') do |builder|
94
+ builder.add_file(name: 'broken', io: broken)
95
+ end
96
+ }.to raise_error(/has a size of 0/)
97
+ end
98
+ end
data/wetransfer.gemspec CHANGED
@@ -1,11 +1,11 @@
1
1
 
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'wetransfer/version'
4
+ require 'we_transfer_client/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'wetransfer'
8
- spec.version = WeTransfer::VERSION
8
+ spec.version = WeTransferClient::VERSION
9
9
  spec.authors = ['Noah Berman', 'David Bosveld']
10
10
  spec.email = ['noah@wetransfer.com', 'david@wetransfer.com', 'developers@wetransfer.com']
11
11
 
@@ -23,18 +23,15 @@ Gem::Specification.new do |spec|
23
23
  'public gem pushes.'
24
24
  end
25
25
 
26
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
- # Make sure large fixture files are not packaged with the gem every time
28
- f.match(%r{^spec/fixtures/})
29
- end
26
+ spec.files = `git ls-files -z`.split("\x0")
30
27
  spec.bindir = 'exe'
31
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
29
  spec.require_paths = ['lib']
33
30
 
34
- spec.add_dependency 'faraday', '~> 0.13'
35
- spec.add_dependency 'dotenv', '~> 2.2'
31
+ spec.add_dependency 'faraday', '~> 0.15'
36
32
  spec.add_dependency 'ks', '~> 0.0.1'
37
33
 
34
+ spec.add_development_dependency 'dotenv', '~> 2.2'
38
35
  spec.add_development_dependency 'bundler', '~> 1.16'
39
36
  spec.add_development_dependency 'pry', '~> 0.11'
40
37
  spec.add_development_dependency 'rake', '~> 10.0'