files.com 1.1.813 → 1.1.814
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/_VERSION +1 -1
- data/doc/Files/APIConnectionError.html +1 -1
- data/doc/Files/APIError.html +1 -1
- data/doc/Files/AccountAlreadyExistsError.html +1 -1
- data/doc/Files/AccountLineItem.html +1 -1
- data/doc/Files/AccountOverdueError.html +1 -1
- data/doc/Files/Action.html +1 -1
- data/doc/Files/ActionLog.html +1 -1
- data/doc/Files/ActionNotificationExport.html +1 -1
- data/doc/Files/ActionNotificationExportResult.html +1 -1
- data/doc/Files/AdditionalAuthenticationRequiredError.html +1 -1
- data/doc/Files/AgentNode.html +1 -1
- data/doc/Files/AgentNodeConnection.html +1 -1
- data/doc/Files/AgentNodeInstance.html +1 -1
- data/doc/Files/AgentPushUpdate.html +1 -1
- data/doc/Files/AgentPushUpdateBlockedError.html +1 -1
- data/doc/Files/AgentUnavailableError.html +1 -1
- data/doc/Files/AgentUpgradeRequiredError.html +1 -1
- data/doc/Files/AiAssistantPersonality.html +1 -1
- data/doc/Files/AiTask.html +1 -1
- data/doc/Files/AiTaskCannotBeRunManuallyError.html +1 -1
- data/doc/Files/AlreadyCompletedError.html +1 -1
- data/doc/Files/Api.html +20 -13
- data/doc/Files/ApiClient/RequestLogContext.html +29 -29
- data/doc/Files/ApiClient/TransferSettings.html +721 -0
- data/doc/Files/ApiClient.html +756 -204
- data/doc/Files/ApiKey.html +1 -1
- data/doc/Files/ApiKeyIsDisabledError.html +1 -1
- data/doc/Files/ApiKeyIsPathRestrictedError.html +1 -1
- data/doc/Files/ApiKeyNotFoundError.html +1 -1
- data/doc/Files/ApiKeyOnlyForDesktopAppError.html +1 -1
- data/doc/Files/ApiKeyOnlyForFileOperationsError.html +1 -1
- data/doc/Files/ApiKeyOnlyForMobileAppError.html +1 -1
- data/doc/Files/ApiKeyOnlyForOfficeIntegrationError.html +1 -1
- data/doc/Files/ApiKeySessionsNotSupportedError.html +1 -1
- data/doc/Files/ApiRequestLog.html +1 -1
- data/doc/Files/App.html +1 -1
- data/doc/Files/As2IncomingMessage.html +1 -1
- data/doc/Files/As2OutgoingMessage.html +1 -1
- data/doc/Files/As2Partner.html +1 -1
- data/doc/Files/As2Station.html +1 -1
- data/doc/Files/AttachmentTooLargeError.html +1 -1
- data/doc/Files/AuthenticationError.html +1 -1
- data/doc/Files/AuthenticationRequiredError.html +1 -1
- data/doc/Files/Auto.html +1 -1
- data/doc/Files/Automation.html +1 -1
- data/doc/Files/AutomationAuthoringSchema.html +1 -1
- data/doc/Files/AutomationCannotBeRunManuallyError.html +1 -1
- data/doc/Files/AutomationExecutionNode.html +1 -1
- data/doc/Files/AutomationLog.html +1 -1
- data/doc/Files/AutomationRun.html +1 -1
- data/doc/Files/AutomationsUnavailableError.html +1 -1
- data/doc/Files/BadRequestError.html +1 -1
- data/doc/Files/BandwidthSnapshot.html +1 -1
- data/doc/Files/Behavior.html +1 -1
- data/doc/Files/BehaviorNotAllowedOnRemoteServerError.html +1 -1
- data/doc/Files/BillingInformationHiddenError.html +1 -1
- data/doc/Files/BillingPermissionRequiredError.html +1 -1
- data/doc/Files/BufferedUploadDisabledForThisDestinationError.html +1 -1
- data/doc/Files/Bundle.html +1 -1
- data/doc/Files/BundleAction.html +1 -1
- data/doc/Files/BundleDownload.html +1 -1
- data/doc/Files/BundleMaximumUsesReachedError.html +1 -1
- data/doc/Files/BundleNotification.html +1 -1
- data/doc/Files/BundleOnlyAllowsPreviewsError.html +1 -1
- data/doc/Files/BundleOperationRequiresSubfolderError.html +1 -1
- data/doc/Files/BundlePath.html +1 -1
- data/doc/Files/BundlePathNotFoundError.html +1 -1
- data/doc/Files/BundlePermissionRequiredError.html +1 -1
- data/doc/Files/BundleRecipient.html +1 -1
- data/doc/Files/BundleRegistration.html +1 -1
- data/doc/Files/BundleRegistrationCodeFailedError.html +1 -1
- data/doc/Files/BundleRegistrationNotFoundError.html +1 -1
- data/doc/Files/CannotAdministerHigherLevelUserError.html +1 -1
- data/doc/Files/CannotDownloadDirectoryError.html +1 -1
- data/doc/Files/CannotLoginWhileUsingKeyError.html +1 -1
- data/doc/Files/CantActForOtherUserError.html +1 -1
- data/doc/Files/CantMoveWithMultipleLocationsError.html +1 -1
- data/doc/Files/ChatMessage.html +1 -1
- data/doc/Files/ChatSession.html +1 -1
- data/doc/Files/ChildSiteManagementPolicy.html +1 -1
- data/doc/Files/Clickwrap.html +1 -1
- data/doc/Files/CodeNotFoundError.html +1 -1
- data/doc/Files/ConfigurationLockedPathError.html +1 -1
- data/doc/Files/ContactAdminForPasswordChangeHelpError.html +1 -1
- data/doc/Files/CouldNotCreateParentError.html +1 -1
- data/doc/Files/CustomDomain.html +1 -1
- data/doc/Files/DatetimeParseError.html +1 -1
- data/doc/Files/DesktopConfigurationProfile.html +1 -1
- data/doc/Files/DestinationExistsError.html +1 -1
- data/doc/Files/DestinationFolderLimitedError.html +1 -1
- data/doc/Files/DestinationParentConflictError.html +1 -1
- data/doc/Files/DestinationParentDoesNotExistError.html +1 -1
- data/doc/Files/DestinationSameError.html +1 -1
- data/doc/Files/DestinationSiteMismatchError.html +1 -1
- data/doc/Files/DirectConnectionInfo.html +1 -1
- data/doc/Files/DnsRecord.html +1 -1
- data/doc/Files/DoesNotSupportSortingError.html +1 -1
- data/doc/Files/DuplicateShareRecipientError.html +1 -1
- data/doc/Files/EmailIncomingMessage.html +1 -1
- data/doc/Files/EmailLog.html +1 -1
- data/doc/Files/Error.html +2 -2
- data/doc/Files/Errors.html +1 -1
- data/doc/Files/EventChannel.html +1 -1
- data/doc/Files/EventDeliveryAttempt.html +1 -1
- data/doc/Files/EventRecord.html +1 -1
- data/doc/Files/EventSubscription.html +1 -1
- data/doc/Files/EventTarget.html +1 -1
- data/doc/Files/ExavaultApiRequestLog.html +1 -1
- data/doc/Files/ExceededRuntimeLimitError.html +1 -1
- data/doc/Files/Expectation.html +1 -1
- data/doc/Files/ExpectationAlreadyHasOpenWindowError.html +1 -1
- data/doc/Files/ExpectationEvaluation.html +1 -1
- data/doc/Files/ExpectationIncident.html +1 -1
- data/doc/Files/ExpectationNotManualTriggerError.html +1 -1
- data/doc/Files/ExpiredPrivateKeyError.html +1 -1
- data/doc/Files/ExpiredPublicKeyError.html +1 -1
- data/doc/Files/ExportFailureError.html +1 -1
- data/doc/Files/ExportNotReadyError.html +1 -1
- data/doc/Files/ExternalEvent.html +1 -1
- data/doc/Files/FailedToChangePasswordError.html +1 -1
- data/doc/Files/File.html +2019 -1515
- data/doc/Files/FileAction.html +1 -1
- data/doc/Files/FileActionError.html +1 -1
- data/doc/Files/FileComment.html +1 -1
- data/doc/Files/FileCommentReaction.html +1 -1
- data/doc/Files/FileLockedError.html +1 -1
- data/doc/Files/FileMigration.html +1 -1
- data/doc/Files/FileMigrationLog.html +1 -1
- data/doc/Files/FileNotFoundError.html +1 -1
- data/doc/Files/FileNotUploadedError.html +1 -1
- data/doc/Files/FilePendingProcessingError.html +1 -1
- data/doc/Files/FileProcessingErrorError.html +1 -1
- data/doc/Files/FileTooBigToDecryptError.html +1 -1
- data/doc/Files/FileTooBigToEncryptError.html +1 -1
- data/doc/Files/FileUploadNotFoundError.html +1 -1
- data/doc/Files/FileUploadPart.html +1 -1
- data/doc/Files/FileUploadedToWrongRegionError.html +1 -1
- data/doc/Files/FileUtils.html +1 -1
- data/doc/Files/FilenameTooLongError.html +1 -1
- data/doc/Files/FilesAgentFailedAuthorizationError.html +1 -1
- data/doc/Files/FilesNativeStorageOverloadedError.html +1 -1
- data/doc/Files/Folder.html +1 -1
- data/doc/Files/FolderAdminOrBillingPermissionRequiredError.html +1 -1
- data/doc/Files/FolderAdminPermissionRequiredError.html +1 -1
- data/doc/Files/FolderLockedError.html +1 -1
- data/doc/Files/FolderMustNotBeAFileError.html +1 -1
- data/doc/Files/FolderNotEmptyError.html +1 -1
- data/doc/Files/FoldersNotAllowedError.html +1 -1
- data/doc/Files/FormField.html +1 -1
- data/doc/Files/FormFieldSet.html +1 -1
- data/doc/Files/FtpActionLog.html +1 -1
- data/doc/Files/FullPermissionRequiredError.html +1 -1
- data/doc/Files/GpgKey.html +1 -1
- data/doc/Files/Group.html +1 -1
- data/doc/Files/GroupNotFoundError.html +1 -1
- data/doc/Files/GroupUser.html +1 -1
- data/doc/Files/History.html +1 -1
- data/doc/Files/HistoryExport.html +1 -1
- data/doc/Files/HistoryExportResult.html +1 -1
- data/doc/Files/HistoryPermissionRequiredError.html +1 -1
- data/doc/Files/HistoryUnavailableError.html +1 -1
- data/doc/Files/HolidayCalendar.html +1 -1
- data/doc/Files/HolidayRegion.html +1 -1
- data/doc/Files/Image.html +1 -1
- data/doc/Files/InAppAiAssistantUnavailableError.html +1 -1
- data/doc/Files/InboundS3Log.html +1 -1
- data/doc/Files/InboxNotFoundError.html +1 -1
- data/doc/Files/InboxRecipient.html +1 -1
- data/doc/Files/InboxRegistration.html +1 -1
- data/doc/Files/InboxRegistrationCodeFailedError.html +1 -1
- data/doc/Files/InboxUpload.html +1 -1
- data/doc/Files/InsufficientPermissionForParamsError.html +1 -1
- data/doc/Files/InsufficientPermissionForSiteError.html +1 -1
- data/doc/Files/IntegrationCentricProfile.html +1 -1
- data/doc/Files/InternalGeneralErrorError.html +1 -1
- data/doc/Files/InvalidBodyError.html +1 -1
- data/doc/Files/InvalidBundleCodeError.html +1 -1
- data/doc/Files/InvalidCredentialsError.html +1 -1
- data/doc/Files/InvalidCursorError.html +1 -1
- data/doc/Files/InvalidCursorTypeForSortError.html +1 -1
- data/doc/Files/InvalidEtagsError.html +1 -1
- data/doc/Files/InvalidFileTypeError.html +1 -1
- data/doc/Files/InvalidFilenameError.html +1 -1
- data/doc/Files/InvalidFilterAliasCombinationError.html +1 -1
- data/doc/Files/InvalidFilterFieldError.html +1 -1
- data/doc/Files/InvalidFilterParamError.html +1 -1
- data/doc/Files/InvalidFilterParamFormatError.html +1 -1
- data/doc/Files/InvalidFilterParamValueError.html +1 -1
- data/doc/Files/InvalidInputEncodingError.html +1 -1
- data/doc/Files/InvalidInterfaceError.html +1 -1
- data/doc/Files/InvalidOauthError.html +1 -1
- data/doc/Files/InvalidOauthProviderError.html +1 -1
- data/doc/Files/InvalidOrExpiredCodeError.html +1 -1
- data/doc/Files/InvalidParameterError.html +5 -1
- data/doc/Files/InvalidPathError.html +1 -1
- data/doc/Files/InvalidPriorityColorError.html +1 -1
- data/doc/Files/InvalidRangeError.html +1 -1
- data/doc/Files/InvalidReturnToUrlError.html +1 -1
- data/doc/Files/InvalidSearchQueryError.html +1 -1
- data/doc/Files/InvalidSessionError.html +1 -1
- data/doc/Files/InvalidSiteError.html +1 -1
- data/doc/Files/InvalidSortFieldError.html +1 -1
- data/doc/Files/InvalidSortFilterCombinationError.html +1 -1
- data/doc/Files/InvalidUploadOffsetError.html +1 -1
- data/doc/Files/InvalidUploadPartGapError.html +1 -1
- data/doc/Files/InvalidUploadPartSizeError.html +1 -1
- data/doc/Files/InvalidUsernameOrPasswordError.html +1 -1
- data/doc/Files/InvalidWorkspaceIdHeaderError.html +1 -1
- data/doc/Files/InvalidZipFileError.html +1 -1
- data/doc/Files/Invoice.html +1 -1
- data/doc/Files/InvoiceLineItem.html +1 -1
- data/doc/Files/IpAddress.html +1 -1
- data/doc/Files/KeyLifecycleRule.html +1 -1
- data/doc/Files/List.html +1 -1
- data/doc/Files/Lock.html +1 -1
- data/doc/Files/LockOperationBusyError.html +1 -1
- data/doc/Files/LockedOutError.html +1 -1
- data/doc/Files/LockoutRegionMismatchError.html +1 -1
- data/doc/Files/MetadataCategory.html +1 -1
- data/doc/Files/MetadataNotSupportedOnRemotesError.html +1 -1
- data/doc/Files/MethodNotAllowedError.html +1 -1
- data/doc/Files/MigrationInProgressError.html +1 -1
- data/doc/Files/MissingParameterError.html +1 -1
- data/doc/Files/ModelSaveErrorError.html +1 -1
- data/doc/Files/MoverAccessDeniedError.html +1 -1
- data/doc/Files/MoverPackageRequiredError.html +1 -1
- data/doc/Files/MultipleProcessingErrorsError.html +1 -1
- data/doc/Files/MultipleSortParamsNotAllowedError.html +1 -1
- data/doc/Files/MustAuthenticateWithApiKeyError.html +1 -1
- data/doc/Files/NeedAdminPermissionForInboxError.html +1 -1
- data/doc/Files/NestedNotFoundError.html +1 -1
- data/doc/Files/NoAccountForSiteError.html +1 -1
- data/doc/Files/NoValidInputParamsError.html +1 -1
- data/doc/Files/NonAdminsMustQueryByFolderOrPathError.html +1 -1
- data/doc/Files/NotAllowedToCreateBundleError.html +1 -1
- data/doc/Files/NotAuthenticatedError.html +1 -1
- data/doc/Files/NotAuthorizedError.html +1 -1
- data/doc/Files/NotEnqueuableSyncError.html +1 -1
- data/doc/Files/NotFoundError.html +1 -1
- data/doc/Files/NotImplementedError.html +1 -1
- data/doc/Files/Notification.html +1 -1
- data/doc/Files/OffsetUploadNotAllowedWithMalwareScanningError.html +1 -1
- data/doc/Files/OneTimePasswordIncorrectError.html +1 -1
- data/doc/Files/OutboundConnectionLog.html +1 -1
- data/doc/Files/PartNumberTooLargeError.html +1 -1
- data/doc/Files/Partner.html +1 -1
- data/doc/Files/PartnerChannel.html +1 -1
- data/doc/Files/PartnerChannelTemplate.html +1 -1
- data/doc/Files/PartnerConnection.html +1 -1
- data/doc/Files/PartnerSite.html +1 -1
- data/doc/Files/PartnerSiteRequest.html +1 -1
- data/doc/Files/PasswordChangeNotRequiredError.html +1 -1
- data/doc/Files/PasswordChangeRequiredError.html +1 -1
- data/doc/Files/PathCannotHaveTrailingWhitespaceError.html +1 -1
- data/doc/Files/PathTooLongError.html +1 -1
- data/doc/Files/PathUtil.html +1 -1
- data/doc/Files/Payment.html +1 -1
- data/doc/Files/PaymentLineItem.html +1 -1
- data/doc/Files/PaymentMethodErrorError.html +1 -1
- data/doc/Files/PendingWorkEvent.html +1 -1
- data/doc/Files/Permission.html +1 -1
- data/doc/Files/PlanNotFoundError.html +1 -1
- data/doc/Files/Preview.html +1 -1
- data/doc/Files/PreviewOnlyPermissionCannotDownloadError.html +1 -1
- data/doc/Files/ProcessingFailureError.html +1 -1
- data/doc/Files/PublicHostingRequestLog.html +1 -1
- data/doc/Files/PublicIpAddress.html +1 -1
- data/doc/Files/PublicKey.html +1 -1
- data/doc/Files/RateLimitedError.html +1 -1
- data/doc/Files/ReadOnlySessionError.html +1 -1
- data/doc/Files/ReadPermissionRequiredError.html +1 -1
- data/doc/Files/ReauthenticationFailedError.html +1 -1
- data/doc/Files/ReauthenticationFailedFinalError.html +1 -1
- data/doc/Files/ReauthenticationNeededActionError.html +1 -1
- data/doc/Files/ReauthenticationNeededFieldsError.html +1 -1
- data/doc/Files/ReauthenticationRateLimitedError.html +1 -1
- data/doc/Files/RecaptchaFailedError.html +1 -1
- data/doc/Files/RecipientAlreadySharedError.html +1 -1
- data/doc/Files/RemoteBandwidthSnapshot.html +1 -1
- data/doc/Files/RemoteDesktopDebugLoggingDisabledError.html +1 -1
- data/doc/Files/RemoteEntryReadOnlyError.html +1 -1
- data/doc/Files/RemoteMountBackend.html +1 -1
- data/doc/Files/RemoteServer.html +1 -1
- data/doc/Files/RemoteServerConfigurationFile.html +1 -1
- data/doc/Files/RemoteServerCredential.html +1 -1
- data/doc/Files/RemoteServerErrorError.html +1 -1
- data/doc/Files/Request.html +1 -1
- data/doc/Files/RequestBodyTooLargeError.html +1 -1
- data/doc/Files/RequestParamsContainInvalidCharacterError.html +1 -1
- data/doc/Files/RequestParamsInvalidError.html +1 -1
- data/doc/Files/RequestParamsRequiredError.html +1 -1
- data/doc/Files/ResourceBelongsToParentSiteError.html +1 -1
- data/doc/Files/ResourceLockedError.html +1 -1
- data/doc/Files/Response.html +1 -1
- data/doc/Files/Restore.html +1 -1
- data/doc/Files/RootFolderBehaviorSiteAdminRequiredError.html +1 -1
- data/doc/Files/RootFolderBehaviorSkipSiteAdminRequiredError.html +1 -1
- data/doc/Files/Schedule.html +1 -1
- data/doc/Files/ScheduledExport.html +1 -1
- data/doc/Files/ScimLog.html +1 -1
- data/doc/Files/SearchAllOnChildPathError.html +1 -1
- data/doc/Files/SearchUnavailableError.html +1 -1
- data/doc/Files/Secret.html +1 -1
- data/doc/Files/SelfManagedRequiredError.html +1 -1
- data/doc/Files/ServiceUnavailableError.html +1 -1
- data/doc/Files/Session.html +1 -1
- data/doc/Files/SettingsChange.html +1 -1
- data/doc/Files/SftpActionLog.html +1 -1
- data/doc/Files/SftpHostKey.html +1 -1
- data/doc/Files/ShareGroup.html +1 -1
- data/doc/Files/ShareGroupMember.html +1 -1
- data/doc/Files/SiemHttpDestination.html +1 -1
- data/doc/Files/SiemHttpDestinationEvent.html +1 -1
- data/doc/Files/Site.html +1 -1
- data/doc/Files/SiteAdminOrPartnerAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminOrFolderAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminOrPartnerAdminOrFolderAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminOrPartnerAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminRequiredError.html +1 -1
- data/doc/Files/SiteConfigurationError.html +1 -1
- data/doc/Files/SiteDisabledError.html +1 -1
- data/doc/Files/SiteFilesAreImmutableError.html +1 -1
- data/doc/Files/SiteNotFoundError.html +1 -1
- data/doc/Files/SiteSubdomainRedirect.html +1 -1
- data/doc/Files/SiteWasRemovedError.html +1 -1
- data/doc/Files/SizableIO.html +1 -1
- data/doc/Files/Snapshot.html +1 -1
- data/doc/Files/SsoEvent.html +1 -1
- data/doc/Files/SsoStrategy.html +1 -1
- data/doc/Files/Status.html +1 -1
- data/doc/Files/Style.html +1 -1
- data/doc/Files/SubfolderLockedError.html +1 -1
- data/doc/Files/Sync.html +1 -1
- data/doc/Files/SyncInProgressError.html +1 -1
- data/doc/Files/SyncLog.html +1 -1
- data/doc/Files/SyncRun.html +1 -1
- data/doc/Files/SyncRunLiveTransfer.html +1 -1
- data/doc/Files/SystemProfiler.html +1 -1
- data/doc/Files/TooManyConcurrentLoginsError.html +1 -1
- data/doc/Files/TooManyConcurrentRequestsError.html +1 -1
- data/doc/Files/TooManyLoginAttemptsError.html +1 -1
- data/doc/Files/TooManyRequestsError.html +1 -1
- data/doc/Files/TooManySharesError.html +1 -1
- data/doc/Files/Transfer/BufferLimitError.html +175 -0
- data/doc/Files/Transfer/ByteBudget/Reservation.html +301 -0
- data/doc/Files/Transfer/ByteBudget.html +748 -0
- data/doc/Files/Transfer/CancelledError.html +235 -0
- data/doc/Files/Transfer/Checkpoint.html +1371 -0
- data/doc/Files/Transfer/CheckpointError.html +173 -0
- data/doc/Files/Transfer/ClientTransport/ClientResponse.html +661 -0
- data/doc/Files/Transfer/ClientTransport/Reader.html +530 -0
- data/doc/Files/Transfer/ClientTransport.html +675 -0
- data/doc/Files/Transfer/Clock.html +189 -0
- data/doc/Files/Transfer/Control.html +667 -0
- data/doc/Files/Transfer/Controller/Attempt.html +465 -0
- data/doc/Files/Transfer/Controller.html +1166 -0
- data/doc/Files/Transfer/Coordinator/Timing.html +568 -0
- data/doc/Files/Transfer/Coordinator.html +534 -0
- data/doc/Files/Transfer/DownloadCoordinator/FileSink.html +976 -0
- data/doc/Files/Transfer/DownloadCoordinator/IoSink.html +1168 -0
- data/doc/Files/Transfer/DownloadCoordinator/Piece.html +875 -0
- data/doc/Files/Transfer/DownloadCoordinator.html +728 -0
- data/doc/Files/Transfer/FileSlots.html +480 -0
- data/doc/Files/Transfer/FrontendResponseError.html +235 -0
- data/doc/Files/Transfer/Holds.html +505 -0
- data/doc/Files/Transfer/HttpTransport.html +579 -0
- data/doc/Files/Transfer/Job.html +2338 -0
- data/doc/Files/Transfer/Options.html +924 -0
- data/doc/Files/Transfer/PausedError.html +319 -0
- data/doc/Files/Transfer/Policy.html +1642 -0
- data/doc/Files/Transfer/RequestPermits.html +160 -0
- data/doc/Files/Transfer/SourceChangedError.html +235 -0
- data/doc/Files/Transfer/StorageResponse.html +546 -0
- data/doc/Files/Transfer/StorageRetry.html +1098 -0
- data/doc/Files/Transfer/UploadCoordinator/Grant.html +1048 -0
- data/doc/Files/Transfer/UploadCoordinator/Part.html +1048 -0
- data/doc/Files/Transfer/UploadCoordinator/Region.html +295 -0
- data/doc/Files/Transfer/UploadCoordinator/Segments.html +299 -0
- data/doc/Files/Transfer/UploadCoordinator/Source.html +725 -0
- data/doc/Files/Transfer/UploadCoordinator/StreamSource.html +664 -0
- data/doc/Files/Transfer/UploadCoordinator.html +546 -0
- data/doc/Files/Transfer/UploadPlan.html +522 -0
- data/doc/Files/Transfer/VersionChangedError.html +235 -0
- data/doc/Files/Transfer.html +134 -0
- data/doc/Files/TrialExpiredError.html +1 -1
- data/doc/Files/TrialLockedError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationCodeAlreadySentError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationCountryBlacklistedError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationErrorError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationGeneralErrorError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationMethodUnsupportedErrorError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationRequiredError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationSetupExpiredError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationUnsubscribedRecipientError.html +1 -1
- data/doc/Files/URI.html +1 -1
- data/doc/Files/UnrecognizedSortIndexError.html +1 -1
- data/doc/Files/UnsupportedCurrencyError.html +1 -1
- data/doc/Files/UnsupportedHttpResponseFormatError.html +1 -1
- data/doc/Files/UnsupportedMediaTypeError.html +1 -1
- data/doc/Files/UpdatesNotAllowedForRemotesError.html +1 -1
- data/doc/Files/UploadsUnavailableError.html +1 -1
- data/doc/Files/UsageByTopLevelDir.html +1 -1
- data/doc/Files/UsageDailySnapshot.html +1 -1
- data/doc/Files/UsageSnapshot.html +1 -1
- data/doc/Files/User.html +1 -1
- data/doc/Files/UserAdditionalEmailRecipient.html +1 -1
- data/doc/Files/UserCipherUse.html +1 -1
- data/doc/Files/UserIdInvalidError.html +1 -1
- data/doc/Files/UserIdOnUserEndpointError.html +1 -1
- data/doc/Files/UserIdWithoutSiteAdminError.html +1 -1
- data/doc/Files/UserLifecycleRule.html +1 -1
- data/doc/Files/UserNotFoundError.html +1 -1
- data/doc/Files/UserRequest.html +1 -1
- data/doc/Files/UserRequestsEnabledRequiredError.html +1 -1
- data/doc/Files/UserRequiredError.html +1 -1
- data/doc/Files/UserSecurityEvent.html +1 -1
- data/doc/Files/UserSftpClientUse.html +1 -1
- data/doc/Files/Util/LogSettings.html +1 -1
- data/doc/Files/Util.html +1 -1
- data/doc/Files/WebDavActionLog.html +1 -1
- data/doc/Files/WebhookTest.html +1 -1
- data/doc/Files/Workspace.html +1 -1
- data/doc/Files/WriteAndBundlePermissionRequiredError.html +1 -1
- data/doc/Files/WritePermissionRequiredError.html +1 -1
- data/doc/Files/ZipListEntry.html +1 -1
- data/doc/Files.html +116 -116
- data/doc/_index.html +314 -4
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +1 -1
- data/doc/index.html +1 -1
- data/doc/method_list.html +4703 -2415
- data/doc/top-level-namespace.html +1 -1
- data/docs/file.md +346 -0
- data/lib/files.com/api.rb +6 -1
- data/lib/files.com/api_client.rb +220 -32
- data/lib/files.com/models/file.rb +124 -17
- data/lib/files.com/transfer/budgets.rb +123 -0
- data/lib/files.com/transfer/checkpoint.rb +113 -0
- data/lib/files.com/transfer/clock.rb +14 -0
- data/lib/files.com/transfer/controller.rb +549 -0
- data/lib/files.com/transfer/download_coordinator.rb +906 -0
- data/lib/files.com/transfer/errors.rb +100 -0
- data/lib/files.com/transfer/http_transport.rb +337 -0
- data/lib/files.com/transfer/job.rb +778 -0
- data/lib/files.com/transfer/options.rb +126 -0
- data/lib/files.com/transfer/policy.rb +91 -0
- data/lib/files.com/transfer/storage_retry.rb +169 -0
- data/lib/files.com/transfer/upload_coordinator.rb +657 -0
- data/lib/files.com/transfer/upload_plan.rb +157 -0
- data/lib/files.com.rb +13 -0
- data/spec/lib/api_client_spec.rb +125 -3
- data/spec/models/file_transfer_spec.rb +53 -4
- data/spec/support/transfer_server.rb +113 -14
- data/spec/transfer/adaptive_download_spec.rb +1102 -0
- data/spec/transfer/adaptive_upload_spec.rb +1602 -0
- data/spec/transfer/concurrency_controller_spec.rb +382 -0
- data/spec/workspace_id_spec.rb +19 -14
- metadata +59 -1
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Files
|
|
4
|
+
module Transfer
|
|
5
|
+
# What adaptive transfers share: their Options, one Controller for each kind of destination with the request limit
|
|
6
|
+
# it sets, the request permits, the buffer budget, the file slots, how long a throttle holds each destination back
|
|
7
|
+
# (Holds), whichever transfer, transport or controller met it, and the worker threads that send content requests.
|
|
8
|
+
#
|
|
9
|
+
# A Hash or true given as transfer: makes a Job for that one call, closed before it returns. Pass a Job instead to
|
|
10
|
+
# share it between calls, from any threads: transfers to the same storage then share one controller and what it
|
|
11
|
+
# learned, every transfer counts against one buffer_bytes and one request limit (a fixed concurrency, or
|
|
12
|
+
# max_concurrency, or 64), and at most max_files run at once. Close it when they are done, or use Job.open with a
|
|
13
|
+
# block.
|
|
14
|
+
class Job
|
|
15
|
+
CLOSE_WAIT_SECONDS = 5
|
|
16
|
+
MAX_NOTES = 64
|
|
17
|
+
# The longest a waiting transfer sleeps before it looks at its deadlines again, so a long Retry-After is waited
|
|
18
|
+
# out in steps, and a cancellation or pause still ends the wait at once.
|
|
19
|
+
MAX_WAIT_SECONDS = 60.0
|
|
20
|
+
|
|
21
|
+
attr_reader :options, :budget, :transport
|
|
22
|
+
|
|
23
|
+
def self.open(**options)
|
|
24
|
+
job = new(**options)
|
|
25
|
+
return job unless block_given?
|
|
26
|
+
|
|
27
|
+
finished = false
|
|
28
|
+
begin
|
|
29
|
+
result = yield job
|
|
30
|
+
finished = true
|
|
31
|
+
result
|
|
32
|
+
ensure
|
|
33
|
+
job.close(strict: finished)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Runs the block with the Job transfer names: transfer itself, or a new Job closed afterwards.
|
|
38
|
+
def self.using(transfer)
|
|
39
|
+
return yield(transfer) if transfer.is_a?(Job)
|
|
40
|
+
|
|
41
|
+
job = new(Options.from(transfer))
|
|
42
|
+
finished = false
|
|
43
|
+
begin
|
|
44
|
+
result = yield job
|
|
45
|
+
finished = true
|
|
46
|
+
result
|
|
47
|
+
ensure
|
|
48
|
+
job.close(strict: finished)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(options = nil, **keywords)
|
|
53
|
+
@options = Options.from(options || keywords)
|
|
54
|
+
@lock = Mutex.new
|
|
55
|
+
@changed = ConditionVariable.new
|
|
56
|
+
@work_ready = ConditionVariable.new
|
|
57
|
+
@controllers = {}
|
|
58
|
+
@budget = ByteBudget.new(@options.buffer_bytes)
|
|
59
|
+
@files = FileSlots.new(@options.max_files)
|
|
60
|
+
@requests = RequestPermits.new(@options.automatic? ? (@options.max_concurrency || Policy::DEFAULT_MAX) : @options.concurrency)
|
|
61
|
+
@holds = Holds.new
|
|
62
|
+
@running = 0
|
|
63
|
+
@tasks = []
|
|
64
|
+
@workers = []
|
|
65
|
+
@idle = 0
|
|
66
|
+
@closed = false
|
|
67
|
+
@transport = HttpTransport.new
|
|
68
|
+
@client_transports = {}.compare_by_identity
|
|
69
|
+
@notes = []
|
|
70
|
+
@started_at = Clock.now
|
|
71
|
+
@accepted_bytes = 0
|
|
72
|
+
@retries = 0
|
|
73
|
+
@options.control&.subscribe(self)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def control
|
|
77
|
+
@options.control
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def closed?
|
|
81
|
+
@closed
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Bytes held right now for parts waiting to be sent or written.
|
|
85
|
+
def held_bytes
|
|
86
|
+
@lock.synchronize { @budget.held }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A snapshot of how the job's transfers ran:
|
|
90
|
+
#
|
|
91
|
+
# accepted_bytes:: Bytes storage acknowledged (uploads) or that were received for writing (downloads), each
|
|
92
|
+
# counted once however often its request was sent.
|
|
93
|
+
# elapsed_seconds:: Seconds since the job began.
|
|
94
|
+
# goodput_bytes_per_second:: accepted_bytes over elapsed_seconds.
|
|
95
|
+
# retries:: Requests sent again after a failure, a throttle or an expired URL.
|
|
96
|
+
# requests:: Content requests in flight now, of at most request_limit.
|
|
97
|
+
# held_bytes:: Bytes held now for parts waiting to be sent or written.
|
|
98
|
+
# controllers:: Each controller's key, policy, phase, limit, attempts admitted against it (admitted) and on the
|
|
99
|
+
# network (inflight), epoch, judged_windows and ineligible_windows (windows closed without judging,
|
|
100
|
+
# because too few requests were on the network) and decisions.
|
|
101
|
+
# throttled:: Each origin a throttle's Retry-After holds back now (Holds), with the seconds left.
|
|
102
|
+
# notes:: How transfers ran, such as a caller's client carrying them one request at a time.
|
|
103
|
+
#
|
|
104
|
+
# It holds no URLs or credentials: a controller's key names only the kind of destination and its host, and a
|
|
105
|
+
# throttled origin only its scheme, host and port.
|
|
106
|
+
def diagnostics
|
|
107
|
+
@lock.synchronize do
|
|
108
|
+
elapsed = Clock.now - @started_at
|
|
109
|
+
{
|
|
110
|
+
accepted_bytes: @accepted_bytes,
|
|
111
|
+
elapsed_seconds: elapsed,
|
|
112
|
+
goodput_bytes_per_second: elapsed.positive? ? @accepted_bytes / elapsed : 0.0,
|
|
113
|
+
retries: @retries,
|
|
114
|
+
requests: @requests.used,
|
|
115
|
+
request_limit: @requests.limit,
|
|
116
|
+
controllers: @controllers.map { |key, controller| controller.snapshot.merge(key: key, decisions: controller.decisions) },
|
|
117
|
+
held_bytes: @budget.held,
|
|
118
|
+
throttled: @holds.to_h,
|
|
119
|
+
notes: @notes.dup
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Stops the job: transfers still running on it stop with an error, and it waits for them and the requests they
|
|
125
|
+
# have in flight, at most five seconds in all. Returns true once nothing is left running. When something still is,
|
|
126
|
+
# it raises Files::Error; with strict: false, as when a transfer on its own Job has already failed, it returns
|
|
127
|
+
# false instead and what is left finishes in the background.
|
|
128
|
+
def close(strict: true)
|
|
129
|
+
deadline = Clock.now + CLOSE_WAIT_SECONDS
|
|
130
|
+
workers = @lock.synchronize do
|
|
131
|
+
unless @closed
|
|
132
|
+
@closed = true
|
|
133
|
+
@work_ready.broadcast
|
|
134
|
+
@changed.broadcast
|
|
135
|
+
end
|
|
136
|
+
loop do
|
|
137
|
+
remaining = deadline - Clock.now
|
|
138
|
+
break if @running.zero? || !remaining.positive?
|
|
139
|
+
|
|
140
|
+
@changed.wait(@lock, remaining)
|
|
141
|
+
end
|
|
142
|
+
@workers.dup
|
|
143
|
+
end
|
|
144
|
+
@options.control&.unsubscribe(self)
|
|
145
|
+
workers.each { |worker| worker.join([ deadline - Clock.now, 0 ].max) }
|
|
146
|
+
live = workers.count(&:alive?)
|
|
147
|
+
running = @lock.synchronize { @running }
|
|
148
|
+
return true if live.zero? && running.zero?
|
|
149
|
+
return false unless strict
|
|
150
|
+
|
|
151
|
+
raise Files::Error, "The transfer job closed with #{running} transfers and #{live} requests still running after #{CLOSE_WAIT_SECONDS} seconds"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Called by the control after it changes.
|
|
155
|
+
def wake
|
|
156
|
+
@lock.synchronize { @changed.broadcast }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def synchronize(&block)
|
|
160
|
+
@lock.synchronize(&block)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# With the lock held: waits for a change, at most seconds when given, and never more than MAX_WAIT_SECONDS at a
|
|
164
|
+
# time when given; the caller looks at its deadline again after it.
|
|
165
|
+
def wait(seconds = nil)
|
|
166
|
+
@changed.wait(@lock, seconds && [ seconds, MAX_WAIT_SECONDS ].min)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# With the lock held: wakes everything waiting for a change.
|
|
170
|
+
def signal
|
|
171
|
+
@changed.broadcast
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Runs the block under the lock and wakes everything waiting.
|
|
175
|
+
def post
|
|
176
|
+
@lock.synchronize do
|
|
177
|
+
yield
|
|
178
|
+
@changed.broadcast
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# With the lock held: the controller for key, made from the policy the block returns the first time.
|
|
183
|
+
def controller(key)
|
|
184
|
+
@controllers[key] ||= Controller.new(yield)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The transport for a transfer's content requests through client, which the caller's thread resolved: the job's
|
|
188
|
+
# own only when client is the SDK's own kind with its default connection unchanged and the job's own proxy
|
|
189
|
+
# (HttpTransport#compatible?), otherwise client itself, one request at a time. A download refuses, with
|
|
190
|
+
# BufferLimitError, a client whose adapter is not one it relies on to stream a response (ClientTransport.streams?),
|
|
191
|
+
# before it sends a content request or writes anything.
|
|
192
|
+
def transport_for(client, download: false)
|
|
193
|
+
return @transport if @transport.compatible?(client)
|
|
194
|
+
raise BufferLimitError, ClientTransport::NOT_STREAMING if download && !ClientTransport.streams?(client)
|
|
195
|
+
|
|
196
|
+
@lock.synchronize do
|
|
197
|
+
@client_transports[client] ||= begin
|
|
198
|
+
note("A caller's client with its own connection or request handling sends this job's content requests, one request at a time")
|
|
199
|
+
ClientTransport.new(client)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# With the lock held.
|
|
205
|
+
def note(message)
|
|
206
|
+
@notes.shift if @notes.size >= MAX_NOTES
|
|
207
|
+
@notes << message.dup.freeze
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Waits for a file slot: returns :running once the caller has one, or control's :cancelled or :paused. Nothing of
|
|
211
|
+
# the transfer is opened before it.
|
|
212
|
+
def acquire_file(control)
|
|
213
|
+
acquire(@files, control) { @running += 1 }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def release_file
|
|
217
|
+
post do
|
|
218
|
+
@files.give
|
|
219
|
+
@running -= 1
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Waits, on a transfer's own thread, for a request permit for a request that thread sends itself to url, once no
|
|
224
|
+
# throttle holds url's destination back: returns :running once it holds one, or control's :cancelled or :paused as
|
|
225
|
+
# soon as it stops. Give it back with release_request.
|
|
226
|
+
def acquire_request(control, url)
|
|
227
|
+
acquire(@requests, control, Holds.destination(url))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Gives back a permit acquire_request took. A throttle the request met, when seconds is given, holds url's
|
|
231
|
+
# destination back first, under the same lock and in the same order as a worker's request (Coordinator#launch),
|
|
232
|
+
# so nothing waiting for the permit is sent before that hold.
|
|
233
|
+
def release_request(url = nil, seconds = nil)
|
|
234
|
+
post do
|
|
235
|
+
hold(url, seconds) if seconds
|
|
236
|
+
@requests.give
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# With the lock held: takes a request permit for a worker's request when one is free.
|
|
241
|
+
def take_request
|
|
242
|
+
@requests.take
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# With the lock held: gives a worker's request permit back and wakes everything waiting for one.
|
|
246
|
+
def give_request
|
|
247
|
+
@requests.give
|
|
248
|
+
@changed.broadcast
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# With the lock held: holds url's destination back for seconds, a throttle's Retry-After, from every new request
|
|
252
|
+
# and retry of the job's transfers, unless it already is for longer, and wakes everything waiting.
|
|
253
|
+
def hold(url, seconds)
|
|
254
|
+
@holds.hold(Holds.destination(url), seconds)
|
|
255
|
+
@changed.broadcast
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# With the lock held: the monotonic time until which a throttle holds url's destination back, or nil.
|
|
259
|
+
def held_until(url)
|
|
260
|
+
@holds.deadline(Holds.destination(url))
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# With the lock held: counts bytes accepted for the first time.
|
|
264
|
+
def count_accepted(bytes)
|
|
265
|
+
@accepted_bytes += bytes
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# With the lock held: counts a request about to be sent again.
|
|
269
|
+
def count_retry
|
|
270
|
+
@retries += 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Takes one of slots, once no throttle holds destination, when given, back; a cancellation or pause ends the wait.
|
|
274
|
+
private def acquire(slots, control, destination = nil)
|
|
275
|
+
@lock.synchronize do
|
|
276
|
+
loop do
|
|
277
|
+
raise Files::Error, "The transfer job is closed" if @closed
|
|
278
|
+
|
|
279
|
+
state = control&.state || :running
|
|
280
|
+
return state unless state == :running
|
|
281
|
+
|
|
282
|
+
held = destination && @holds.deadline(destination)
|
|
283
|
+
if held.nil? && slots.take
|
|
284
|
+
yield if block_given?
|
|
285
|
+
return :running
|
|
286
|
+
end
|
|
287
|
+
wait(held && [ held - Clock.now, 0.001 ].max)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# With the lock held: runs work on a worker thread, which passes it the connections that thread owns.
|
|
293
|
+
def submit(&work)
|
|
294
|
+
raise Files::Error, "The transfer job is closed" if @closed
|
|
295
|
+
|
|
296
|
+
@tasks << work
|
|
297
|
+
if @tasks.size > @idle
|
|
298
|
+
spawn_worker
|
|
299
|
+
else
|
|
300
|
+
@work_ready.signal
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
private def spawn_worker
|
|
305
|
+
@workers << Thread.new do
|
|
306
|
+
Thread.current.report_on_exception = false
|
|
307
|
+
connections = {}
|
|
308
|
+
begin
|
|
309
|
+
while (work = next_task)
|
|
310
|
+
work.call(connections)
|
|
311
|
+
end
|
|
312
|
+
ensure
|
|
313
|
+
@transport.close_all(connections)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private def next_task
|
|
319
|
+
@lock.synchronize do
|
|
320
|
+
@idle += 1
|
|
321
|
+
@work_ready.wait(@lock) while @tasks.empty? && !@closed
|
|
322
|
+
@idle -= 1
|
|
323
|
+
@tasks.shift
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# The loop one transfer runs on its caller's thread. It takes a file slot before it opens anything, plans work and
|
|
329
|
+
# calls the API there, with the context it fixed as it began (api_context), admits each part or range when it is
|
|
330
|
+
# ready and its controller's limit, its own cap, any throttle holding its destination back (Holds), the job's
|
|
331
|
+
# request permits and the buffer budget allow, hands the request to a worker and settles what comes back:
|
|
332
|
+
# acknowledgements, retries (each a new admission, its buffer kept), renewals and failures. Each attempt goes to
|
|
333
|
+
# the target fixed as it was admitted (request_target), whatever is renewed after. The transport tells the
|
|
334
|
+
# controller when each request's send starts and when it is answered, and the worker gives back its permit as the
|
|
335
|
+
# request ends, publishing a throttle's Retry-After for that target's destination to the job under the same lock,
|
|
336
|
+
# so nothing waiting for the permit is sent before the hold. Bytes held for an item are given back to the buffer
|
|
337
|
+
# budget only when they are dropped. Callbacks and writes to the caller's
|
|
338
|
+
# destination happen only on the caller's thread. The first failure, cancellation or pause, in the order they
|
|
339
|
+
# happened, whichever thread saw it, stops admissions; it then waits for the requests in flight, releases everything
|
|
340
|
+
# it holds and raises that first one, finalizing nothing. An error the caller's thread meets itself, from opening
|
|
341
|
+
# the source or destination and checking a resume's through an API request's, a callback's or a refused request's,
|
|
342
|
+
# is ordered the same way: recorded as SDK code there recognizes it (stop!), and in any case before it is raised
|
|
343
|
+
# (arbitrate), so a cancellation or pause made after it never replaces it, and one made before it is raised instead.
|
|
344
|
+
class Coordinator
|
|
345
|
+
# What a transport calls as a request's send starts, as its response headers arrive and once its response body
|
|
346
|
+
# has been read and checked. An upload's attempt is answered as its response headers arrive, after its own body
|
|
347
|
+
# went; a download's (through_body) once its whole body has arrived and been checked, before any of it is written.
|
|
348
|
+
Timing = Struct.new(:controller, :attempt, :through_body) do
|
|
349
|
+
def sending
|
|
350
|
+
controller.sending(attempt)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def headers
|
|
354
|
+
controller.answered(attempt) unless through_body
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def body
|
|
358
|
+
controller.answered(attempt)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# The API options every API request of one transfer starts with, resolved on the caller's thread as
|
|
363
|
+
# Api.send_request and ApiClient#execute_request would resolve them for a request sent now, from frozen copies of
|
|
364
|
+
# the caller's options: the client (options[:client] or the thread's active client), the credential (an options
|
|
365
|
+
# API key; otherwise an options session ID or session, saved first if it has no ID, or Files.session_id, when not
|
|
366
|
+
# empty; otherwise Files.api_key, or none at all), the origin (options[:base_url] or Files.base_url), the
|
|
367
|
+
# region_http_pairs (the options' own, checked, or the client's), the workspace (options[:workspace_id], or the
|
|
368
|
+
# client's, "" for none) and, as request_settings, copies of the library-wide default headers, language, app info,
|
|
369
|
+
# timeouts and region policy and handler (ApiClient.transfer_settings). A later change to the caller's options,
|
|
370
|
+
# even in place, to the thread's client or to any of those settings cannot change what the transfer's API requests
|
|
371
|
+
# send; adopt_context carries what an API request resolved, such as a regional origin, to those after it.
|
|
372
|
+
def self.api_context(options)
|
|
373
|
+
context = (options || {}).to_h.transform_values { |value| Checkpoint.frozen(value) }
|
|
374
|
+
if (session = context.delete(:session))
|
|
375
|
+
session.save unless session.id
|
|
376
|
+
context[:session_id] = Checkpoint.frozen(session.id)
|
|
377
|
+
end
|
|
378
|
+
client = context[:client] = Files::File.client(context)
|
|
379
|
+
if context[:api_key].nil?
|
|
380
|
+
session_id = context[:session_id].nil? ? Checkpoint.frozen(Files.session_id) : context[:session_id]
|
|
381
|
+
if session_id.nil? || session_id == ""
|
|
382
|
+
context[:api_key] = Checkpoint.frozen(Files.api_key)
|
|
383
|
+
else
|
|
384
|
+
context[:session_id] = session_id
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
context[:base_url] ||= Checkpoint.frozen(Files.base_url)
|
|
388
|
+
if context[:region_http_pairs]
|
|
389
|
+
context[:region_http_pairs] = Util.checked_region_http_pairs(context[:region_http_pairs])
|
|
390
|
+
elsif client.respond_to?(:region_http_pairs)
|
|
391
|
+
context[:region_http_pairs] = client.region_http_pairs
|
|
392
|
+
end
|
|
393
|
+
context[:workspace_id] = client.resolved_workspace_id if context[:workspace_id].nil? && client.respond_to?(:resolved_workspace_id)
|
|
394
|
+
context[:request_settings] = ApiClient.transfer_settings
|
|
395
|
+
context
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def initialize(job)
|
|
399
|
+
@job = job
|
|
400
|
+
@control = job.control
|
|
401
|
+
@max_retries = Files.max_network_retries
|
|
402
|
+
@transport = job.transport
|
|
403
|
+
@inbox = []
|
|
404
|
+
@queue = []
|
|
405
|
+
@renewals = []
|
|
406
|
+
@completed = []
|
|
407
|
+
@inflight = 0
|
|
408
|
+
@stop = nil
|
|
409
|
+
@controller = nil
|
|
410
|
+
@connections = {}
|
|
411
|
+
@origin = nil
|
|
412
|
+
@lease = nil
|
|
413
|
+
# The caller's thread, which runs the transfer.
|
|
414
|
+
@thread = Thread.current
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Opens the source or destination, checking a resume's, and transfers, once it holds a file slot. An error on the
|
|
418
|
+
# way is raised only when it came before any cancellation or pause, and then has its say on the destination
|
|
419
|
+
# (after_failure); otherwise the first of those is raised, with a resume's unspent checkpoint when it stopped
|
|
420
|
+
# before it started.
|
|
421
|
+
def run
|
|
422
|
+
state = @job.acquire_file(@control)
|
|
423
|
+
raise CancelledError if state == :cancelled
|
|
424
|
+
raise PausedError, @origin if state == :paused
|
|
425
|
+
|
|
426
|
+
begin
|
|
427
|
+
begin
|
|
428
|
+
open_resources
|
|
429
|
+
transfer
|
|
430
|
+
rescue StandardError => e
|
|
431
|
+
error = arbitrate(e)
|
|
432
|
+
after_failure(error)
|
|
433
|
+
raise if error.equal?(e)
|
|
434
|
+
|
|
435
|
+
raise error, cause: nil
|
|
436
|
+
end
|
|
437
|
+
ensure
|
|
438
|
+
begin
|
|
439
|
+
drain
|
|
440
|
+
ensure
|
|
441
|
+
begin
|
|
442
|
+
close_resources
|
|
443
|
+
ensure
|
|
444
|
+
@job.release_file
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
private
|
|
451
|
+
|
|
452
|
+
def pump
|
|
453
|
+
loop do
|
|
454
|
+
plan if @stop.nil?
|
|
455
|
+
completed, done = @job.synchronize { await }
|
|
456
|
+
deliver(completed)
|
|
457
|
+
return if done
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# With the lock held: returns what was accepted for deliver, and whether the transfer is done.
|
|
462
|
+
def await
|
|
463
|
+
loop do
|
|
464
|
+
observe_stops
|
|
465
|
+
completed = @completed.shift(@completed.size)
|
|
466
|
+
return [ completed, false ] unless completed.empty?
|
|
467
|
+
|
|
468
|
+
if @stop
|
|
469
|
+
release_waiting
|
|
470
|
+
return [ [], true ] if @inflight.zero?
|
|
471
|
+
else
|
|
472
|
+
return [ [], true ] if finished?
|
|
473
|
+
|
|
474
|
+
admit
|
|
475
|
+
return [ [], false ] if wants_planning?
|
|
476
|
+
end
|
|
477
|
+
@job.wait(wait_seconds)
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def observe_control
|
|
482
|
+
return unless @control
|
|
483
|
+
|
|
484
|
+
if @control.cancelled?
|
|
485
|
+
@stop ||= CancelledError.new
|
|
486
|
+
elsif @control.paused?
|
|
487
|
+
@stop ||= :paused
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# With the lock held: records every stop seen so far, oldest first: what the workers sent back, then a
|
|
492
|
+
# cancellation or pause and the job's closing. Every path that records a stop calls it first.
|
|
493
|
+
def observe_stops
|
|
494
|
+
settle_inbox
|
|
495
|
+
observe_control
|
|
496
|
+
@stop ||= Files::Error.new("The transfer job was closed") if @job.closed?
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# With the lock held: settles what the workers sent back, in the order they sent it. A request that ended after
|
|
500
|
+
# the control had already stopped the transfer is settled after that stop, and one that ended before it, before,
|
|
501
|
+
# so the first of a failure and a cancellation or pause wins.
|
|
502
|
+
def settle_inbox
|
|
503
|
+
@inbox.shift(@inbox.size).each do |item, outcome, after_stop|
|
|
504
|
+
observe_control if after_stop
|
|
505
|
+
settle(item, outcome)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Whether the control has stopped the transfer, as a worker's request ends.
|
|
510
|
+
def stopped?
|
|
511
|
+
!@control.nil? && @control.state != :running
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# With the lock held. Nothing is admitted while a throttle's Retry-After holds its destination back, whichever of
|
|
515
|
+
# the job's transfers met it. Due items are admitted in queue order, but one waiting for buffer bytes the budget
|
|
516
|
+
# cannot give yet lets a later item that already holds its own, such as a retry keeping its bytes, go ahead of it:
|
|
517
|
+
# that takes nothing more from the budget, and gives its bytes back once it ends. No later item needing new bytes
|
|
518
|
+
# passes the one waiting.
|
|
519
|
+
def admit
|
|
520
|
+
now = Clock.now
|
|
521
|
+
waiting = false
|
|
522
|
+
while (item = @queue.find { |queued| queued.ready_at <= now && (!waiting || queued.reservation) })
|
|
523
|
+
break if @controller.admitted >= @controller.limit || @inflight >= item_cap
|
|
524
|
+
|
|
525
|
+
if expiring?(item)
|
|
526
|
+
@queue.delete(item)
|
|
527
|
+
@renewals << item
|
|
528
|
+
next
|
|
529
|
+
end
|
|
530
|
+
target = request_target(item)
|
|
531
|
+
break unless ready?(item) && @job.held_until(target[0]).nil?
|
|
532
|
+
|
|
533
|
+
bytes = item.reservation ? 0 : reservation_bytes(item)
|
|
534
|
+
unless @job.budget.available?(bytes)
|
|
535
|
+
waiting = true
|
|
536
|
+
next
|
|
537
|
+
end
|
|
538
|
+
break unless @job.take_request
|
|
539
|
+
|
|
540
|
+
item.reservation ||= @job.budget.reserve(bytes)
|
|
541
|
+
@queue.delete(item)
|
|
542
|
+
launch(item, target)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# With the lock held and a request permit taken for item, which goes to target, the request_target it was admitted
|
|
547
|
+
# for, whatever is renewed before its worker sends it. The attempt counts against the controller's limit from now,
|
|
548
|
+
# but the transport measures it only from its send until it is answered; the worker gives the permit back as soon
|
|
549
|
+
# as the request ends, its response read, before its outcome is settled here, and publishes a throttle's
|
|
550
|
+
# Retry-After for target's destination to the job first, so it holds that destination back for every transfer at
|
|
551
|
+
# once.
|
|
552
|
+
def launch(item, target)
|
|
553
|
+
controller = @controller
|
|
554
|
+
attempt = controller.admit
|
|
555
|
+
timing = Timing.new(controller, attempt, measured_through_body?)
|
|
556
|
+
@job.submit do |connections|
|
|
557
|
+
outcome = [ :fail, Files::Error.new("The transfer worker stopped") ]
|
|
558
|
+
begin
|
|
559
|
+
outcome = attempt_item(item, target, connections, timing)
|
|
560
|
+
rescue Passthrough => e
|
|
561
|
+
outcome = [ :fail, e.error ]
|
|
562
|
+
rescue NetworkFailure => e
|
|
563
|
+
outcome = e.retryable? ? [ :retry, StorageRetry::RETRY_DELAY, :failure, e ] : [ :fail, e ]
|
|
564
|
+
rescue StandardError => e
|
|
565
|
+
outcome = [ :fail, e ]
|
|
566
|
+
ensure
|
|
567
|
+
begin
|
|
568
|
+
measure(controller, attempt, outcome)
|
|
569
|
+
ensure
|
|
570
|
+
@job.post do
|
|
571
|
+
@job.hold(target[0], outcome[1]) if outcome[0] == :retry && outcome[2] == :throttle
|
|
572
|
+
@job.give_request
|
|
573
|
+
@inbox << [ item, outcome, stopped? ]
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
@inflight += 1
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# On the worker's thread, as its request ends.
|
|
582
|
+
def measure(controller, attempt, outcome)
|
|
583
|
+
case outcome[0]
|
|
584
|
+
when :ok then controller.finish_attempt(attempt, :success, bytes: outcome[2])
|
|
585
|
+
when :retry then controller.finish_attempt(attempt, outcome[2], retry_after: outcome[2] == :throttle ? outcome[1] : nil)
|
|
586
|
+
else controller.finish_attempt(attempt, :ignored)
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# With the lock held. outcome is [:ok, value, bytes], [:retry, seconds, :failure or :throttle, error],
|
|
591
|
+
# [:renew, error] or [:fail, error]. A retry, and a renewal of a URL that expired after the request was sent,
|
|
592
|
+
# each count against the item's retries and need it to allow them. Wakes everything waiting, since settling can
|
|
593
|
+
# release bytes another transfer waits for.
|
|
594
|
+
def settle(item, outcome)
|
|
595
|
+
@inflight -= 1
|
|
596
|
+
case outcome[0]
|
|
597
|
+
when :ok
|
|
598
|
+
entry = accept(item, outcome[1])
|
|
599
|
+
@completed << entry if entry
|
|
600
|
+
when :retry
|
|
601
|
+
_, delay, _signal, error = outcome
|
|
602
|
+
if again?(item)
|
|
603
|
+
item.ready_at = Clock.now + delay
|
|
604
|
+
enqueue(item)
|
|
605
|
+
else
|
|
606
|
+
give_up(item, error)
|
|
607
|
+
end
|
|
608
|
+
when :renew
|
|
609
|
+
again?(item) ? @renewals << item : give_up(item, outcome[1])
|
|
610
|
+
else
|
|
611
|
+
give_up(item, outcome[1])
|
|
612
|
+
end
|
|
613
|
+
@job.signal
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# With the lock held: whether item may be sent again, counting it when it may.
|
|
617
|
+
def again?(item)
|
|
618
|
+
return false unless @stop.nil? && retry_allowed?(item) && item.attempts < @max_retries
|
|
619
|
+
|
|
620
|
+
item.attempts += 1
|
|
621
|
+
@job.count_retry
|
|
622
|
+
true
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def enqueue(item)
|
|
626
|
+
@queue.insert(@queue.bsearch_index { |queued| queued.number > item.number } || @queue.size, item)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# With the lock held.
|
|
630
|
+
def give_up(item, error)
|
|
631
|
+
discard(item)
|
|
632
|
+
@stop ||= public_error(error)
|
|
633
|
+
@job.signal
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# On the caller's thread: gives item up for error, after every stop recorded before it.
|
|
637
|
+
def fail_item(item, error)
|
|
638
|
+
@job.synchronize do
|
|
639
|
+
observe_stops
|
|
640
|
+
give_up(item, error)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Records a failure on the caller's thread, as SDK code there recognizes it, after every stop recorded before it:
|
|
645
|
+
# the workers' failures sent back earlier, and a cancellation or pause that came first. Returns error, to raise.
|
|
646
|
+
def stop!(error)
|
|
647
|
+
@job.synchronize do
|
|
648
|
+
observe_stops
|
|
649
|
+
@stop ||= public_error(error)
|
|
650
|
+
@job.signal
|
|
651
|
+
end
|
|
652
|
+
error
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# With the lock held: drops what item holds and gives its bytes back to the buffer budget, together, so the
|
|
656
|
+
# budget never counts less than is held.
|
|
657
|
+
def discard(item)
|
|
658
|
+
item.data = nil if item.respond_to?(:data=)
|
|
659
|
+
@job.budget.release(item.reservation)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def release_waiting
|
|
663
|
+
(@queue + @renewals).each { |item| discard(item) }
|
|
664
|
+
@queue.clear
|
|
665
|
+
@renewals.clear
|
|
666
|
+
@job.signal
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# With the lock held: how long to wait at most: until the next retry is due or a throttle holding back the
|
|
670
|
+
# destination of a waiting item has passed.
|
|
671
|
+
def wait_seconds
|
|
672
|
+
now = Clock.now
|
|
673
|
+
deadlines = @queue.map(&:ready_at)
|
|
674
|
+
deadlines.concat(@queue.map { |item| request_target(item)[0] }.uniq.filter_map { |url| @job.held_until(url) })
|
|
675
|
+
pending = deadlines.select { |at| at > now }.min
|
|
676
|
+
pending && [ pending - now, 0.001 ].max
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Waits for the requests in flight, however the transfer ended, and releases what it holds.
|
|
680
|
+
def drain
|
|
681
|
+
@job.synchronize do
|
|
682
|
+
@stop ||= :aborted unless @inflight.zero?
|
|
683
|
+
until @inflight.zero?
|
|
684
|
+
settle_inbox
|
|
685
|
+
@job.wait if @inflight.positive?
|
|
686
|
+
end
|
|
687
|
+
@inbox.clear
|
|
688
|
+
all_items.each { |item| discard(item) }
|
|
689
|
+
@job.signal
|
|
690
|
+
end
|
|
691
|
+
@job.transport.close_all(@connections)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def raise_outcome
|
|
695
|
+
case @stop
|
|
696
|
+
when nil then nil
|
|
697
|
+
when :paused then raise PausedError, pause_checkpoint
|
|
698
|
+
else raise @stop
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# A resume paused before it started gives back the checkpoint it was given, still usable; any other transfer its
|
|
703
|
+
# own, none while its source or destination is not yet open (checkpoint).
|
|
704
|
+
def pause_checkpoint
|
|
705
|
+
@lease && !@lease.started? ? @origin : checkpoint
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# On the caller's thread, for an error that ended the transfer there, from opening its source or destination to
|
|
709
|
+
# its last request: records it after every stop recorded before it, a cancellation or pause the control already
|
|
710
|
+
# holds included, unless stop! recorded it already as SDK code recognized it, and returns the first of them,
|
|
711
|
+
# which the transfer raises. Nothing recorded later replaces it, and an error raise_outcome raised is that first
|
|
712
|
+
# stop already.
|
|
713
|
+
def arbitrate(error)
|
|
714
|
+
first = @job.synchronize do
|
|
715
|
+
observe_stops
|
|
716
|
+
@stop ||= public_error(error)
|
|
717
|
+
end
|
|
718
|
+
return error if first.equal?(error) || (first == :paused && error.is_a?(PausedError))
|
|
719
|
+
|
|
720
|
+
first == :paused ? PausedError.new(pause_checkpoint) : first
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# Carries the context an API request resolved, such as the regional origin a lockout region mismatch moved it to,
|
|
724
|
+
# to the transfer's API requests after it. What it leaves unset stays as the transfer began.
|
|
725
|
+
def adopt_context(record)
|
|
726
|
+
options = record.respond_to?(:options) ? record.options : nil
|
|
727
|
+
@options = @options.merge(options.compact).merge(client: @options[:client]) if options.is_a?(Hash)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
# Opens the transfer's source or destination, once it holds a file slot.
|
|
731
|
+
def open_resources; end
|
|
732
|
+
|
|
733
|
+
# Closes what open_resources opened, however the transfer ended.
|
|
734
|
+
def close_resources; end
|
|
735
|
+
|
|
736
|
+
# What the transfer does with its destination once error, the first stop, ended it, before what it holds is
|
|
737
|
+
# released.
|
|
738
|
+
def after_failure(_error); end
|
|
739
|
+
|
|
740
|
+
# Where item's request goes now, frozen, its URL first, whose destination a throttle may hold back. An admitted
|
|
741
|
+
# attempt keeps the one it was admitted for (launch), is sent there by attempt_item and holds back its destination.
|
|
742
|
+
def request_target(_item)
|
|
743
|
+
[ nil ].freeze
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def item_cap
|
|
747
|
+
Float::INFINITY
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Whether an attempt is on the network until its response body has been read and checked, as a download's is, or
|
|
751
|
+
# only until its response headers arrive, as an upload's is.
|
|
752
|
+
def measured_through_body?
|
|
753
|
+
false
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Whether item can be sent: queued items are admitted in order, so one that is not stops admission there.
|
|
757
|
+
def ready?(_item)
|
|
758
|
+
true
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def wants_planning?
|
|
762
|
+
!@renewals.empty?
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def expiring?(_item)
|
|
766
|
+
false
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def retry_allowed?(_item)
|
|
770
|
+
true
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def public_error(error)
|
|
774
|
+
error
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
end
|