files.com 1.1.786 → 1.1.787

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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/_VERSION +1 -1
  4. data/lib/files.com/api.rb +20 -5
  5. data/lib/files.com/api_client.rb +196 -17
  6. data/lib/files.com/list.rb +4 -2
  7. data/lib/files.com/models/action_log.rb +4 -1
  8. data/lib/files.com/models/action_notification_export.rb +3 -1
  9. data/lib/files.com/models/action_notification_export_result.rb +4 -1
  10. data/lib/files.com/models/ai_assistant_personality.rb +20 -7
  11. data/lib/files.com/models/ai_task.rb +22 -10
  12. data/lib/files.com/models/api_key.rb +15 -7
  13. data/lib/files.com/models/api_request_log.rb +4 -1
  14. data/lib/files.com/models/app.rb +4 -1
  15. data/lib/files.com/models/as2_incoming_message.rb +4 -1
  16. data/lib/files.com/models/as2_outgoing_message.rb +4 -1
  17. data/lib/files.com/models/as2_partner.rb +18 -7
  18. data/lib/files.com/models/as2_station.rb +14 -7
  19. data/lib/files.com/models/automation.rb +47 -13
  20. data/lib/files.com/models/automation_log.rb +4 -1
  21. data/lib/files.com/models/automation_run.rb +16 -9
  22. data/lib/files.com/models/bandwidth_snapshot.rb +4 -1
  23. data/lib/files.com/models/behavior.rb +27 -9
  24. data/lib/files.com/models/bundle.rb +57 -10
  25. data/lib/files.com/models/bundle_action.rb +4 -1
  26. data/lib/files.com/models/bundle_download.rb +4 -1
  27. data/lib/files.com/models/bundle_notification.rb +20 -7
  28. data/lib/files.com/models/bundle_recipient.rb +6 -1
  29. data/lib/files.com/models/bundle_registration.rb +4 -1
  30. data/lib/files.com/models/chat_session.rb +5 -2
  31. data/lib/files.com/models/child_site_management_policy.rb +18 -7
  32. data/lib/files.com/models/clickwrap.rb +14 -7
  33. data/lib/files.com/models/custom_domain.rb +18 -8
  34. data/lib/files.com/models/desktop_configuration_profile.rb +21 -7
  35. data/lib/files.com/models/dns_record.rb +4 -1
  36. data/lib/files.com/models/email_incoming_message.rb +4 -1
  37. data/lib/files.com/models/email_log.rb +4 -1
  38. data/lib/files.com/models/event_channel.rb +20 -7
  39. data/lib/files.com/models/event_delivery_attempt.rb +5 -2
  40. data/lib/files.com/models/event_record.rb +5 -2
  41. data/lib/files.com/models/event_subscription.rb +25 -7
  42. data/lib/files.com/models/event_target.rb +22 -7
  43. data/lib/files.com/models/exavault_api_request_log.rb +4 -1
  44. data/lib/files.com/models/expectation.rb +23 -10
  45. data/lib/files.com/models/expectation_evaluation.rb +5 -2
  46. data/lib/files.com/models/expectation_incident.rb +20 -11
  47. data/lib/files.com/models/external_event.rb +6 -2
  48. data/lib/files.com/models/file.rb +110 -37
  49. data/lib/files.com/models/file_comment.rb +14 -7
  50. data/lib/files.com/models/file_comment_reaction.rb +6 -3
  51. data/lib/files.com/models/file_migration.rb +1 -1
  52. data/lib/files.com/models/file_migration_log.rb +4 -1
  53. data/lib/files.com/models/folder.rb +11 -3
  54. data/lib/files.com/models/form_field_set.rb +26 -7
  55. data/lib/files.com/models/ftp_action_log.rb +4 -1
  56. data/lib/files.com/models/gpg_key.rb +15 -7
  57. data/lib/files.com/models/group.rb +30 -7
  58. data/lib/files.com/models/group_user.rb +16 -6
  59. data/lib/files.com/models/history.rb +23 -8
  60. data/lib/files.com/models/history_export.rb +2 -1
  61. data/lib/files.com/models/history_export_result.rb +4 -1
  62. data/lib/files.com/models/holiday_calendar.rb +15 -7
  63. data/lib/files.com/models/holiday_region.rb +4 -1
  64. data/lib/files.com/models/inbound_s3_log.rb +4 -1
  65. data/lib/files.com/models/inbox_recipient.rb +6 -1
  66. data/lib/files.com/models/inbox_registration.rb +4 -1
  67. data/lib/files.com/models/inbox_upload.rb +4 -1
  68. data/lib/files.com/models/integration_centric_profile.rb +17 -7
  69. data/lib/files.com/models/invoice.rb +5 -2
  70. data/lib/files.com/models/ip_address.rb +16 -4
  71. data/lib/files.com/models/key_lifecycle_rule.rb +17 -7
  72. data/lib/files.com/models/lock.rb +16 -6
  73. data/lib/files.com/models/metadata_category.rb +19 -9
  74. data/lib/files.com/models/notification.rb +39 -7
  75. data/lib/files.com/models/outbound_connection_log.rb +4 -1
  76. data/lib/files.com/models/partner.rb +32 -7
  77. data/lib/files.com/models/partner_channel.rb +17 -7
  78. data/lib/files.com/models/partner_channel_template.rb +17 -7
  79. data/lib/files.com/models/partner_site.rb +5 -3
  80. data/lib/files.com/models/partner_site_request.rb +10 -4
  81. data/lib/files.com/models/payment.rb +5 -2
  82. data/lib/files.com/models/pending_work_event.rb +5 -2
  83. data/lib/files.com/models/permission.rb +12 -4
  84. data/lib/files.com/models/public_hosting_request_log.rb +4 -1
  85. data/lib/files.com/models/public_key.rb +15 -7
  86. data/lib/files.com/models/remote_bandwidth_snapshot.rb +4 -1
  87. data/lib/files.com/models/remote_mount_backend.rb +25 -10
  88. data/lib/files.com/models/remote_server.rb +46 -14
  89. data/lib/files.com/models/remote_server_credential.rb +14 -7
  90. data/lib/files.com/models/request.rb +17 -6
  91. data/lib/files.com/models/restore.rb +8 -1
  92. data/lib/files.com/models/schedule.rb +14 -7
  93. data/lib/files.com/models/scheduled_export.rb +18 -7
  94. data/lib/files.com/models/scim_log.rb +5 -2
  95. data/lib/files.com/models/secret.rb +15 -7
  96. data/lib/files.com/models/session.rb +1 -0
  97. data/lib/files.com/models/settings_change.rb +4 -1
  98. data/lib/files.com/models/sftp_action_log.rb +4 -1
  99. data/lib/files.com/models/sftp_host_key.rb +17 -7
  100. data/lib/files.com/models/share_group.rb +14 -7
  101. data/lib/files.com/models/siem_http_destination.rb +67 -7
  102. data/lib/files.com/models/siem_http_destination_event.rb +5 -2
  103. data/lib/files.com/models/site.rb +109 -0
  104. data/lib/files.com/models/site_subdomain_redirect.rb +10 -5
  105. data/lib/files.com/models/snapshot.rb +19 -10
  106. data/lib/files.com/models/sso_event.rb +5 -2
  107. data/lib/files.com/models/sso_strategy.rb +10 -5
  108. data/lib/files.com/models/style.rb +9 -6
  109. data/lib/files.com/models/sync.rb +36 -13
  110. data/lib/files.com/models/sync_log.rb +4 -1
  111. data/lib/files.com/models/sync_run.rb +5 -2
  112. data/lib/files.com/models/usage_daily_snapshot.rb +4 -1
  113. data/lib/files.com/models/usage_snapshot.rb +4 -1
  114. data/lib/files.com/models/user.rb +124 -16
  115. data/lib/files.com/models/user_additional_email_recipient.rb +14 -7
  116. data/lib/files.com/models/user_cipher_use.rb +4 -1
  117. data/lib/files.com/models/user_lifecycle_rule.rb +26 -7
  118. data/lib/files.com/models/user_request.rb +11 -5
  119. data/lib/files.com/models/user_security_event.rb +5 -2
  120. data/lib/files.com/models/user_sftp_client_use.rb +4 -1
  121. data/lib/files.com/models/web_dav_action_log.rb +4 -1
  122. data/lib/files.com/models/webhook_test.rb +3 -0
  123. data/lib/files.com/models/workspace.rb +14 -7
  124. data/lib/files.com/util.rb +32 -1
  125. data/lib/files.com.rb +54 -1
  126. data/spec/lib/api_client_spec.rb +483 -0
  127. data/spec/list_spec.rb +31 -2
  128. data/spec/models/persistence_spec.rb +1 -1
  129. data/spec/models/user_spec.rb +20 -0
  130. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 297ac56fe0c1ad0e834dcd371599ce418443650c0c136adbd6530e42da051e09
4
- data.tar.gz: 20c046024f6830e02e7b73daccbe7dbe32ec9cce8ca38509a8c932bc4b826ab3
3
+ metadata.gz: 3be6daf56fa9341e6485d6cb0bb2308fd12187b7192dec2d34e9027f215e8e50
4
+ data.tar.gz: ba1c8e66a7043f1beeded14ddbd3767168cb69cf5ea5a2ab1b46bbe961c7c9ee
5
5
  SHA512:
6
- metadata.gz: a0ceefe70ae7603de8bdfcaa0856a40c08201ec2a601ec2a4003cb093c8cb0eab0ace565d4a704ce93e487cc1d57bce3b85363da27a80f426f74c0000918a118
7
- data.tar.gz: 6e9c93ff74b7f96d603b1da2249373c57c4843fbfbc66508d46b62e55ff1880a1d80c33427247db753f633649adfdc7f66dc17fcac9894fa756d8707a354e78f
6
+ metadata.gz: 840ca503641785a0c6c699c0de6397718de6f2eda8297ae25f14f23a16a066df3c585566b85ab89a81049891a891825c79ceb8c324ec0f2eedcfa0bfc7074f25
7
+ data.tar.gz: d6c74d466ff98e3b8eb31f7d7bbb4b38a603fb67b0ec6c2eae183d55901d6dfd40870dfa32bf42f7b346fecc8847d9919144a4ce22c4f2893b32d8d91ddee7cb
data/README.md CHANGED
@@ -305,7 +305,7 @@ end
305
305
 
306
306
  ```ruby title="Range Filter Example"
307
307
  begin
308
- # users who haven't logged in since 2024-01-01
308
+ # users whose last login was on or after 2024-01-01
309
309
  Files::User.list(
310
310
  filter_gteq: { "last_login_at": "2024-01-01" }
311
311
  )
@@ -320,7 +320,7 @@ end
320
320
  begin
321
321
  # users whose usernames start with 'test'
322
322
  Files::User.list(
323
- filter_pre: { username: "test" }
323
+ filter_prefix: { username: "test" }
324
324
  )
325
325
  rescue Files::NotAuthenticatedError => e
326
326
  puts "Authentication Error Occurred (#{e.class.to_s}): " + e.message
data/_VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.786
1
+ 1.1.787
data/lib/files.com/api.rb CHANGED
@@ -3,14 +3,21 @@
3
3
  module Files
4
4
  class Api
5
5
  # A given block receives the response and can reject it by raising. The
6
- # caller's options take on the resolved client and credentials only after
7
- # the request, and that check, succeed.
6
+ # returned options take on the resolved client and credentials, the
7
+ # regional origin a lockout region mismatch moved the request to, and the
8
+ # region_http_pairs the request used, only after the request, and that
9
+ # check, succeed; the caller's options are not changed. An options base_url
10
+ # sends the request there instead of to Files.base_url, and an options
11
+ # region_http_pairs is used instead of the client's.
8
12
  def self.send_request(path, verb, params, options)
9
13
  warn_on_options_in_params(params)
10
14
 
15
+ options = options.dup
11
16
  headers = options.clone
12
17
  api_key = headers.delete(:api_key)
18
+ base_url = headers.delete(:base_url)
13
19
  client = headers.delete(:client) || ApiClient.active_client
20
+ region_http_pairs = headers.delete(:region_http_pairs)
14
21
  session_id = headers.delete(:session_id)
15
22
  workspace_id = headers.delete(:workspace_id)
16
23
  if session = headers.delete(:session)
@@ -18,13 +25,21 @@ module Files
18
25
  session_id = session.id
19
26
  end
20
27
 
21
- resp, api_key, session_id = client.execute_request(
22
- verb, path, api_key: api_key, headers: headers, params: params, session_id: session_id, workspace_id: workspace_id
23
- )
28
+ request_options = { api_key: api_key, headers: headers, params: params, session_id: session_id, workspace_id: workspace_id }
29
+ # Only an origin or pairs the request carries are passed on; without them the client uses its own as before.
30
+ request_options[:base_url] = base_url unless base_url.nil?
31
+ request_options[:region_http_pairs] = region_http_pairs unless region_http_pairs.nil?
32
+ regional_base_url = nil
33
+ resp, api_key, session_id = client.execute_request(verb, path, **request_options) do |origin, used_region_http_pairs|
34
+ regional_base_url = origin
35
+ region_http_pairs = used_region_http_pairs
36
+ end
24
37
  yield resp if block_given?
25
38
  options[:client] = client
26
39
  options[:api_key] = api_key
27
40
  options[:session_id] = session_id
41
+ options[:base_url] = regional_base_url if regional_base_url
42
+ options[:region_http_pairs] = region_http_pairs unless region_http_pairs.nil?
28
43
 
29
44
  # Hash#select returns an array before 1.9
30
45
  options_to_persist = {}
@@ -12,6 +12,11 @@ module Files
12
12
  USE_GLOBAL = Object.new.freeze
13
13
  private_constant :USE_GLOBAL
14
14
 
15
+ # Local and Files.com development domains. A lockout region mismatch moves a request to an https:// origin on one
16
+ # of these, or on a name under one, only with Files.region_handler's approval.
17
+ DEVELOPMENT_DOMAINS = %w[localhost local filesrails.test files.test].freeze
18
+ private_constant :DEVELOPMENT_DOMAINS
19
+
15
20
  # Creates a client that sends requests through +conn+, or through a new default connection when +conn+ is nil.
16
21
  #
17
22
  # By default the client logs its ordinary API requests, responses and errors through the library-wide Files.logger and
@@ -32,11 +37,18 @@ module Files
32
37
  # +workspace_id+ gives this client its own workspace; 0 is sent as "0". A request's own workspace_id option still
33
38
  # takes precedence, and a request whose workspace_id is nil uses this client's. Pass workspace_id: nil to name no
34
39
  # workspace for this client's requests even when Files.workspace_id is set.
35
- def initialize(conn = nil, logger: USE_GLOBAL, log_level: USE_GLOBAL, workspace_id: USE_GLOBAL)
40
+ #
41
+ # A lockout region mismatch moves its requests over plain HTTP only from the source to the destination of a pair in
42
+ # the library-wide Files.region_http_pairs, read at the start of each request, unless +region_http_pairs+ gives this
43
+ # client its own pairs, in the same form. The client keeps a frozen copy of them, and [] allows none. A request's own
44
+ # region_http_pairs option, such as the one a returned record keeps, takes precedence over both. Plain HTTP is for
45
+ # your own development servers only (see Files.region_http_pairs=).
46
+ def initialize(conn = nil, logger: USE_GLOBAL, log_level: USE_GLOBAL, workspace_id: USE_GLOBAL, region_http_pairs: USE_GLOBAL)
36
47
  self.conn = conn || self.class.default_conn
37
48
  @logger = logger
38
49
  @log_level = log_level.equal?(USE_GLOBAL) ? log_level : Util.normalize_log_level(log_level)
39
50
  @workspace_id = workspace_id
51
+ @region_http_pairs = region_http_pairs.equal?(USE_GLOBAL) ? region_http_pairs : Util.checked_region_http_pairs(region_http_pairs)
40
52
  @system_profiler = SystemProfiler.new
41
53
  @last_request_metrics = nil
42
54
  end
@@ -96,8 +108,9 @@ module Files
96
108
  return false if num_retries >= Files.max_network_retries
97
109
  return true if error.is_a?(Faraday::TimeoutError)
98
110
  return true if error.is_a?(Faraday::ConnectionFailed)
99
- return true if error.is_a?(Faraday::ServerError)
100
- return true if error.is_a?(Faraday::ClientError) and error.response_status == 405
111
+ # 501 Not Implemented means the API does not support the request, so repeating it cannot succeed.
112
+ return true if error.is_a?(Faraday::ServerError) and error.response_status != 501
113
+ return true if error.is_a?(Faraday::ClientError) and [ 405, 429 ].include?(error.response_status)
101
114
 
102
115
  false
103
116
  end
@@ -124,7 +137,17 @@ module Files
124
137
  end
125
138
  end
126
139
 
127
- def execute_request(method, path, base_url: nil, api_key: nil, session_id: nil, workspace_id: nil, headers: {}, params: {})
140
+ # The frozen [source, destination] pairs of http:// origins this client's requests use when they name none of their
141
+ # own: the client's own, or Files.region_http_pairs.
142
+ def region_http_pairs
143
+ @region_http_pairs.equal?(USE_GLOBAL) ? Files.region_http_pairs : @region_http_pairs
144
+ end
145
+
146
+ # Returns the response and the API key and session ID the request used. +region_http_pairs+, such as the pairs a
147
+ # returned record keeps, is used instead of this client's. The block, if given, receives the request's context once
148
+ # its response has arrived: the regional origin a lockout region mismatch moved it to, or nil, and the frozen
149
+ # region_http_pairs it used.
150
+ def execute_request(method, path, base_url: nil, api_key: nil, session_id: nil, workspace_id: nil, region_http_pairs: nil, headers: {}, params: {}, &on_request_context)
128
151
  base_url ||= Files.base_url
129
152
  if api_key.nil?
130
153
  session_id ||= Files.session_id
@@ -155,12 +178,22 @@ module Files
155
178
  context = RequestLogContext.new
156
179
  context.api_key = api_key
157
180
  context.body = body
181
+ context.headers = headers
158
182
  context.method = method
159
183
  context.path = path
160
184
  context.query_params = query_params if query_params
161
185
  context.session_id = session_id
162
186
 
163
- http_resp = execute_request_with_rescues(base_url, context) do
187
+ regional_base_url = nil
188
+ # Fixed before the request starts, so later changes to the caller's or the library-wide pairs cannot affect it.
189
+ http_pairs = region_http_pairs.nil? ? self.region_http_pairs : Util.checked_region_http_pairs(region_http_pairs)
190
+ relocate = lambda do |error_response|
191
+ regional_base_url = region_continuation_base_url(error_response, base_url, body, http_pairs)
192
+ url = api_url(path, regional_base_url) if regional_base_url
193
+ regional_base_url
194
+ end
195
+
196
+ http_resp = execute_request_with_rescues(base_url, context, relocate: relocate) do
164
197
  conn.run_request(method, url, body, headers) do |req|
165
198
  req.options.open_timeout = Files.open_timeout
166
199
  req.options.timeout = Files.read_timeout
@@ -175,6 +208,7 @@ module Files
175
208
  end
176
209
 
177
210
  @last_response = resp
211
+ on_request_context&.call(regional_base_url, http_pairs)
178
212
  [ resp, api_key, session_id ]
179
213
  end
180
214
 
@@ -287,15 +321,143 @@ module Files
287
321
  end
288
322
 
289
323
  private def api_url(url = "", base_url = nil)
324
+ begin
325
+ root = Addressable::URI.parse(base_url)
326
+ rescue Addressable::URI::InvalidURIError
327
+ # The parser's message can include the whole URL and its credentials, so only the cause carries it.
328
+ raise InvalidParameterError, "Invalid Files.com base URL. Use an http:// or https:// URL with a valid host and port, such as https://app.files.com."
329
+ end
330
+
331
+ # Other schemes fail inside Net::HTTP with a NoMethodError rather than a Files::Error.
332
+ raise InvalidParameterError, "Unsupported Files.com base URL scheme \"#{root.scheme}\". Use an http:// or https:// base URL, such as https://app.files.com." unless root.scheme.nil? || %w[http https].include?(root.scheme.downcase)
333
+
290
334
  uri = Addressable::URI.new
291
- uri.host = Addressable::URI.parse(base_url).host
292
- uri.port = Addressable::URI.parse(base_url).port
335
+ uri.host = root.host
336
+ uri.port = root.port
293
337
  uri.path = "/api/rest/v1#{Files::URI.normalized_path(url)}"
294
- uri.scheme = Addressable::URI.parse(base_url).scheme
338
+ uri.scheme = root.scheme
295
339
 
296
340
  uri.to_s
297
341
  end
298
342
 
343
+ # The origin a lockout region mismatch moves this request to, or nil to raise the error as it is. The API answers
344
+ # with that error, naming the site's own origin in data.host, when the site is pinned to another region or to a
345
+ # custom domain. Only the configured HTTPS origin, reached over this client's verified connection, may send the
346
+ # credentials elsewhere, apart from an http:// origin to the destination +http_pairs+ pairs it with.
347
+ private def region_continuation_base_url(error_response, base_url, body, http_pairs)
348
+ return if Files.region_policy == :disabled
349
+
350
+ error_object = error_response.data
351
+ return unless error_response.http_status == 403 && error_object[:type] == "not-authenticated/lockout-region-mismatch"
352
+ return unless replayable?(body)
353
+
354
+ host = error_object[:data].is_a?(Hash) ? error_object[:data][:host] : nil
355
+ return http_pair_origin(base_url, host, http_pairs) unless verified_https_connection?(base_url)
356
+
357
+ configured = Addressable::URI.parse(base_url)
358
+ original_origin = https_origin(configured.normalized_host, configured.inferred_port)
359
+ target_origin, approval_required = region_target(host)
360
+ # A mismatch that names the origin it came from cannot be resolved by moving, so the handler is not asked.
361
+ return if target_origin.nil? || target_origin == original_origin
362
+
363
+ if Files.region_handler
364
+ target_origin = approved_region_origin(original_origin, target_origin, approval_required) { |decision| region_target(decision)&.first }
365
+ elsif approval_required
366
+ return
367
+ end
368
+ return if target_origin.nil? || target_origin == original_origin
369
+
370
+ Util.log_info("Region continuation", { from: original_origin, to: target_origin }, log_settings)
371
+ target_origin
372
+ end
373
+
374
+ # The origin Files.region_handler approves: the target for true, or another origin it returns that the block gives
375
+ # back unchanged, as the canonical form of an origin the request may move to. nil when it declines. The handler
376
+ # gets frozen copies, so it cannot change the origins used here.
377
+ private def approved_region_origin(original_origin, target_origin, approval_required)
378
+ event = { reason: :lockout_region_mismatch, original_origin: original_origin.dup.freeze, target_origin: target_origin.dup.freeze, approval_required: approval_required }.freeze
379
+ decision = Files.region_handler.call(event)
380
+ return target_origin if decision == true
381
+ return unless decision.is_a?(String)
382
+
383
+ approved_origin = yield(decision)
384
+ approved_origin if approved_origin == decision
385
+ end
386
+
387
+ # The http:// origin data.host names when +http_pairs+ pairs it with +base_url+'s own http:// origin, or nil. Plain
388
+ # HTTP proves nothing about who answered, so only that exact pair, the caller's own choice, gives the authority to
389
+ # continue. Within it a bare host means that host over plain HTTP, on port 80 unless it names another port. The
390
+ # region handler may still decline, or choose another destination paired with the same source; the request then
391
+ # continues at that pair's own frozen destination, not at the String the handler returned.
392
+ private def http_pair_origin(base_url, host, http_pairs)
393
+ original_origin = Util.http_origin(base_url)
394
+ target_origin = Util.http_origin(host.is_a?(String) && !host.include?("://") ? "http://#{host}" : host)
395
+ return unless original_origin && target_origin != original_origin && http_pairs.include?([ original_origin, target_origin ])
396
+
397
+ if Files.region_handler
398
+ target_origin = approved_region_origin(original_origin, target_origin, false) do |decision|
399
+ http_pairs.find { |source, destination| source == original_origin && destination == decision }&.last
400
+ end
401
+ end
402
+ return if target_origin.nil? || target_origin == original_origin
403
+
404
+ Util.log_info("Region continuation", { from: original_origin, to: target_origin }, log_settings)
405
+ target_origin
406
+ end
407
+
408
+ # Whether a response through this client came from +base_url+ over verified HTTPS: the SDK's own middleware and a
409
+ # Net::HTTP adapter, with certificate and host name verification on. Other middleware or adapters, such as a
410
+ # caller's, could produce a response that never came from there.
411
+ private def verified_https_connection?(base_url)
412
+ return false unless Addressable::URI.parse(base_url).scheme.to_s.casecmp?("https") && conn.is_a?(Faraday::Connection)
413
+
414
+ ssl = conn.ssl
415
+ ssl.verify? && ssl.verify_hostname? && [ nil, OpenSSL::SSL::VERIFY_PEER ].include?(ssl.verify_mode) &&
416
+ conn.builder.handlers == self.class.build_default_conn.builder.handlers &&
417
+ [ Faraday::Adapter::NetHttp, Faraday::Adapter::NetHttpPersistent ].include?(conn.builder.adapter&.klass)
418
+ end
419
+
420
+ # Whether +body+ can be sent again. A file in a multipart body has already been read.
421
+ private def replayable?(body)
422
+ case body
423
+ when Hash then body.values.all? { |value| replayable?(value) }
424
+ when Array then body.all? { |value| replayable?(value) }
425
+ else !body.respond_to?(:read)
426
+ end
427
+ end
428
+
429
+ # The HTTPS origin data.host names and whether continuing there needs the region handler's approval, or nil when
430
+ # data.host is not a plain origin. It may be a host name, with or without a port, or an https:// origin whose path
431
+ # is empty or "/"; credentials, a query, a fragment, a path, another scheme, spaces or backslashes are refused.
432
+ # Without approval the request continues only to a DNS name on port 443: an IP address, a local or Files.com
433
+ # development name (DEVELOPMENT_DOMAINS, or a name without a dot) or another port needs the handler to allow it.
434
+ private def region_target(host)
435
+ return unless host.is_a?(String) && !host.match?(/[[:space:][:cntrl:]\\]/)
436
+
437
+ origin = host.include?("://") ? host : "https://#{host}"
438
+ return unless origin.match?(/\Ahttps:\/\/[^\/?#@]+\/?\z/i)
439
+
440
+ uri = Addressable::URI.parse(origin)
441
+ name = uri.normalized_host.to_s
442
+ port = uri.port || 443
443
+ ip_address = name.match?(/\A\[[0-9a-f:.]+\]\z/) || name.match?(/\A\d{1,3}(\.\d{1,3}){3}\z/)
444
+ return unless port.between?(1, 65_535) && (ip_address || dns_name?(name))
445
+
446
+ local = ip_address || !name.include?(".") || DEVELOPMENT_DOMAINS.any? { |domain| name == domain || name.end_with?(".#{domain}") }
447
+ [ https_origin(name, port), local || port != 443 ]
448
+ rescue Addressable::URI::InvalidURIError, ArgumentError
449
+ nil
450
+ end
451
+
452
+ private def dns_name?(name)
453
+ labels = name.split(".", -1)
454
+ name.length <= 253 && labels.all? { |label| label.match?(/\A[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\z/) } && !labels.last.match?(/\A\d+\z/)
455
+ end
456
+
457
+ private def https_origin(host, port)
458
+ port == 443 ? "https://#{host}" : "https://#{host}:#{port}"
459
+ end
460
+
299
461
  private def check_api_key!(api_key)
300
462
  raise AuthenticationError, "No Files.com API key provided. Set your API key using \"Files.api_key = <API-KEY>\". You can generate API keys from the Files.com's web interface. " unless api_key
301
463
  raise AuthenticationError, "Your API key must be a string" unless api_key.is_a?(String)
@@ -311,20 +473,23 @@ module Files
311
473
  raise AuthenticationError, "The provided Session ID is invalid (it contains whitespace)"
312
474
  end
313
475
 
314
- def execute_request_with_rescues(base_url, context, skip_body_logging: false, is_transfer: false)
476
+ # +relocate+, given only for ordinary API requests, receives a structured error response and returns the regional
477
+ # origin to send the request to instead, or nil. The request is sent there at most once, and retries before and
478
+ # after count together.
479
+ def execute_request_with_rescues(base_url, context, skip_body_logging: false, is_transfer: false, relocate: nil)
315
480
  num_retries = 0
316
481
  begin
317
482
  request_start = Time.now
318
483
  log_request(context, num_retries, no_body: skip_body_logging)
319
484
  resp = yield
320
- log_response(context, request_start, resp.status, resp.body, no_body: skip_body_logging)
485
+ log_response(context, request_start, resp.status, resp.body, headers: resp.headers, no_body: skip_body_logging)
321
486
  rescue StandardError => e
322
487
  error_context = context
323
488
 
324
489
  if e.respond_to?(:response) && e.response
325
490
  error_context = context
326
491
  log_response(error_context, request_start,
327
- e.response[:status], e.response[:body], no_body: skip_body_logging
492
+ e.response[:status], e.response[:body], headers: e.response[:headers], no_body: skip_body_logging
328
493
  )
329
494
  else
330
495
  log_response_error(error_context, request_start, e)
@@ -332,7 +497,9 @@ module Files
332
497
 
333
498
  if self.class.should_retry?(e, num_retries)
334
499
  num_retries += 1
335
- sleep self.class.sleep_time(num_retries)
500
+ wait_seconds = self.class.sleep_time(num_retries)
501
+ log_retry(context, num_retries, e, wait_seconds) unless is_transfer
502
+ sleep wait_seconds
336
503
  retry
337
504
  end
338
505
 
@@ -340,7 +507,14 @@ module Files
340
507
 
341
508
  case e
342
509
  when Faraday::ClientError, Faraday::ServerError
343
- if (error_response = api_error_response(e.response))
510
+ error_response = api_error_response(e.response)
511
+ if relocate && error_response && (regional_base_url = relocate.call(error_response))
512
+ relocate = nil
513
+ base_url = regional_base_url
514
+ retry
515
+ end
516
+
517
+ if error_response
344
518
  handle_error_response(error_response, error_context, is_transfer: is_transfer)
345
519
  else
346
520
  handle_network_error(e, error_context, num_retries, base_url, is_transfer: is_transfer)
@@ -348,6 +522,7 @@ module Files
348
522
  else
349
523
  raise e.exception("Transfer request failed (#{e.class})"), cause: nil if is_transfer
350
524
 
525
+ handle_network_error(e, error_context, num_retries, base_url) if e.is_a?(Faraday::ConnectionFailed) || e.is_a?(Faraday::SSLError)
351
526
  raise
352
527
  end
353
528
  end
@@ -536,12 +711,12 @@ module Files
536
711
 
537
712
  private def log_request(context, num_retries, no_body: false)
538
713
  Util.log_info("Request", { method: context.method, num_retries: num_retries, path: context.path }, log_settings)
539
- Util.log_debug("Request details", { body: context.body, query_params: context.query_params }, log_settings) unless no_body
714
+ Util.log_debug("Request details", { body: context.body, headers: context.headers, query_params: context.query_params }, log_settings) unless no_body
540
715
  end
541
716
 
542
- private def log_response(context, request_start, status, body, no_body: false)
717
+ private def log_response(context, request_start, status, body, headers: nil, no_body: false)
543
718
  Util.log_info("Response", { elapsed: Time.now - request_start, method: context.method, path: context.path, status: status }, log_settings)
544
- Util.log_debug("Response details", { body: body }, log_settings) unless no_body
719
+ Util.log_debug("Response details", { body: body, headers: headers }, log_settings) unless no_body
545
720
  end
546
721
 
547
722
  private def log_response_error(context, request_start, error)
@@ -549,6 +724,10 @@ module Files
549
724
  Util.log_debug("Error details", { error_message: error.message }, log_settings)
550
725
  end
551
726
 
727
+ private def log_retry(context, attempt, error, wait_seconds)
728
+ Util.log_info("Retry", { attempt: attempt, error_type: error.class, max_retries: Files.max_network_retries, method: context.method, path: context.path, status: error.response_status, wait_seconds: wait_seconds }, log_settings)
729
+ end
730
+
552
731
  # This client's own logger and level where it set them, and the library-wide ones otherwise.
553
732
  private def log_settings
554
733
  Util::LogSettings.new(@logger.equal?(USE_GLOBAL) ? Files.logger : @logger,
@@ -562,7 +741,7 @@ module Files
562
741
  end
563
742
 
564
743
  class RequestLogContext
565
- attr_accessor :body, :api_key, :method, :path, :query_params, :session_id
744
+ attr_accessor :body, :api_key, :headers, :method, :path, :query_params, :session_id
566
745
  end
567
746
  end
568
747
  end
@@ -118,8 +118,10 @@ module Files
118
118
  def request_page(cursor)
119
119
  response, options = @request.call(cursor ? @params.merge(cursor: cursor) : @params) { |reply| reply.require_list(@resource_wrapper) }
120
120
  records = response.data.map { |attributes| @resource_wrapper.new(attributes, options) }
121
- next_cursor = response.http_headers["x-files-cursor"]
122
- Page.new(records, cursor, (next_cursor unless next_cursor.to_s.strip.empty?))
121
+ # The API documents X-Files-Cursor-Next for the next page's cursor; X-Files-Cursor is the older header. A blank
122
+ # cursor means there is no next page.
123
+ next_cursor = %w[x-files-cursor-next x-files-cursor].map { |name| response.http_headers[name] }.find { |value| !value.to_s.strip.empty? }
124
+ Page.new(records, cursor, next_cursor)
123
125
  end
124
126
 
125
127
  def with_page(page)
@@ -105,8 +105,11 @@ module Files
105
105
  raise InvalidParameterError.new("Bad parameter: filter_lt must be an Hash") if !params[:filter_lt].nil? and !params[:filter_lt].is_a?(Hash)
106
106
  raise InvalidParameterError.new("Bad parameter: filter_lteq must be an Hash") if !params[:filter_lteq].nil? and !params[:filter_lteq].is_a?(Hash)
107
107
 
108
+ list_options = options.dup
108
109
  List.new(ActionLog, params) do |page_params, &check_page|
109
- Api.send_request("/action_logs", :get, page_params, options, &check_page)
110
+ response, page_options = Api.send_request("/action_logs", :get, page_params, list_options, &check_page)
111
+ list_options = list_options.merge(page_options)
112
+ [ response, page_options ]
110
113
  end
111
114
  end
112
115
 
@@ -152,6 +152,7 @@ module Files
152
152
  else
153
153
  new_obj = ActionNotificationExport.create(@attributes, @options)
154
154
  @attributes = new_obj.attributes
155
+ @options = @options.merge(new_obj.options)
155
156
  end
156
157
 
157
158
  true
@@ -165,7 +166,7 @@ module Files
165
166
  #
166
167
  # Returns a Files::ActionNotificationExport.
167
168
  def self.find(id, params = {}, options = {})
168
- params ||= {}
169
+ params = (params || {}).dup
169
170
  params[:id] = id
170
171
  raise InvalidParameterError.new("Bad parameter: id must be an Integer") if !params[:id].nil? and !params[:id].is_a?(Integer)
171
172
  raise MissingParameterError.new("Parameter missing: id") if params[:id].nil?
@@ -204,6 +205,7 @@ module Files
204
205
  raise InvalidParameterError.new("Bad parameter: query_request_method must be an String") if !params[:query_request_method].nil? and !params[:query_request_method].is_a?(String)
205
206
  raise InvalidParameterError.new("Bad parameter: query_request_url must be an String") if !params[:query_request_url].nil? and !params[:query_request_url].is_a?(String)
206
207
  raise InvalidParameterError.new("Bad parameter: query_status must be an String") if !params[:query_status].nil? and !params[:query_status].is_a?(String)
208
+ raise InvalidParameterError.new("Bad parameter: query_success must be a single value, not #{params[:query_success].class}") if [ Array, Hash ].any? { |klass| params[:query_success].is_a?(klass) }
207
209
  raise InvalidParameterError.new("Bad parameter: query_path must be an String") if !params[:query_path].nil? and !params[:query_path].is_a?(String)
208
210
  raise InvalidParameterError.new("Bad parameter: query_folder must be an String") if !params[:query_folder].nil? and !params[:query_folder].is_a?(String)
209
211
 
@@ -78,8 +78,11 @@ module Files
78
78
  raise InvalidParameterError.new("Bad parameter: action_notification_export_id must be an Integer") if !params[:action_notification_export_id].nil? and !params[:action_notification_export_id].is_a?(Integer)
79
79
  raise MissingParameterError.new("Parameter missing: action_notification_export_id") if params[:action_notification_export_id].nil?
80
80
 
81
+ list_options = options.dup
81
82
  List.new(ActionNotificationExportResult, params) do |page_params, &check_page|
82
- Api.send_request("/action_notification_export_results", :get, page_params, options, &check_page)
83
+ response, page_options = Api.send_request("/action_notification_export_results", :get, page_params, list_options, &check_page)
84
+ list_options = list_options.merge(page_options)
85
+ [ response, page_options ]
83
86
  end
84
87
  end
85
88
 
@@ -82,29 +82,34 @@ module Files
82
82
  # use_by_default - boolean - Whether this personality is the default personality for the Workspace.
83
83
  # workspace_id - int64 - Workspace ID. `0` means the default workspace.
84
84
  def update(params = {})
85
- params ||= {}
85
+ params = (params || {}).dup
86
86
  params[:id] = @attributes[:id]
87
87
  raise MissingParameterError.new("Current object doesn't have a id") unless @attributes[:id]
88
88
  raise InvalidParameterError.new("Bad parameter: id must be an Integer") if !params[:id].nil? and !params[:id].is_a?(Integer)
89
+ raise InvalidParameterError.new("Bad parameter: apply_to_all_workspaces must be a single value, not #{params[:apply_to_all_workspaces].class}") if [ Array, Hash ].any? { |klass| params[:apply_to_all_workspaces].is_a?(klass) }
89
90
  raise InvalidParameterError.new("Bad parameter: name must be an String") if !params[:name].nil? and !params[:name].is_a?(String)
90
91
  raise InvalidParameterError.new("Bad parameter: system_prompt must be an String") if !params[:system_prompt].nil? and !params[:system_prompt].is_a?(String)
92
+ raise InvalidParameterError.new("Bad parameter: use_by_default must be a single value, not #{params[:use_by_default].class}") if [ Array, Hash ].any? { |klass| params[:use_by_default].is_a?(klass) }
91
93
  raise InvalidParameterError.new("Bad parameter: workspace_id must be an Integer") if !params[:workspace_id].nil? and !params[:workspace_id].is_a?(Integer)
92
94
  raise MissingParameterError.new("Parameter missing: id") if params[:id].nil?
93
95
 
94
96
  response, options = Api.send_request("/ai_assistant_personalities/#{@attributes[:id]}", :patch, params, @options) { |reply| reply.require_object("AiAssistantPersonality") }
95
97
  @attributes = response.data
98
+ @options = @options.merge(options)
96
99
  [ response, options ]
97
100
  end
98
101
 
99
102
  # Returns the [response, options] pair from Files::Api.send_request.
100
103
  def delete(params = {})
101
- params ||= {}
104
+ params = (params || {}).dup
102
105
  params[:id] = @attributes[:id]
103
106
  raise MissingParameterError.new("Current object doesn't have a id") unless @attributes[:id]
104
107
  raise InvalidParameterError.new("Bad parameter: id must be an Integer") if !params[:id].nil? and !params[:id].is_a?(Integer)
105
108
  raise MissingParameterError.new("Parameter missing: id") if params[:id].nil?
106
109
 
107
- Api.send_request("/ai_assistant_personalities/#{@attributes[:id]}", :delete, params, @options)
110
+ response, options = Api.send_request("/ai_assistant_personalities/#{@attributes[:id]}", :delete, params, @options)
111
+ @options = @options.merge(options)
112
+ [ response, options ]
108
113
  end
109
114
 
110
115
  # Alias for #delete. Returns nil.
@@ -119,6 +124,7 @@ module Files
119
124
  else
120
125
  new_obj = AiAssistantPersonality.create(@attributes, @options)
121
126
  @attributes = new_obj.attributes
127
+ @options = @options.merge(new_obj.options)
122
128
  end
123
129
 
124
130
  true
@@ -140,8 +146,11 @@ module Files
140
146
  raise InvalidParameterError.new("Bad parameter: sort_by must be an Hash") if !params[:sort_by].nil? and !params[:sort_by].is_a?(Hash)
141
147
  raise InvalidParameterError.new("Bad parameter: filter must be an Hash") if !params[:filter].nil? and !params[:filter].is_a?(Hash)
142
148
 
149
+ list_options = options.dup
143
150
  List.new(AiAssistantPersonality, params) do |page_params, &check_page|
144
- Api.send_request("/ai_assistant_personalities", :get, page_params, options, &check_page)
151
+ response, page_options = Api.send_request("/ai_assistant_personalities", :get, page_params, list_options, &check_page)
152
+ list_options = list_options.merge(page_options)
153
+ [ response, page_options ]
145
154
  end
146
155
  end
147
156
 
@@ -157,7 +166,7 @@ module Files
157
166
  #
158
167
  # Returns a Files::AiAssistantPersonality.
159
168
  def self.find(id, params = {}, options = {})
160
- params ||= {}
169
+ params = (params || {}).dup
161
170
  params[:id] = id
162
171
  raise InvalidParameterError.new("Bad parameter: id must be an Integer") if !params[:id].nil? and !params[:id].is_a?(Integer)
163
172
  raise MissingParameterError.new("Parameter missing: id") if params[:id].nil?
@@ -182,8 +191,10 @@ module Files
182
191
  #
183
192
  # Returns a Files::AiAssistantPersonality.
184
193
  def self.create(params = {}, options = {})
194
+ raise InvalidParameterError.new("Bad parameter: apply_to_all_workspaces must be a single value, not #{params[:apply_to_all_workspaces].class}") if [ Array, Hash ].any? { |klass| params[:apply_to_all_workspaces].is_a?(klass) }
185
195
  raise InvalidParameterError.new("Bad parameter: name must be an String") if !params[:name].nil? and !params[:name].is_a?(String)
186
196
  raise InvalidParameterError.new("Bad parameter: system_prompt must be an String") if !params[:system_prompt].nil? and !params[:system_prompt].is_a?(String)
197
+ raise InvalidParameterError.new("Bad parameter: use_by_default must be a single value, not #{params[:use_by_default].class}") if [ Array, Hash ].any? { |klass| params[:use_by_default].is_a?(klass) }
187
198
  raise InvalidParameterError.new("Bad parameter: workspace_id must be an Integer") if !params[:workspace_id].nil? and !params[:workspace_id].is_a?(Integer)
188
199
  raise MissingParameterError.new("Parameter missing: name") if params[:name].nil?
189
200
  raise MissingParameterError.new("Parameter missing: system_prompt") if params[:system_prompt].nil?
@@ -204,11 +215,13 @@ module Files
204
215
  #
205
216
  # Returns a Files::AiAssistantPersonality.
206
217
  def self.update(id, params = {}, options = {})
207
- params ||= {}
218
+ params = (params || {}).dup
208
219
  params[:id] = id
209
220
  raise InvalidParameterError.new("Bad parameter: id must be an Integer") if !params[:id].nil? and !params[:id].is_a?(Integer)
221
+ raise InvalidParameterError.new("Bad parameter: apply_to_all_workspaces must be a single value, not #{params[:apply_to_all_workspaces].class}") if [ Array, Hash ].any? { |klass| params[:apply_to_all_workspaces].is_a?(klass) }
210
222
  raise InvalidParameterError.new("Bad parameter: name must be an String") if !params[:name].nil? and !params[:name].is_a?(String)
211
223
  raise InvalidParameterError.new("Bad parameter: system_prompt must be an String") if !params[:system_prompt].nil? and !params[:system_prompt].is_a?(String)
224
+ raise InvalidParameterError.new("Bad parameter: use_by_default must be a single value, not #{params[:use_by_default].class}") if [ Array, Hash ].any? { |klass| params[:use_by_default].is_a?(klass) }
212
225
  raise InvalidParameterError.new("Bad parameter: workspace_id must be an Integer") if !params[:workspace_id].nil? and !params[:workspace_id].is_a?(Integer)
213
226
  raise MissingParameterError.new("Parameter missing: id") if params[:id].nil?
214
227
 
@@ -221,7 +234,7 @@ module Files
221
234
  #
222
235
  # Returns nil.
223
236
  def self.delete(id, params = {}, options = {})
224
- params ||= {}
237
+ params = (params || {}).dup
225
238
  params[:id] = id
226
239
  raise InvalidParameterError.new("Bad parameter: id must be an Integer") if !params[:id].nil? and !params[:id].is_a?(Integer)
227
240
  raise MissingParameterError.new("Parameter missing: id") if params[:id].nil?