files.com 1.1.813 → 1.1.815
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 +911 -731
- 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 +597 -386
- 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 +5368 -3048
- data/doc/top-level-namespace.html +1 -1
- data/docs/file.md +346 -0
- data/docs/site.md +10 -0
- data/docs/user.md +8 -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/models/site.rb +16 -0
- data/lib/files.com/models/user.rb +19 -0
- 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,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Files
|
|
7
|
+
module Transfer
|
|
8
|
+
# How an upload sizes its parts. Nothing in the API's answers grants this by name: an upload chooses its own part size
|
|
9
|
+
# only where its first answer shows every part going straight to Amazon S3 as one ordinary multipart upload (route),
|
|
10
|
+
# whose signed URLs bind neither a part's length nor its bytes. There partsize and next_partsize are recommendations,
|
|
11
|
+
# and each part is sent with a Content-Length of exactly its bytes. Everywhere else the API sizes the parts.
|
|
12
|
+
# parallel_parts says only whether parts may be sent at once, never how large they may be.
|
|
13
|
+
#
|
|
14
|
+
# The limits are the SDK's own for planning an upload to S3, within S3's: parts but the last of at least 5 MiB; parts
|
|
15
|
+
# of at most 1 GiB, which bounds what one part holds for its retries; at most 10,000 parts, or fewer when the first
|
|
16
|
+
# answer's available_parts says so; and files of at most 5 TiB. They promise nothing of Files.com's or S3's own
|
|
17
|
+
# limits, and bind no other destination: a file they cannot plan is sized by the API, unless it is of at most 5 TiB
|
|
18
|
+
# and buffer_bytes could not hold even the smallest part that fits it in the parts available, which the API's plan
|
|
19
|
+
# would need as well. That file is refused (part_size), and its upload keeps no plan for it.
|
|
20
|
+
module UploadPlan
|
|
21
|
+
MIB = 1024 * 1024
|
|
22
|
+
MIN_PART_BYTES = 5 * MIB
|
|
23
|
+
MAX_PART_BYTES = 1024 * MIB
|
|
24
|
+
MAX_PARTS = 10_000
|
|
25
|
+
MAX_FILE_BYTES = 5 * 1024 * 1024 * MIB
|
|
26
|
+
# The size every part but the last has, unless the file needs larger parts to fit the parts available, or the
|
|
27
|
+
# caller's buffer_bytes smaller ones.
|
|
28
|
+
PREFERRED_PART_BYTES = 8 * MIB
|
|
29
|
+
# The query parameters a presigned UploadPart may have: those naming the part and its upload, and SigV4's own.
|
|
30
|
+
QUERY_PARAMETERS = %w[partNumber uploadId X-Amz-Algorithm X-Amz-Credential X-Amz-Date X-Amz-Expires X-Amz-SignedHeaders X-Amz-Signature
|
|
31
|
+
X-Amz-Security-Token].freeze
|
|
32
|
+
|
|
33
|
+
# The size of every part but the last of an upload of total bytes: PREFERRED_PART_BYTES, or the larger size, in
|
|
34
|
+
# whole MiB, that fits total in the parts available, but no larger than most, the largest part buffer_bytes holds
|
|
35
|
+
# as the upload reserves it (Float::INFINITY when parts stream), and never smaller than the least that fits: the
|
|
36
|
+
# parts available, and 5 MiB unless the whole file is one part. A size above most is a plan buffer_bytes cannot
|
|
37
|
+
# hold, refused before anything is read or sent. Nil, for the API to size the parts, when total is larger than
|
|
38
|
+
# MAX_FILE_BYTES, or needs parts larger than MAX_PART_BYTES in the parts available: those are only the SDK's
|
|
39
|
+
# limits. A total needing parts that large and larger than most too raises BufferLimitError instead, never giving
|
|
40
|
+
# a size: the API's parts, in the parts available, could not be smaller, so no plan could send it within
|
|
41
|
+
# buffer_bytes, and no size it gives is ever larger than MAX_PART_BYTES.
|
|
42
|
+
def self.part_size(total, available_parts, most)
|
|
43
|
+
parts = available_parts.is_a?(Integer) && available_parts.positive? ? [ available_parts, MAX_PARTS ].min : MAX_PARTS
|
|
44
|
+
return if total > MAX_FILE_BYTES
|
|
45
|
+
|
|
46
|
+
needed = (total + parts - 1) / parts
|
|
47
|
+
if needed > MAX_PART_BYTES
|
|
48
|
+
return if needed <= most
|
|
49
|
+
|
|
50
|
+
raise BufferLimitError, "The #{total}-byte upload needs parts of at least #{needed} bytes in its #{parts} available parts, " \
|
|
51
|
+
"more than the #{most} bytes buffer_bytes can hold for a part"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
preferred = [ (([ PREFERRED_PART_BYTES, needed ].max + MIB - 1) / MIB) * MIB, MAX_PART_BYTES ].min
|
|
55
|
+
least = [ needed, MIN_PART_BYTES ].max
|
|
56
|
+
[ [ preferred, most ].min, total <= least ? total : least ].max
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The ordinary multipart upload straight to Amazon S3 that upload, the API's answer for part number, is for: a
|
|
60
|
+
# digest of its endpoint, object and upload ID, which is all a checkpoint keeps of it, or nil when the answer is not
|
|
61
|
+
# exactly that. Every condition is read from the answer as the API gave it, and all must hold:
|
|
62
|
+
#
|
|
63
|
+
# action put/write: an ordinary write, not an append or restart
|
|
64
|
+
# http_method PUT
|
|
65
|
+
# part_number number
|
|
66
|
+
# upload_uri https on the default port, without user information or fragment, to an S3
|
|
67
|
+
# general-purpose endpoint (general_purpose_endpoint?), and a SigV4 presigned UploadPart:
|
|
68
|
+
# partNumber naming number; uploadId; X-Amz-Algorithm AWS4-HMAC-SHA256; X-Amz-Credential
|
|
69
|
+
# scoped to s3; X-Amz-SignedHeaders host alone, so that neither the length nor the bytes
|
|
70
|
+
# are signed; X-Amz-Signature; each once, and no parameter but QUERY_PARAMETERS, so none
|
|
71
|
+
# binding a checksum, digest or length
|
|
72
|
+
# headers, parameters none
|
|
73
|
+
# send exactly the part's size as its Content-Length and its bytes as the body
|
|
74
|
+
# direct_connection_info none
|
|
75
|
+
#
|
|
76
|
+
# Anything else, such as a host that only resembles S3's, a custom or S3-compatible endpoint, a Files.com or agent
|
|
77
|
+
# URL, or a parallel flag on its own, is not this route. The URL is only read: the part goes to it exactly as given.
|
|
78
|
+
def self.route(upload, number)
|
|
79
|
+
attributes = upload.respond_to?(:attributes) && upload.attributes.is_a?(Hash) ? upload.attributes : {}
|
|
80
|
+
url = attributes[:upload_uri]
|
|
81
|
+
return unless attributes[:action] == "put/write" && attributes[:http_method] == "PUT" && attributes[:part_number] == number &&
|
|
82
|
+
attributes[:direct_connection_info].nil? && none?(attributes[:headers]) && none?(attributes[:parameters]) &&
|
|
83
|
+
ordinary_send?(attributes[:send]) && url.is_a?(String) && url.include?("?") && !url.include?("#")
|
|
84
|
+
|
|
85
|
+
uri = ::URI.parse(url)
|
|
86
|
+
return unless uri.scheme.to_s.casecmp?("https") && uri.port == 443 && uri.userinfo.nil? && uri.path.to_s.length > 1 &&
|
|
87
|
+
general_purpose_endpoint?(uri.host.to_s)
|
|
88
|
+
|
|
89
|
+
resource, query = url.split("?", 2)
|
|
90
|
+
values = signed_query(query)
|
|
91
|
+
return unless values && !values["uploadId"].to_s.empty? && values["partNumber"] == number.to_s &&
|
|
92
|
+
values["X-Amz-Algorithm"] == "AWS4-HMAC-SHA256" && values["X-Amz-Credential"].to_s.end_with?("/s3/aws4_request") &&
|
|
93
|
+
values["X-Amz-SignedHeaders"] == "host" && !values["X-Amz-Signature"].to_s.empty?
|
|
94
|
+
|
|
95
|
+
"sha256:#{Digest::SHA256.new.update(resource).update("\n").update(values["uploadId"]).hexdigest}"
|
|
96
|
+
rescue ::URI::InvalidURIError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# s3, s3.<region>, s3-<region> or s3.dualstack.<region> under amazonaws.com or amazonaws.com.cn, alone, with the
|
|
101
|
+
# bucket in the path, or after a bucket of one or more labels. Not S3's accelerate, FIPS, website, access point,
|
|
102
|
+
# Object Lambda, Outposts, Express or control endpoints, which this SDK does not size parts for.
|
|
103
|
+
def self.general_purpose_endpoint?(host)
|
|
104
|
+
host = host.downcase
|
|
105
|
+
within = %w[.amazonaws.com .amazonaws.com.cn].filter_map { |suffix| host.delete_suffix(suffix) if host.end_with?(suffix) }.first
|
|
106
|
+
return false if within.nil? || within.empty?
|
|
107
|
+
|
|
108
|
+
labels = within.split(".", -1)
|
|
109
|
+
endpoint = labels.rindex("s3")
|
|
110
|
+
if endpoint
|
|
111
|
+
after = labels[(endpoint + 1)..]
|
|
112
|
+
return false unless after.empty? || (after.size == 1 && region?(after[0])) || (after.size == 2 && after[0] == "dualstack" && region?(after[1]))
|
|
113
|
+
else
|
|
114
|
+
endpoint = labels.size - 1
|
|
115
|
+
return false unless labels[endpoint].start_with?("s3-") && region?(labels[endpoint].delete_prefix("s3-"))
|
|
116
|
+
end
|
|
117
|
+
labels[0...endpoint].none?(&:empty?)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A region such as us-east-1, eu-central-2 or us-gov-west-1: two letters, words of letters, then a number.
|
|
121
|
+
private_class_method def self.region?(label)
|
|
122
|
+
words = label.split("-", -1)
|
|
123
|
+
words.size >= 3 && words[0].length == 2 && words[0...-1].all? { |word| word.match?(/\A[a-z]+\z/) } && words[-1].match?(/\A[0-9]+\z/)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The query's parameters, each name with its percent-decoded value, or nil unless every one is among
|
|
127
|
+
# QUERY_PARAMETERS, as it is written there, given once and with a value.
|
|
128
|
+
private_class_method def self.signed_query(query)
|
|
129
|
+
values = {}
|
|
130
|
+
query.split("&", -1).each do |pair|
|
|
131
|
+
name, value = pair.split("=", 2)
|
|
132
|
+
return nil if value.nil? || !QUERY_PARAMETERS.include?(name) || values.key?(name)
|
|
133
|
+
|
|
134
|
+
values[name] = unescape(value)
|
|
135
|
+
end
|
|
136
|
+
values
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Percent-decoding alone, as a query has it: a plus sign stays as it is.
|
|
140
|
+
private_class_method def self.unescape(text)
|
|
141
|
+
text.b.gsub(/%\h\h/) { |escape| escape[1, 2].hex.chr }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private_class_method def self.none?(field)
|
|
145
|
+
field.nil? || (field.is_a?(Hash) && field.empty?)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Exactly the part's size as its Content-Length and its bytes as the body.
|
|
149
|
+
private_class_method def self.ordinary_send?(instructions)
|
|
150
|
+
return false unless instructions.is_a?(Hash) && instructions.size == 2
|
|
151
|
+
|
|
152
|
+
instructions = instructions.transform_keys(&:to_s)
|
|
153
|
+
instructions["partsize"].is_a?(String) && instructions["partsize"].casecmp?("required-header Content-Length") && instructions["partdata"] == "body"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/files.com.rb
CHANGED
|
@@ -32,6 +32,19 @@ require "files.com/util"
|
|
|
32
32
|
require "files.com/list"
|
|
33
33
|
require "files.com/uri"
|
|
34
34
|
require "files.com/path_util"
|
|
35
|
+
require "files.com/transfer/errors"
|
|
36
|
+
require "files.com/transfer/options"
|
|
37
|
+
require "files.com/transfer/clock"
|
|
38
|
+
require "files.com/transfer/policy"
|
|
39
|
+
require "files.com/transfer/controller"
|
|
40
|
+
require "files.com/transfer/budgets"
|
|
41
|
+
require "files.com/transfer/storage_retry"
|
|
42
|
+
require "files.com/transfer/http_transport"
|
|
43
|
+
require "files.com/transfer/checkpoint"
|
|
44
|
+
require "files.com/transfer/job"
|
|
45
|
+
require "files.com/transfer/upload_plan"
|
|
46
|
+
require "files.com/transfer/upload_coordinator"
|
|
47
|
+
require "files.com/transfer/download_coordinator"
|
|
35
48
|
|
|
36
49
|
require "files.com/models/account_line_item"
|
|
37
50
|
require "files.com/models/action"
|
data/spec/lib/api_client_spec.rb
CHANGED
|
@@ -583,11 +583,133 @@ RSpec.describe Files::ApiClient do
|
|
|
583
583
|
end
|
|
584
584
|
|
|
585
585
|
describe "User-Agent" do
|
|
586
|
-
|
|
586
|
+
around do |example|
|
|
587
|
+
original = [ Files.app_info, Files.default_headers, Files.api_key ]
|
|
588
|
+
example.run
|
|
589
|
+
ensure
|
|
590
|
+
Files.app_info, Files.default_headers, Files.api_key = original
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
let(:user_agents) { [] }
|
|
594
|
+
let(:client) do
|
|
595
|
+
sent = user_agents
|
|
596
|
+
stubbed_api_client do |stub|
|
|
597
|
+
stub.get("/api/rest/v1/workspaces/1") do |env|
|
|
598
|
+
sent << env.request_headers["User-Agent"]
|
|
599
|
+
json_response(200, { id: 1 })
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
let(:identity) { "Files.com Ruby SDK v#{Files::VERSION}" }
|
|
604
|
+
|
|
605
|
+
def find_workspace(options = {})
|
|
606
|
+
Files::Workspace.find(1, {}, { api_key: "key", client: client }.merge(options))
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# identity followed by the one comment the SDK's own User-Agent ends with: a coarse operating system family and the
|
|
610
|
+
# Ruby runtime, with its version when it can carry it.
|
|
611
|
+
def sdk_user_agent(identity)
|
|
612
|
+
/\A#{Regexp.escape(identity)}(?<metadata> \(files-sdk-meta\/1; os=(?<os>[a-z][a-z0-9-]{0,15}); runtime=ruby(?:\/(?<version>[0-9][A-Za-z0-9.+_-]{0,47}))?\))\z/
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
it "names the package's version, then this Ruby and its operating system's family in one comment, on API requests" do
|
|
587
616
|
package_version = Gem::Specification.load(File.expand_path("../../files.com.gemspec", __dir__)).version.to_s
|
|
588
|
-
user_agent = described_class.new.send(:request_headers, nil, nil, :get, nil)["User-Agent"]
|
|
589
617
|
|
|
590
|
-
|
|
618
|
+
find_workspace
|
|
619
|
+
|
|
620
|
+
user_agent = user_agents.first
|
|
621
|
+
parts = sdk_user_agent("Files.com Ruby SDK v#{package_version}").match(user_agent)
|
|
622
|
+
expect(parts).not_to be_nil, "#{user_agent.inspect} is not the SDK's identity followed by its metadata comment"
|
|
623
|
+
expect(user_agent.bytesize).to be <= 190
|
|
624
|
+
expect(parts[:metadata].bytesize).to be <= 128
|
|
625
|
+
family = { /android/ => "android", /darwin/ => "macos", /linux/ => "linux", /mswin|mingw/ => "windows" }.find { |pattern, _| RbConfig::CONFIG["host_os"].match?(pattern) }&.last
|
|
626
|
+
expect(parts[:os]).to eq(family) if family
|
|
627
|
+
expect(parts[:version]).to eq(RUBY_VERSION) if RUBY_ENGINE == "ruby"
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
it "sends a User-Agent the caller sets exactly as given, however much it looks like the SDK's own" do
|
|
631
|
+
sdk_shaped = "Files.com Ruby SDK v0.0.1 (files-sdk-meta/1; os=linux; runtime=ruby/1.0)"
|
|
632
|
+
|
|
633
|
+
Files.default_headers = { "User-Agent" => sdk_shaped }
|
|
634
|
+
find_workspace
|
|
635
|
+
Files.default_headers = -> { { "User-Agent" => "Reporter from a Proc" } }
|
|
636
|
+
find_workspace
|
|
637
|
+
find_workspace("User-Agent" => "Reporter for one request")
|
|
638
|
+
|
|
639
|
+
expect(user_agents).to eq([ sdk_shaped, "Reporter from a Proc", "Reporter for one request" ])
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
it "keeps app info where it was, and adds no comment after app info that is not printable ASCII or leaves it no room" do
|
|
643
|
+
Files.app_info = { name: "Reporter", version: "2.1", url: "https://reporter.example" }
|
|
644
|
+
find_workspace
|
|
645
|
+
Files.app_info = { name: "Café" }
|
|
646
|
+
find_workspace
|
|
647
|
+
Files.app_info = { name: "R" * 160 }
|
|
648
|
+
find_workspace
|
|
649
|
+
|
|
650
|
+
expect(user_agents[0]).to match(sdk_user_agent("#{identity} Reporter/2.1 (https://reporter.example)"))
|
|
651
|
+
expect(user_agents[1..]).to eq([ "#{identity} Café", "#{identity} #{'R' * 160}" ])
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
it "gives a transfer's API requests the app info and default headers the transfer began with, whatever changes after" do
|
|
655
|
+
Files.app_info = { name: "Reporter", version: "2.1" }
|
|
656
|
+
began_with_app_info = described_class.transfer_settings
|
|
657
|
+
Files.app_info = nil
|
|
658
|
+
Files.default_headers = { "User-Agent" => "Captured reporter" }
|
|
659
|
+
began_with_headers = described_class.transfer_settings
|
|
660
|
+
Files.app_info = { name: "Later" }
|
|
661
|
+
Files.default_headers = {}
|
|
662
|
+
|
|
663
|
+
[ began_with_app_info, began_with_headers ].each { |settings| client.execute_request(:get, "/workspaces/1", api_key: "key", settings: settings) }
|
|
664
|
+
find_workspace
|
|
665
|
+
|
|
666
|
+
expect(user_agents[0]).to match(sdk_user_agent("#{identity} Reporter/2.1"))
|
|
667
|
+
expect(user_agents[1]).to eq("Captured reporter")
|
|
668
|
+
expect(user_agents[2]).to match(sdk_user_agent("#{identity} Later"))
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
it "leaves out a runtime version it cannot carry, and then the whole comment, rather than shorten the SDK's identity" do
|
|
672
|
+
sdk = "Files.com Ruby SDK v1.2.3"
|
|
673
|
+
with_metadata = lambda do |name = sdk, **facts|
|
|
674
|
+
described_class.user_agent_with_metadata(name, **{ host_os: "linux-gnu", ruby_engine: "ruby", ruby_version: "3.4.4" }.merge(facts))
|
|
675
|
+
end
|
|
676
|
+
# sdk, padded to bytes.
|
|
677
|
+
padded = ->(bytes) { sdk + ("0" * (bytes - sdk.bytesize)) }
|
|
678
|
+
|
|
679
|
+
expect(with_metadata.call).to eq("#{sdk} (files-sdk-meta/1; os=linux; runtime=ruby/3.4.4)")
|
|
680
|
+
{ "darwin24" => "macos", "mingw32" => "windows", "linux-android" => "android", "freebsd14.1" => "freebsd", "solaris2.11" => "other", "" => "unknown" }.each do |host_os, family|
|
|
681
|
+
expect(with_metadata.call(host_os: host_os)).to eq("#{sdk} (files-sdk-meta/1; os=#{family}; runtime=ruby/3.4.4)")
|
|
682
|
+
end
|
|
683
|
+
[ "3.5.0 dev", "", nil ].each do |version|
|
|
684
|
+
expect(with_metadata.call(ruby_version: version)).to eq("#{sdk} (files-sdk-meta/1; os=linux; runtime=ruby)")
|
|
685
|
+
end
|
|
686
|
+
# Another engine's RUBY_VERSION is the language it follows, not its own.
|
|
687
|
+
expect(with_metadata.call(ruby_engine: "jruby", ruby_version: "3.1.4")).to eq("#{sdk} (files-sdk-meta/1; os=linux; runtime=ruby)")
|
|
688
|
+
expect(with_metadata.call(padded.call(141))).to eq("#{padded.call(141)} (files-sdk-meta/1; os=linux; runtime=ruby/3.4.4)")
|
|
689
|
+
expect(with_metadata.call(padded.call(142))).to eq("#{padded.call(142)} (files-sdk-meta/1; os=linux; runtime=ruby)")
|
|
690
|
+
expect(with_metadata.call(padded.call(147))).to eq("#{padded.call(147)} (files-sdk-meta/1; os=linux; runtime=ruby)")
|
|
691
|
+
expect(with_metadata.call(padded.call(148))).to eq(padded.call(148))
|
|
692
|
+
expect(with_metadata.call("#{sdk} Café")).to eq("#{sdk} Café")
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
it "sends a transfer request with only the User-Agent it is given, and no API credential" do
|
|
696
|
+
Files.api_key = "library-wide-key"
|
|
697
|
+
url = "https://storage.example.test/part?X-Amz-Signature=signed"
|
|
698
|
+
sent = []
|
|
699
|
+
connection = Faraday.new do |builder|
|
|
700
|
+
builder.adapter :test do |stub|
|
|
701
|
+
stub.put(url) do |env|
|
|
702
|
+
sent << env.request_headers
|
|
703
|
+
[ 200, { "ETag" => "\"part\"" }, "" ]
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
described_class.new(connection).remote_request(:put, url, { "User-Agent" => "Storage client" }, "part")
|
|
709
|
+
|
|
710
|
+
expect(sent.size).to eq(1)
|
|
711
|
+
expect(sent.first["User-Agent"]).to eq("Storage client")
|
|
712
|
+
expect(sent.first.keys.map(&:downcase)).not_to include("x-filesapi-key", "x-filesapi-auth", "x-files-client-user-agent")
|
|
591
713
|
end
|
|
592
714
|
end
|
|
593
715
|
|
|
@@ -221,13 +221,18 @@ RSpec.describe Files::File, "transfers" do
|
|
|
221
221
|
expect(requests.map(&:first)).to eq(%i[begin part])
|
|
222
222
|
end
|
|
223
223
|
|
|
224
|
-
it "
|
|
224
|
+
it "refuses a part answered with a non-success status it returns, before ending the upload, when no progress is given" do
|
|
225
225
|
requests = []
|
|
226
226
|
client = upload_api(requests, partsize: 4, part_response: ->(_number) { [ 302, {}, "" ] })
|
|
227
227
|
|
|
228
|
-
with_local_file("abcdefghij")
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
with_local_file("abcdefghij") do |path|
|
|
229
|
+
expect { described_class.upload_file(path, "report.bin", { client: client }) }
|
|
230
|
+
.to raise_error(Files::APIError, "Transfer request failed") { |error|
|
|
231
|
+
expect(error.http_status).to eq(302)
|
|
232
|
+
expect(error.full_message).not_to include("storage.example.test")
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
expect(requests.map(&:first)).to eq(%i[begin part])
|
|
231
236
|
end
|
|
232
237
|
|
|
233
238
|
it "stops at a part answered 500, reporting nothing and not ending the upload" do
|
|
@@ -254,6 +259,26 @@ RSpec.describe Files::File, "transfers" do
|
|
|
254
259
|
end
|
|
255
260
|
end
|
|
256
261
|
|
|
262
|
+
describe ".upload_io" do
|
|
263
|
+
it "uploads a stream without a size one part at a time, to its end, and ends the upload with what it read" do
|
|
264
|
+
requests = []
|
|
265
|
+
reader = short_reader("abcdefghij", max_read: 3)
|
|
266
|
+
|
|
267
|
+
described_class.upload_io(reader, "report.bin", { client: upload_api(requests, partsize: 4) }, transfer: true)
|
|
268
|
+
|
|
269
|
+
expect(parts_sent(requests)).to eq([ [ 1, "abcd" ], [ 2, "efgh" ], [ 3, "ij" ] ])
|
|
270
|
+
expect(requests.last[0..1]).to match([ :end, hash_including("action" => "end", "size" => "10") ])
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
it "refuses a size that is not a non-negative Integer before sending anything" do
|
|
274
|
+
requests = []
|
|
275
|
+
|
|
276
|
+
expect { described_class.upload_io(StringIO.new("ab"), "report.bin", { client: upload_api(requests, partsize: 4) }, size: -1, transfer: true) }
|
|
277
|
+
.to raise_error(Files::InvalidParameterError)
|
|
278
|
+
expect(requests).to be_empty
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
257
282
|
describe "#download_content" do
|
|
258
283
|
def download(raw_response, range: [])
|
|
259
284
|
server = serve(raw_response)
|
|
@@ -372,6 +397,17 @@ RSpec.describe Files::File, "transfers" do
|
|
|
372
397
|
expect(sink.string).to be_empty
|
|
373
398
|
end
|
|
374
399
|
|
|
400
|
+
it "refuses a response from the Files.com web app before writing any of it, and closes the caller's IO" do
|
|
401
|
+
page = "<html>Sign in</html>"
|
|
402
|
+
server = serve(response("200 OK", { "X-Files-Frontend-App" => "true", "Content-Length" => page.bytesize }, page))
|
|
403
|
+
|
|
404
|
+
expect { remote_file(server).download_content(sink) }.to raise_error(Files::Transfer::FrontendResponseError) { |error|
|
|
405
|
+
expect(error.full_message).not_to include("/objects/", "X-Amz", "fixture-signature")
|
|
406
|
+
}
|
|
407
|
+
expect(sink.string).to be_empty
|
|
408
|
+
expect(sink).to be_closed
|
|
409
|
+
end
|
|
410
|
+
|
|
375
411
|
it "raises the caller IO's own write error, even when closing that IO fails too" do
|
|
376
412
|
write_error = IOError.new("destination is full")
|
|
377
413
|
failing_sink = Class.new(StringIO) do
|
|
@@ -439,6 +475,19 @@ RSpec.describe Files::File, "transfers" do
|
|
|
439
475
|
file
|
|
440
476
|
end
|
|
441
477
|
|
|
478
|
+
it "refuses transfer: and progress: before it opens a pipe or starts a download" do
|
|
479
|
+
requests = []
|
|
480
|
+
server = serve { |_client| requests << :download }
|
|
481
|
+
file = remote_file(server)
|
|
482
|
+
pipes = open_pipes.size
|
|
483
|
+
|
|
484
|
+
expect { file.read_io(transfer: true) }.to raise_error(Files::InvalidParameterError, /read_io cannot transfer adaptively/)
|
|
485
|
+
expect { file.read_io(progress: ->(_report) {}) }.to raise_error(Files::InvalidParameterError)
|
|
486
|
+
expect(open_pipes.size).to be <= pipes
|
|
487
|
+
expect(server.requests).to be_empty
|
|
488
|
+
expect(requests).to be_empty
|
|
489
|
+
end
|
|
490
|
+
|
|
442
491
|
{
|
|
443
492
|
"read" => ->(reader) { reader.read },
|
|
444
493
|
"read(n) until eof?" => ->(reader) { reader.read(1024) until reader.eof? },
|
|
@@ -3,9 +3,18 @@ require "socket"
|
|
|
3
3
|
# A loopback HTTP server for transfer specs. It answers each connection with
|
|
4
4
|
# one canned response, written byte for byte, so a spec controls exactly the
|
|
5
5
|
# framing a storage provider sends, including bodies that end early. Every
|
|
6
|
-
# request it receives is recorded.
|
|
6
|
+
# request it receives is recorded, with its method and body.
|
|
7
|
+
#
|
|
8
|
+
# With concurrent: true it answers each connection on its own thread, so a spec
|
|
9
|
+
# can see requests that are really in flight at once: each Request records how
|
|
10
|
+
# many were (itself included) when it arrived, and peak_in_flight the most at
|
|
11
|
+
# any time. A request counts as in flight from when it has been read until its
|
|
12
|
+
# response is written.
|
|
7
13
|
class TransferServer
|
|
8
|
-
Request = Struct.new(:target, :headers)
|
|
14
|
+
Request = Struct.new(:target, :headers, :http_method, :body, :in_flight)
|
|
15
|
+
|
|
16
|
+
# How long close waits for each concurrent connection still being answered.
|
|
17
|
+
SHUTDOWN_SECONDS = 5
|
|
9
18
|
|
|
10
19
|
attr_reader :requests
|
|
11
20
|
|
|
@@ -15,10 +24,19 @@ class TransferServer
|
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
# The block receives each client socket once its request has been read, and
|
|
18
|
-
# the connection closes when the block returns.
|
|
19
|
-
|
|
27
|
+
# the connection closes when the block returns. With concurrent: true it
|
|
28
|
+
# receives the Request instead and returns the response to write: a String,
|
|
29
|
+
# or an Array of Strings written in turn and callables called between them,
|
|
30
|
+
# such as a sleep that holds a body back after its headers.
|
|
31
|
+
def initialize(concurrent: false, &respond)
|
|
20
32
|
@respond = respond
|
|
33
|
+
@concurrent = concurrent
|
|
21
34
|
@requests = []
|
|
35
|
+
@lock = Mutex.new
|
|
36
|
+
@changed = ConditionVariable.new
|
|
37
|
+
@in_flight = 0
|
|
38
|
+
@peak = 0
|
|
39
|
+
@handlers = []
|
|
22
40
|
@listener = TCPServer.new("127.0.0.1", 0)
|
|
23
41
|
@thread = Thread.new { serve }
|
|
24
42
|
end
|
|
@@ -27,9 +45,48 @@ class TransferServer
|
|
|
27
45
|
"http://127.0.0.1:#{@listener.addr[1]}#{target}"
|
|
28
46
|
end
|
|
29
47
|
|
|
48
|
+
def peak_in_flight
|
|
49
|
+
@lock.synchronize { @peak }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Waits until at least count requests have been in flight at once, for at
|
|
53
|
+
# most timeout seconds, and returns whether they were.
|
|
54
|
+
def wait_for_peak(count, timeout: 5)
|
|
55
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
56
|
+
@lock.synchronize do
|
|
57
|
+
until @peak >= count
|
|
58
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
59
|
+
return false unless remaining.positive?
|
|
60
|
+
|
|
61
|
+
@changed.wait(@lock, remaining)
|
|
62
|
+
end
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Waits until at least count requests have arrived at a concurrent server, for
|
|
68
|
+
# at most timeout seconds, and returns whether they did.
|
|
69
|
+
def wait_for_requests(count, timeout: 5)
|
|
70
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
71
|
+
@lock.synchronize do
|
|
72
|
+
until @requests.size >= count
|
|
73
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
74
|
+
return false unless remaining.positive?
|
|
75
|
+
|
|
76
|
+
@changed.wait(@lock, remaining)
|
|
77
|
+
end
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Stops accepting connections and returns once the server has stopped
|
|
83
|
+
# writing. A concurrent connection still being answered after
|
|
84
|
+
# SHUTDOWN_SECONDS is stopped.
|
|
30
85
|
def close
|
|
31
86
|
@listener.close
|
|
32
87
|
@thread.join
|
|
88
|
+
handlers = @lock.synchronize { @handlers.dup }
|
|
89
|
+
handlers.each { |handler| handler.join(SHUTDOWN_SECONDS) || handler.kill }
|
|
33
90
|
end
|
|
34
91
|
|
|
35
92
|
private
|
|
@@ -37,24 +94,66 @@ class TransferServer
|
|
|
37
94
|
def serve
|
|
38
95
|
loop do
|
|
39
96
|
client = @listener.accept
|
|
40
|
-
@
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
client&.close
|
|
97
|
+
if @concurrent
|
|
98
|
+
@lock.synchronize { @handlers << Thread.new(client) { |connection| answer(connection) } }
|
|
99
|
+
else
|
|
100
|
+
answer_serially(client)
|
|
101
|
+
end
|
|
46
102
|
end
|
|
47
103
|
rescue IOError, Errno::EBADF
|
|
48
104
|
nil # The listener was closed.
|
|
49
105
|
end
|
|
50
106
|
|
|
107
|
+
def answer_serially(client)
|
|
108
|
+
request = read_request(client)
|
|
109
|
+
return unless request
|
|
110
|
+
|
|
111
|
+
@requests << request
|
|
112
|
+
@respond.call(client)
|
|
113
|
+
rescue Errno::EPIPE, Errno::ECONNRESET
|
|
114
|
+
nil # The client stopped reading, which some specs do on purpose.
|
|
115
|
+
ensure
|
|
116
|
+
client.close
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def answer(client)
|
|
120
|
+
request = read_request(client)
|
|
121
|
+
return unless request
|
|
122
|
+
|
|
123
|
+
@lock.synchronize do
|
|
124
|
+
@in_flight += 1
|
|
125
|
+
@peak = [ @peak, @in_flight ].max
|
|
126
|
+
request.in_flight = @in_flight
|
|
127
|
+
@requests << request
|
|
128
|
+
@changed.broadcast
|
|
129
|
+
end
|
|
130
|
+
begin
|
|
131
|
+
response = @respond.call(request)
|
|
132
|
+
ensure
|
|
133
|
+
@lock.synchronize do
|
|
134
|
+
@in_flight -= 1
|
|
135
|
+
@changed.broadcast
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
Array(response).each { |piece| piece.respond_to?(:call) ? piece.call : client.write(piece) }
|
|
139
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, IOError
|
|
140
|
+
nil # The client stopped reading or the server is closing.
|
|
141
|
+
ensure
|
|
142
|
+
client.close
|
|
143
|
+
end
|
|
144
|
+
|
|
51
145
|
def read_request(client)
|
|
52
|
-
|
|
146
|
+
line = client.gets("\r\n")
|
|
147
|
+
return unless line
|
|
148
|
+
|
|
149
|
+
method, target = line.split
|
|
53
150
|
headers = {}
|
|
54
|
-
while (
|
|
55
|
-
name, value =
|
|
151
|
+
while (header = client.gets("\r\n")) && header != "\r\n"
|
|
152
|
+
name, value = header.chomp("\r\n").split(": ", 2)
|
|
56
153
|
headers[name.downcase] = value
|
|
57
154
|
end
|
|
58
|
-
|
|
155
|
+
length = headers["content-length"].to_i
|
|
156
|
+
body = length.positive? ? client.read(length).to_s : ""
|
|
157
|
+
Request.new(target, headers, method, body.b)
|
|
59
158
|
end
|
|
60
159
|
end
|