wetransfer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'dotenv'
4
+ Dotenv.load
5
+
6
+ require 'wetransfer/version'
7
+ require 'wetransfer/client'
8
+ require 'wetransfer/transfer'
9
+ require 'wetransfer/transfer_builder'
10
+ require 'wetransfer/item'
11
+ require 'wetransfer/item_builder'
12
+ require 'wetransfer/connection'
13
+
14
+ module WeTransfer
15
+
16
+ end
@@ -0,0 +1,126 @@
1
+ module WeTransfer
2
+ class Client
3
+ attr_accessor :api_key
4
+ attr_reader :api_connection
5
+ CHUNK_SIZE = 6_291_456
6
+
7
+ # Initializes a new Client object
8
+ def initialize(api_key:)
9
+ @api_path = ENV.fetch('WT_API_CONNECTION_PATH') { '' }
10
+ @api_key = api_key
11
+ @api_bearer_token ||= request_jwt
12
+ @api_connection ||= WeTransfer::Connection.new(client: self, api_bearer_token: @api_bearer_token)
13
+ end
14
+
15
+ def request_jwt
16
+ # Create a connection request without a bearer token for authorization
17
+ # since authorization is what you need to do to retrieve the token.
18
+ auth_connection = WeTransfer::Connection.new(client: self)
19
+ auth_connection.authorization_request
20
+ end
21
+
22
+ # If you pass in items to the transfer it'll create the transfer with them,
23
+ # otherwise it creates a "blank" transfer object. You can also leave off the
24
+ # name and description, and it will be auto-generated.
25
+ def create_transfer(name: nil, description: nil, items: [])
26
+ raise ArgumentError, 'The items field must be an array' unless items.is_a?(Array)
27
+ @transfer = build_transfer_object(name, description).transfer
28
+ items.any? ? create_transfer_with_items(items: items) : create_initial_transfer
29
+ @transfer
30
+ end
31
+
32
+ # Once you've created a "blank" transfer you can use this to add items to it.
33
+ # Items must have the structure defined in the README, otherwise information will be auto-generated for them.
34
+ def add_items(transfer:, items:)
35
+ @transfer ||= transfer
36
+ create_transfer_items(items: items)
37
+ send_items_to_transfer
38
+ upload_and_complete_items
39
+ @transfer
40
+ end
41
+
42
+ def create_transfer_with_items(items: [])
43
+ raise ArgumentError, 'Items array cannot be empty' if items.empty?
44
+ create_transfer_items(items: items)
45
+ create_initial_transfer
46
+ upload_and_complete_items
47
+ end
48
+
49
+ private
50
+
51
+ def build_transfer_object(name, description)
52
+ transfer_builder = TransferBuilder.new
53
+ transfer_builder.name_description(name: name, description: description)
54
+ transfer_builder
55
+ end
56
+
57
+ def create_transfer_items(items:)
58
+ items.each do |item|
59
+ item_builder = ItemBuilder.new
60
+ item_builder.path(path: item)
61
+ item_builder.content_identifier
62
+ item_builder.local_identifier
63
+ item_builder.name
64
+ item_builder.size
65
+ @transfer.items.push(item_builder.item)
66
+ end
67
+ end
68
+
69
+ def create_initial_transfer
70
+ response = @api_connection.post_request(path: '/transfers', body: @transfer.transfer_params)
71
+ TransferBuilder.id(transfer: @transfer, id: response['id'])
72
+ TransferBuilder.shortened_url(transfer: @transfer, url: response['shortened_url'])
73
+ update_item_objects(response_items: response['items']) if response['items'].any?
74
+ end
75
+
76
+ def send_items_to_transfer
77
+ response = @api_connection.post_request(path: "/transfers/#{@transfer.id}/items", body: {items: @transfer.items_params})
78
+ update_item_objects(response_items: response)
79
+ end
80
+
81
+ def update_item_objects(response_items:)
82
+ response_items.each do |item|
83
+ item_object = @transfer.items.select { |t| t.name == item['name'] }.first
84
+ item_builder = ItemBuilder.new(item: item_object)
85
+ item_builder.id(item: item_object, id: item['id'])
86
+ item_builder.upload_url(item: item_object, url: item['upload_url'])
87
+ item_builder.multipart_parts(item: item_object, part_count: item['meta']['multipart_parts'])
88
+ item_builder.multipart_id(item: item_object, multi_id: item['meta']['multipart_upload_id'])
89
+ item_builder.upload_id(item: item_object, upload_id: item['upload_id'])
90
+ add_item_upload_url(item: item_builder.item) if item_builder.item.multipart_parts > 1
91
+ end
92
+ end
93
+
94
+ def add_item_upload_url(item:)
95
+ upload_urls = []
96
+ item.multipart_parts.times do |part|
97
+ part += 1
98
+ response = @api_connection.get_request(path: "/files/#{item.id}/uploads/#{part}/#{item.multipart_id}")
99
+ upload_urls << response['upload_url']
100
+ end
101
+ item.upload_url = upload_urls
102
+ end
103
+
104
+ def upload_and_complete_items
105
+ upload_files
106
+ complete_transfer
107
+ end
108
+
109
+ def upload_files
110
+ @transfer.items.each do |item|
111
+ file_object = File.open(item.path)
112
+ item.upload_url.each do |url|
113
+ chunk = file_object.read(CHUNK_SIZE)
114
+ @api_connection.upload(file: chunk, url: url)
115
+ end
116
+ file_object.close
117
+ end
118
+ end
119
+
120
+ def complete_transfer
121
+ @transfer.items.each do |item|
122
+ @api_connection.post_request(path: "/files/#{item.id}/uploads/complete")
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,77 @@
1
+ module WeTransfer
2
+ class Connection
3
+ class ApiRequestError < StandardError; end
4
+
5
+ attr_reader :api_connection, :api_bearer_token, :api_key, :api_path
6
+
7
+ def initialize(client:, api_bearer_token: '')
8
+ @api_url = ENV.fetch('WT_API_URL') { 'https://dev.wetransfer.com' }
9
+ @api_key = client.api_key
10
+ @api_connection ||= create_api_connection_object!
11
+ @api_bearer_token = api_bearer_token
12
+ @api_path = ENV.fetch('WT_API_CONNECTION_PATH') { '' }
13
+ end
14
+
15
+ def authorization_request
16
+ response = @api_connection.post do |req|
17
+ req.url("#{@api_path}/authorize")
18
+ request_header_params(req: req)
19
+ end
20
+ response_validation!(response: response)
21
+ response['token']
22
+ end
23
+
24
+ def post_request(path:, body: nil)
25
+ response = @api_connection.post do |req|
26
+ req.url(@api_path + path)
27
+ request_header_params(req: req)
28
+ req.body = body.to_json unless body.nil?
29
+ end
30
+ response_validation!(response: response)
31
+ JSON.parse(response.body)
32
+ end
33
+
34
+ def get_request(path:)
35
+ response = @api_connection.get do |req|
36
+ req.url(@api_path + path)
37
+ request_header_params(req: req)
38
+ end
39
+ response_validation!(response: response)
40
+ JSON.parse(response.body)
41
+ end
42
+
43
+ def upload(file:, url:)
44
+ conn = Faraday.new(url: url) do |faraday|
45
+ faraday.request :multipart
46
+ faraday.adapter :net_http
47
+ end
48
+ resp = conn.put do |req|
49
+ req.headers['Content-Length'] = file.size.to_s
50
+ req.body = file
51
+ end
52
+ response_validation!(response: resp)
53
+ end
54
+
55
+ private
56
+
57
+ def request_header_params(req:)
58
+ req.headers['X-API-Key'] = @api_key
59
+ req.headers['Authorization'] = 'Bearer ' + @api_bearer_token unless @api_bearer_token.nil?
60
+ req.headers['Content-Type'] = 'application/json'
61
+ end
62
+
63
+ # If you need extra logging for your requests, switch it on by setting WT_API_LOGGING_ON in your .env file.
64
+ def create_api_connection_object!
65
+ conn = Faraday.new(url: @api_url) do |faraday|
66
+ faraday.response :logger if ENV.fetch('WT_API_LOGGING_ON') { nil } # log requests to STDOUT if ENVVAR is present
67
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
68
+ end
69
+ conn
70
+ end
71
+
72
+ def response_validation!(response:)
73
+ raise ApiRequestError, response.reason_phrase if response.status == 401
74
+ raise ApiRequestError, response.reason_phrase if response.status == 403
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,14 @@
1
+ module WeTransfer
2
+ class Item
3
+ attr_accessor :id,
4
+ :content_identifier,
5
+ :local_identifier,
6
+ :multipart_parts,
7
+ :multipart_id,
8
+ :name,
9
+ :size,
10
+ :upload_url,
11
+ :upload_id,
12
+ :path
13
+ end
14
+ end
@@ -0,0 +1,59 @@
1
+ module WeTransfer
2
+ class ItemBuilder
3
+ class FileDoesNotExistError < ArgumentError; end
4
+ attr_reader :item
5
+
6
+ def initialize(item: nil)
7
+ @item = if item.nil?
8
+ Item.new
9
+ else
10
+ item
11
+ end
12
+ end
13
+
14
+ def path(path:)
15
+ @item.path = path
16
+ end
17
+
18
+ def content_identifier
19
+ @item.content_identifier = 'file'
20
+ end
21
+
22
+ def local_identifier
23
+ # only take the file name and shorten it to 36 characters if it's longer
24
+ @item.local_identifier = @item.path.split('/').last.gsub(' ', '')[0..35]
25
+ end
26
+
27
+ def name
28
+ @item.name = @item.path.split('/').last
29
+ end
30
+
31
+ def size
32
+ @item.size = File.size(@item.path)
33
+ end
34
+
35
+ def id(item:, id:)
36
+ item.id = id
37
+ end
38
+
39
+ def upload_url(item:, url:)
40
+ item.upload_url = [url]
41
+ end
42
+
43
+ def multipart_parts(item:, part_count:)
44
+ item.multipart_parts = part_count
45
+ end
46
+
47
+ def multipart_id(item:, multi_id:)
48
+ item.multipart_id = multi_id
49
+ end
50
+
51
+ def upload_id(item:, upload_id:)
52
+ item.upload_id = upload_id
53
+ end
54
+
55
+ def validate_file
56
+ raise FileDoesNotExistError, "#{@item} does not exist" unless File.exist?(@item.path)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ module WeTransfer
2
+ class Transfer
3
+ attr_accessor :id, :name, :description, :shortened_url, :items
4
+
5
+ def initialize
6
+ @items = []
7
+ end
8
+
9
+ def transfer_params
10
+ {
11
+ name: name,
12
+ description: description,
13
+ items: items_params
14
+ }
15
+ end
16
+
17
+ def items_params
18
+ transfer_items = []
19
+ items.each do |item|
20
+ transfer_items << {
21
+ local_identifier: item.local_identifier,
22
+ content_identifier: item.content_identifier,
23
+ filename: item.name,
24
+ filesize: item.size
25
+ }
26
+ end
27
+ transfer_items
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module WeTransfer
2
+ class TransferBuilder
3
+ def initialize(transfer: nil)
4
+ @transfer = if transfer.nil?
5
+ Transfer.new
6
+ else
7
+ transfer
8
+ end
9
+ end
10
+
11
+ def name_description(name: nil, description: nil)
12
+ @transfer.name = name || "File Transfer: #{Time.now.strftime('%d-%m-%Y')}"
13
+ @transfer.description = description || 'Transfer generated with WeTransfer Ruby SDK'
14
+ end
15
+
16
+ def self.id(transfer:, id:)
17
+ transfer.id = id
18
+ end
19
+
20
+ def self.shortened_url(transfer:, url:)
21
+ transfer.shortened_url = url
22
+ end
23
+
24
+ def transfer
25
+ @transfer
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module WeTransfer
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe WeTransfer::Client do
4
+ describe 'Client#new' do
5
+ it 'returns a error when no api_key is given' do
6
+ expect {
7
+ described_class.new
8
+ }.to raise_error(ArgumentError, /missing keyword: api_key/)
9
+ end
10
+
11
+ it 'on initialization a active connection object is created' do
12
+ client = described_class.new(api_key: 'api-key')
13
+ expect(client.api_connection).to be_kind_of WeTransfer::Connection
14
+ end
15
+ end
16
+
17
+ describe 'Client#create_transfer' do
18
+ let(:client) { described_class.new(api_key: 'api-key') }
19
+
20
+ it 'create transfer should return a transfer object' do
21
+ transfer = client.create_transfer
22
+ expect(transfer.shortened_url).to start_with('http://we.tl/s-')
23
+ expect(transfer).to be_kind_of WeTransfer::Transfer
24
+ end
25
+
26
+ it 'when no name/description is send, a default name/description is generated' do
27
+ transfer = client.create_transfer
28
+ expect(transfer.name).to eq("File Transfer: #{Time.now.strftime('%d-%m-%Y')}")
29
+ expect(transfer.description).to eq('Transfer generated with WeTransfer Ruby SDK')
30
+ end
31
+
32
+ it 'when a name/description is send, transfer has that name/description' do
33
+ transfer = client.create_transfer(
34
+ name: 'WeTransfer Test Transfer',
35
+ description: "Moving along… Good news, everyone! I've
36
+ taught the toaster to feel love! Humans dating robots is
37
+ sick. You people wonder why I'm still single? It's 'cause
38
+ all the fine robot sisters are dating humans!")
39
+ expect(transfer.name).to eq('WeTransfer Test Transfer')
40
+ expect(transfer.description).to start_with('Moving along… Good news, everyone!')
41
+ end
42
+
43
+ it 'when no items are send, a itemless transfer is created' do
44
+ transfer = client.create_transfer
45
+ expect(transfer.items).to be_empty
46
+ end
47
+
48
+ it 'when items are sended, the transfer has items' do
49
+ transfer = client.create_transfer(items: ["#{__dir__}/fixtures/war-and-peace.txt"])
50
+ expect(transfer).to be_kind_of WeTransfer::Transfer
51
+ expect(transfer.items.count).to eq(1)
52
+ end
53
+
54
+ it 'returns an error when items are not sended inside an array' do
55
+ expect {
56
+ client.create_transfer(items: "#{__dir__}/war-end-peace.txt")
57
+ }.to raise_error(StandardError, 'The items field must be an array')
58
+ end
59
+
60
+ it 'completes a item after item upload' do
61
+ transfer = client.create_transfer(items: ["#{__dir__}/fixtures/war-and-peace.txt"])
62
+ expect(transfer).to be_kind_of WeTransfer::Transfer
63
+ end
64
+ end
65
+
66
+ describe 'Client#add_item' do
67
+ let(:client) { described_class.new(api_key: 'api-key') }
68
+
69
+ it 'add items to an already created transfer' do
70
+ transfer = client.create_transfer
71
+ expect(transfer.items.count).to eq(0)
72
+ transfer = client.add_items(transfer: transfer, items: ["#{__dir__}/fixtures/war-and-peace.txt"])
73
+ expect(transfer.items.count).to eq(1)
74
+ end
75
+
76
+ it 'raises an error when no transfer is being send to add_items_to_transfer method' do
77
+ expect {
78
+ client.add_items(items: ["#{__dir__}/fixtures/war-and-peace.txt"])
79
+ }.to raise_error(ArgumentError, 'missing keyword: transfer')
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe WeTransfer::Connection do
4
+ describe 'Connection#new' do
5
+ it 'creates a new connection when the client is passed' do
6
+ client = OpenStruct.new(api_key: 'api-key-12345')
7
+ connection = described_class.new(client: client)
8
+ expect(connection.api_connection).to be_kind_of Faraday::Connection
9
+ end
10
+
11
+ it 'contains the api_key inside the connection' do
12
+ client = OpenStruct.new(api_key: 'api-key-12345')
13
+ connection = described_class.new(client: client)
14
+ expect(connection.api_key).to eq(client.api_key)
15
+ end
16
+
17
+ it 'correctly handles the connection path variable' do
18
+ ENV['WT_API_CONNECTION_PATH'] = '/this_path_does_not_exist'
19
+ client = OpenStruct.new(api_key: 'api-key-12345')
20
+ connection = described_class.new(client: client)
21
+ expect(connection.api_path).to eq('/this_path_does_not_exist')
22
+ ENV['WT_API_CONNECTION_PATH'] = '/v1'
23
+ connection = described_class.new(client: client)
24
+ expect(connection.api_path).to eq('/v1')
25
+ end
26
+ end
27
+
28
+ describe 'Connection#post_request' do
29
+ it 'returns with a response body when a post request is made' do
30
+ client = OpenStruct.new(api_key: 'api-key-12345')
31
+ connection = described_class.new(client: client)
32
+ response = connection.post_request(path: '/authorize')
33
+ expect(response['status']).to eq('success')
34
+ end
35
+
36
+ it 'returns with a response body when a post request is made' do
37
+ client = OpenStruct.new(api_key: 'api-key-12345')
38
+ connection = described_class.new(client: client)
39
+ response = connection.post_request(path: '/transfers', body: {name: 'test_transfer', description: 'this is a test transfer', items: []})
40
+ expect(response['shortened_url']).to start_with('http://we.tl/s-')
41
+ expect(response['name']).to eq('test_transfer')
42
+ expect(response['description']).to eq('this is a test transfer')
43
+ expect(response['items'].count).to eq(0)
44
+ end
45
+
46
+ it 'returns with a ApiRequestError when request is forbidden' do
47
+ client = OpenStruct.new(api_key: 'api-key-12345')
48
+ connection = described_class.new(client: client)
49
+ expect {
50
+ connection.post_request(path: '/forbidden')
51
+ }.to raise_error(WeTransfer::Connection::ApiRequestError, 'Forbidden')
52
+ end
53
+ end
54
+
55
+ describe 'Connection#get_request' do
56
+ it 'returns with a response body when a get request is made for upload urls' do
57
+ client = OpenStruct.new(api_key: 'api-key-12345')
58
+ connection = described_class.new(client: client)
59
+ response = connection.get_request(path: '/files/1337/uploads/1/7331')
60
+ expect(response['upload_url']).to include('upload')
61
+ expect(response['part_number']).to eq(1)
62
+ expect(response['upload_id']).to_not be_nil
63
+ expect(response['upload_expires_at']).to_not be_nil
64
+ end
65
+
66
+ it 'returns with a ApiRequestError when request is forbidden' do
67
+ client = OpenStruct.new(api_key: 'api-key-12345')
68
+ connection = described_class.new(client: client)
69
+ expect {
70
+ connection.get_request(path: '/forbidden')
71
+ }.to raise_error(WeTransfer::Connection::ApiRequestError, 'Forbidden')
72
+ end
73
+ end
74
+ end