wetransfer 0.1.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.
@@ -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