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

Commit

Permalink
DRYed up File and MiniIO interaction
Browse files Browse the repository at this point in the history
-

- 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
  • Loading branch information
arnoFleming committed Mar 2, 2019
1 parent 274b4f1 commit 9ee34dc
Show file tree
Hide file tree
Showing 46 changed files with 816 additions and 1,752 deletions.
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
inherit_gem:
wetransfer_style: ruby/default.yml
wetransfer_style: ruby/default.yml
203 changes: 144 additions & 59 deletions lib/we_transfer/communication_helper.rb
Original file line number Diff line number Diff line change
@@ -1,82 +1,167 @@
module WeTransfer
class CommunicationError < StandardError; end

module CommunicationHelper
API_URI_BASE = 'https://dev.wetransfer.com'.freeze
extend Forwardable

API_URL_BASE = "https://dev.wetransfer.com"
DEFAULT_HEADERS = {
"User-Agent" => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}",
"Content-Type" => "application/json"
}.freeze

class << self
attr_accessor :logger, :api_key
end
attr_accessor :logger, :api_key, :bearer_token

def logger
@logger ||= CommunicationHelper.logger
end
def reset_authentication!
@api_key = nil
@bearer_token = nil
@request_as = nil
end

def api_key
@api_key ||= CommunicationHelper.api_key
end
def find_transfer(transfer_id)
response = request_as.get("/v2/transfers/%s" % [transfer_id])
ensure_ok_status!(response)
response_body = remote_transfer_params(response.body)
found_transfer = Transfer.new(message: response_body[:message])
setup_transfer(
transfer: found_transfer,
data: response_body
)
end

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

@request_as ||= Faraday.new(API_URI_BASE) do |c|
c.response :logger, logger
c.adapter Faraday.default_adapter
c.headers = auth_headers.merge(
"User-Agent" => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}",
"Content-Type" => "application/json",
JSON.parse(response.body).fetch("url")
end

def persist_transfer(transfer)
response = request_as.post(
"/v2/transfers",
transfer.as_request_params.to_json,
)
ensure_ok_status!(response)

handle_new_transfer_data(
transfer: transfer,
data: remote_transfer_params(response.body)
)
end
end

private
def finalize_transfer(transfer)
response = request_as.put("/v2/transfers/%s/finalize" % transfer.id)
ensure_ok_status!(response)
handle_new_transfer_data(
transfer: transfer,
data: remote_transfer_params(response.body)
)
end

def auth_headers
authorize_if_no_bearer_token!
def remote_transfer_params(response_body)
JSON.parse(response_body, symbolize_names: true)
end

{
'X-API-Key' => api_key,
'Authorization' => ('Bearer %s' % @bearer_token),
}
end
def upload_chunk(put_url, chunk_contents)
@chunk_uploader ||= Faraday.new { |c| minimal_faraday_config(c) }

def ensure_ok_status!(response)
case response.status
when 200..299
true
when 400..499
logger.error response
raise WeTransfer::CommunicationError, JSON.parse(response.body)["message"]
when 500..504
logger.error response
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, we could retry"
else
logger.error response
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, no idea what to do with that"
@chunk_uploader.put(
put_url,
chunk_contents.read,
'Content-Type' => 'binary/octet-stream',
'Content-Length' => chunk_contents.size.to_s
)
end
end

def authorize_if_no_bearer_token!
return @bearer_token if @bearer_token
def complete_file(transfer_id, file_id, chunks)
response = request_as.put(
"/v2/transfers/%s/files/%s/upload-complete" % [transfer_id, file_id],
{ part_numbers: chunks }.to_json
)

ensure_ok_status!(response)
remote_transfer_params(response.body)
end

private

def request_as
@request_as ||= Faraday.new(API_URL_BASE) do |c|
minimal_faraday_config(c)
c.headers = auth_headers.merge DEFAULT_HEADERS
end
end

def setup_transfer(transfer:, data:)
data[:files].each do |file_params|
transfer.add_file(
name: file_params[:name],
size: file_params[:size],
)
end

handle_new_transfer_data(transfer: transfer, data: data)
end

def handle_new_transfer_data(transfer:, data:)
%i[id state url].each do |i_var|
transfer.instance_variable_set "@#{i_var}", data[i_var]
end

RemoteFile.upgrade(
transfer: transfer,
files_response: data[:files]
)
transfer
end

response = Faraday.new(API_URI_BASE) do |c|
c.response :logger, logger
c.adapter Faraday.default_adapter
c.headers = {
"User-Agent" => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}",
"Content-Type" => "application/json",
def auth_headers
authorize_if_no_bearer_token!

{
'X-API-Key' => api_key,
'Authorization' => "Bearer #{@bearer_token}"
}
end.post(
'/v2/authorize',
'',
'Content-Type' => 'application/json',
'X-API-Key' => CommunicationHelper.api_key,
)
ensure_ok_status!(response)
bearer_token = JSON.parse(response.body)['token']
if bearer_token.nil? || bearer_token.empty?
raise WeTransfer::CommunicationError, "The authorization call returned #{response.body} and no usable :token key could be found there"
end
@bearer_token = bearer_token

def ensure_ok_status!(response)
case response.status
when 200..299
true
when 400..499
logger.error response
raise WeTransfer::CommunicationError, JSON.parse(response.body)["message"]
when 500..504
logger.error response
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, we could retry"
else
logger.error response
raise WeTransfer::CommunicationError, "Response had a #{response.status} code, no idea what to do with that"
end
end

def authorize_if_no_bearer_token!
return @bearer_token if @bearer_token

response = Faraday.new(API_URL_BASE) do |c|
minimal_faraday_config(c)
c.headers = DEFAULT_HEADERS.merge('X-API-Key' => api_key)
end.post(
'/v2/authorize',
)
ensure_ok_status!(response)
bearer_token = JSON.parse(response.body)['token']
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?
@bearer_token = bearer_token
end

def minimal_faraday_config(config)
config.response :logger, logger
config.adapter Faraday.default_adapter
end
end

def_delegator self, :minimal_faraday_config
end
end
63 changes: 57 additions & 6 deletions lib/we_transfer/mini_io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,40 @@ module WeTransfer
# Wrapper around an IO object, delegating only methods we need for creating
# and sending a file in chunks.
#

class MiniIO
def self.mini_io_able?(io)
return false if io.is_a? WeTransfer::NullMiniIO

io.seek(0)
io.read(1)
io.rewind
io.size
true
rescue
false
end

# Make sure MiniIO does not wrap a MiniIO instance
#
# @param io [anything] An object that responds to #read, #rewind, #seek, #size
#
def self.new(io)
return WeTransfer::NullMiniIO.instance if io.nil?
return io if io.is_a? WeTransfer::MiniIO

super
end

# Initialize with an io object to wrap
#
# @param io [anything] An object that responds to #read, #rewind, #seek, #size
#
def initialize(io)
ensure_mini_io_able!(io)

@io = io
@io.rewind
end

def read(*args)
Expand All @@ -23,25 +50,49 @@ def seek(*args)
@io.seek(*args)
end

# The size delegated to io.
# If io is nil, we return nil.
# The size, delegated to io.
#
# nil is fine, since this method is used only as the default size for a
# WeTransferFile
# @returns [Integer, nil] the size of the io. See IO#size
# @returns [Integer] the size of the io
#
def size
@io&.size
@io.size
end

# The name of the io, guessed using File.basename. If this raises a TypeError
# we swallow the error, since this is used only as the default name for a
# WeTransferFile
# we swallow the error, since this is used only as the default name for a
# WeTransferFile
#
def name
File.basename(@io)
rescue TypeError
# yeah, what?
end

private

def ensure_mini_io_able!(io)
return if self.class.mini_io_able?(io)

raise ArgumentError, "The io must respond to seek(), read(), size() and rewind(), but #{io.inspect} did not"
end
end

class NullMiniIO
require 'singleton'
include Singleton

instance.freeze

def read(*); end

def rewind; end

def seek(*); end

def size; end

def name; end
end
end
33 changes: 33 additions & 0 deletions lib/we_transfer/remote_file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module WeTransfer
module RemoteFile
class FileMismatchError < StandardError; end
class NoIoError < StandardError; end

class Multipart < ::Ks.strict(:chunks, :chunk_size)
def to_h
%i[chunks chunk_size].each_with_object({}) do |prop, memo|
memo[prop] = send(prop)
end
end
end

def self.upgrade(files_response:, transfer:)
files_response.each do |file_response|
local_file = transfer.find_file_by_name(file_response[:name])

local_file.instance_variable_set(
:@id,
file_response[:id]
)

local_file.instance_variable_set(
:@multipart,
Multipart.new(
chunks: file_response[:multipart][:part_numbers],
chunk_size: file_response[:multipart][:chunk_size],
)
)
end
end
end
end
Loading

0 comments on commit 9ee34dc

Please sign in to comment.