cdnconnect-api 0.1.2 → 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: 87036814444c22863b8635b0d8004733180fe3c7
4
- data.tar.gz: e570f55d47c36fda2ddb196e5371ec35b18178aa
3
+ metadata.gz: 003b2a6ea479c7d10c830a1e8990ebbcb2d3cf2d
4
+ data.tar.gz: a152d8dc8d7bc6c393c968b409ba4cf3eacc9e7f
5
5
  SHA512:
6
- metadata.gz: a948d0f7bbdbdc95767ad1a5dc2bb1c033e0ef084951250b349816fe5c9682d56e0eadf0e90f5e3d7d85cac69453bf32e45d482cb39e937fcb6c9ec25f82a273
7
- data.tar.gz: c9246b9728a2ea0b098f95b6075cd53133dd8eb8e8e202ebebc5f177418a12d0ee0dadb0cdacca17650d6e613c1493b728c9de5f599c4266af6dc552fce19b2c
6
+ metadata.gz: b9b447175cbae4a5b6111b08e95c62566f1438810819720b11ed2bfd387e89cbd77ea9aef0f2f800a8d94b5602b3791ef039f6e23425aa3b1de29cf7c2e147fa
7
+ data.tar.gz: b0952dbce2737aa3214b5f96361903ca53c4a52c6bece83f80d4178dadd27a695a90acd12a90fd5189e055e6bd7ae759c3cf99b982b4573ea931267e1a7a1e28
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # CDN Connect API Ruby Client, v0.1.2
1
+ # CDN Connect API Ruby Client, v0.2.0
2
2
 
3
- CDN Connect makes it easier to manage production assets for teams of developers and designers, all while serving files from a fast content delivery network. Features include image optimization, resizing, cropping, filters, etc. The CDN Connect API Ruby Client makes it easier to upload files and interact with the API. Most interactions with CDN Connect APIs require users to authorize applications via OAuth 2.0. This library simplifies the communication with CDN Connect even further allowing you to easily upload files and get information with only a few lines of code.
3
+ CDN Connect makes it easier to manage production assets for teams of developers and designers, all while serving files from a fast content delivery network. Features include image optimization, resizing, cropping, filters, changing output formats, convert to WebP image format, etc. The CDN Connect API Ruby Client makes it easier to upload files and interact with the API with only a few lines of code.
4
4
 
5
5
  [View the full CDN Connect API documentation](http://api.cdnconnect.com/)
6
6
 
@@ -11,72 +11,150 @@ CDN Connect makes it easier to manage production assets for teams of developers
11
11
  [RubyGems.org: cdnconnect-api](https://rubygems.org/gems/cdnconnect-api)
12
12
 
13
13
 
14
- ## API Key
14
+ ## Setup the API Client
15
15
 
16
- An API Key can be created for a specific app within your CDN Connect's account. Sign into your account and go to the "API Key" tab for the app you want to interact with. Next click "Add API Key" and use this value when creating a new API client within the code. The API Key can be revoked
17
- by you at any time and numerous keys can be created.
16
+ First step is to create an api client instance which will be used to connect to your CDN Connect app. The required options are `app_host` and `api_key`.
18
17
 
18
+ #### App Host
19
+ The CDN Connect App host includes your app subdomain and the `cdnconnect.com` domain. For example, `demo.cdnconnect.com` is a CDN Connect app host. The app host should not include `https://`, `http://` or a URL path such as `/images`.
19
20
 
20
- ## Upload Example
21
+ #### API Key
22
+
23
+ Most interactions with CDN Connect APIs require users to authorize applications via OAuth 2.0. An API Key can be created for a specific app within your CDN Connect's account. Sign into your account and go to the "API Key" tab for the app you want to interact with. Next click "Add API Key" and use this value when creating a new API client within the code. The API Key can be revoked by you at any time and numerous keys can be created.
24
+
25
+
26
+ #### Example API Client
21
27
 
22
- # Initialize the CDN Connect API client
23
28
  require 'cdnconnect_api'
24
- api_client = CDNConnect::APIClient.new(:api_key => YOUR_API_KEY)
29
+
30
+ api_client = CDNConnect::APIClient.new(:app_host => 'YOUR_APP.cdnconnect.com',
31
+ :api_key => 'YOUR_API_KEY')
32
+
33
+
34
+ ## Upload Files
35
+
36
+ Upload a file or multiple files from a local machine to a folder within a CDN Connect app. The `upload` method provides numerous ways to upload files or files, to include recursively drilling down through local folders and uploading only files that match your chosen extensions. If any of the folders within the upload path do not already exist then they will be created automatically.
37
+
38
+ Below are the possible parameters for the `upload` method. You must set `destination_path` and use one of the options to select where the source files are uploaded from.
39
+
40
+ - `destination_path` : The URL of the CDN Connect folder to upload to. If the destination folder does not already exist it will automatically be created.
41
+ - `source_file_path` : A string of a source file's local path to upload to the destination folder. If you have more than one file to upload it'd be better to use `source_file_paths` or `source_folder_path` instead.
42
+ - `source_file_paths` : A list of a source file's local paths to upload. This option uploads all of the files to the destination folder. If you want to upload files in a local folder then `source_folder_path` option may would be easier than listing out files manually.
43
+ - `source_folder_path` : A string of a source folder's local path to upload. This will upload all of the files in this source folder to the destination url. By using the `valid_extensions` parameter you can also restrict which files should be uploaded according to extension.
44
+ - `valid_extensions` : An array of valid extensions which should be uploaded. This is only applied when the `source_folder_path` options is used. If nothing is provided, which is the default, all files within the folder are uploaded. The extensions should be in all lower case, and they should not contain a period or asterisks. Example `valid_extensions => ['js', 'css', 'jpg', jpeg', 'png', 'gif', 'webp']`
45
+ - `recursive_local_folders` : A true or false value indicating if this call should recursively upload all of the local folder's sub-folders, and their sub-folders, etc. This option is only used when the `source_folder_path` option is used.
46
+ - `async` : A true or false value indicating if the processing of the data should be asynchronous or not. The default value is false meaning that the processing of the data will be synchronous. An async response will be faster because the resposne doesn't wait on the system to complete processing the data. However, because an async response does not wait for the data to complete processing then the response will not contain any information about the data which was just uploaded. Use async only if you do not need to know the details of the upload.
47
+ - `webhook_url` : A URL which the system should `POST` the response to. This works for both synchronous and asynchronous calls. The data sent to the `webhook_url` will be the same as the data that is sent in a synchronous response. By default there is not webhook URL.
48
+
49
+ ### Upload One File: `source_file_path`
50
+
51
+ Use this option if you simply want to upload just one file. If you have many files to upload we recommend using either `source_file_paths` or `source_folder_path`.
52
+
53
+ response = api_client.upload(:destination_path => '/images',
54
+ :source_file_path => '/Users/Ellie/Pictures/meowzers.jpg')
25
55
 
26
- # Upload to a folder in the app the API Key was created for
27
- response = api_client.upload(:destination_folder_url => 'demo.cdnconnect.com/images',
28
- :source_file_local_path => 'meowzers.jpg')
56
+ ### Upload A List Of Files: `source_file_paths`
57
+
58
+ Specify a list of local files that should be uploaded to an app folder. Use this option if you want to manually select which files should be uploaded. Use the `source_folder_path` option if you want to easily upload all of the files
59
+ in a folder.
60
+
61
+ response = api_client.upload(:destination_path => '/images/kitty',
62
+ :source_file_paths => [
63
+ '/Users/Ellie/Pictures/furball.jpg',
64
+ '/Users/Ellie/Pictures/smuckers.jpg',
65
+ '/Users/Ellie/Pictures/socks.jpg'
66
+ ])
67
+
68
+ ### Upload All Of The Files In The Folder: `source_folder_path`
69
+
70
+ All files within the local `Pictures` folder will be uploaded. Additionally, by default all files within its subfolders will also be uploaded. Refer to the `recursive_local_folders` parameter if you do not want to recursively upload files in subfolders.
71
+
72
+ response = api_client.upload(:destination_path => '/images/',
73
+ :source_folder_path => '/Users/Ellie/Pictures/')
74
+
75
+
76
+ ## Get File or Folder Information
77
+
78
+ Both files and folders are considered "objects", and object data contains information stating if it is a file or a folder. A folder can contain many sub-folders, and many files, and a file is contained by a folder. The concept of files and folders is no different than how your computer handles them, and their hierarchy is what builds the URL. Getting information about a file or a folder both use `get_object`.
79
+
80
+
81
+ #### Get File Information
82
+
83
+ response = api_client.get_object(:path => '/images/spacewalk.jpg')
84
+
85
+
86
+ #### Get Folder Information
87
+
88
+ response = api_client.get_object(:path => '/images')
89
+
90
+
91
+ ## Rename File or Folder
92
+
93
+ Renames a file or folder, which are both also known as an object.
94
+
95
+ response = api_client.rename_object(:path => '/images/tv-shows/night-rider.jpeg',
96
+ :new_name => 'knight-rider.jpg')
97
+
98
+
99
+ ## Create A Folder Path
100
+
101
+ Creates a folder structure according to the path provided. If any of the folders do not already exist they will be created. The response contains data for every folder in the path, new and existing. The feature of creating the path automatically is also available when uploading files.
102
+
103
+ In the example below, if the folders `images` or `movies` did not already exist with the CDN Conenct app then they would automatically be created.
104
+
105
+ response = api_client.create_path(:path => '/images/movies')
106
+
107
+
108
+ ## API Response
109
+
110
+ HTTP responses will be formatted in json, but the library takes the HTTP response and decodes into a hash for the `APIResponse` class. The `APIResponse` class is used to simpilfy things by using helper functions to read response data. Responses from the API are all structured the same way, and this class is used as a small wrapper to make it easier to get data from it.
111
+
112
+
113
+ - `files` : `array` : A list of all the files that were uploaded. Each file in the array is a hash.
114
+ - `object`: `hash` : Can be either a file or folder, or the first file in the `files` array.
115
+ - `msgs ` : `array` : An array of messages, and each message is a hash. Example message within the `msgs` array: `{"text" => "info about the message", "status" => "error"}`
116
+ - `is_success` : `bool` : Successful API call, the response should contain the data your looking for.
117
+ - `is_error` : `bool` : Unsuccessful API call. Could be a client error (400) or a server error (500).
118
+ - `is_client_error` : `bool` : Unsuccessful API call due to a client error. Review the `msgs` array for more info.
119
+ - `is_bad_request` : `bool` : Unsuccessful API call due to sending invalid data. Review the `msgs` array for more info.
120
+ - `is_unauthorized` : `bool` : Unsuccessful API call due to not being authorized.
121
+ - `is_not_found` : `bool` : Unsuccessful API call because the resource does not exist.
122
+ - `is_server_error` : `bool` : Unsuccessful API call because server is having issues (its also possible, but hopefully you'll never see this).
123
+
124
+
125
+ #### Example Upload Response
29
126
 
30
- # Read the response
31
127
  if response.is_success
32
- # "Woot!"
33
- end
34
128
 
129
+ for file in response.files
130
+ puts "Uploaded " + file["name"]
131
+ end
132
+
133
+ end
35
134
 
36
- ## API Requests
37
135
 
38
- Following the [API documentation](http://api.cdnconnect.com/), you can use these methods to build a requests and return an APIResponse object.
136
+ #### Example Get Object Response
39
137
 
40
- * `get` GET Request. Used when needing to just read data.
41
- * `post` POST Request. Used when creating data.
42
- * `put` PUT Request. Used when updating data.
43
- * `delete` DELETE Request. Used when deleting data.
138
+ if response.is_success
44
139
 
45
- Each of these methods take one parameter which is the API path you want to request. Depending on which method you use, it will send the request with the correct HTTP verb.
140
+ puts "Got object " + response.object["name"]
46
141
 
47
- response = api_client.get('/v1/demo.cdnconnect.com/images/meowzers.jpg.json')
48
- puts response.results['object']['name'] #=> "meowzers.jpg"
142
+ end
49
143
 
50
- The path in the API request is broken down as:
51
144
 
52
- * `/v1/` The API version, which must always prefix an API request path.
53
- * `demo.cdnconnect.com/images/meowzers.jpg` The URL which you want to get information about.
54
- * `.json` The response format, which can be `json` or `xml`.
145
+ #### Example Error Response
55
146
 
147
+ if response.is_error
56
148
 
57
- ## Response Object
149
+ puts "CDN Connect Error"
58
150
 
59
- All responses will be structured the same with both a `results` and `msgs` object at the root level, such as:
151
+ for msg in response.msgs
152
+ puts msg["status"] + ": " + msg["text"]
153
+ end
60
154
 
61
- {
62
- "results":
63
- {
64
- "object":
65
- {
66
- "id": "bU1SS1JyvF9I",
67
- "status": 1,
68
- "name": "images",
69
- "created": "2013-03-12T17:02Z",
70
- "parent_id": "iF637hnbwI4G",
71
- "folder": true,
72
- "files": [],
73
- "folders": []
74
- }
75
- },
76
- "msgs":[]
77
- }
155
+ end
78
156
 
79
- Be sure to view the [API documentation](http://api.cdnconnect.com/) describing what each response object will contain depending on the API resource.
157
+ Note that this HTTP response will be parsed and can be easily read using the APIResponse. Be sure to view the [API documentation](http://api.cdnconnect.com/) describing what each response object will contain depending on the API resource.
80
158
 
81
159
 
82
160
  ## Support
@@ -20,12 +20,15 @@ require 'cdnconnect_api/response'
20
20
 
21
21
  module CDNConnect
22
22
 
23
+ ##
24
+ # Used to easily interact with CDN Connect API.
23
25
  class APIClient
24
26
 
25
27
  @@application_name = 'cdnconnect-api-ruby'
26
- @@application_version = '0.1.1'
27
- @@api_host = 'https://api.cdnconnect.com'
28
+ @@application_version = '0.2.0'
28
29
  @@user_agent = @@application_name + ' v' + @@application_version
30
+ @@api_host = 'https://api.cdnconnect.com'
31
+ @@api_version = 'v1'
29
32
 
30
33
  ##
31
34
  # Creates a client to authorize interactions with the API using the OAuth 2.0 protocol.
@@ -58,6 +61,13 @@ module CDNConnect
58
61
  # The redirection URI used in the initial request.
59
62
  # - <code>:access_token</code> -
60
63
  # The current access token for this client, also known as the API Key.
64
+ # access_token and api_key options are interchangeable.
65
+ # - <code>:api_key</code> -
66
+ # The current access token for this client, also known as the access token.
67
+ # access_token and api_key options are interchangeable.
68
+ # - <code>:app_host</code> -
69
+ # The CDN Connect App host. For example, demo.cdnconnect.com is a CDN Connect
70
+ # app host. The app host should not include https://, http:// or a URL path.
61
71
  # - <code>:debug</code> -
62
72
  # Print out any debugging information. Default is false.
63
73
  def initialize(options={})
@@ -73,9 +83,16 @@ module CDNConnect
73
83
  @redirect_uri = options["redirect_uri"]
74
84
  options["access_token"] = options["access_token"] || options["api_key"] # both work
75
85
  @access_token = options["access_token"]
86
+ @app_host = options["app_host"]
76
87
  @debug = options["debug"] || false
77
88
  @prefetched_upload_urls = {}
89
+ @upload_queue = {}
90
+ @failed_uploads = []
78
91
 
92
+ if options["api_key"] != nil and options["app_host"] == nil
93
+ raise ArgumentError, 'app_host option required when using api_key option'
94
+ end
95
+
79
96
  # Create the OAuth2 client which will be used to authorize the requests
80
97
  @client = Signet::OAuth2::Client.new(:client_id => client_id,
81
98
  :client_secret => @client_secret,
@@ -89,174 +106,426 @@ module CDNConnect
89
106
 
90
107
 
91
108
  ##
92
- # Executes a GET request to an API URL and returns a response object.
93
- # GET requests are used when reading data.
109
+ # Upload a file or multiple files from a local machine to a folder within
110
+ # a CDN Connect app. The upload method provides numerous ways to upload files or files,
111
+ # to include recursively drilling down through local folders and uploading only files
112
+ # that match your chosen extensions. If any of the folders within the upload path do not
113
+ # already exist then they will be created automatically.
94
114
  #
95
- # @param path [String] The API path to send the GET request to.
115
+ # @param [Hash] options
116
+ # The configuration parameters for the client.
117
+ # - <code>:destination_path</code> -
118
+ # The path of the CDN Connect folder to upload to. If the destination folder does
119
+ # not already exist it will automatically be created.
120
+ # - <code>:source_file_path</code> -
121
+ # A string of a source file's local path to upload to the destination folder.
122
+ # If you have more than one file to upload it'd be better to use
123
+ # `source_file_paths` or `source_folder_path` instead.
124
+ # - <code>:source_file_paths</code> -
125
+ # A list of a source file's local paths to upload. This option uploads all of
126
+ # the files to the destination folder. If you want to upload files in a
127
+ # local folder then `source_folder_path` option may would be easier
128
+ # than listing out files manually.
129
+ # - <code>:source_folder_path</code> -
130
+ # A string of a source folder's local path to upload. This will upload all of the
131
+ # files in this source folder to the destination url. By using the `valid_extensions`
132
+ # parameter you can also restrict which files should be uploaded according to extension.
133
+ # - <code>:valid_extensions</code> -
134
+ # An array of valid extensions which should be uploaded. This is only applied when the
135
+ # `source_folder_path` options is used. If nothing is provided, which is the
136
+ # default, all files within the folder are uploaded. The extensions should be in all
137
+ # lower case, and they should not contain a period or asterisks.
138
+ # Example `valid_extensions` array => ['js', 'css', 'jpg', jpeg', 'png', 'gif', 'webp']
139
+ # - <code>:recursive_local_folders</code> -
140
+ # A true or false value indicating if this call should recursively upload all of the
141
+ # local folder's sub-folders, and their sub-folders, etc. This option is only used
142
+ # when the `source_folder_path` option is used. Default is true.
143
+ # - <code>:async</code> -
144
+ # A true or false value indicating if the processing of the data should be asynchronous
145
+ # or not. The default value is false. An async response will be faster because
146
+ # the resposne doesn't wait on the system to complete processing the data. However,
147
+ # because an async response does not wait for the data to complete processing then the
148
+ # response will not contain any information about the data which was just uploaded.
149
+ # Use async only if you do not need to know the details of the upload.
150
+ # - <code>:webhook_url</code> -
151
+ # A URL which the system should `POST` the response to. This works for both synchronous
152
+ # and asynchronous calls. The data sent to the `webhook_url` will be the same as the
153
+ # data that is sent in a synchronous response. By default there is not webhook URL.
96
154
  # @return [APIResponse] A response object with helper methods to read the response.
97
- def get(path)
98
- return self.fetch(:path => path, :method => 'GET')
155
+ def upload(options={})
156
+ # Make sure we've got good source data before starting the upload
157
+ prepare_upload(options)
158
+
159
+ # Place all of the source files in an upload queue for each destination folder.
160
+ # Up to 25 files can be sent in one POST request. As uploads are successful
161
+ # the files will be removed from the queue and uploading will stop when
162
+ # each directory's upload queue is empty.
163
+ build_upload_queue(options)
164
+
165
+ # The returning response object. Its empty to start with then as
166
+ # uploads complete it fills this up with each upload's response info
167
+ api_response = CDNConnect::APIResponse.new()
168
+
169
+ # If there are files in the upload_queue then start the upload process
170
+ while @upload_queue.length > 0
171
+
172
+ # Get the destination_path in the list of upload queues
173
+ destination_path = @upload_queue.keys[0]
174
+ if @debug
175
+ puts "Upload destination_path: #{destination_path}"
176
+ end
177
+
178
+ # Check if we have a prefetched upload url before requesting a new one
179
+ upload_url = get_prefetched_upload_url(destination_path)
180
+ if upload_url == nil
181
+ # We do not already have an upload url created. The first upload request
182
+ # will need to make a request for an upload url. After the first upload
183
+ # each upload response will also include a new upload url which can be used
184
+ # for the next upload when uploading to the same folder.
185
+ upload_url_response = self.get_upload_url(destination_path)
186
+ if upload_url_response.is_error
187
+ return upload_url_response
188
+ end
189
+ upload_url = upload_url_response.get_result('upload_url')
190
+ if @debug
191
+ puts "Received upload url"
192
+ end
193
+ end
194
+
195
+ # Build the data that gets sent in the POST request
196
+ post_data = build_post_data(destination_path,
197
+ max_files_per_request = 25,
198
+ max_request_size = 25165824,
199
+ async = options.fetch(:async, false))
200
+
201
+ # Build the request to send to the API
202
+ # Uses the Faraday: https://github.com/lostisland/faraday
203
+ conn = Faraday.new() do |req|
204
+ req.headers['User-Agent'] = @@user_agent
205
+ req.headers['Authorization'] = 'Bearer ' + @access_token
206
+ req.request :multipart
207
+ req.adapter :net_http
208
+ end
209
+
210
+ # Kick off the request!
211
+ http_response = conn.post upload_url, post_data
212
+
213
+ # w00t! Convert the http response into APIResponse and see what's up
214
+ upload_response = APIResponse.new(http_response)
215
+ if @debug
216
+ for msg in upload_response.msgs
217
+ puts "Upload " + msg["status"] + ": " + msg["text"]
218
+ end
219
+ end
220
+
221
+ # merge the two together so we build one awesome response
222
+ # object with everything you need to know about every upload
223
+ api_response.merge(upload_response)
224
+
225
+ # Read the response and see what we got
226
+ if upload_response.is_server_error
227
+ # There was a server error, empty the active upload queue
228
+ failed_upload_attempt(destination_path)
229
+
230
+ else
231
+ # successful upload, clear out the active upload queue
232
+ # and remove uploaded files from the upload queue
233
+ successful_upload_attempt(destination_path)
234
+
235
+ # an upload response also contains a new upload url.
236
+ # Save it for the next upload to the same destination.
237
+ set_prefetched_upload_url(destination_path,
238
+ upload_response.get_result('upload_url'))
239
+ end
240
+
241
+ end
242
+
243
+ return api_response
99
244
  end
245
+
246
+
247
+ ##
248
+ # Build the POST data that gets sent in the request
249
+ # @!visibility private
250
+ def build_post_data(destination_path, max_files_per_request = 25, max_request_size = 25165824, async = false)
251
+ # @active_uploads will hold all of the upload keys
252
+ # which are actively being uploaded.
253
+ @active_uploads = []
254
+
255
+ # post_data will contain all of the data that gets sent
256
+ post_data = {}
257
+
258
+ # have the API also create the next upload url
259
+ post_data[:create_upload_url] = 'true'
260
+
261
+ # Processing of the data can be async. However, an async response will
262
+ # not contain any information about the data uploaded.
263
+ post_data[:async] = async
264
+
265
+ # Mime type doesn't matter because it gets figured out on the server-side
266
+ # using the file extension. So be sure file extensions are valid!
267
+ mime_type = 'application/octet-stream'
268
+
269
+ # the 'file' parameter will hold the actual file data
270
+ post_data[:file] = []
271
+
272
+ # tally up how large of a request this will be (in bytes)
273
+ total_request_size = 0
274
+
275
+ total_files = 0
276
+
277
+ # Add each source file in the queue to the request as multipart-post data
278
+ @upload_queue[destination_path].each_pair do |source_file_path, value|
279
+
280
+ # Figure out how large this file is
281
+ file_size = File.stat(source_file_path).size
282
+
283
+ # Add this file's size to the overall request size total
284
+ total_request_size += file_size
285
+
286
+ # Increment the upload attempts for this file
287
+ @upload_queue[destination_path][source_file_path]['attempts'] += 1
288
+
289
+ # Set that this file is actively being uploaded
290
+ @upload_queue[destination_path][source_file_path]['active'] = true
291
+
292
+ # Add the source file it to the request's post data
293
+ post_data[:file].push( Faraday::UploadIO.new(source_file_path, mime_type) )
294
+
295
+ total_files = post_data[:file].length
296
+
297
+ if total_request_size > max_request_size
298
+ # If the total request size is larger than the max
299
+ # then do not add any more files
300
+ break
301
+ elsif total_files >= max_files_per_request
302
+ # only add XX files per post request
303
+ # any left over will be picked up in the next upload
304
+ break
305
+ end
306
+ end
307
+
308
+ if @debug
309
+ puts "Upload request, File Count: #{total_files}, File Size: #{total_request_size} bytes"
310
+ end
100
311
 
101
-
312
+ return post_data
313
+ end
314
+
315
+
102
316
  ##
103
- # Executes a POST request to an API URL and returns a response object.
104
- # POST requests are used when creating data.
105
- #
106
- # @param path [String] The API path to send the POST request to.
107
- # @return [APIResponse] A response object with helper methods to read the response.
108
- def post(path)
109
- return self.fetch(:path => path, :method => 'POST')
317
+ # Upload was successful, clear it out from the upload queue.
318
+ # @!visibility private
319
+ def successful_upload_attempt(destination_path)
320
+ # Loop through each active upload for the destination folder url
321
+ if @upload_queue.has_key?(destination_path)
322
+ # Loop through each file for this destination folder
323
+ @upload_queue[destination_path].each_pair do |source_file_path, value|
324
+ # If the file was actively being uploaded then remove it
325
+ if @upload_queue[destination_path][source_file_path]['active']
326
+ remove_source_from_queue(destination_path, source_file_path)
327
+ end
328
+ end
329
+ end
110
330
  end
111
-
112
-
331
+
332
+
113
333
  ##
114
- # Executes a PUT request to an API URL and returns a response object.
115
- # PUT requests are used when updating data.
116
- #
117
- # @param path [String] The API path to send the POST request to.
118
- # @return [APIResponse] A response object with helper methods to read the response.
119
- def put(path)
120
- return self.fetch(:path => path, :method => 'PUT')
334
+ # Upload failed, clear it out from the active upload queue.
335
+ # If it was attempted too many times then remove it from the queue.
336
+ # @!visibility private
337
+ def failed_upload_attempt(destination_path)
338
+ if @debug
339
+ puts "failed_upload_attempt: #{destination_path}"
340
+ end
341
+ # Loop through each active upload for the destination folder url
342
+ if @upload_queue.has_key?(destination_path)
343
+ # Loop through each file for this destination folder
344
+ @upload_queue[destination_path].each_pair do |source_file_path, value|
345
+ # If the file was actively being uploaded then reset it to false
346
+ if @upload_queue[destination_path][source_file_path]['active']
347
+ @upload_queue[destination_path][source_file_path]['active'] = false
348
+ # If it was attempted too many times, then remove it
349
+ if @upload_queue[destination_path][source_file_path]['attempts'] >= 3
350
+ @failed_uploads.push(source_file_path)
351
+ remove_source_from_queue(destination_path, source_file_path)
352
+ end
353
+ end
354
+ end
355
+ end
121
356
  end
122
-
123
-
357
+
358
+
124
359
  ##
125
- # Executes a DELETE request to an API URL and returns a response object.
126
- # DELETE requests are used when (you guessed it) deleting data.
127
- #
128
- # @param path [String] The API path to send the DELETE request to.
129
- # @return [APIResponse] A response object with helper methods to read the response.
130
- def delete(url)
131
- return self.fetch(:path => path, :method => 'DELETE')
360
+ # Add source files to an upload queue for each destination folder.
361
+ # Up to 25 files can be sent in one POST request. As uploads are successful
362
+ # the files will be removed from the queue and uploading will stop when
363
+ # each directory's upload queue is empty.
364
+ # @!visibility private
365
+ def build_upload_queue(options)
366
+
367
+ if options[:source_folder_path] != nil
368
+ # Queue from all of the files in a folder
369
+ build_upload_queue_from_folder(options[:destination_path],
370
+ options[:source_folder_path],
371
+ options[:valid_extensions],
372
+ options.fetch(:recursive_local_folders, true))
373
+
374
+ elsif options[:source_file_paths] != nil
375
+ # Queue from all of the files in an array
376
+ for source_file_path in options[:source_file_paths]
377
+ add_source_to_upload_queue(options[:destination_path],
378
+ source_file_path)
379
+ end
380
+
381
+ elsif options[:source_file_path] != nil
382
+ # Queue from just one path
383
+ add_source_to_upload_queue(options[:destination_path],
384
+ options[:source_file_path])
385
+
386
+ end
387
+
132
388
  end
133
-
134
-
389
+
390
+
135
391
  ##
136
- # Used to upload a file or files within a folder to a destination folder within
137
- # a CDN Connect app. This method requires either a CDN Connect URL, or both an app_id
138
- # and obj_id. If you are uploading many files be sure to use the same client instance.
139
- #
140
- # @param [Hash] options
141
- # The configuration parameters for the client.
142
- # - <code>:destination_folder_url</code> -
143
- # The URL of the folder to upload to. If the destination folder URL option
144
- # is not provided then you must use the app_id and obj_id options.
145
- # - <code>:app_id</code> -
146
- # The app_id of the app to upload to. If the app_id or obj_id options
147
- # are not provided then you must use the url option.
148
- # - <code>:obj_id</code> -
149
- # The obj_id of the folder to upload to. If the app_id or obj_id options
150
- # are not provided then you must use the url option.
151
- # - <code>:source_file_local_path</code> -
152
- # A string of a source file's local paths to upload.
153
- # @return [APIResponse] A response object with helper methods to read the response.
154
- def upload(options={})
155
- # Make sure we've got good data before starting the upload
156
- prepare_upload(options)
157
-
158
- i = 1
159
- begin
160
-
161
- # Check if we have a prefetched upload url before requesting a new one
162
- upload_url = get_prefetched_upload_url(options[:destination_folder_url],
163
- options[:app_id],
164
- options[:obj_id])
165
- if upload_url == nil
166
- # We do not already have an upload url created. The first upload request
167
- # will need to make a request for an upload url. After the first upload
168
- # each upload response will also include a new upload url which can be used
169
- # for the next upload when uploading to the same folder.
170
- upload_url_response = self.get_upload_url(options)
171
- if upload_url_response.is_error
172
- return upload_url_response
173
- end
174
- upload_url = upload_url_response.get_result('upload_url')
392
+ # Add files to the destination folder's upload queue by going through the given
393
+ # local folder. By default all files will be added, but with the regex you can
394
+ # narrow down which files within the folder should be uploaded.
395
+ # @!visibility private
396
+ def build_upload_queue_from_folder(destination_path, source_folder_path, valid_extensions, recursive_local_folders)
397
+ # Queue from all of the files in a folder
398
+
399
+ Dir.foreach(source_folder_path) do |name|
400
+ # Ignore certain names and don't bother uploading them
401
+ next if name == '.' or name == '..' or name == '.DS_Store' or name == 'Thumbs.db'
402
+
403
+ # Build the full local path for the item
404
+ full_local_path = source_folder_path + '/' + name
405
+
406
+ if File.file?(full_local_path)
407
+ # This item is a file
408
+
409
+ # Get this file's extension
410
+ file_extension = File.extname(full_local_path)
411
+
412
+ # only upload if it has a file extension (required by cdn connect)
413
+ if file_extension != nil and file_extension != ''
414
+ # normalize the extension, lower case and remove the dot
415
+ file_extension = file_extension.downcase
416
+ file_extension.slice! "."
417
+
418
+ if valid_extensions == nil or valid_extensions.include? file_extension
419
+ add_source_to_upload_queue(destination_path, full_local_path)
420
+ end
421
+
422
+ end
423
+
424
+ elsif recursive_local_folders and File.directory?(full_local_path)
425
+ # This item is a folder and we want to recursively drill down through it
426
+ destination_sub_folder_url = destination_path + '/' + name
427
+ build_upload_queue_from_folder(destination_sub_folder_url, full_local_path, valid_extensions, recursive_local_folders)
428
+
429
+ end
430
+
175
431
  end
432
+ end
433
+
176
434
 
177
- # Create the POST data that gets sent in the request
178
- post_data = {}
179
- post_data[:create_upload_url] = 'true' # have the API also create the next upload url
180
- post_data[:file] = Faraday::UploadIO.new(options[:source_file_local_path], options[:mime_type])
181
-
182
- # Build the request to the API
183
- conn = Faraday.new() do |req|
184
- # https://github.com/lostisland/faraday
185
- req.headers['User-Agent'] = @@user_agent
186
- req.headers['Authorization'] = 'Bearer ' + @access_token
187
- req.request :multipart
188
- req.adapter :net_http
435
+ ##
436
+ # Add a source file to the upload queue for its destination folder.
437
+ # @!visibility private
438
+ def add_source_to_upload_queue(destination_path, source_file_path)
439
+ # Build a unique key for the destination folder for the @upload_queue.
440
+ # Each destination folder holds its own list of files to upload.
441
+ if not @upload_queue.has_key?(destination_path)
442
+ # Create an array for this destination to hold all of its uploads
443
+ @upload_queue[destination_path] = {}
189
444
  end
190
445
 
191
- # Kick it off!
192
- if @debug
193
- puts 'upload, source: ' + options[:source_file_local_path] + ', destination: ' + options[:destination_folder_url]
446
+ # Check if this source file has already been added for this destination
447
+ if @upload_queue[destination_path].has_key?(source_file_path)
448
+ # This upload already exists for this destination, don't add it again
449
+ return
194
450
  end
195
- api_response = conn.post upload_url, post_data
196
-
197
- # Woot! Convert the response to our model and see what's up
198
- response = APIResponse.new(api_response)
199
-
200
- # an upload response also contains a new upload url. Save it for the next upload.
201
- set_prefetched_upload_url(options[:destination_folder_url],
202
- options[:app_id],
203
- options[:obj_id],
204
- response.get_result('upload_url'))
205
-
206
- # Rettempt the upload a max of two times if there was a server error
207
- # Otherwise return the response data
208
- if not response.is_server_error or i > 2
209
- return response
451
+
452
+ # add to this local path to this destination's upload queue
453
+ # Its valud is the number of times its been attempted to upload
454
+ @upload_queue[destination_path][source_file_path] = { 'attempts' => 0, 'active' => false }
455
+ end
456
+
457
+
458
+ ##
459
+ # Remove a source file from the destination folders upload queue
460
+ # @!visibility private
461
+ def remove_source_from_queue(destination_path, source_file_path)
462
+ if @upload_queue.has_key?(destination_path)
463
+ if @upload_queue[destination_path].has_key?(source_file_path)
464
+ # remove from the upload_queue
465
+ @upload_queue[destination_path].delete(source_file_path)
466
+ end
467
+ if @upload_queue[destination_path].length == 0
468
+ @upload_queue.delete(destination_path)
469
+ end
210
470
  end
211
- i += 1
212
- end while i <= 3
213
-
214
471
  end
215
-
472
+
216
473
 
217
474
  ##
218
475
  # This method should not be called directly, but is used by the upload method
219
476
  # to get the options all ready to go and validated before uploading a file(s).
220
477
  # @!visibility private
221
478
  def prepare_upload(options={})
222
- # Validate we've got a source file
223
- source_file_local_path = options[:source_file_local_path]
224
- if source_file_local_path == nil
225
- raise ArgumentError, 'source_file_local_path required'
479
+
480
+ # Check if we've got valid source files
481
+ if options[:source_folder_path] != nil
482
+ # Check that the source folder exists
483
+ if not File.directory?(options[:source_folder_path])
484
+ raise ArgumentError, 'source_folder_path "' + options[:source_folder_path] + '" is not a valid directory'
485
+ end
486
+
487
+ elsif options[:source_file_paths] != nil
488
+ # Check that source_file_paths is an array
489
+ if not options[:source_file_paths].kind_of?(Array)
490
+ raise ArgumentError, 'source_file_paths must be an array of strings'
491
+ end
492
+ # Check that each source file in the array exists
493
+ for source_file_path in options[:source_file_paths]
494
+ if not File.file?(source_file_path)
495
+ raise ArgumentError, 'source_file_path "' + source_file_path + '" is not a valid file'
496
+ end
497
+ end
498
+
499
+ elsif options[:source_file_path] != nil
500
+ # Check that the single file exists
501
+ if not File.file?(options[:source_file_path])
502
+ raise ArgumentError, 'source_file_path "' + options[:source_file_path] + '" is not a valid file'
503
+ end
504
+
505
+ else
506
+ # Did not pass in any of the valid options for source files, raise error
507
+ raise ArgumentError, 'source file(s) required'
508
+
226
509
  end
227
510
 
228
511
  # Validate we've got a destination folder to upload to
229
- destination_folder_url = options[:destination_folder_url]
230
- app_id = options[:app_id]
231
- obj_id = options[:obj_id]
232
- if destination_folder_url == nil and (app_id == nil or obj_id == nil)
233
- raise ArgumentError, 'destination_folder_url or app_id/obj_id required'
512
+ destination_path = options[:destination_path]
513
+ if destination_path == nil
514
+ raise ArgumentError, 'destination_path required'
234
515
  end
235
516
 
236
- # Ideally it'd be awesome to already set what the mime type is, but getting that
237
- # info accurately is a pain. If you do not send in the mime_type we will
238
- # figure it out for you by the file extension (so ALWAYS have an extension)
239
- # This will only work when using the source_file option, and will not
240
- # work with the source_files or source_folder option.
241
- if options[:mime_type] == nil
242
- options[:mime_type] = 'application/octet-stream'
243
- end
244
-
245
- options
517
+ return options
246
518
  end
247
-
519
+
248
520
 
249
521
  ##
250
522
  # This method should not be called directly, but is used to check if we
251
523
  # already have an upload url ready to go for the folder we're uploading to.
252
524
  # @!visibility private
253
- def get_prefetched_upload_url(destination_url, app_id, obj_id)
525
+ def get_prefetched_upload_url(destination_path)
254
526
  # Build a unique key for the folder which was used to save an new upload url
255
- key = destination_url || ''
256
- key += app_id || ''
257
- key += obj_id || ''
258
- rtn_url = @prefetched_upload_urls[key]
259
- @prefetched_upload_urls[key] = nil
527
+ rtn_url = @prefetched_upload_urls[destination_path]
528
+ @prefetched_upload_urls[destination_path] = nil
260
529
  return rtn_url
261
530
  end
262
531
 
@@ -265,12 +534,9 @@ module CDNConnect
265
534
  # This method should not be called directly, but is used to remember an upload url
266
535
  # for the next upload to this folder.
267
536
  # @!visibility private
268
- def set_prefetched_upload_url(destination_url, app_id, obj_id, upload_url)
537
+ def set_prefetched_upload_url(destination_path, upload_url)
269
538
  # Build a unique key for the folder to save an new upload url value to
270
- key = destination_url || ''
271
- key += app_id || ''
272
- key += obj_id || ''
273
- @prefetched_upload_urls[key] = upload_url
539
+ @prefetched_upload_urls[destination_path] = upload_url
274
540
  end
275
541
 
276
542
 
@@ -279,21 +545,12 @@ module CDNConnect
279
545
  # upload url is received, all upload responses contain another upload which can be
280
546
  # used to eliminate the need to do seperate requests for an upload url.
281
547
  # @!visibility private
282
- def get_upload_url(options={})
283
- destination_folder_url = options[:destination_folder_url]
284
-
285
- path = nil
286
- if destination_folder_url != nil
287
- path = destination_folder_url + '/upload'
288
- else
289
- path = generate_obj_path(options) + '/upload'
290
- end
291
-
292
- path = '/v1/' + path + '.json'
548
+ def get_upload_url(destination_path)
549
+ api_path = destination_path + '/upload.json'
293
550
 
294
551
  i = 1
295
552
  begin
296
- response = self.fetch(:path => path)
553
+ response = get(api_path)
297
554
  if not response.is_server_error or i > 2
298
555
  return response
299
556
  end
@@ -301,53 +558,119 @@ module CDNConnect
301
558
  end while i <= 3
302
559
  end
303
560
 
561
+
562
+ ##
563
+ # Get object info, which can be either a file or folder.
564
+ #
565
+ # @param [Hash] options
566
+ # - <code>:path</code> -
567
+ # The path to the CDN Connect object to get. (required)
568
+ # @return [APIResponse] A response object with helper methods to read the response.
569
+ def get_object(options={})
570
+ api_path = options[:path] + '.json'
571
+ get(api_path)
572
+ end
304
573
 
574
+
305
575
  ##
306
- # This method should not be called directly, but is used to build the api
307
- # path common needed by a few methods.
308
- # @!visibility private
309
- def generate_obj_path(options={})
310
- path = options[:path]
311
- if path != nil
312
- return path
313
- end
576
+ # Rename object, which can be either a file or folder.
577
+ #
578
+ # @param [Hash] options
579
+ # - <code>:path</code> -
580
+ # The path to the CDN Connect object to get. (required)
581
+ # - <code>:new_name</code> -
582
+ # The new filename or folder name for the object. (required)
583
+ # @return [APIResponse] A response object with helper methods to read the response.
584
+ def rename_object(options={})
585
+ api_path = options[:path] + '/rename.json'
586
+ data = { :new_name => options[:new_name] }
587
+ put(api_path, data)
588
+ end
314
589
 
315
- app_id = options[:app_id]
316
- obj_id = options[:obj_id]
317
- uri = options[:uri] || options[:url]
318
- path = nil
319
-
320
- # An object's path can either be made up of an app_id and an obj_id
321
- # Or it can be made up of the entire URI
322
- if app_id != nil and obj_id != nil
323
- path = 'apps/' + app_id + '/objects/' + obj_id
324
- elsif uri != nil
325
- path = uri
326
- end
590
+
591
+ ##
592
+ # Delete object info, which can be either a file or folder.
593
+ #
594
+ # @param [Hash] options
595
+ # - <code>:path</code> -
596
+ # The path to the CDN Connect object to delete. (required)
597
+ # @return [APIResponse] A response object with helper methods to read the response.
598
+ def delete_object(options={})
599
+ api_path = options[:path] + '.json'
600
+ delete(api_path)
601
+ end
327
602
 
328
- if path == nil
329
- raise ArgumentError, "missing url or both app_id and obj_id"
330
- end
603
+
604
+ ##
605
+ # Create a folder path. If any of the folders within the given path do not
606
+ # already exist they will be created.
607
+ #
608
+ # @return [APIResponse] A response object with helper methods to read the response.
609
+ def create_path(options={})
610
+ api_path = options[:path] + '/create-path.json'
611
+ get(api_path)
612
+ end
613
+
331
614
 
332
- return path
615
+ ##
616
+ # Executes a GET request to an API URL and returns a response object.
617
+ # GET requests are used when reading data.
618
+ #
619
+ # @param api_path [String] The API path to send the GET request to.
620
+ # @return [APIResponse] A response object with helper methods to read the response.
621
+ def get(api_path)
622
+ fetch(:api_path => api_path, :method => 'GET')
333
623
  end
334
624
 
335
625
 
626
+ ##
627
+ # Executes a POST request to an API URL and returns a response object.
628
+ # POST requests are used when creating data.
629
+ #
630
+ # @param api_path [String] The API path to send the POST request to.
631
+ # @return [APIResponse] A response object with helper methods to read the response.
632
+ def post(api_path, body)
633
+ fetch(:api_path => api_path, :method => 'POST', :body => body)
634
+ end
635
+
636
+
637
+ ##
638
+ # Executes a PUT request to an API URL and returns a response object.
639
+ # PUT requests are used when updating data.
640
+ #
641
+ # @param api_path [String] The API path to send the POST request to.
642
+ # @return [APIResponse] A response object with helper methods to read the response.
643
+ def put(api_path, body)
644
+ fetch(:api_path => api_path, :method => 'PUT', :body => body)
645
+ end
646
+
647
+
648
+ ##
649
+ # Executes a DELETE request to an API URL and returns a response object.
650
+ # DELETE requests are used when (you guessed it) deleting data.
651
+ #
652
+ # @param api_path [String] The API path to send the DELETE request to.
653
+ # @return [APIResponse] A response object with helper methods to read the response.
654
+ def delete(api_path)
655
+ fetch(:api_path => api_path, :method => 'DELETE')
656
+ end
657
+
658
+
336
659
  ##
337
660
  # This method should not be called directly, but is used to validate data
338
661
  # and make it all pretty before firing off the request to the API.
339
662
  # @!visibility private
340
663
  def prepare(options={})
341
- if options[:path] == nil
664
+ if options[:api_path] == nil
342
665
  raise ArgumentError, 'missing api path'
343
666
  end
344
667
 
345
668
  options[:headers] = { 'User-Agent' => @@user_agent }
346
- options[:uri] = @@api_host + options[:path]
669
+ options[:uri] = @@api_host + '/' + @@api_version + '/' + @app_host + options[:api_path]
347
670
  options[:url] = options[:uri]
348
671
  options[:method] = options[:method] || 'GET'
349
-
350
- return options
672
+
673
+ options
351
674
  end
352
675
 
353
676
 
@@ -356,27 +679,40 @@ module CDNConnect
356
679
  # @!visibility private
357
680
  def fetch(options={})
358
681
  # Prepare the data to be shipped in the request
359
- options = self.prepare(options)
682
+ options = prepare(options)
360
683
 
361
684
  if @debug
362
- puts 'fetch: ' + options[:uri]
685
+ puts options[:method] + ': ' + options[:uri]
363
686
  end
364
687
 
365
688
  begin
366
689
  # Send the request and get the response
367
- response = @client.fetch_protected_resource(options)
690
+ http_response = @client.fetch_protected_resource(options)
368
691
 
369
692
  # Return the API response
370
- return APIResponse.new(response)
371
- rescue Signet::AuthorizationError => detail
372
- return APIResponse.new(detail.response)
693
+ api_response = APIResponse.new(http_response)
694
+
695
+ if @debug
696
+ for msg in api_response.msgs
697
+ puts msg["status"] + ": " + msg["text"]
698
+ end
699
+ end
700
+
701
+ return api_response
702
+ rescue Signet::AuthorizationError => authorization_error
703
+ # whoopsy doodle. Probably an incorrect API Key or App Host.
704
+ # Validate your authorization info.
705
+ if @debug
706
+ puts authorization_error
707
+ end
708
+ return APIResponse.new(authorization_error.response)
373
709
  end
374
710
 
375
711
  end
376
712
 
377
713
 
378
714
  ##
379
- # A unique identifier issued to the client to identify itself to CDN Connect's
715
+ # OAuth2 parameter. A unique identifier issued to the client to identify itself to CDN Connect's
380
716
  # authorization server. This is issued by CDN Connect to external clients.
381
717
  # This is only needed if an API Key isn't already known.
382
718
  #
@@ -387,7 +723,7 @@ module CDNConnect
387
723
 
388
724
 
389
725
  ##
390
- # A secret issued by the CDN Connect's authorization server,
726
+ # OAuth2 parameter. A secret issued by the CDN Connect's authorization server,
391
727
  # which is used to authenticate the client. Do not confuse this is an access_token
392
728
  # or an api_key. This is only required if an API Key
393
729
  # isn't already known. A client secret should not be shared.
@@ -399,7 +735,7 @@ module CDNConnect
399
735
 
400
736
 
401
737
  ##
402
- # The scope of the access request, expressed either as an Array
738
+ # OAuth2 parameter. The scope of the access request, expressed either as an Array
403
739
  # or as a space-delimited String. This is only required if an API Key
404
740
  # isn't already known.
405
741
  #
@@ -410,7 +746,7 @@ module CDNConnect
410
746
 
411
747
 
412
748
  ##
413
- # An unguessable random string designed to allow the client to maintain state
749
+ # OAuth2 parameter. An unguessable random string designed to allow the client to maintain state
414
750
  # to protect against cross-site request forgery attacks.
415
751
  # This is only required if an API Key isn't already known.
416
752
  #
@@ -420,29 +756,58 @@ module CDNConnect
420
756
  end
421
757
 
422
758
 
759
+ ##
760
+ # OAuth2 value. The authorization code received from the authorization server.
423
761
  # @return [String]
424
762
  def code
425
763
  @code
426
764
  end
427
765
 
428
766
 
767
+ ##
768
+ # OAuth2 value. The redirection URI used in the initial request.
429
769
  # @return [String]
430
770
  def redirect_uri
431
771
  @redirect_uri
432
772
  end
433
773
 
434
774
 
775
+ ##
776
+ # OAuth2 value. An API Key (commonly known as an access_token) which was previously
777
+ # created within CDN Connect's account for a specific app.
778
+ #
435
779
  # @return [String]
436
780
  def access_token
437
781
  @access_token
438
782
  end
439
783
 
440
784
 
785
+ ##
786
+ # The CDN Connect App host. For example, demo.cdnconnect.com is a CDN Connect app host.
787
+ # The app host value should not include https://, http:// or a URL path.
788
+ #
441
789
  # @return [String]
442
- def api_key
443
- @access_token
790
+ def app_host
791
+ @app_host
792
+ end
793
+
794
+
795
+ ##
796
+ # The current files queued to be uploaded.
797
+ #
798
+ # @return [Hash]
799
+ # @!visibility private
800
+ def upload_queue
801
+ @upload_queue
802
+ end
803
+
804
+ ##
805
+ # An array of files which failed.
806
+ #
807
+ # @return [Array]
808
+ def failed_uploads
809
+ @failed_uploads
444
810
  end
445
-
446
811
 
447
812
  end
448
813