files.com 1.1.767 → 1.1.769
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +47 -7
- data/_VERSION +1 -1
- data/docs/event_record.md +2 -2
- data/docs/event_subscription.md +8 -8
- data/docs/file.md +37 -0
- data/docs/public_key.md +1 -1
- data/lib/files.com/api.rb +9 -4
- data/lib/files.com/api_client.rb +181 -55
- data/lib/files.com/list.rb +98 -66
- data/lib/files.com/models/action_log.rb +2 -2
- data/lib/files.com/models/action_notification_export.rb +3 -3
- data/lib/files.com/models/action_notification_export_result.rb +2 -2
- data/lib/files.com/models/ai_assistant_personality.rb +10 -8
- data/lib/files.com/models/ai_task.rb +10 -8
- data/lib/files.com/models/api_key.rb +12 -10
- data/lib/files.com/models/api_request_log.rb +2 -2
- data/lib/files.com/models/app.rb +2 -2
- data/lib/files.com/models/as2_incoming_message.rb +2 -2
- data/lib/files.com/models/as2_outgoing_message.rb +2 -2
- data/lib/files.com/models/as2_partner.rb +10 -8
- data/lib/files.com/models/as2_station.rb +10 -8
- data/lib/files.com/models/automation.rb +12 -10
- data/lib/files.com/models/automation_log.rb +2 -2
- data/lib/files.com/models/automation_run.rb +6 -6
- data/lib/files.com/models/bandwidth_snapshot.rb +2 -2
- data/lib/files.com/models/behavior.rb +12 -10
- data/lib/files.com/models/bundle.rb +10 -8
- data/lib/files.com/models/bundle_action.rb +2 -2
- data/lib/files.com/models/bundle_download.rb +2 -2
- data/lib/files.com/models/bundle_notification.rb +10 -8
- data/lib/files.com/models/bundle_recipient.rb +4 -4
- data/lib/files.com/models/bundle_registration.rb +2 -2
- data/lib/files.com/models/chat_session.rb +3 -3
- data/lib/files.com/models/child_site_management_policy.rb +10 -8
- data/lib/files.com/models/clickwrap.rb +10 -8
- data/lib/files.com/models/custom_domain.rb +11 -9
- data/lib/files.com/models/desktop_configuration_profile.rb +10 -8
- data/lib/files.com/models/dns_record.rb +2 -2
- data/lib/files.com/models/email_incoming_message.rb +2 -2
- data/lib/files.com/models/email_log.rb +2 -2
- data/lib/files.com/models/event_channel.rb +10 -8
- data/lib/files.com/models/event_delivery_attempt.rb +3 -3
- data/lib/files.com/models/event_record.rb +4 -4
- data/lib/files.com/models/event_subscription.rb +14 -12
- data/lib/files.com/models/event_target.rb +10 -8
- data/lib/files.com/models/exavault_api_request_log.rb +2 -2
- data/lib/files.com/models/expectation.rb +11 -9
- data/lib/files.com/models/expectation_evaluation.rb +3 -3
- data/lib/files.com/models/expectation_incident.rb +6 -6
- data/lib/files.com/models/external_event.rb +5 -5
- data/lib/files.com/models/file.rb +23 -15
- data/lib/files.com/models/file_comment.rb +9 -7
- data/lib/files.com/models/file_comment_reaction.rb +2 -2
- data/lib/files.com/models/file_migration.rb +1 -1
- data/lib/files.com/models/file_migration_log.rb +2 -2
- data/lib/files.com/models/folder.rb +3 -3
- data/lib/files.com/models/form_field_set.rb +10 -8
- data/lib/files.com/models/ftp_action_log.rb +2 -2
- data/lib/files.com/models/gpg_key.rb +10 -8
- data/lib/files.com/models/group.rb +10 -8
- data/lib/files.com/models/group_user.rb +9 -7
- data/lib/files.com/models/history.rb +10 -10
- data/lib/files.com/models/history_export.rb +3 -3
- data/lib/files.com/models/history_export_result.rb +2 -2
- data/lib/files.com/models/holiday_calendar.rb +10 -8
- data/lib/files.com/models/holiday_region.rb +2 -2
- data/lib/files.com/models/inbound_s3_log.rb +2 -2
- data/lib/files.com/models/inbox_recipient.rb +4 -4
- data/lib/files.com/models/inbox_registration.rb +2 -2
- data/lib/files.com/models/inbox_upload.rb +2 -2
- data/lib/files.com/models/integration_centric_profile.rb +10 -8
- data/lib/files.com/models/invoice.rb +3 -3
- data/lib/files.com/models/ip_address.rb +8 -8
- data/lib/files.com/models/key_lifecycle_rule.rb +10 -8
- data/lib/files.com/models/lock.rb +3 -3
- data/lib/files.com/models/metadata_category.rb +12 -10
- data/lib/files.com/models/notification.rb +10 -8
- data/lib/files.com/models/outbound_connection_log.rb +2 -2
- data/lib/files.com/models/partner.rb +10 -8
- data/lib/files.com/models/partner_channel.rb +10 -8
- data/lib/files.com/models/partner_channel_template.rb +10 -8
- data/lib/files.com/models/partner_site_request.rb +4 -4
- data/lib/files.com/models/payment.rb +3 -3
- data/lib/files.com/models/pending_work_event.rb +3 -3
- data/lib/files.com/models/permission.rb +4 -4
- data/lib/files.com/models/public_hosting_request_log.rb +2 -2
- data/lib/files.com/models/public_key.rb +11 -9
- data/lib/files.com/models/remote_bandwidth_snapshot.rb +2 -2
- data/lib/files.com/models/remote_mount_backend.rb +10 -8
- data/lib/files.com/models/remote_server.rb +13 -11
- data/lib/files.com/models/remote_server_credential.rb +10 -8
- data/lib/files.com/models/request.rb +6 -6
- data/lib/files.com/models/restore.rb +4 -4
- data/lib/files.com/models/schedule.rb +10 -8
- data/lib/files.com/models/scheduled_export.rb +10 -8
- data/lib/files.com/models/scim_log.rb +3 -3
- data/lib/files.com/models/secret.rb +10 -8
- data/lib/files.com/models/session.rb +2 -2
- data/lib/files.com/models/settings_change.rb +2 -2
- data/lib/files.com/models/sftp_action_log.rb +2 -2
- data/lib/files.com/models/sftp_host_key.rb +10 -8
- data/lib/files.com/models/share_group.rb +10 -8
- data/lib/files.com/models/siem_http_destination.rb +10 -8
- data/lib/files.com/models/siem_http_destination_event.rb +3 -3
- data/lib/files.com/models/site.rb +3 -3
- data/lib/files.com/models/site_subdomain_redirect.rb +3 -3
- data/lib/files.com/models/snapshot.rb +10 -8
- data/lib/files.com/models/sso_event.rb +3 -3
- data/lib/files.com/models/sso_strategy.rb +3 -3
- data/lib/files.com/models/style.rb +6 -5
- data/lib/files.com/models/sync.rb +10 -8
- data/lib/files.com/models/sync_log.rb +2 -2
- data/lib/files.com/models/sync_run.rb +3 -3
- data/lib/files.com/models/usage_daily_snapshot.rb +2 -2
- data/lib/files.com/models/usage_snapshot.rb +2 -2
- data/lib/files.com/models/user.rb +10 -8
- data/lib/files.com/models/user_additional_email_recipient.rb +10 -8
- data/lib/files.com/models/user_cipher_use.rb +2 -2
- data/lib/files.com/models/user_lifecycle_rule.rb +10 -8
- data/lib/files.com/models/user_request.rb +5 -5
- data/lib/files.com/models/user_security_event.rb +3 -3
- data/lib/files.com/models/user_sftp_client_use.rb +2 -2
- data/lib/files.com/models/web_dav_action_log.rb +2 -2
- data/lib/files.com/models/webhook_test.rb +2 -2
- data/lib/files.com/models/workspace.rb +10 -8
- data/lib/files.com/response.rb +24 -0
- data/lib/files.com/sizable_io.rb +65 -4
- data/lib/files.com/version.rb +1 -1
- data/lib/files.com.rb +23 -1
- data/spec/language_spec.rb +20 -10
- data/spec/lib/api_client_spec.rb +173 -18
- data/spec/list_spec.rb +203 -187
- data/spec/models/file_transfer_spec.rb +346 -0
- data/spec/models/persistence_spec.rb +167 -0
- data/spec/spec_helper.rb +122 -6
- data/spec/support/stubbed_api.rb +14 -0
- data/spec/support/transfer_server.rb +60 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '00813c437bd03f8e8d5f2f33678070fb59d257993674718005d410593edf036e'
|
|
4
|
+
data.tar.gz: 61dd556a50fd3450fda1aec7187278cfd2638bf2badee307a7fc3983def6dd2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96eaaea7de07df44e3cca9b4973f36e745e683e1ed45afadb7f1201535cc268d691311fda5f992b120ef74447a345f309cb85453a23b25cd0278b2d2fd7602c0
|
|
7
|
+
data.tar.gz: f61bad4e74ca51a96eb9cae670c249015134bb00abbf56eb5f8dd5d701fa4db895ff6ab1c94fa5854f0d5bb29e53c63dbb28d47bc73ca21d74c784eebcf98005
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -410,9 +410,9 @@ A user's or group's `workspace_id` identifies the Workspace the account belongs
|
|
|
410
410
|
| Default Workspace user | Create an `admin` Permission for the user on a Custom Workspace's root folder. | Each Custom Workspace with a root grant. |
|
|
411
411
|
| Default Workspace group | Create an `admin` Permission for the group on a Custom Workspace's root folder. | Every member inherits administration of each Workspace with a root grant. |
|
|
412
412
|
|
|
413
|
-
`workspace_admin` is not a summary of a user's effective administrative access. A Default Workspace user can administer a Custom Workspace through a direct or group root grant while their `workspace_admin` remains `false`. Groups have no `workspace_admin` field. See [Users](/ruby/resources/user-accounts/users) and [Groups](/ruby/resources/user-accounts/groups) for account fields.
|
|
413
|
+
`workspace_admin` is not a summary of a user's effective administrative access. A Default Workspace user can administer a Custom Workspace through a direct or group root grant while their `workspace_admin` remains `false`. Groups have no `workspace_admin` field. See [Users](https://developers.files.com/ruby/resources/user-accounts/users) and [Groups](https://developers.files.com/ruby/resources/user-accounts/groups) for account fields.
|
|
414
414
|
|
|
415
|
-
An `admin` grant on the **Custom Workspace root** provides full Workspace Administrator authority over its files, users, groups, Partners, workflows, and integrations. An `admin` grant on a subfolder provides Folder Admin authority over that folder and its descendants; it does not provide Workspace administration. Other permission levels provide their corresponding folder access without Workspace administration. [Permissions](/ruby/resources/user-accounts/permissions) defines the levels.
|
|
415
|
+
An `admin` grant on the **Custom Workspace root** provides full Workspace Administrator authority over its files, users, groups, Partners, workflows, and integrations. An `admin` grant on a subfolder provides Folder Admin authority over that folder and its descendants; it does not provide Workspace administration. Other permission levels provide their corresponding folder access without Workspace administration. [Permissions](https://developers.files.com/ruby/resources/user-accounts/permissions) defines the levels.
|
|
416
416
|
|
|
417
417
|
Site Administrators manage cross-Workspace assignments to Default Workspace accounts. Workspace Administrators manage accounts and permissions within their own scope. Site Administrators retain access to every Workspace; adding a Workspace grant does not narrow Site Administrator authority. The [product documentation](https://www.files.com/docs/workspaces/workspace-administrators) explains the administrator's operational scope and site-wide controls.
|
|
418
418
|
|
|
@@ -424,7 +424,7 @@ A resource's `workspace_id` request field describes the resource's Workspace mem
|
|
|
424
424
|
|
|
425
425
|
Selecting another Workspace with an API key requires a **Full Access key created in the Default Workspace**. A user key follows that user's current access, including group permissions. A site-wide Full Access key created in the Default Workspace has Site Administrator authority in every Workspace. A Files Only key stays in its creation Workspace, even if its user has cross-Workspace access. Any key created in a Custom Workspace stays within that Workspace. Selecting another context with these confined keys is rejected with `bad-request/invalid-workspace-id-header`.
|
|
426
426
|
|
|
427
|
-
An account belonging to a Custom Workspace is scoped there when it authenticates normally. For a Default Workspace user, explicitly select the intended Workspace for an integration rather than relying on an interactive login preference. [API Keys](/ruby/resources/developers/api-keys) and [Authentication](/ruby/overview/authentication) cover credentials.
|
|
427
|
+
An account belonging to a Custom Workspace is scoped there when it authenticates normally. For a Default Workspace user, explicitly select the intended Workspace for an integration rather than relying on an interactive login preference. [API Keys](https://developers.files.com/ruby/resources/developers/api-keys) and [Authentication](https://developers.files.com/ruby/overview/authentication) cover credentials.
|
|
428
428
|
|
|
429
429
|
The Files.com Ruby SDK supports workspace scoping by using the `Files.workspace_id` configuration attribute. Scope a single request by passing `workspace_id` in the request options.
|
|
430
430
|
|
|
@@ -442,13 +442,13 @@ Files::Folder.list_for("", {}, workspace_id: 456)
|
|
|
442
442
|
|
|
443
443
|
An operations team already represented by a Default Workspace group can administer a Custom Workspace through one root Permission. The group and its members stay in the Default Workspace, so the same team can receive different access in other Workspaces.
|
|
444
444
|
|
|
445
|
-
First retrieve the target [Workspace](/ruby/resources/settings/workspaces) and [Group](/ruby/resources/user-accounts/groups) IDs as a Site Administrator in Workspace `0`. The examples use Workspace `123`, group `456`, and member user `789`; replace them with your own IDs. Confirm that the group belongs to Workspace `0` and that the intended user is a member.
|
|
445
|
+
First retrieve the target [Workspace](https://developers.files.com/ruby/resources/settings/workspaces) and [Group](https://developers.files.com/ruby/resources/user-accounts/groups) IDs as a Site Administrator in Workspace `0`. The examples use Workspace `123`, group `456`, and member user `789`; replace them with your own IDs. Confirm that the group belongs to Workspace `0` and that the intended user is a member.
|
|
446
446
|
|
|
447
447
|
Create the Permission using a Default Workspace Full Access site-wide key or a Full Access user key belonging to a Site Administrator. Keep the request context at `0` and use the qualified root path `_/Workspaces/123`. Set `group_id` to the group's ID, `permission` to `admin`, and `recursive` to `true`. Save the returned Permission `id` for later removal. For an individual Default Workspace user, use `user_id` instead of `group_id`.
|
|
448
448
|
|
|
449
449
|
For a Default Workspace group, a Site Administrator can also select Workspace `123` and use an empty `path` to grant access to its root. The qualified path in Workspace `0` works for both Default Workspace users and groups and keeps the account scope and target Workspace explicit. Appending a subfolder to the path would grant Folder Admin access instead of Workspace Administrator authority.
|
|
450
450
|
|
|
451
|
-
After the grant,
|
|
451
|
+
After the grant, make a request as the member, using their own credential with Workspace `123` selected, such as listing that Workspace's root folder. That member can work with the Workspace's files and perform Workspace Administrator operations, such as managing its users, Partners, and integrations. A Site Administrator's successful request does not establish that the member has the intended access.
|
|
452
452
|
|
|
453
453
|
```ruby title="Grant group administration"
|
|
454
454
|
require 'files.com'
|
|
@@ -463,6 +463,12 @@ grant = Files::Permission.create({
|
|
|
463
463
|
puts grant.id
|
|
464
464
|
```
|
|
465
465
|
|
|
466
|
+
```ruby title="Request as the group member"
|
|
467
|
+
Files::Folder.list_for("", {}, api_key: "YOUR_MEMBER_API_KEY", workspace_id: 123).each do |entry|
|
|
468
|
+
puts entry.path
|
|
469
|
+
end
|
|
470
|
+
```
|
|
471
|
+
|
|
466
472
|
### Permission Inspection and Removal
|
|
467
473
|
|
|
468
474
|
List the member's Permissions with `user_id` and `include_groups=true` to include grants inherited through group membership. Listing only direct user grants can miss the Permission that provides Workspace administration. In Workspace `0`, the Custom Workspace root appears as `_/Workspaces/123`; in Workspace `123`, paths are relative to that root. Inspect the root path and `permission=admin`, rather than treating the user's `workspace_admin` field as their effective administrative access.
|
|
@@ -476,7 +482,7 @@ Group membership maintained through SCIM follows the same rule. A Group Admin al
|
|
|
476
482
|
Delete the Permission by its returned `id` as the Site Administrator in Workspace `0`. The removal examples use Permission ID `9001`; replace it with the ID returned by your create request. Permissions are created and deleted, rather than updated in place. If narrower folder access is still needed, assign it explicitly; deleting a broad grant does not restore narrower grants it previously replaced.
|
|
477
483
|
|
|
478
484
|
```ruby title="Inspect member grants and remove the group grant"
|
|
479
|
-
Files::Permission.list({user_id: "789", include_groups: true}, workspace_id: 0).
|
|
485
|
+
Files::Permission.list({user_id: "789", include_groups: true}, workspace_id: 0).auto_paging_each do |item|
|
|
480
486
|
puts [item.path, item.permission, item.group_id].inspect
|
|
481
487
|
end
|
|
482
488
|
Files::Permission.delete(9001, {}, workspace_id: 0)
|
|
@@ -772,7 +778,13 @@ Files::FolderAdminPermissionRequiredError -> Files::NotAuthorizedError -> Files:
|
|
|
772
778
|
Certain API operations return lists of objects. When the number of objects in the list is large,
|
|
773
779
|
the API will paginate the results.
|
|
774
780
|
|
|
775
|
-
The Files.com Ruby SDK
|
|
781
|
+
The Files.com Ruby SDK provides multiple ways to paginate through lists of objects.
|
|
782
|
+
|
|
783
|
+
### Automatic Pagination
|
|
784
|
+
|
|
785
|
+
The `auto_paging_each` method iterates through every object on every page, requesting each page
|
|
786
|
+
only when iteration reaches it. Without a block it returns a lazy enumerator, so methods such as
|
|
787
|
+
`first(10)` request only the pages they need.
|
|
776
788
|
|
|
777
789
|
```ruby title="Example Request" hasDataFormatSelector
|
|
778
790
|
begin
|
|
@@ -789,6 +801,34 @@ rescue Files::Error => e
|
|
|
789
801
|
end
|
|
790
802
|
```
|
|
791
803
|
|
|
804
|
+
### Manual Pagination
|
|
805
|
+
|
|
806
|
+
A list operation returns its first page. `each`, and every other `Enumerable` method, covers the
|
|
807
|
+
objects on that page. `has_next_page?` reports whether another page follows, and `next_page`
|
|
808
|
+
requests it. After the last page, `next_page` returns an empty page without making a request.
|
|
809
|
+
|
|
810
|
+
All pages of a list share one position for `next_page`, so calling it again on an earlier page
|
|
811
|
+
continues after the newest page. `auto_paging_each` keeps its own position: it starts at the page
|
|
812
|
+
you call it on and does not move `next_page`.
|
|
813
|
+
|
|
814
|
+
```ruby title="Example Request"
|
|
815
|
+
begin
|
|
816
|
+
page = Files::Folder.list_for(path, per_page: 100)
|
|
817
|
+
loop do
|
|
818
|
+
page.each do |file|
|
|
819
|
+
# Operate on file
|
|
820
|
+
end
|
|
821
|
+
break unless page.has_next_page?
|
|
822
|
+
|
|
823
|
+
page = page.next_page
|
|
824
|
+
end
|
|
825
|
+
rescue Files::NotAuthenticatedError => e
|
|
826
|
+
puts "Authentication Error Occurred (#{e.class.to_s}): " + e.message
|
|
827
|
+
rescue Files::Error => e
|
|
828
|
+
puts "Unknown Error Occurred (#{e.class.to_s}): " + e.message
|
|
829
|
+
end
|
|
830
|
+
```
|
|
831
|
+
|
|
792
832
|
## Mock Server
|
|
793
833
|
|
|
794
834
|
Files.com publishes a mock Files.com API server, which is useful for testing your use of the Files.com
|
data/_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.769
|
data/docs/event_record.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"id": 1,
|
|
8
8
|
"workspace_id": 1,
|
|
9
9
|
"event_uuid": "example",
|
|
10
|
-
"event_type": "
|
|
10
|
+
"event_type": "sync_run.failure.v1",
|
|
11
11
|
"severity": "example",
|
|
12
12
|
"source_type": "example",
|
|
13
13
|
"source_id": 1,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* `id` (int64): Event Record ID
|
|
30
30
|
* `workspace_id` (int64): Workspace ID. 0 means the default workspace or site-wide.
|
|
31
31
|
* `event_uuid` (string): Stable event UUID.
|
|
32
|
-
* `event_type` (string): Versioned event type string.
|
|
32
|
+
* `event_type` (string): Versioned event type string. Valid values: `automation_run.canceled.v1`, `automation_run.failure.v1`, `automation_run.failure_will_retry.v1`, `automation_run.partial_failure.v1`, `automation_run.partial_failure_will_retry.v1`, `automation_run.skipped.v1`, `automation_run.success.v1`, `expectation_evaluation.invalid.v1`, `expectation_evaluation.late.v1`, `expectation_evaluation.missing.v1`, `expectation_evaluation.success.v1`, `expectation_incident.acknowledged.v1`, `expectation_incident.open.v1`, `expectation_incident.resolved.v1`, `expectation_incident.snoozed.v1`, `external_event.client_log.failure.v1`, `external_event.client_log.partial_failure.v1`, `external_event.client_log.skipped.v1`, `external_event.client_log.success.v1`, `pending_work_event.failure.v1`, `pending_work_event.partial_failure.v1`, `pending_work_event.skipped.v1`, `pending_work_event.success.v1`, `siem_http_destination_event.failure.v1`, `siem_http_destination_event.partial_failure.v1`, `siem_http_destination_event.skipped.v1`, `siem_http_destination_event.success.v1`, `sso_event.ldap_login.failure.v1`, `sso_event.ldap_login.partial_failure.v1`, `sso_event.ldap_login.skipped.v1`, `sso_event.ldap_login.success.v1`, `sso_event.ldap_sync.failure.v1`, `sso_event.ldap_sync.partial_failure.v1`, `sso_event.ldap_sync.skipped.v1`, `sso_event.ldap_sync.success.v1`, `sso_event.saml_login.failure.v1`, `sso_event.saml_login.partial_failure.v1`, `sso_event.saml_login.skipped.v1`, `sso_event.saml_login.success.v1`, `sync_run.failure.v1`, `sync_run.partial_failure.v1`, `sync_run.skipped.v1`, `sync_run.success.v1`, `user_security_event.lockout.v1`
|
|
33
33
|
* `severity` (string): Event severity.
|
|
34
34
|
* `source_type` (string): Source record type.
|
|
35
35
|
* `source_id` (int64): Source record ID.
|
data/docs/event_subscription.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"message_only": true,
|
|
15
15
|
"enabled": true,
|
|
16
16
|
"event_types": [
|
|
17
|
-
"
|
|
17
|
+
"sync_run.failure.v1"
|
|
18
18
|
],
|
|
19
19
|
"filter": "example",
|
|
20
20
|
"delivery_policy": "example",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* `message` (string): Custom message to include in notification emails.
|
|
36
36
|
* `message_only` (boolean): If true, notification email bodies contain only the custom message, omitting event details and the review button. Requires a custom message, defaults to false, and does not affect non-email targets.
|
|
37
37
|
* `enabled` (boolean): Whether this Event Subscription can dispatch events.
|
|
38
|
-
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types.
|
|
38
|
+
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types. Valid values: `automation_run.canceled.v1`, `automation_run.failure.v1`, `automation_run.failure_will_retry.v1`, `automation_run.partial_failure.v1`, `automation_run.partial_failure_will_retry.v1`, `automation_run.skipped.v1`, `automation_run.success.v1`, `expectation_evaluation.invalid.v1`, `expectation_evaluation.late.v1`, `expectation_evaluation.missing.v1`, `expectation_evaluation.success.v1`, `expectation_incident.acknowledged.v1`, `expectation_incident.open.v1`, `expectation_incident.resolved.v1`, `expectation_incident.snoozed.v1`, `external_event.client_log.failure.v1`, `external_event.client_log.partial_failure.v1`, `external_event.client_log.skipped.v1`, `external_event.client_log.success.v1`, `pending_work_event.failure.v1`, `pending_work_event.partial_failure.v1`, `pending_work_event.skipped.v1`, `pending_work_event.success.v1`, `siem_http_destination_event.failure.v1`, `siem_http_destination_event.partial_failure.v1`, `siem_http_destination_event.skipped.v1`, `siem_http_destination_event.success.v1`, `sso_event.ldap_login.failure.v1`, `sso_event.ldap_login.partial_failure.v1`, `sso_event.ldap_login.skipped.v1`, `sso_event.ldap_login.success.v1`, `sso_event.ldap_sync.failure.v1`, `sso_event.ldap_sync.partial_failure.v1`, `sso_event.ldap_sync.skipped.v1`, `sso_event.ldap_sync.success.v1`, `sso_event.saml_login.failure.v1`, `sso_event.saml_login.partial_failure.v1`, `sso_event.saml_login.skipped.v1`, `sso_event.saml_login.success.v1`, `sync_run.failure.v1`, `sync_run.partial_failure.v1`, `sync_run.skipped.v1`, `sync_run.success.v1`, `user_security_event.lockout.v1`
|
|
39
39
|
* `filter` (object): Structured event payload filter.
|
|
40
40
|
* `delivery_policy` (object): Event Subscription delivery policy.
|
|
41
41
|
* `event_target_ids` (array(int64)): Event Target IDs this subscription sends to.
|
|
@@ -86,7 +86,7 @@ Files::EventSubscription.create(
|
|
|
86
86
|
message: "example",
|
|
87
87
|
message_only: true,
|
|
88
88
|
enabled: true,
|
|
89
|
-
event_types: ["
|
|
89
|
+
event_types: ["sync_run.failure.v1"],
|
|
90
90
|
delivery_policy: "example",
|
|
91
91
|
event_target_ids: [1]
|
|
92
92
|
)
|
|
@@ -102,7 +102,7 @@ Files::EventSubscription.create(
|
|
|
102
102
|
* `message` (string): Custom message to include in notification emails.
|
|
103
103
|
* `message_only` (boolean): If true, notification email bodies contain only the custom message, omitting event details and the review button. Requires a custom message, defaults to false, and does not affect non-email targets.
|
|
104
104
|
* `enabled` (boolean): Whether this Event Subscription can dispatch events.
|
|
105
|
-
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types.
|
|
105
|
+
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types. Valid values: `automation_run.canceled.v1`, `automation_run.failure.v1`, `automation_run.failure_will_retry.v1`, `automation_run.partial_failure.v1`, `automation_run.partial_failure_will_retry.v1`, `automation_run.skipped.v1`, `automation_run.success.v1`, `expectation_evaluation.invalid.v1`, `expectation_evaluation.late.v1`, `expectation_evaluation.missing.v1`, `expectation_evaluation.success.v1`, `expectation_incident.acknowledged.v1`, `expectation_incident.open.v1`, `expectation_incident.resolved.v1`, `expectation_incident.snoozed.v1`, `external_event.client_log.failure.v1`, `external_event.client_log.partial_failure.v1`, `external_event.client_log.skipped.v1`, `external_event.client_log.success.v1`, `pending_work_event.failure.v1`, `pending_work_event.partial_failure.v1`, `pending_work_event.skipped.v1`, `pending_work_event.success.v1`, `siem_http_destination_event.failure.v1`, `siem_http_destination_event.partial_failure.v1`, `siem_http_destination_event.skipped.v1`, `siem_http_destination_event.success.v1`, `sso_event.ldap_login.failure.v1`, `sso_event.ldap_login.partial_failure.v1`, `sso_event.ldap_login.skipped.v1`, `sso_event.ldap_login.success.v1`, `sso_event.ldap_sync.failure.v1`, `sso_event.ldap_sync.partial_failure.v1`, `sso_event.ldap_sync.skipped.v1`, `sso_event.ldap_sync.success.v1`, `sso_event.saml_login.failure.v1`, `sso_event.saml_login.partial_failure.v1`, `sso_event.saml_login.skipped.v1`, `sso_event.saml_login.success.v1`, `sync_run.failure.v1`, `sync_run.partial_failure.v1`, `sync_run.skipped.v1`, `sync_run.success.v1`, `user_security_event.lockout.v1`
|
|
106
106
|
* `filter` (object): Structured event payload filter.
|
|
107
107
|
* `delivery_policy` (object): Event Subscription delivery policy.
|
|
108
108
|
* `event_target_ids` (array(int64)): Event Target IDs this subscription sends to.
|
|
@@ -122,7 +122,7 @@ Files::EventSubscription.update(id,
|
|
|
122
122
|
message: "example",
|
|
123
123
|
message_only: true,
|
|
124
124
|
enabled: true,
|
|
125
|
-
event_types: ["
|
|
125
|
+
event_types: ["sync_run.failure.v1"],
|
|
126
126
|
delivery_policy: "example",
|
|
127
127
|
event_target_ids: [1]
|
|
128
128
|
)
|
|
@@ -139,7 +139,7 @@ Files::EventSubscription.update(id,
|
|
|
139
139
|
* `message` (string): Custom message to include in notification emails.
|
|
140
140
|
* `message_only` (boolean): If true, notification email bodies contain only the custom message, omitting event details and the review button. Requires a custom message, defaults to false, and does not affect non-email targets.
|
|
141
141
|
* `enabled` (boolean): Whether this Event Subscription can dispatch events.
|
|
142
|
-
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types.
|
|
142
|
+
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types. Valid values: `automation_run.canceled.v1`, `automation_run.failure.v1`, `automation_run.failure_will_retry.v1`, `automation_run.partial_failure.v1`, `automation_run.partial_failure_will_retry.v1`, `automation_run.skipped.v1`, `automation_run.success.v1`, `expectation_evaluation.invalid.v1`, `expectation_evaluation.late.v1`, `expectation_evaluation.missing.v1`, `expectation_evaluation.success.v1`, `expectation_incident.acknowledged.v1`, `expectation_incident.open.v1`, `expectation_incident.resolved.v1`, `expectation_incident.snoozed.v1`, `external_event.client_log.failure.v1`, `external_event.client_log.partial_failure.v1`, `external_event.client_log.skipped.v1`, `external_event.client_log.success.v1`, `pending_work_event.failure.v1`, `pending_work_event.partial_failure.v1`, `pending_work_event.skipped.v1`, `pending_work_event.success.v1`, `siem_http_destination_event.failure.v1`, `siem_http_destination_event.partial_failure.v1`, `siem_http_destination_event.skipped.v1`, `siem_http_destination_event.success.v1`, `sso_event.ldap_login.failure.v1`, `sso_event.ldap_login.partial_failure.v1`, `sso_event.ldap_login.skipped.v1`, `sso_event.ldap_login.success.v1`, `sso_event.ldap_sync.failure.v1`, `sso_event.ldap_sync.partial_failure.v1`, `sso_event.ldap_sync.skipped.v1`, `sso_event.ldap_sync.success.v1`, `sso_event.saml_login.failure.v1`, `sso_event.saml_login.partial_failure.v1`, `sso_event.saml_login.skipped.v1`, `sso_event.saml_login.success.v1`, `sync_run.failure.v1`, `sync_run.partial_failure.v1`, `sync_run.skipped.v1`, `sync_run.success.v1`, `user_security_event.lockout.v1`
|
|
143
143
|
* `filter` (object): Structured event payload filter.
|
|
144
144
|
* `delivery_policy` (object): Event Subscription delivery policy.
|
|
145
145
|
* `event_target_ids` (array(int64)): Event Target IDs this subscription sends to.
|
|
@@ -174,7 +174,7 @@ event_subscription.update(
|
|
|
174
174
|
message: "example",
|
|
175
175
|
message_only: true,
|
|
176
176
|
enabled: true,
|
|
177
|
-
event_types: ["
|
|
177
|
+
event_types: ["sync_run.failure.v1"],
|
|
178
178
|
delivery_policy: "example",
|
|
179
179
|
event_target_ids: [1]
|
|
180
180
|
)
|
|
@@ -191,7 +191,7 @@ event_subscription.update(
|
|
|
191
191
|
* `message` (string): Custom message to include in notification emails.
|
|
192
192
|
* `message_only` (boolean): If true, notification email bodies contain only the custom message, omitting event details and the review button. Requires a custom message, defaults to false, and does not affect non-email targets.
|
|
193
193
|
* `enabled` (boolean): Whether this Event Subscription can dispatch events.
|
|
194
|
-
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types.
|
|
194
|
+
* `event_types` (array(string)): Event type strings matched by this subscription. Blank means all event types. Valid values: `automation_run.canceled.v1`, `automation_run.failure.v1`, `automation_run.failure_will_retry.v1`, `automation_run.partial_failure.v1`, `automation_run.partial_failure_will_retry.v1`, `automation_run.skipped.v1`, `automation_run.success.v1`, `expectation_evaluation.invalid.v1`, `expectation_evaluation.late.v1`, `expectation_evaluation.missing.v1`, `expectation_evaluation.success.v1`, `expectation_incident.acknowledged.v1`, `expectation_incident.open.v1`, `expectation_incident.resolved.v1`, `expectation_incident.snoozed.v1`, `external_event.client_log.failure.v1`, `external_event.client_log.partial_failure.v1`, `external_event.client_log.skipped.v1`, `external_event.client_log.success.v1`, `pending_work_event.failure.v1`, `pending_work_event.partial_failure.v1`, `pending_work_event.skipped.v1`, `pending_work_event.success.v1`, `siem_http_destination_event.failure.v1`, `siem_http_destination_event.partial_failure.v1`, `siem_http_destination_event.skipped.v1`, `siem_http_destination_event.success.v1`, `sso_event.ldap_login.failure.v1`, `sso_event.ldap_login.partial_failure.v1`, `sso_event.ldap_login.skipped.v1`, `sso_event.ldap_login.success.v1`, `sso_event.ldap_sync.failure.v1`, `sso_event.ldap_sync.partial_failure.v1`, `sso_event.ldap_sync.skipped.v1`, `sso_event.ldap_sync.success.v1`, `sso_event.saml_login.failure.v1`, `sso_event.saml_login.partial_failure.v1`, `sso_event.saml_login.skipped.v1`, `sso_event.saml_login.success.v1`, `sync_run.failure.v1`, `sync_run.partial_failure.v1`, `sync_run.skipped.v1`, `sync_run.success.v1`, `user_security_event.lockout.v1`
|
|
195
195
|
* `filter` (object): Structured event payload filter.
|
|
196
196
|
* `delivery_policy` (object): Event Subscription delivery policy.
|
|
197
197
|
* `event_target_ids` (array(int64)): Event Target IDs this subscription sends to.
|
data/docs/file.md
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
The Files::File object emulates a Ruby File object and can be used in
|
|
4
4
|
may places where a Ruby File object can be used.
|
|
5
5
|
|
|
6
|
+
## Downloads
|
|
7
|
+
|
|
8
|
+
`download_file(local_path)`, `download_content(io)` and `read_io` stream a
|
|
9
|
+
file's content without holding all of it in memory. When the SDK can tell that
|
|
10
|
+
a download did not finish, it raises instead of returning part of the file:
|
|
11
|
+
|
|
12
|
+
* A transfer the server refuses raises the `Net::HTTP` error for its status,
|
|
13
|
+
for example `Net::HTTPClientException` with the message `403 "Forbidden"`.
|
|
14
|
+
The error's response has no readable body, because the SDK stops before
|
|
15
|
+
reading it.
|
|
16
|
+
* A body that stops short of its `Content-Length`, or a chunked body that
|
|
17
|
+
stops before its last chunk, raises `EOFError`. A lost connection raises its
|
|
18
|
+
own error, such as `Errno::ECONNRESET`.
|
|
19
|
+
* An error writing to the IO given to `download_content` is raised unchanged.
|
|
20
|
+
|
|
21
|
+
Two kinds of incomplete body cannot be detected. A body sent with neither a
|
|
22
|
+
`Content-Length` nor chunked encoding ends when the connection closes, so it
|
|
23
|
+
has no expected length. A gzip or deflate body that Net::HTTP decompresses has
|
|
24
|
+
a `Content-Length` that counts compressed bytes, so a decompressed body framed
|
|
25
|
+
only by `Content-Length` is not checked. Chunked bodies and ranges are checked
|
|
26
|
+
whatever their encoding.
|
|
27
|
+
|
|
28
|
+
When a download raises, whatever arrived before the failure has already been
|
|
29
|
+
written: `download_file` can leave a partial file behind, and the IO given to
|
|
30
|
+
`download_content` holds the partial content. `download_content` leaves that
|
|
31
|
+
IO open, except that it closes the IO when the server refuses the transfer or
|
|
32
|
+
writing to the IO fails.
|
|
33
|
+
|
|
34
|
+
`read_io` returns a pipe that fills as the download proceeds. `read`, `gets`,
|
|
35
|
+
`readline`, `readpartial`, `each_line`, `each` and `eof?` raise a download
|
|
36
|
+
failure no later than they reach the end of the data. Other reads, such as
|
|
37
|
+
`getbyte` or `IO.copy_stream`, see an ordinary end of file, so check the
|
|
38
|
+
download by closing the pipe: `close` always releases it, and then raises a
|
|
39
|
+
failure that no read has raised yet, unless another exception is already
|
|
40
|
+
propagating. `Files::File#read`, `#gets`, `#readline`, `#readpartial` and
|
|
41
|
+
`#each_line` read from `read_io` and raise the same way.
|
|
42
|
+
|
|
6
43
|
## Example File Object
|
|
7
44
|
|
|
8
45
|
```
|
data/docs/public_key.md
CHANGED
|
@@ -58,7 +58,7 @@ Files::PublicKey.list(
|
|
|
58
58
|
* `cursor` (string): Used for pagination. When a list request has more records available, cursors are provided in the response headers `X-Files-Cursor-Next` and `X-Files-Cursor-Prev`. Send one of those cursor value here to resume an existing list from the next available record. Note: many of our SDKs have iterator methods that will automatically handle cursor-based pagination.
|
|
59
59
|
* `per_page` (int64): Number of records to show per page. (Max: 10000, 1,000 or less is recommended).
|
|
60
60
|
* `sort_by` (object): If set, sort records by the specified field in either `asc` or `desc` direction. Valid fields are `workspace_id`, `user_id`, `title` or `created_at`.
|
|
61
|
-
* `filter` (object): If set, return records where the specified field is equal to the supplied value. Valid fields are `created_at` and `workspace_id`.
|
|
61
|
+
* `filter` (object): If set, return records where the specified field is equal to the supplied value. Valid fields are `created_at` and `workspace_id`. Valid field combinations are `[ workspace_id, created_at ]`.
|
|
62
62
|
* `filter_gt` (object): If set, return records where the specified field is greater than the supplied value. Valid fields are `created_at`.
|
|
63
63
|
* `filter_gteq` (object): If set, return records where the specified field is greater than or equal the supplied value. Valid fields are `created_at`.
|
|
64
64
|
* `filter_lt` (object): If set, return records where the specified field is less than the supplied value. Valid fields are `created_at`.
|
data/lib/files.com/api.rb
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Files
|
|
4
4
|
class Api
|
|
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.
|
|
5
8
|
def self.send_request(path, verb, params, options)
|
|
6
9
|
warn_on_options_in_params(params)
|
|
7
10
|
|
|
8
|
-
options[:client] ||= ApiClient.active_client
|
|
9
|
-
|
|
10
11
|
headers = options.clone
|
|
11
12
|
api_key = headers.delete(:api_key)
|
|
12
|
-
client = headers.delete(:client)
|
|
13
|
+
client = headers.delete(:client) || ApiClient.active_client
|
|
13
14
|
session_id = headers.delete(:session_id)
|
|
14
15
|
workspace_id = headers.delete(:workspace_id)
|
|
15
16
|
if session = headers.delete(:session)
|
|
@@ -17,9 +18,13 @@ module Files
|
|
|
17
18
|
session_id = session.id
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
resp,
|
|
21
|
+
resp, api_key, session_id = client.execute_request(
|
|
21
22
|
verb, path, api_key: api_key, headers: headers, params: params, session_id: session_id, workspace_id: workspace_id
|
|
22
23
|
)
|
|
24
|
+
yield resp if block_given?
|
|
25
|
+
options[:client] = client
|
|
26
|
+
options[:api_key] = api_key
|
|
27
|
+
options[:session_id] = session_id
|
|
23
28
|
|
|
24
29
|
# Hash#select returns an array before 1.9
|
|
25
30
|
options_to_persist = {}
|
data/lib/files.com/api_client.rb
CHANGED
|
@@ -140,7 +140,7 @@ module Files
|
|
|
140
140
|
begin
|
|
141
141
|
resp = Response.from_faraday_response(http_resp)
|
|
142
142
|
rescue JSON::ParserError
|
|
143
|
-
raise
|
|
143
|
+
raise unreadable_response_error(http_resp), cause: nil
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
@last_response = resp
|
|
@@ -164,22 +164,7 @@ module Files
|
|
|
164
164
|
|
|
165
165
|
def stream_download(uri, io, range)
|
|
166
166
|
if conn.adapter == Faraday::Adapter::NetHttp
|
|
167
|
-
|
|
168
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
169
|
-
request = Net::HTTP::Get.new uri
|
|
170
|
-
request["RANGE"] = "bytes=#{range[0]}-#{range[1]}" unless range.empty?
|
|
171
|
-
http.request request do |response|
|
|
172
|
-
io.fulfill_content_length(response.content_length) if io.respond_to?(:fulfill_content_length)
|
|
173
|
-
response.read_body do |chunk|
|
|
174
|
-
response.error! if response.code.to_i >= 300
|
|
175
|
-
io.ready! if io.respond_to?(:ready!)
|
|
176
|
-
io << chunk
|
|
177
|
-
rescue StandardError => e
|
|
178
|
-
io.do_set_error(e) if io.respond_to?(:do_set_error)
|
|
179
|
-
io.close
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
167
|
+
stream_download_with_net_http(URI(uri), io, range)
|
|
183
168
|
else
|
|
184
169
|
response = remote_request(:get, uri)
|
|
185
170
|
io.fulfill_content_length(response.content_length) if io.respond_to?(:fulfill_content_length)
|
|
@@ -187,6 +172,85 @@ module Files
|
|
|
187
172
|
end
|
|
188
173
|
end
|
|
189
174
|
|
|
175
|
+
# Writes the body of a signed transfer URL to io as it arrives.
|
|
176
|
+
#
|
|
177
|
+
# Any failure while the response is open is raised only after Net::HTTP has
|
|
178
|
+
# let go of the connection. The Net::HTTP bundled with Ruby 3.0 retries a GET
|
|
179
|
+
# when an IOError or EOFError escapes the response block, which would write
|
|
180
|
+
# the body into io a second time.
|
|
181
|
+
private def stream_download_with_net_http(uri, io, range)
|
|
182
|
+
request = Net::HTTP::Get.new(uri)
|
|
183
|
+
request["Range"] = "bytes=#{range[0]}-#{range[1]}" unless range.empty?
|
|
184
|
+
|
|
185
|
+
failure = nil
|
|
186
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
187
|
+
http.request(request) do |response|
|
|
188
|
+
write_download_body(response, io)
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
failure = e
|
|
191
|
+
break
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
raise failure if failure
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private def write_download_body(response, io)
|
|
198
|
+
reject_unsuccessful_download(response, io)
|
|
199
|
+
io.fulfill_content_length(response.content_length) if io.respond_to?(:fulfill_content_length)
|
|
200
|
+
|
|
201
|
+
expected_bytes = expected_download_bytes(response)
|
|
202
|
+
received_bytes = 0
|
|
203
|
+
response.read_body do |chunk|
|
|
204
|
+
io.ready! if io.respond_to?(:ready!)
|
|
205
|
+
write_download_chunk(io, chunk)
|
|
206
|
+
received_bytes += chunk.bytesize
|
|
207
|
+
end
|
|
208
|
+
return if expected_bytes.nil? || received_bytes == expected_bytes
|
|
209
|
+
|
|
210
|
+
raise EOFError, "Transfer response ended after #{received_bytes} of #{expected_bytes} bytes"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# How many bytes read_body must yield, when the response says. Content-Length
|
|
214
|
+
# counts bytes on the wire, which read_body yields unchanged unless the body is
|
|
215
|
+
# chunked or Net::HTTP decompresses it. Net::HTTP quietly accepts a
|
|
216
|
+
# fixed-length body that stops short, and the version bundled with Ruby 3.0
|
|
217
|
+
# has no ignore_eof= to change that, so write_download_body counts the bytes.
|
|
218
|
+
private def expected_download_bytes(response)
|
|
219
|
+
response.content_length unless response.chunked? || decompressed_by_net_http?(response)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Net::HTTP decompresses a gzip or deflate body as it reads it, but never a
|
|
223
|
+
# range. Ask before reading: decompressing removes the Content-Encoding.
|
|
224
|
+
private def decompressed_by_net_http?(response)
|
|
225
|
+
response.decode_content && !response.key?("content-range") &&
|
|
226
|
+
%w[gzip x-gzip deflate].include?(response["content-encoding"]&.downcase)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private def reject_unsuccessful_download(response, io)
|
|
230
|
+
response.error! unless response.is_a?(Net::HTTPSuccess)
|
|
231
|
+
rescue Net::HTTPExceptions => e
|
|
232
|
+
close_destination_after_failure(io, e)
|
|
233
|
+
raise
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private def write_download_chunk(io, chunk)
|
|
237
|
+
io << chunk
|
|
238
|
+
rescue StandardError => e
|
|
239
|
+
close_destination_after_failure(io, e)
|
|
240
|
+
raise
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A refused transfer or a failing destination closes the destination, as it
|
|
244
|
+
# always has. Connection failures and short bodies leave it open so the caller
|
|
245
|
+
# can retry. Pipe readers learn why the download failed before the pipe
|
|
246
|
+
# closes, and a failure to close never replaces the download's own error.
|
|
247
|
+
private def close_destination_after_failure(io, failure)
|
|
248
|
+
io.do_set_error(failure) if io.respond_to?(:do_set_error)
|
|
249
|
+
io.close
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
Util.log_debug("Download destination close error", error_type: e.class, error_message: e.message)
|
|
252
|
+
end
|
|
253
|
+
|
|
190
254
|
def cursor
|
|
191
255
|
@last_response.http_headers["x-files-cursor"]
|
|
192
256
|
end
|
|
@@ -245,8 +309,8 @@ module Files
|
|
|
245
309
|
|
|
246
310
|
case e
|
|
247
311
|
when Faraday::ClientError, Faraday::ServerError
|
|
248
|
-
if
|
|
249
|
-
handle_error_response(
|
|
312
|
+
if (error_response = api_error_response(e.response))
|
|
313
|
+
handle_error_response(error_response, error_context, is_transfer: is_transfer)
|
|
250
314
|
else
|
|
251
315
|
handle_network_error(e, error_context, num_retries, base_url, is_transfer: is_transfer)
|
|
252
316
|
end
|
|
@@ -260,8 +324,13 @@ module Files
|
|
|
260
324
|
resp
|
|
261
325
|
end
|
|
262
326
|
|
|
263
|
-
|
|
264
|
-
|
|
327
|
+
# A successful response whose body is not JSON gave the SDK no usable
|
|
328
|
+
# answer. The body stays available as http_body but is left out of the
|
|
329
|
+
# message, and the parser error, which quotes it, is not kept as the cause.
|
|
330
|
+
private def unreadable_response_error(http_resp)
|
|
331
|
+
APIConnectionError.new("The Files.com API returned a response that is not valid JSON (HTTP response code was #{http_resp.status})",
|
|
332
|
+
http_status: http_resp.status, http_headers: http_resp.headers, http_body: http_resp.body
|
|
333
|
+
)
|
|
265
334
|
end
|
|
266
335
|
|
|
267
336
|
private def format_app_info(info)
|
|
@@ -271,21 +340,21 @@ module Files
|
|
|
271
340
|
str
|
|
272
341
|
end
|
|
273
342
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
end
|
|
343
|
+
# A failed response carrying a Files.com error object: a JSON object with an
|
|
344
|
+
# error message or type. Any other body, JSON or not, is not an answer the
|
|
345
|
+
# SDK can use.
|
|
346
|
+
private def api_error_response(http_resp)
|
|
347
|
+
return unless http_resp
|
|
348
|
+
|
|
349
|
+
resp = Response.from_faraday_hash(http_resp)
|
|
350
|
+
error_object = resp.data
|
|
351
|
+
resp if error_object.is_a?(Hash) && (error_object[:error] || error_object[:errors] || error_object[:type])
|
|
352
|
+
rescue JSON::ParserError
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
287
355
|
|
|
288
|
-
|
|
356
|
+
private def handle_error_response(resp, context, is_transfer: false)
|
|
357
|
+
error = specific_api_error(resp, context, is_transfer: is_transfer)
|
|
289
358
|
|
|
290
359
|
error.response = resp
|
|
291
360
|
raise error, cause: nil if is_transfer
|
|
@@ -293,30 +362,93 @@ module Files
|
|
|
293
362
|
raise error
|
|
294
363
|
end
|
|
295
364
|
|
|
296
|
-
private def specific_api_error(resp,
|
|
297
|
-
|
|
365
|
+
private def specific_api_error(resp, _context, is_transfer: false)
|
|
366
|
+
error_data = error_details(resp.data)
|
|
367
|
+
message = if is_transfer
|
|
368
|
+
"Transfer request failed"
|
|
369
|
+
elsif error_data[:message].is_a?(String)
|
|
370
|
+
without_url_credentials(error_data[:message])
|
|
371
|
+
else
|
|
372
|
+
"The Files.com API returned an error without a message (HTTP response code was #{resp.http_status})"
|
|
373
|
+
end
|
|
298
374
|
Util.log_error("API error", status: resp.http_status, error_message: message)
|
|
299
375
|
|
|
300
|
-
|
|
376
|
+
api_error_class(resp.data[:type]).new(message,
|
|
301
377
|
http_body: resp.http_body,
|
|
302
378
|
http_headers: resp.http_headers,
|
|
303
379
|
http_status: resp.http_status,
|
|
304
380
|
json_body: resp.data,
|
|
305
|
-
code: error_data[:code] || resp.http_status
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return APIError.new(message, **opts) unless resp&.data&.dig(:type)
|
|
381
|
+
code: error_data[:code] || resp.http_status
|
|
382
|
+
)
|
|
383
|
+
end
|
|
309
384
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
385
|
+
# Where a scheme-qualified URL starts. A scheme begins at the first letter of
|
|
386
|
+
# a run of scheme characters, and only a run's first character can start
|
|
387
|
+
# one, so each run is read a bounded number of times however long it is.
|
|
388
|
+
URL_START = /(?<![a-z0-9+.-])[0-9+.-]*+[a-z][a-z0-9+.-]*+:\/\//i
|
|
389
|
+
private_constant :URL_START
|
|
390
|
+
|
|
391
|
+
# A URL in server text, with the apostrophe that opens it if it is quoted.
|
|
392
|
+
# Before its query or fragment, a URL ends at whitespace, a character that
|
|
393
|
+
# cannot appear in a URL, or where the next URL starts, so each URL in a
|
|
394
|
+
# list is redacted on its own. Apostrophes can appear in a URL, so they stay
|
|
395
|
+
# part of it. A query or fragment runs to the end of the text around it.
|
|
396
|
+
URL_IN_TEXT = /(?<opening>'?)(?<url>#{URL_START}(?:(?!'?#{URL_START})[^\s"<>?#])*+(?:[?#][^\s"<>]*)?)/i
|
|
397
|
+
private_constant :URL_IN_TEXT
|
|
398
|
+
|
|
399
|
+
# Server text can quote a URL whose user info, query or fragment is a
|
|
400
|
+
# credential, such as a signed transfer URL. Error messages and logs below
|
|
401
|
+
# DEBUG keep where the URL points and drop those parts; the raw response
|
|
402
|
+
# fields keep them.
|
|
403
|
+
private def without_url_credentials(text)
|
|
404
|
+
text.gsub(URL_IN_TEXT) do
|
|
405
|
+
opening, url = Regexp.last_match.captures
|
|
406
|
+
closing = opening.empty? ? "" : closing_quote(url)
|
|
407
|
+
"#{opening}#{without_credentials(url.delete_suffix(closing))}#{closing}"
|
|
317
408
|
end
|
|
318
409
|
end
|
|
319
410
|
|
|
411
|
+
# The apostrophe that closes a URL quoted with apostrophes, with any
|
|
412
|
+
# punctuation after it, or "" when the URL does not end that way.
|
|
413
|
+
private def closing_quote(url)
|
|
414
|
+
quote = url.rindex("'")
|
|
415
|
+
quote && url[quote..].match?(/\A'[.,;:!?)]*\z/) ? url[quote..] : ""
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# The authority, which may be empty, ends at the first "/", "?" or "#";
|
|
419
|
+
# only an "@" inside it marks user info.
|
|
420
|
+
private def without_credentials(url)
|
|
421
|
+
scheme, _, rest = url.partition("://")
|
|
422
|
+
authority, separator, path = rest.partition(/[\/?#]/)
|
|
423
|
+
authority = "[redacted]@#{authority.rpartition('@').last}" if authority.include?("@")
|
|
424
|
+
path = "#{separator}#{path}"
|
|
425
|
+
credentials_at = path.index(/[?#]/)
|
|
426
|
+
path = "#{path[0...credentials_at]}?[redacted]" if credentials_at
|
|
427
|
+
"#{scheme}://#{authority}#{path}"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# The message and code in an error object's "error" or "errors" field, which
|
|
431
|
+
# holds a message, an object with a message and code, or a list of either.
|
|
432
|
+
private def error_details(error_object)
|
|
433
|
+
details = error_object[:error] || error_object[:errors]
|
|
434
|
+
details = details.first if details.is_a?(Array)
|
|
435
|
+
details = { message: details } if details.is_a?(String)
|
|
436
|
+
details.is_a?(Hash) ? details : {}
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# The APIError subclass an error type names, such as NotFoundError for
|
|
440
|
+
# "not-found" or LockoutRegionMismatchError for
|
|
441
|
+
# "not-authenticated/lockout-region-mismatch". Any other type is an APIError.
|
|
442
|
+
private def api_error_class(type)
|
|
443
|
+
return APIError unless type.is_a?(String)
|
|
444
|
+
|
|
445
|
+
name = type.split("/").last.to_s.split("-").map(&:capitalize).join
|
|
446
|
+
error_class = Files.const_get("#{name}Error", false)
|
|
447
|
+
error_class.is_a?(Class) && error_class < APIError ? error_class : APIError
|
|
448
|
+
rescue NameError
|
|
449
|
+
APIError
|
|
450
|
+
end
|
|
451
|
+
|
|
320
452
|
private def handle_network_error(error, _context, num_retries, base_url = nil, is_transfer: false)
|
|
321
453
|
base_url ||= Files.base_url
|
|
322
454
|
|
|
@@ -371,12 +503,6 @@ module Files
|
|
|
371
503
|
headers
|
|
372
504
|
end
|
|
373
505
|
|
|
374
|
-
private def has_error_type?(e)
|
|
375
|
-
Response.from_faraday_hash(e.response).data&.dig(:type) ? true : false
|
|
376
|
-
rescue JSON::ParserError, Error
|
|
377
|
-
false
|
|
378
|
-
end
|
|
379
|
-
|
|
380
506
|
private def log_request(context, num_retries, no_body: false)
|
|
381
507
|
Util.log_info("Request", method: context.method, num_retries: num_retries, path: context.path)
|
|
382
508
|
Util.log_debug("Request details", body: context.body, query_params: context.query_params) unless no_body
|