Skip to content
This repository was archived by the owner on Dec 11, 2020. It is now read-only.

Commit 4fab8b0

Browse files
committed
DRYed up File and MiniIO interaction
- - DRY up request headers - NullMiniIO: all methods are empty - rename old spec files so they wont run, move them to a specific directory transform request params to JSON as late (and as once) as possible add more old spec files to the ignore list - Add RemoteFile and RemoteTransfer modules that change the corresponding instances *in place*. It sets instance variables and getter methods. - Transfer.find works, very limited test, though - Make upgrading a transfer (a bit) less magical. finalize call for transfers operational File upload implemented Implement file completion Refactorings for readability, less complexity Move communication to CommunicationHelper class More focus in integration test Expose #to_json on a transfer Refactoring * use class<<self instead of private_class_method on def self.foobar things refactor: Remove duplication remove unused image refactor get rid of useless example
1 parent dfd22cf commit 4fab8b0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+816
-1752
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
inherit_gem:
2-
wetransfer_style: ruby/default.yml
2+
wetransfer_style: ruby/default.yml
Lines changed: 144 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,167 @@
11
module WeTransfer
22
class CommunicationError < StandardError; end
3+
34
module CommunicationHelper
4-
API_URI_BASE = 'https://dev.wetransfer.com'.freeze
5+
extend Forwardable
6+
7+
API_URL_BASE = "https://dev.wetransfer.com"
8+
DEFAULT_HEADERS = {
9+
"User-Agent" => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}",
10+
"Content-Type" => "application/json"
11+
}.freeze
512

613
class << self
7-
attr_accessor :logger, :api_key
8-
end
14+
attr_accessor :logger, :api_key, :bearer_token
915

10-
def logger
11-
@logger ||= CommunicationHelper.logger
12-
end
16+
def reset_authentication!
17+
@api_key = nil
18+
@bearer_token = nil
19+
@request_as = nil
20+
end
1321

14-
def api_key
15-
@api_key ||= CommunicationHelper.api_key
16-
end
22+
def find_transfer(transfer_id)
23+
response = request_as.get("/v2/transfers/%s" % [transfer_id])
24+
ensure_ok_status!(response)
25+
response_body = remote_transfer_params(response.body)
26+
found_transfer = Transfer.new(message: response_body[:message])
27+
setup_transfer(
28+
transfer: found_transfer,
29+
data: response_body
30+
)
31+
end
1732

18-
def request_as
19-
authorize_if_no_bearer_token!
33+
def upload_url_for_chunk(transfer_id, file_id, chunk)
34+
response = request_as.get("/v2/transfers/%s/files/%s/upload-url/%s" % [transfer_id, file_id, chunk])
35+
ensure_ok_status!(response)
2036

21-
@request_as ||= Faraday.new(API_URI_BASE) do |c|
22-
c.response :logger, logger
23-
c.adapter Faraday.default_adapter
24-
c.headers = auth_headers.merge(
25-
"User-Agent" => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}",
26-
"Content-Type" => "application/json",
37+
JSON.parse(response.body).fetch("url")
38+
end
39+
40+
def persist_transfer(transfer)
41+
response = request_as.post(
42+
"/v2/transfers",
43+
transfer.as_request_params.to_json,
44+
)
45+
ensure_ok_status!(response)
46+
47+
handle_new_transfer_data(
48+
transfer: transfer,
49+
data: remote_transfer_params(response.body)
2750
)
2851
end
29-
end
3052

31-
private
53+
def finalize_transfer(transfer)
54+
response = request_as.put("/v2/transfers/%s/finalize" % transfer.id)
55+
ensure_ok_status!(response)
56+
handle_new_transfer_data(
57+
transfer: transfer,
58+
data: remote_transfer_params(response.body)
59+
)
60+
end
3261

33-
def auth_headers
34-
authorize_if_no_bearer_token!
62+
def remote_transfer_params(response_body)
63+
JSON.parse(response_body, symbolize_names: true)
64+
end
3565

36-
{
37-
'X-API-Key' => api_key,
38-
'Authorization' => ('Bearer %s' % @bearer_token),
39-
}
40-
end
66+
def upload_chunk(put_url, chunk_contents)
67+
@chunk_uploader ||= Faraday.new { |c| minimal_faraday_config(c) }
4168

42-
def ensure_ok_status!(response)
43-
case response.status
44-
when 200..299
45-
true
46-
when 400..499
47-
logger.error response
48-
raise WeTransfer::CommunicationError, JSON.parse(response.body)["message"]
49-
when 500..504
50-
logger.error response
51-
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, we could retry"
52-
else
53-
logger.error response
54-
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, no idea what to do with that"
69+
@chunk_uploader.put(
70+
put_url,
71+
chunk_contents.read,
72+
'Content-Type' => 'binary/octet-stream',
73+
'Content-Length' => chunk_contents.size.to_s
74+
)
5575
end
56-
end
5776

58-
def authorize_if_no_bearer_token!
59-
return @bearer_token if @bearer_token
77+
def complete_file(transfer_id, file_id, chunks)
78+
response = request_as.put(
79+
"/v2/transfers/%s/files/%s/upload-complete" % [transfer_id, file_id],
80+
{ part_numbers: chunks }.to_json
81+
)
82+
83+
ensure_ok_status!(response)
84+
remote_transfer_params(response.body)
85+
end
86+
87+
private
88+
89+
def request_as
90+
@request_as ||= Faraday.new(API_URL_BASE) do |c|
91+
minimal_faraday_config(c)
92+
c.headers = auth_headers.merge DEFAULT_HEADERS
93+
end
94+
end
95+
96+
def setup_transfer(transfer:, data:)
97+
data[:files].each do |file_params|
98+
transfer.add_file(
99+
name: file_params[:name],
100+
size: file_params[:size],
101+
)
102+
end
103+
104+
handle_new_transfer_data(transfer: transfer, data: data)
105+
end
106+
107+
def handle_new_transfer_data(transfer:, data:)
108+
%i[id state url].each do |i_var|
109+
transfer.instance_variable_set "@#{i_var}", data[i_var]
110+
end
111+
112+
RemoteFile.upgrade(
113+
transfer: transfer,
114+
files_response: data[:files]
115+
)
116+
transfer
117+
end
60118

61-
response = Faraday.new(API_URI_BASE) do |c|
62-
c.response :logger, logger
63-
c.adapter Faraday.default_adapter
64-
c.headers = {
65-
"User-Agent" => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}",
66-
"Content-Type" => "application/json",
119+
def auth_headers
120+
authorize_if_no_bearer_token!
121+
122+
{
123+
'X-API-Key' => api_key,
124+
'Authorization' => "Bearer #{@bearer_token}"
67125
}
68-
end.post(
69-
'/v2/authorize',
70-
'',
71-
'Content-Type' => 'application/json',
72-
'X-API-Key' => CommunicationHelper.api_key,
73-
)
74-
ensure_ok_status!(response)
75-
bearer_token = JSON.parse(response.body)['token']
76-
if bearer_token.nil? || bearer_token.empty?
77-
raise WeTransfer::CommunicationError, "The authorization call returned #{response.body} and no usable :token key could be found there"
78126
end
79-
@bearer_token = bearer_token
127+
128+
def ensure_ok_status!(response)
129+
case response.status
130+
when 200..299
131+
true
132+
when 400..499
133+
logger.error response
134+
raise WeTransfer::CommunicationError, JSON.parse(response.body)["message"]
135+
when 500..504
136+
logger.error response
137+
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, we could retry"
138+
else
139+
logger.error response
140+
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, no idea what to do with that"
141+
end
142+
end
143+
144+
def authorize_if_no_bearer_token!
145+
return @bearer_token if @bearer_token
146+
147+
response = Faraday.new(API_URL_BASE) do |c|
148+
minimal_faraday_config(c)
149+
c.headers = DEFAULT_HEADERS.merge('X-API-Key' => api_key)
150+
end.post(
151+
'/v2/authorize',
152+
)
153+
ensure_ok_status!(response)
154+
bearer_token = JSON.parse(response.body)['token']
155+
raise WeTransfer::CommunicationError, "The authorization call returned #{response.body} and no usable :token key could be found there" if bearer_token.nil? || bearer_token.empty?
156+
@bearer_token = bearer_token
157+
end
158+
159+
def minimal_faraday_config(config)
160+
config.response :logger, logger
161+
config.adapter Faraday.default_adapter
162+
end
80163
end
164+
165+
def_delegator self, :minimal_faraday_config
81166
end
82167
end

lib/we_transfer/mini_io.rb

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,40 @@ module WeTransfer
22
# Wrapper around an IO object, delegating only methods we need for creating
33
# and sending a file in chunks.
44
#
5+
56
class MiniIO
7+
def self.mini_io_able?(io)
8+
return false if io.is_a? WeTransfer::NullMiniIO
9+
10+
io.seek(0)
11+
io.read(1)
12+
io.rewind
13+
io.size
14+
true
15+
rescue
16+
false
17+
end
18+
19+
# Make sure MiniIO does not wrap a MiniIO instance
20+
#
21+
# @param io [anything] An object that responds to #read, #rewind, #seek, #size
22+
#
23+
def self.new(io)
24+
return WeTransfer::NullMiniIO.instance if io.nil?
25+
return io if io.is_a? WeTransfer::MiniIO
26+
27+
super
28+
end
29+
630
# Initialize with an io object to wrap
731
#
832
# @param io [anything] An object that responds to #read, #rewind, #seek, #size
933
#
1034
def initialize(io)
35+
ensure_mini_io_able!(io)
36+
1137
@io = io
38+
@io.rewind
1239
end
1340

1441
def read(*args)
@@ -23,25 +50,49 @@ def seek(*args)
2350
@io.seek(*args)
2451
end
2552

26-
# The size delegated to io.
27-
# If io is nil, we return nil.
53+
# The size, delegated to io.
2854
#
2955
# nil is fine, since this method is used only as the default size for a
3056
# WeTransferFile
31-
# @returns [Integer, nil] the size of the io. See IO#size
57+
# @returns [Integer] the size of the io
3258
#
3359
def size
34-
@io&.size
60+
@io.size
3561
end
3662

3763
# The name of the io, guessed using File.basename. If this raises a TypeError
38-
# we swallow the error, since this is used only as the default name for a
39-
# WeTransferFile
64+
# we swallow the error, since this is used only as the default name for a
65+
# WeTransferFile
4066
#
4167
def name
4268
File.basename(@io)
4369
rescue TypeError
4470
# yeah, what?
4571
end
72+
73+
private
74+
75+
def ensure_mini_io_able!(io)
76+
return if self.class.mini_io_able?(io)
77+
78+
raise ArgumentError, "The io must respond to seek(), read(), size() and rewind(), but #{io.inspect} did not"
79+
end
80+
end
81+
82+
class NullMiniIO
83+
require 'singleton'
84+
include Singleton
85+
86+
instance.freeze
87+
88+
def read(*); end
89+
90+
def rewind; end
91+
92+
def seek(*); end
93+
94+
def size; end
95+
96+
def name; end
4697
end
4798
end

lib/we_transfer/remote_file.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module WeTransfer
2+
module RemoteFile
3+
class FileMismatchError < StandardError; end
4+
class NoIoError < StandardError; end
5+
6+
class Multipart < ::Ks.strict(:chunks, :chunk_size)
7+
def to_h
8+
%i[chunks chunk_size].each_with_object({}) do |prop, memo|
9+
memo[prop] = send(prop)
10+
end
11+
end
12+
end
13+
14+
def self.upgrade(files_response:, transfer:)
15+
files_response.each do |file_response|
16+
local_file = transfer.find_file_by_name(file_response[:name])
17+
18+
local_file.instance_variable_set(
19+
:@id,
20+
file_response[:id]
21+
)
22+
23+
local_file.instance_variable_set(
24+
:@multipart,
25+
Multipart.new(
26+
chunks: file_response[:multipart][:part_numbers],
27+
chunk_size: file_response[:multipart][:chunk_size],
28+
)
29+
)
30+
end
31+
end
32+
end
33+
end

0 commit comments

Comments
 (0)