wetransfer 0.1.0 → 0.2.0

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