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,657 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Files
|
|
7
|
+
module Transfer
|
|
8
|
+
# Uploads one local file, or a caller's stream of a known size, through a Job, its parts sent concurrently.
|
|
9
|
+
#
|
|
10
|
+
# On the caller's thread it asks the API for parts five at a time (begin_upload, with with_direct_connection_info),
|
|
11
|
+
# checks each part's instructions, sends progress and finalizes. Every API request of the upload uses one context
|
|
12
|
+
# the caller's thread fixed as it began (Coordinator.api_context), updated by each grant's own options, which carry
|
|
13
|
+
# any regional origin the API moved the upload to, for the requests after it. The API chooses each part's size,
|
|
14
|
+
# unless the upload plans its own, as below. Parts go to cloud storage under the storage policy, or under the direct
|
|
15
|
+
# policy when the first part's direct_connection_info is a valid version 1 description (server_name, addresses,
|
|
16
|
+
# direct_uri and ca_pem); the parts still go to the upload_uri the API gave. A part whose instructions say
|
|
17
|
+
# parallel_parts: false makes the file send one part at a time.
|
|
18
|
+
#
|
|
19
|
+
# An upload plans its own part sizes only when every part of the API's first answer is for one ordinary multipart
|
|
20
|
+
# upload straight to Amazon S3 (UploadPlan.route) and UploadPlan can plan the file within its limits. All its parts
|
|
21
|
+
# but the last then have the size UploadPlan.part_size gives, which buffer_bytes must hold before anything is read;
|
|
22
|
+
# partsize and next_partsize are only recommendations, and each part keeps its offset and size through renewals,
|
|
23
|
+
# retries and a resume, whatever the API recommends later. Every later part and renewal must be for that same S3
|
|
24
|
+
# upload, as the first answer described it, or the upload stops before sending the part.
|
|
25
|
+
#
|
|
26
|
+
# Through the job's own transport, a part streams from the opened file at its offset and holds at most 64 KiB in
|
|
27
|
+
# memory; through a caller's client it is read whole first, so it must fit buffer_bytes. A caller's stream is read
|
|
28
|
+
# on the caller's thread, part by part in order, each part only once the buffer budget holds all reading and
|
|
29
|
+
# sending it may need, and what it then holds stays held, through renewals and retries, until storage acknowledges
|
|
30
|
+
# the part or the upload stops; no part is sent before its bytes are read. Each read's bytes are copied before the
|
|
31
|
+
# next read, so a stream whose reads reuse one String still sends exactly what it read, and its Strings are never
|
|
32
|
+
# changed. Over the job's own transport a part read in pieces keeps them apart and is sent through a 64 KiB slice
|
|
33
|
+
# it holds with them; a caller's client takes one String, so a part read in pieces is copied into one, within
|
|
34
|
+
# twice its bytes, after which it holds its bytes alone.
|
|
35
|
+
#
|
|
36
|
+
# A part counts as acknowledged only for a 2xx response with exactly one nonempty ETag, and is reported to progress,
|
|
37
|
+
# on the caller's thread, once. A signed URL near its expiry is renewed before it is used, at most twice for a part;
|
|
38
|
+
# one still expired after that stops the upload without being sent. A part whose URL expired after it was sent is
|
|
39
|
+
# renewed and sent again only when its instructions allow retries, as one of its retries. A renewal must keep the
|
|
40
|
+
# part's instructions: the reference, part number, size (the planned one, for an upload planning its own), method,
|
|
41
|
+
# headers and retry rules. Before each part and
|
|
42
|
+
# before finalizing, the local file must still be the one opened, with the same size and modification time.
|
|
43
|
+
class UploadCoordinator < Coordinator
|
|
44
|
+
STREAM_BUFFER_BYTES = 64 * 1024
|
|
45
|
+
RENEWALS = 2
|
|
46
|
+
ACK_FAILURE = "Transfer response did not acknowledge the part"
|
|
47
|
+
# The caller's upload parameters a checkpoint keeps, when they are true, false or an Integer.
|
|
48
|
+
CHECKPOINT_PARAMS = %i[mkdir_parents with_rename size].freeze
|
|
49
|
+
|
|
50
|
+
# data holds a caller's stream's bytes for the part, from when they are read until they are given up.
|
|
51
|
+
Part = Struct.new(:number, :offset, :bytesize, :granted, :grant, :fingerprint, :data, :reservation, :attempts, :renewals, :ready_at, keyword_init: true)
|
|
52
|
+
# route is the grant's UploadPlan.route, only for an upload planning its own part sizes.
|
|
53
|
+
Grant = Struct.new(:ref, :number, :partsize, :http_method, :url, :headers, :expires_at, :retry, :parallel, :direct, :route, keyword_init: true)
|
|
54
|
+
|
|
55
|
+
# The opened local file. Each part reads its bytes at its own offset under a lock, so a retry sends exactly what
|
|
56
|
+
# the first attempt did without keeping it in memory.
|
|
57
|
+
class Source
|
|
58
|
+
attr_reader :path, :size, :mtime, :identity
|
|
59
|
+
|
|
60
|
+
def initialize(path)
|
|
61
|
+
@path = path
|
|
62
|
+
@file = ::File.open(path, "rb")
|
|
63
|
+
@lock = Mutex.new
|
|
64
|
+
stat = @file.stat
|
|
65
|
+
@identity = Checkpoint.identity(stat)
|
|
66
|
+
@size = stat.size
|
|
67
|
+
@mtime = stat.mtime
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check!
|
|
71
|
+
raise SourceChangedError unless Checkpoint.identity(@lock.synchronize { @file.stat }) == @identity
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read(offset, length)
|
|
75
|
+
data = @lock.synchronize do
|
|
76
|
+
@file.seek(offset)
|
|
77
|
+
@file.read(length)
|
|
78
|
+
end
|
|
79
|
+
raise SourceChangedError, "The local file ended early during the upload" unless data && data.bytesize == length
|
|
80
|
+
|
|
81
|
+
data
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def close
|
|
85
|
+
@file.close unless @file.closed?
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A caller's IO of a known size, read once, in order, from where it stands: exactly size bytes, through as many
|
|
90
|
+
# reads as it takes, however short each is, with read(length) alone. The coordinator asks for each part's bytes in
|
|
91
|
+
# turn, on its own thread, once the buffer budget holds them, and keeps them with the part. The IO is never
|
|
92
|
+
# rewound, closed or read past size.
|
|
93
|
+
class StreamSource
|
|
94
|
+
attr_reader :size
|
|
95
|
+
|
|
96
|
+
def initialize(io, size)
|
|
97
|
+
@io = io
|
|
98
|
+
@size = size
|
|
99
|
+
@position = 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def path; end
|
|
103
|
+
|
|
104
|
+
def mtime; end
|
|
105
|
+
|
|
106
|
+
def identity; end
|
|
107
|
+
|
|
108
|
+
def check!; end
|
|
109
|
+
|
|
110
|
+
# The next part's bytes, which start at offset; the parts are read in order, never skipping one. Each read's
|
|
111
|
+
# bytes are copied (String#b, which shares them until either String changes) before the next read, so an IO whose
|
|
112
|
+
# reads reuse one String still gives each part exactly what it read, and the IO's Strings are never changed.
|
|
113
|
+
# With whole, the part comes as one String: the first read's copy when that read gave all of it, and otherwise
|
|
114
|
+
# one String of the part's size each read is appended to. Otherwise the copies are kept apart, frozen and in
|
|
115
|
+
# order, as one String when a single read gave the whole part.
|
|
116
|
+
def read_next(offset, length, whole: false)
|
|
117
|
+
raise ArgumentError, "the upload stream is at #{@position}, not #{offset}" unless offset == @position
|
|
118
|
+
|
|
119
|
+
pieces = []
|
|
120
|
+
joined = nil
|
|
121
|
+
read = 0
|
|
122
|
+
while read < length
|
|
123
|
+
chunk = @io.read(length - read)
|
|
124
|
+
raise SourceChangedError, "The upload stream ended after #{@position + read} of its #{@size} bytes" if chunk.nil? || chunk.empty?
|
|
125
|
+
raise SourceChangedError, "The upload stream gave more than the part's bytes" if chunk.bytesize > length - read
|
|
126
|
+
|
|
127
|
+
read += chunk.bytesize
|
|
128
|
+
if whole && (joined || read < length)
|
|
129
|
+
joined ||= String.new(capacity: length, encoding: Encoding::BINARY)
|
|
130
|
+
joined << chunk.b
|
|
131
|
+
else
|
|
132
|
+
pieces << chunk.b.freeze
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
@position += length
|
|
136
|
+
return (joined || pieces.first || String.new).freeze if whole
|
|
137
|
+
|
|
138
|
+
pieces.size == 1 ? pieces.first : pieces
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def close; end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# One part's bytes, read from the source as Net::HTTP streams them. Its source's errors pass through the
|
|
145
|
+
# transport as they are, never taken for network errors.
|
|
146
|
+
class Region
|
|
147
|
+
def initialize(source, offset, length)
|
|
148
|
+
@source = source
|
|
149
|
+
@offset = offset
|
|
150
|
+
@remaining = length
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def read(length = nil, outbuf = nil)
|
|
154
|
+
if @remaining.zero?
|
|
155
|
+
outbuf&.clear
|
|
156
|
+
return length.to_i.positive? ? nil : String.new
|
|
157
|
+
end
|
|
158
|
+
wanted = [ length || STREAM_BUFFER_BYTES, @remaining, STREAM_BUFFER_BYTES ].min
|
|
159
|
+
data = @source.read(@offset, wanted)
|
|
160
|
+
@offset += wanted
|
|
161
|
+
@remaining -= wanted
|
|
162
|
+
outbuf ? outbuf.replace(data) : data
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
raise Passthrough.new(e), cause: nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# A caller's stream's part read in pieces, sent from them in turn as Net::HTTP streams it, without joining them.
|
|
169
|
+
# Each read copies a slice of at most 64 KiB of a piece, which the part holds a reservation for (held_bytes).
|
|
170
|
+
class Segments
|
|
171
|
+
def initialize(segments)
|
|
172
|
+
@segments = segments
|
|
173
|
+
@index = 0
|
|
174
|
+
@offset = 0
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def read(length = nil, outbuf = nil)
|
|
178
|
+
segment = @segments[@index]
|
|
179
|
+
if segment.nil?
|
|
180
|
+
outbuf&.clear
|
|
181
|
+
return length.to_i.positive? ? nil : String.new
|
|
182
|
+
end
|
|
183
|
+
wanted = [ length || STREAM_BUFFER_BYTES, segment.bytesize - @offset, STREAM_BUFFER_BYTES ].min
|
|
184
|
+
data = segment.byteslice(@offset, wanted)
|
|
185
|
+
@offset += wanted
|
|
186
|
+
if @offset == segment.bytesize
|
|
187
|
+
@index += 1
|
|
188
|
+
@offset = 0
|
|
189
|
+
end
|
|
190
|
+
outbuf ? outbuf.replace(data) : data
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Opens path only once the job gives the upload a file slot.
|
|
195
|
+
def self.upload(job, path, destination, options, params:, progress:)
|
|
196
|
+
new(job, -> { Source.new(path) }, destination, options, params: params, progress: progress).run
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Uploads exactly size bytes of the caller's io, which stays open.
|
|
200
|
+
def self.upload_io(job, io, size, destination, options, params:, progress:)
|
|
201
|
+
new(job, -> { StreamSource.new(io, size) }, destination, options, params: params, progress: progress).run
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def self.resume(job, checkpoint, options, progress:)
|
|
205
|
+
checkpoint.resuming(:upload) do |lease|
|
|
206
|
+
new(job, -> { Source.new(checkpoint.local_path) }, checkpoint.path, options, params: checkpoint.state[:params].dup, progress: progress,
|
|
207
|
+
checkpoint: checkpoint, lease: lease
|
|
208
|
+
).run
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def initialize(job, open_source, destination, options, params:, progress:, checkpoint: nil, lease: nil)
|
|
213
|
+
super(job)
|
|
214
|
+
@open_source = open_source
|
|
215
|
+
@source = nil
|
|
216
|
+
@destination = destination
|
|
217
|
+
@options = Coordinator.api_context(options)
|
|
218
|
+
@params = params.to_h.transform_keys(&:to_sym)
|
|
219
|
+
@progress = progress
|
|
220
|
+
@client = @options[:client]
|
|
221
|
+
@transport = job.transport_for(@client)
|
|
222
|
+
@parts = []
|
|
223
|
+
@accepted = {}
|
|
224
|
+
@accepted_bytes = 0
|
|
225
|
+
@ref = nil
|
|
226
|
+
@planned_bytes = 0
|
|
227
|
+
@next_read = 0
|
|
228
|
+
@file_cap = Float::INFINITY
|
|
229
|
+
# For an upload planning its own part sizes, its route (UploadPlan.route) and the size of its parts but the last.
|
|
230
|
+
@route = nil
|
|
231
|
+
@part_size = nil
|
|
232
|
+
@origin = checkpoint
|
|
233
|
+
@lease = lease
|
|
234
|
+
restore(checkpoint) if checkpoint
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
# A resumed upload spends its checkpoint here, once the local file proved unchanged and before any request. A file
|
|
240
|
+
# that cannot be opened, or changed, stops the transfer as it is found (stop!), so a cancellation or pause after it
|
|
241
|
+
# cannot come first, and one before it leaves the checkpoint unspent.
|
|
242
|
+
def open_resources
|
|
243
|
+
@source = @open_source.call
|
|
244
|
+
raise CheckpointError, "Bad parameter: the local file changed after the upload paused" if @origin && @source.identity != @origin.source
|
|
245
|
+
|
|
246
|
+
@lease&.start!
|
|
247
|
+
@params[:size] ||= @source.size
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
raise stop!(e)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def close_resources
|
|
253
|
+
@source&.close
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Finalizes the upload once every part is acknowledged. Progress measures this call from here, and its rate counts
|
|
257
|
+
# only the parts the call acknowledges, not those a resumed upload's checkpoint already held (accept).
|
|
258
|
+
def transfer
|
|
259
|
+
@started_at = Clock.now
|
|
260
|
+
@start_accepted = @accepted_bytes
|
|
261
|
+
pump
|
|
262
|
+
raise_outcome
|
|
263
|
+
finalize
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# The local file must still be the one opened before the upload ends. A change found, or the finalize request's
|
|
267
|
+
# failure, stops the transfer as it is met (stop!), so a cancellation or pause after it cannot come first.
|
|
268
|
+
def finalize
|
|
269
|
+
@source.check!
|
|
270
|
+
etags = @parts.map { |part| { etag: @accepted.fetch(part.number), part: part.number } }
|
|
271
|
+
params = { action: "end", etags: etags, ref: @ref, size: @source.size }
|
|
272
|
+
params[:provided_mtime] = @source.mtime.to_s if @source.mtime
|
|
273
|
+
Files::File.create(@destination, params, @options)
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
raise stop!(e)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def restore(checkpoint)
|
|
279
|
+
state = checkpoint.state
|
|
280
|
+
@ref = state[:ref]
|
|
281
|
+
@planned_bytes = state[:planned_bytes]
|
|
282
|
+
@file_cap = state[:file_cap]
|
|
283
|
+
# A checkpoint without a plan of its own, as an older one, resumes with the API choosing the sizes.
|
|
284
|
+
plan = state[:plan]
|
|
285
|
+
if plan && plan[:mode] == :local
|
|
286
|
+
@route = plan[:route]
|
|
287
|
+
@part_size = plan[:partsize]
|
|
288
|
+
end
|
|
289
|
+
@accepted_bytes = checkpoint.completed_bytes
|
|
290
|
+
state[:layout].each do |number, offset, size, granted, fingerprint|
|
|
291
|
+
part = Part.new(number: number, offset: offset, bytesize: size, granted: granted, fingerprint: fingerprint, attempts: 0, renewals: 0, ready_at: 0)
|
|
292
|
+
@parts << part
|
|
293
|
+
if state[:etags].key?(number)
|
|
294
|
+
@accepted[number] = state[:etags][number]
|
|
295
|
+
else
|
|
296
|
+
@renewals << part
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Nil before the source is open, as when a pause comes while it opens, and for a caller's stream, which cannot be
|
|
302
|
+
# read again. Of an upload planning its own part sizes it keeps only the planned size and its route's digest.
|
|
303
|
+
def checkpoint
|
|
304
|
+
return if @source.nil? || @source.path.nil?
|
|
305
|
+
|
|
306
|
+
layout = @parts.map { |part| [ part.number, part.offset, part.bytesize, part.granted, part.fingerprint ] }
|
|
307
|
+
plan = @route ? { mode: :local, route: @route, partsize: @part_size } : { mode: :api }
|
|
308
|
+
Checkpoint.new(direction: :upload, path: @destination, local_path: @source.path, total_bytes: @source.size, completed_bytes: @accepted_bytes,
|
|
309
|
+
source: @source.identity,
|
|
310
|
+
state: { ref: @ref, params: checkpoint_params, planned_bytes: @planned_bytes, file_cap: @file_cap, layout: layout, etags: @accepted.dup,
|
|
311
|
+
plan: plan }
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def checkpoint_params
|
|
316
|
+
@params.slice(*CHECKPOINT_PARAMS).select { |_, value| value == true || value == false || value.is_a?(Integer) }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# A digest of what a part's instructions bind it to, without its URL: a renewal must give the same one. Its size is
|
|
320
|
+
# the planned one, so a part the upload sized itself is not bound to the size the API recommends.
|
|
321
|
+
def fingerprint(grant)
|
|
322
|
+
Digest::SHA256.hexdigest(JSON.generate([ grant.ref, grant.number, planned_size(grant), grant.http_method, grant.headers.sort, grant.retry, grant.parallel ]))
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# The size of a grant's part, but the last: the API's partsize, or the upload's own when it plans them.
|
|
326
|
+
def planned_size(grant)
|
|
327
|
+
@route ? @part_size : grant.partsize
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def plan
|
|
331
|
+
renew_parts
|
|
332
|
+
request_grants while @stop.nil? && more_needed?
|
|
333
|
+
read_parts
|
|
334
|
+
rescue StandardError => e
|
|
335
|
+
stop!(e)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def more_source?
|
|
339
|
+
@planned_bytes < @source.size || (@source.size.zero? && @parts.empty?)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def more_needed?
|
|
343
|
+
more_source? && @queue.size < (@controller ? @controller.limit : 1)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def wants_planning?
|
|
347
|
+
!@renewals.empty? || more_needed? || readable?
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# With the lock held: whether the next part of a caller's stream can be read now, all it may need held.
|
|
351
|
+
def readable?
|
|
352
|
+
part = @source.path.nil? && @parts[@next_read]
|
|
353
|
+
part ? @job.budget.available?(reservation_bytes(part)) : false
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# From a caller's stream: reads the next parts' bytes, in order, each only once the buffer budget holds all it may
|
|
357
|
+
# need (reservation_bytes). Once read_next has returned, and with it every String of the reads it no longer keeps,
|
|
358
|
+
# the part gives back all but what it holds (held_bytes).
|
|
359
|
+
def read_parts
|
|
360
|
+
while @stop.nil? && (part = @source.path.nil? && @parts[@next_read])
|
|
361
|
+
bytes = reservation_bytes(part)
|
|
362
|
+
reserved = @job.synchronize { @job.budget.available?(bytes) && (part.reservation = @job.budget.reserve(bytes)) }
|
|
363
|
+
break unless reserved
|
|
364
|
+
|
|
365
|
+
@next_read += 1
|
|
366
|
+
part.data = @source.read_next(part.offset, part.bytesize, whole: @transport.serialized?)
|
|
367
|
+
@job.post { @job.budget.shrink(part.reservation, held_bytes(part)) }
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# What a caller's stream's part holds once read: its bytes, and, kept in pieces for the job's own transport, the
|
|
372
|
+
# slice Segments sends them through.
|
|
373
|
+
def held_bytes(part)
|
|
374
|
+
part.data.is_a?(Array) ? part.bytesize + [ part.bytesize, STREAM_BUFFER_BYTES ].min : part.bytesize
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Each part of a caller's stream is sent only once its bytes are read.
|
|
378
|
+
def ready?(part)
|
|
379
|
+
!@source.path.nil? || !part.data.nil?
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def finished?
|
|
383
|
+
!more_source? && @accepted.size == @parts.size
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def request_grants
|
|
387
|
+
first = (@parts.last&.number || 0) + 1
|
|
388
|
+
uploads = begin_upload(first, Policy::UPLOAD_BATCH_PARTS)
|
|
389
|
+
choose_plan(uploads, first) if @ref.nil?
|
|
390
|
+
uploads.each_with_index do |upload, index|
|
|
391
|
+
break unless more_source?
|
|
392
|
+
|
|
393
|
+
grant = grant_from(upload, first + index)
|
|
394
|
+
raise changed_instructions unless grant.route == @route
|
|
395
|
+
|
|
396
|
+
remaining = @source.size - @planned_bytes
|
|
397
|
+
size = planned_size(grant)
|
|
398
|
+
raise invalid_instructions if size.zero? && remaining.positive?
|
|
399
|
+
|
|
400
|
+
part = Part.new(number: grant.number, offset: @planned_bytes, bytesize: [ size, remaining ].min, granted: size, grant: grant,
|
|
401
|
+
fingerprint: fingerprint(grant), attempts: 0, renewals: 0, ready_at: 0
|
|
402
|
+
)
|
|
403
|
+
@job.budget.check!(reservation_bytes(part))
|
|
404
|
+
@parts << part
|
|
405
|
+
@planned_bytes += part.bytesize
|
|
406
|
+
enqueue(part)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Renews the URLs of the parts that found theirs expired, before they are sent or after (renewals settle allowed
|
|
411
|
+
# as retries). A part whose renewals still give an expired URL stops the upload unsent. A part keeps the bytes
|
|
412
|
+
# it holds while it renews.
|
|
413
|
+
def renew_parts
|
|
414
|
+
while @stop.nil? && (part = @renewals.shift)
|
|
415
|
+
if part.renewals >= RENEWALS
|
|
416
|
+
fail_item(part, Files::APIError.new("Transfer request failed", http_status: 403))
|
|
417
|
+
next
|
|
418
|
+
end
|
|
419
|
+
part.renewals += 1
|
|
420
|
+
grant = grant_from(begin_upload(part.number, 1).first, part.number)
|
|
421
|
+
unless grant.route == @route && planned_size(grant) == part.granted && fingerprint(grant) == part.fingerprint
|
|
422
|
+
fail_item(part, Files::APIError.new("The renewed upload instructions changed"))
|
|
423
|
+
next
|
|
424
|
+
end
|
|
425
|
+
part.grant = grant
|
|
426
|
+
part.ready_at = 0
|
|
427
|
+
enqueue(part)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def begin_upload(first, count)
|
|
432
|
+
result = Files::File.begin_upload(@destination, @params.merge(ref: @ref, parts: count, part: first, with_direct_connection_info: true), @options)
|
|
433
|
+
grants = result.is_a?(Array) ? result : [ result ]
|
|
434
|
+
raise invalid_instructions if grants.empty?
|
|
435
|
+
|
|
436
|
+
adopt_context(grants.first)
|
|
437
|
+
grants
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def grant_from(upload, number)
|
|
441
|
+
attributes = upload.respond_to?(:attributes) && upload.attributes.is_a?(Hash) ? upload.attributes : {}
|
|
442
|
+
ref = upload.ref
|
|
443
|
+
size = upload.partsize
|
|
444
|
+
method = upload.http_method.to_s.upcase
|
|
445
|
+
url = upload.upload_uri
|
|
446
|
+
headers = attributes[:headers] || {}
|
|
447
|
+
valid = ref.is_a?(String) && !ref.empty? && (@ref.nil? || ref == @ref) && upload.part_number == number &&
|
|
448
|
+
size.is_a?(Integer) && size >= 0 && %w[PUT POST].include?(method) && http_url?(url) &&
|
|
449
|
+
headers.is_a?(Hash) && headers.all? { |name, value| (name.is_a?(String) || name.is_a?(Symbol)) && value.is_a?(String) }
|
|
450
|
+
raise invalid_instructions unless valid
|
|
451
|
+
|
|
452
|
+
@ref ||= ref
|
|
453
|
+
grant = Grant.new(ref: ref, number: number, partsize: size, http_method: method, url: url, headers: headers.transform_keys(&:to_s).freeze,
|
|
454
|
+
expires_at: StorageRetry.expires_at(url, attributes[:expires]), retry: attributes[:retry_parts] != false,
|
|
455
|
+
parallel: attributes[:parallel_parts] != false, direct: direct_server(attributes[:direct_connection_info]),
|
|
456
|
+
route: (UploadPlan.route(upload, number) if @route)
|
|
457
|
+
)
|
|
458
|
+
bind(grant) unless @controller
|
|
459
|
+
@file_cap = 1 unless grant.parallel
|
|
460
|
+
grant
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# The direct server a valid version 1 direct_connection_info names, or nil.
|
|
464
|
+
def direct_server(info)
|
|
465
|
+
return unless info.is_a?(Hash)
|
|
466
|
+
|
|
467
|
+
info = info.transform_keys(&:to_s)
|
|
468
|
+
addresses = info["addresses"]
|
|
469
|
+
valid = info["version"] == 1 && text?(info["server_name"]) && addresses.is_a?(Array) && !addresses.empty? &&
|
|
470
|
+
addresses.all? { |address| text?(address) } && http_url?(info["direct_uri"]) && text?(info["ca_pem"]) &&
|
|
471
|
+
info["ca_pem"].include?("BEGIN CERTIFICATE")
|
|
472
|
+
info["server_name"] if valid
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def bind(grant)
|
|
476
|
+
host = ::URI.parse(grant.url).host
|
|
477
|
+
options = @job.options
|
|
478
|
+
key, policy = if @transport.serialized?
|
|
479
|
+
[ [ :upload, :client, @client.object_id ], Policy.fixed(1) ]
|
|
480
|
+
elsif !options.automatic?
|
|
481
|
+
[ [ :upload, :fixed, grant.direct || host ], Policy.fixed(options.concurrency) ]
|
|
482
|
+
elsif grant.direct
|
|
483
|
+
[ [ :upload, :direct, grant.direct ], Policy.direct_upload(max_concurrency: options.max_concurrency) ]
|
|
484
|
+
else
|
|
485
|
+
[ [ :upload, :storage, host ], Policy.storage_upload(startup: options.startup, max_concurrency: options.max_concurrency) ]
|
|
486
|
+
end
|
|
487
|
+
@controller = @job.synchronize { @job.controller(key) { policy } }
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def item_cap
|
|
491
|
+
@file_cap
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# A part read whole first holds all of its bytes.
|
|
495
|
+
def whole_parts?
|
|
496
|
+
@transport.serialized? || @source.path.nil?
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# The bytes a part reserves before anything of it is read: a 64 KiB slice for a file's part streamed over the
|
|
500
|
+
# job's own connections, and all its bytes for a part read whole. A caller's stream's part also reserves what
|
|
501
|
+
# reading it may take besides, given back once it is read unless it still holds it (held_bytes): over the job's
|
|
502
|
+
# own connections, the slice its pieces are sent through; for a caller's client, which takes it as one String,
|
|
503
|
+
# its bytes again, for the String its pieces are copied into while a read's own String is still held.
|
|
504
|
+
def reservation_bytes(part)
|
|
505
|
+
return [ part.bytesize, STREAM_BUFFER_BYTES ].min unless whole_parts?
|
|
506
|
+
return part.bytesize unless @source.path.nil?
|
|
507
|
+
|
|
508
|
+
part.bytesize + (@transport.serialized? ? part.bytesize : [ part.bytesize, STREAM_BUFFER_BYTES ].min)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# The largest part buffer_bytes holds as reservation_bytes reserves it: any, for a file's part streamed over the
|
|
512
|
+
# job's own connections.
|
|
513
|
+
def largest_part
|
|
514
|
+
limit = @job.budget.limit
|
|
515
|
+
return Float::INFINITY unless whole_parts?
|
|
516
|
+
return limit unless @source.path.nil?
|
|
517
|
+
|
|
518
|
+
@transport.serialized? ? limit / 2 : limit - STREAM_BUFFER_BYTES
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# From the API's first answer for the upload (uploads, from part first), before any of its parts: the upload plans
|
|
522
|
+
# its own part sizes when every part the answer gives is for one and the same route and UploadPlan can plan the
|
|
523
|
+
# file's size in the parts available, as the first part names them, no larger than buffer_bytes holds unless the
|
|
524
|
+
# plan needs it to be (largest_part). Otherwise, and for an empty file, the API chooses them. A file no plan could
|
|
525
|
+
# send within buffer_bytes is refused here (UploadPlan.part_size), before the upload keeps a size, a route or the
|
|
526
|
+
# answer's reference, so a pause that came before the refusal leaves a checkpoint that plans afresh as it resumes.
|
|
527
|
+
# Each choice starts from no plan, so one a resume makes, from a checkpoint paused before any reference, keeps
|
|
528
|
+
# nothing of the plan it restored: however the choice ends, the upload has a route and a size together, or neither.
|
|
529
|
+
def choose_plan(uploads, first)
|
|
530
|
+
@route = @part_size = nil
|
|
531
|
+
routes = uploads.each_with_index.map { |upload, index| UploadPlan.route(upload, first + index) }
|
|
532
|
+
return unless @source.size.positive? && routes.first && routes.uniq.size == 1
|
|
533
|
+
|
|
534
|
+
@part_size = UploadPlan.part_size(@source.size, uploads.first.attributes[:available_parts], largest_part)
|
|
535
|
+
@route = routes.first if @part_size
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# An expired URL is never sent; renew_parts stops the upload once a part's renewals run out.
|
|
539
|
+
def expiring?(part)
|
|
540
|
+
StorageRetry.expired?(part.grant.expires_at)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# The part's URL, with the grant it came from.
|
|
544
|
+
def request_target(part)
|
|
545
|
+
[ part.grant.url, part.grant ].freeze
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def retry_allowed?(part)
|
|
549
|
+
part.grant.retry
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# On a worker thread, under the grant the part was admitted with (target). A caller's stream's part sends the
|
|
553
|
+
# bytes it holds; a file's part is read before the send through a caller's client, or as it streams otherwise.
|
|
554
|
+
# timing starts with the send.
|
|
555
|
+
def attempt_item(part, target, connections, timing = nil)
|
|
556
|
+
grant = target[1]
|
|
557
|
+
@source.check!
|
|
558
|
+
headers = { "Content-Type" => "application/octet-stream" }.merge(grant.headers)
|
|
559
|
+
declared = headers.find { |name, _| name.casecmp?("content-length") }&.last
|
|
560
|
+
return [ :fail, invalid_instructions ] if declared && declared != part.bytesize.to_s
|
|
561
|
+
|
|
562
|
+
headers.delete_if { |name, _| name.casecmp?("content-length") }
|
|
563
|
+
headers["Content-Length"] = part.bytesize.to_s
|
|
564
|
+
body = if part.bytesize.zero?
|
|
565
|
+
String.new
|
|
566
|
+
elsif @source.path.nil?
|
|
567
|
+
part.data.is_a?(Array) ? Segments.new(part.data) : part.data
|
|
568
|
+
elsif whole_parts?
|
|
569
|
+
@source.read(part.offset, part.bytesize)
|
|
570
|
+
else
|
|
571
|
+
Region.new(@source, part.offset, part.bytesize)
|
|
572
|
+
end
|
|
573
|
+
acknowledge(part, grant, @transport.perform(grant.http_method, grant.url, headers, connections, body: body, length: part.bytesize, timing: timing))
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def acknowledge(part, grant, response)
|
|
577
|
+
if response.status.between?(200, 299)
|
|
578
|
+
etags = response.headers.fetch("etag", [])
|
|
579
|
+
etag = etags.first.to_s.strip
|
|
580
|
+
return [ :ok, etags.first, part.bytesize ] if etags.size == 1 && !etag.empty? && !etag.match?(/"\s*,\s*(?:W\/)?"/)
|
|
581
|
+
|
|
582
|
+
return [ :fail, Files::APIError.new(ACK_FAILURE, http_status: response.status) ]
|
|
583
|
+
end
|
|
584
|
+
return [ :renew, refused(response) ] if response.status == 403 && StorageRetry.expired?(grant.expires_at)
|
|
585
|
+
|
|
586
|
+
decision, delay = StorageRetry.classify(response.status, response.headers, response.body, truncated: response.truncated)
|
|
587
|
+
case decision
|
|
588
|
+
when :throttle then [ :retry, delay, :throttle, refused(response) ]
|
|
589
|
+
when :retry then [ :retry, delay, :failure, refused(response) ]
|
|
590
|
+
else [ :fail, refused(response) ]
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# With the lock held: records an acknowledged part once, drops the bytes it held, and returns its progress report:
|
|
595
|
+
# total_bytes, acknowledged_bytes (every part acknowledged, a resumed upload's before its pause included),
|
|
596
|
+
# part_number, part_bytes, elapsed_seconds of this call and bytes_per_second, the bytes this call acknowledged
|
|
597
|
+
# over them, each part once however often it was sent, and 0.0 before any time has passed.
|
|
598
|
+
def accept(part, etag)
|
|
599
|
+
discard(part)
|
|
600
|
+
return if @accepted.key?(part.number)
|
|
601
|
+
|
|
602
|
+
@accepted[part.number] = etag
|
|
603
|
+
@accepted_bytes += part.bytesize
|
|
604
|
+
@job.count_accepted(part.bytesize)
|
|
605
|
+
elapsed = Clock.now - @started_at
|
|
606
|
+
{ total_bytes: @source.size, acknowledged_bytes: @accepted_bytes, part_number: part.number, part_bytes: part.bytesize, elapsed_seconds: elapsed,
|
|
607
|
+
bytes_per_second: elapsed.positive? ? (@accepted_bytes - @start_accepted) / elapsed : 0.0 }
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# On the caller's thread. Nothing is reported after a failure or a cancellation.
|
|
611
|
+
def deliver(reports)
|
|
612
|
+
return unless @progress
|
|
613
|
+
|
|
614
|
+
reports.each do |report|
|
|
615
|
+
break unless @stop.nil? || @stop == :paused
|
|
616
|
+
|
|
617
|
+
@progress.call(report)
|
|
618
|
+
end
|
|
619
|
+
rescue StandardError => e
|
|
620
|
+
stop!(e)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def all_items
|
|
624
|
+
@parts
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def public_error(error)
|
|
628
|
+
return Files::APIConnectionError.new("Transfer request failed (#{error.original.class})") if error.is_a?(NetworkFailure)
|
|
629
|
+
|
|
630
|
+
error
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def refused(response)
|
|
634
|
+
Files::APIError.new("Transfer request failed", http_status: response.status)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def invalid_instructions
|
|
638
|
+
Files::APIError.new("The upload instructions the API returned are not usable")
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# For a later part of an upload planning its own part sizes that is not for its route.
|
|
642
|
+
def changed_instructions
|
|
643
|
+
Files::APIError.new("The upload instructions changed during the upload")
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def http_url?(url)
|
|
647
|
+
url.is_a?(String) && %w[http https].include?(::URI.parse(url).scheme)
|
|
648
|
+
rescue ::URI::InvalidURIError
|
|
649
|
+
false
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def text?(value)
|
|
653
|
+
value.is_a?(String) && !value.strip.empty?
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|