files.com 1.1.812 → 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.
Files changed (462) hide show
  1. checksums.yaml +4 -4
  2. data/_VERSION +1 -1
  3. data/doc/Files/APIConnectionError.html +1 -1
  4. data/doc/Files/APIError.html +1 -1
  5. data/doc/Files/AccountAlreadyExistsError.html +1 -1
  6. data/doc/Files/AccountLineItem.html +1 -1
  7. data/doc/Files/AccountOverdueError.html +1 -1
  8. data/doc/Files/Action.html +1 -1
  9. data/doc/Files/ActionLog.html +1 -1
  10. data/doc/Files/ActionNotificationExport.html +1 -1
  11. data/doc/Files/ActionNotificationExportResult.html +1 -1
  12. data/doc/Files/AdditionalAuthenticationRequiredError.html +1 -1
  13. data/doc/Files/AgentNode.html +1 -1
  14. data/doc/Files/AgentNodeConnection.html +1 -1
  15. data/doc/Files/AgentNodeInstance.html +1 -1
  16. data/doc/Files/AgentPushUpdate.html +1 -1
  17. data/doc/Files/AgentPushUpdateBlockedError.html +1 -1
  18. data/doc/Files/AgentUnavailableError.html +1 -1
  19. data/doc/Files/AgentUpgradeRequiredError.html +1 -1
  20. data/doc/Files/AiAssistantPersonality.html +1 -1
  21. data/doc/Files/AiTask.html +1 -1
  22. data/doc/Files/AiTaskCannotBeRunManuallyError.html +1 -1
  23. data/doc/Files/AlreadyCompletedError.html +1 -1
  24. data/doc/Files/Api.html +20 -13
  25. data/doc/Files/ApiClient/RequestLogContext.html +29 -29
  26. data/doc/Files/ApiClient/TransferSettings.html +721 -0
  27. data/doc/Files/ApiClient.html +756 -204
  28. data/doc/Files/ApiKey.html +1 -1
  29. data/doc/Files/ApiKeyIsDisabledError.html +1 -1
  30. data/doc/Files/ApiKeyIsPathRestrictedError.html +1 -1
  31. data/doc/Files/ApiKeyNotFoundError.html +1 -1
  32. data/doc/Files/ApiKeyOnlyForDesktopAppError.html +1 -1
  33. data/doc/Files/ApiKeyOnlyForFileOperationsError.html +1 -1
  34. data/doc/Files/ApiKeyOnlyForMobileAppError.html +1 -1
  35. data/doc/Files/ApiKeyOnlyForOfficeIntegrationError.html +1 -1
  36. data/doc/Files/ApiKeySessionsNotSupportedError.html +1 -1
  37. data/doc/Files/ApiRequestLog.html +1 -1
  38. data/doc/Files/App.html +1 -1
  39. data/doc/Files/As2IncomingMessage.html +1 -1
  40. data/doc/Files/As2OutgoingMessage.html +1 -1
  41. data/doc/Files/As2Partner.html +1 -1
  42. data/doc/Files/As2Station.html +1 -1
  43. data/doc/Files/AttachmentTooLargeError.html +1 -1
  44. data/doc/Files/AuthenticationError.html +1 -1
  45. data/doc/Files/AuthenticationRequiredError.html +1 -1
  46. data/doc/Files/Auto.html +1 -1
  47. data/doc/Files/Automation.html +1 -1
  48. data/doc/Files/AutomationAuthoringSchema.html +1 -1
  49. data/doc/Files/AutomationCannotBeRunManuallyError.html +1 -1
  50. data/doc/Files/AutomationExecutionNode.html +1 -1
  51. data/doc/Files/AutomationLog.html +1 -1
  52. data/doc/Files/AutomationRun.html +1 -1
  53. data/doc/Files/AutomationsUnavailableError.html +1 -1
  54. data/doc/Files/BadRequestError.html +1 -1
  55. data/doc/Files/BandwidthSnapshot.html +1 -1
  56. data/doc/Files/Behavior.html +1 -1
  57. data/doc/Files/BehaviorNotAllowedOnRemoteServerError.html +1 -1
  58. data/doc/Files/BillingInformationHiddenError.html +1 -1
  59. data/doc/Files/BillingPermissionRequiredError.html +1 -1
  60. data/doc/Files/BufferedUploadDisabledForThisDestinationError.html +1 -1
  61. data/doc/Files/Bundle.html +1 -1
  62. data/doc/Files/BundleAction.html +1 -1
  63. data/doc/Files/BundleDownload.html +1 -1
  64. data/doc/Files/BundleMaximumUsesReachedError.html +1 -1
  65. data/doc/Files/BundleNotification.html +1 -1
  66. data/doc/Files/BundleOnlyAllowsPreviewsError.html +1 -1
  67. data/doc/Files/BundleOperationRequiresSubfolderError.html +1 -1
  68. data/doc/Files/BundlePath.html +1 -1
  69. data/doc/Files/BundlePathNotFoundError.html +1 -1
  70. data/doc/Files/BundlePermissionRequiredError.html +1 -1
  71. data/doc/Files/BundleRecipient.html +1 -1
  72. data/doc/Files/BundleRegistration.html +1 -1
  73. data/doc/Files/BundleRegistrationCodeFailedError.html +1 -1
  74. data/doc/Files/BundleRegistrationNotFoundError.html +1 -1
  75. data/doc/Files/CannotAdministerHigherLevelUserError.html +1 -1
  76. data/doc/Files/CannotDownloadDirectoryError.html +1 -1
  77. data/doc/Files/CannotLoginWhileUsingKeyError.html +1 -1
  78. data/doc/Files/CantActForOtherUserError.html +1 -1
  79. data/doc/Files/CantMoveWithMultipleLocationsError.html +1 -1
  80. data/doc/Files/ChatMessage.html +1 -1
  81. data/doc/Files/ChatSession.html +1 -1
  82. data/doc/Files/ChildSiteManagementPolicy.html +1 -1
  83. data/doc/Files/Clickwrap.html +1 -1
  84. data/doc/Files/CodeNotFoundError.html +1 -1
  85. data/doc/Files/ConfigurationLockedPathError.html +1 -1
  86. data/doc/Files/ContactAdminForPasswordChangeHelpError.html +1 -1
  87. data/doc/Files/CouldNotCreateParentError.html +1 -1
  88. data/doc/Files/CustomDomain.html +1 -1
  89. data/doc/Files/DatetimeParseError.html +1 -1
  90. data/doc/Files/DesktopConfigurationProfile.html +1 -1
  91. data/doc/Files/DestinationExistsError.html +1 -1
  92. data/doc/Files/DestinationFolderLimitedError.html +1 -1
  93. data/doc/Files/DestinationParentConflictError.html +1 -1
  94. data/doc/Files/DestinationParentDoesNotExistError.html +1 -1
  95. data/doc/Files/DestinationSameError.html +1 -1
  96. data/doc/Files/DestinationSiteMismatchError.html +1 -1
  97. data/doc/Files/DirectConnectionInfo.html +1 -1
  98. data/doc/Files/DnsRecord.html +1 -1
  99. data/doc/Files/DoesNotSupportSortingError.html +1 -1
  100. data/doc/Files/DuplicateShareRecipientError.html +1 -1
  101. data/doc/Files/EmailIncomingMessage.html +1 -1
  102. data/doc/Files/EmailLog.html +1 -1
  103. data/doc/Files/Error.html +2 -2
  104. data/doc/Files/Errors.html +1 -1
  105. data/doc/Files/EventChannel.html +1 -1
  106. data/doc/Files/EventDeliveryAttempt.html +1 -1
  107. data/doc/Files/EventRecord.html +1 -1
  108. data/doc/Files/EventSubscription.html +1 -1
  109. data/doc/Files/EventTarget.html +1 -1
  110. data/doc/Files/ExavaultApiRequestLog.html +1 -1
  111. data/doc/Files/ExceededRuntimeLimitError.html +1 -1
  112. data/doc/Files/Expectation.html +1 -1
  113. data/doc/Files/ExpectationAlreadyHasOpenWindowError.html +1 -1
  114. data/doc/Files/ExpectationEvaluation.html +1 -1
  115. data/doc/Files/ExpectationIncident.html +1 -1
  116. data/doc/Files/ExpectationNotManualTriggerError.html +1 -1
  117. data/doc/Files/ExpiredPrivateKeyError.html +1 -1
  118. data/doc/Files/ExpiredPublicKeyError.html +1 -1
  119. data/doc/Files/ExportFailureError.html +1 -1
  120. data/doc/Files/ExportNotReadyError.html +1 -1
  121. data/doc/Files/ExternalEvent.html +1 -1
  122. data/doc/Files/FailedToChangePasswordError.html +1 -1
  123. data/doc/Files/File.html +2019 -1515
  124. data/doc/Files/FileAction.html +1 -1
  125. data/doc/Files/FileActionError.html +1 -1
  126. data/doc/Files/FileComment.html +1 -1
  127. data/doc/Files/FileCommentReaction.html +1 -1
  128. data/doc/Files/FileLockedError.html +1 -1
  129. data/doc/Files/FileMigration.html +1 -1
  130. data/doc/Files/FileMigrationLog.html +1 -1
  131. data/doc/Files/FileNotFoundError.html +1 -1
  132. data/doc/Files/FileNotUploadedError.html +1 -1
  133. data/doc/Files/FilePendingProcessingError.html +1 -1
  134. data/doc/Files/FileProcessingErrorError.html +1 -1
  135. data/doc/Files/FileTooBigToDecryptError.html +1 -1
  136. data/doc/Files/FileTooBigToEncryptError.html +1 -1
  137. data/doc/Files/FileUploadNotFoundError.html +1 -1
  138. data/doc/Files/FileUploadPart.html +1 -1
  139. data/doc/Files/FileUploadedToWrongRegionError.html +1 -1
  140. data/doc/Files/FileUtils.html +1 -1
  141. data/doc/Files/FilenameTooLongError.html +1 -1
  142. data/doc/Files/FilesAgentFailedAuthorizationError.html +1 -1
  143. data/doc/Files/FilesNativeStorageOverloadedError.html +1 -1
  144. data/doc/Files/Folder.html +1 -1
  145. data/doc/Files/FolderAdminOrBillingPermissionRequiredError.html +1 -1
  146. data/doc/Files/FolderAdminPermissionRequiredError.html +1 -1
  147. data/doc/Files/FolderLockedError.html +1 -1
  148. data/doc/Files/FolderMustNotBeAFileError.html +1 -1
  149. data/doc/Files/FolderNotEmptyError.html +1 -1
  150. data/doc/Files/FoldersNotAllowedError.html +1 -1
  151. data/doc/Files/FormField.html +1 -1
  152. data/doc/Files/FormFieldSet.html +1 -1
  153. data/doc/Files/FtpActionLog.html +1 -1
  154. data/doc/Files/FullPermissionRequiredError.html +1 -1
  155. data/doc/Files/GpgKey.html +1 -1
  156. data/doc/Files/Group.html +1 -1
  157. data/doc/Files/GroupNotFoundError.html +1 -1
  158. data/doc/Files/GroupUser.html +1 -1
  159. data/doc/Files/History.html +1 -1
  160. data/doc/Files/HistoryExport.html +1 -1
  161. data/doc/Files/HistoryExportResult.html +1 -1
  162. data/doc/Files/HistoryPermissionRequiredError.html +1 -1
  163. data/doc/Files/HistoryUnavailableError.html +1 -1
  164. data/doc/Files/HolidayCalendar.html +1 -1
  165. data/doc/Files/HolidayRegion.html +1 -1
  166. data/doc/Files/Image.html +1 -1
  167. data/doc/Files/InAppAiAssistantUnavailableError.html +1 -1
  168. data/doc/Files/InboundS3Log.html +1 -1
  169. data/doc/Files/InboxNotFoundError.html +1 -1
  170. data/doc/Files/InboxRecipient.html +1 -1
  171. data/doc/Files/InboxRegistration.html +1 -1
  172. data/doc/Files/InboxRegistrationCodeFailedError.html +1 -1
  173. data/doc/Files/InboxUpload.html +1 -1
  174. data/doc/Files/InsufficientPermissionForParamsError.html +1 -1
  175. data/doc/Files/InsufficientPermissionForSiteError.html +1 -1
  176. data/doc/Files/IntegrationCentricProfile.html +1 -1
  177. data/doc/Files/InternalGeneralErrorError.html +1 -1
  178. data/doc/Files/InvalidBodyError.html +1 -1
  179. data/doc/Files/InvalidBundleCodeError.html +1 -1
  180. data/doc/Files/InvalidCredentialsError.html +1 -1
  181. data/doc/Files/InvalidCursorError.html +1 -1
  182. data/doc/Files/InvalidCursorTypeForSortError.html +1 -1
  183. data/doc/Files/InvalidEtagsError.html +1 -1
  184. data/doc/Files/InvalidFileTypeError.html +1 -1
  185. data/doc/Files/InvalidFilenameError.html +1 -1
  186. data/doc/Files/InvalidFilterAliasCombinationError.html +1 -1
  187. data/doc/Files/InvalidFilterFieldError.html +1 -1
  188. data/doc/Files/InvalidFilterParamError.html +1 -1
  189. data/doc/Files/InvalidFilterParamFormatError.html +1 -1
  190. data/doc/Files/InvalidFilterParamValueError.html +1 -1
  191. data/doc/Files/InvalidInputEncodingError.html +1 -1
  192. data/doc/Files/InvalidInterfaceError.html +1 -1
  193. data/doc/Files/InvalidOauthError.html +1 -1
  194. data/doc/Files/InvalidOauthProviderError.html +1 -1
  195. data/doc/Files/InvalidOrExpiredCodeError.html +1 -1
  196. data/doc/Files/InvalidParameterError.html +5 -1
  197. data/doc/Files/InvalidPathError.html +1 -1
  198. data/doc/Files/InvalidPriorityColorError.html +1 -1
  199. data/doc/Files/InvalidRangeError.html +1 -1
  200. data/doc/Files/InvalidReturnToUrlError.html +1 -1
  201. data/doc/Files/InvalidSearchQueryError.html +1 -1
  202. data/doc/Files/InvalidSessionError.html +1 -1
  203. data/doc/Files/InvalidSiteError.html +1 -1
  204. data/doc/Files/InvalidSortFieldError.html +1 -1
  205. data/doc/Files/InvalidSortFilterCombinationError.html +1 -1
  206. data/doc/Files/InvalidUploadOffsetError.html +1 -1
  207. data/doc/Files/InvalidUploadPartGapError.html +1 -1
  208. data/doc/Files/InvalidUploadPartSizeError.html +1 -1
  209. data/doc/Files/InvalidUsernameOrPasswordError.html +1 -1
  210. data/doc/Files/InvalidWorkspaceIdHeaderError.html +1 -1
  211. data/doc/Files/InvalidZipFileError.html +1 -1
  212. data/doc/Files/Invoice.html +1 -1
  213. data/doc/Files/InvoiceLineItem.html +1 -1
  214. data/doc/Files/IpAddress.html +1 -1
  215. data/doc/Files/KeyLifecycleRule.html +1 -1
  216. data/doc/Files/List.html +1 -1
  217. data/doc/Files/Lock.html +1 -1
  218. data/doc/Files/LockOperationBusyError.html +1 -1
  219. data/doc/Files/LockedOutError.html +1 -1
  220. data/doc/Files/LockoutRegionMismatchError.html +1 -1
  221. data/doc/Files/MetadataCategory.html +1 -1
  222. data/doc/Files/MetadataNotSupportedOnRemotesError.html +1 -1
  223. data/doc/Files/MethodNotAllowedError.html +1 -1
  224. data/doc/Files/MigrationInProgressError.html +1 -1
  225. data/doc/Files/MissingParameterError.html +1 -1
  226. data/doc/Files/ModelSaveErrorError.html +1 -1
  227. data/doc/Files/MoverAccessDeniedError.html +1 -1
  228. data/doc/Files/MoverPackageRequiredError.html +1 -1
  229. data/doc/Files/MultipleProcessingErrorsError.html +1 -1
  230. data/doc/Files/MultipleSortParamsNotAllowedError.html +1 -1
  231. data/doc/Files/MustAuthenticateWithApiKeyError.html +1 -1
  232. data/doc/Files/NeedAdminPermissionForInboxError.html +1 -1
  233. data/doc/Files/NestedNotFoundError.html +1 -1
  234. data/doc/Files/NoAccountForSiteError.html +1 -1
  235. data/doc/Files/NoValidInputParamsError.html +1 -1
  236. data/doc/Files/NonAdminsMustQueryByFolderOrPathError.html +1 -1
  237. data/doc/Files/NotAllowedToCreateBundleError.html +1 -1
  238. data/doc/Files/NotAuthenticatedError.html +1 -1
  239. data/doc/Files/NotAuthorizedError.html +1 -1
  240. data/doc/Files/NotEnqueuableSyncError.html +1 -1
  241. data/doc/Files/NotFoundError.html +1 -1
  242. data/doc/Files/NotImplementedError.html +1 -1
  243. data/doc/Files/Notification.html +1 -1
  244. data/doc/Files/OffsetUploadNotAllowedWithMalwareScanningError.html +1 -1
  245. data/doc/Files/OneTimePasswordIncorrectError.html +1 -1
  246. data/doc/Files/OutboundConnectionLog.html +1 -1
  247. data/doc/Files/PartNumberTooLargeError.html +1 -1
  248. data/doc/Files/Partner.html +1 -1
  249. data/doc/Files/PartnerChannel.html +1 -1
  250. data/doc/Files/PartnerChannelTemplate.html +1 -1
  251. data/doc/Files/PartnerConnection.html +1 -1
  252. data/doc/Files/PartnerSite.html +1 -1
  253. data/doc/Files/PartnerSiteRequest.html +1 -1
  254. data/doc/Files/PasswordChangeNotRequiredError.html +1 -1
  255. data/doc/Files/PasswordChangeRequiredError.html +1 -1
  256. data/doc/Files/PathCannotHaveTrailingWhitespaceError.html +1 -1
  257. data/doc/Files/PathTooLongError.html +1 -1
  258. data/doc/Files/PathUtil.html +1 -1
  259. data/doc/Files/Payment.html +1 -1
  260. data/doc/Files/PaymentLineItem.html +1 -1
  261. data/doc/Files/PaymentMethodErrorError.html +1 -1
  262. data/doc/Files/PendingWorkEvent.html +1 -1
  263. data/doc/Files/Permission.html +1 -1
  264. data/doc/Files/PlanNotFoundError.html +1 -1
  265. data/doc/Files/Preview.html +1 -1
  266. data/doc/Files/PreviewOnlyPermissionCannotDownloadError.html +1 -1
  267. data/doc/Files/ProcessingFailureError.html +1 -1
  268. data/doc/Files/PublicHostingRequestLog.html +1 -1
  269. data/doc/Files/PublicIpAddress.html +1 -1
  270. data/doc/Files/PublicKey.html +1 -1
  271. data/doc/Files/RateLimitedError.html +1 -1
  272. data/doc/Files/ReadOnlySessionError.html +1 -1
  273. data/doc/Files/ReadPermissionRequiredError.html +1 -1
  274. data/doc/Files/ReauthenticationFailedError.html +1 -1
  275. data/doc/Files/ReauthenticationFailedFinalError.html +1 -1
  276. data/doc/Files/ReauthenticationNeededActionError.html +1 -1
  277. data/doc/Files/ReauthenticationNeededFieldsError.html +1 -1
  278. data/doc/Files/ReauthenticationRateLimitedError.html +1 -1
  279. data/doc/Files/RecaptchaFailedError.html +1 -1
  280. data/doc/Files/RecipientAlreadySharedError.html +1 -1
  281. data/doc/Files/RemoteBandwidthSnapshot.html +1 -1
  282. data/doc/Files/RemoteDesktopDebugLoggingDisabledError.html +1 -1
  283. data/doc/Files/RemoteEntryReadOnlyError.html +1 -1
  284. data/doc/Files/RemoteMountBackend.html +1 -1
  285. data/doc/Files/RemoteServer.html +1 -1
  286. data/doc/Files/RemoteServerConfigurationFile.html +1 -1
  287. data/doc/Files/RemoteServerCredential.html +1 -1
  288. data/doc/Files/RemoteServerErrorError.html +1 -1
  289. data/doc/Files/Request.html +1 -1
  290. data/doc/Files/RequestBodyTooLargeError.html +1 -1
  291. data/doc/Files/RequestParamsContainInvalidCharacterError.html +1 -1
  292. data/doc/Files/RequestParamsInvalidError.html +1 -1
  293. data/doc/Files/RequestParamsRequiredError.html +1 -1
  294. data/doc/Files/ResourceBelongsToParentSiteError.html +1 -1
  295. data/doc/Files/ResourceLockedError.html +1 -1
  296. data/doc/Files/Response.html +1 -1
  297. data/doc/Files/Restore.html +1 -1
  298. data/doc/Files/RootFolderBehaviorSiteAdminRequiredError.html +1 -1
  299. data/doc/Files/RootFolderBehaviorSkipSiteAdminRequiredError.html +1 -1
  300. data/doc/Files/Schedule.html +1 -1
  301. data/doc/Files/ScheduledExport.html +1 -1
  302. data/doc/Files/ScimLog.html +1 -1
  303. data/doc/Files/SearchAllOnChildPathError.html +1 -1
  304. data/doc/Files/SearchUnavailableError.html +1 -1
  305. data/doc/Files/Secret.html +1 -1
  306. data/doc/Files/SelfManagedRequiredError.html +1 -1
  307. data/doc/Files/ServiceUnavailableError.html +1 -1
  308. data/doc/Files/Session.html +1 -1
  309. data/doc/Files/SettingsChange.html +1 -1
  310. data/doc/Files/SftpActionLog.html +1 -1
  311. data/doc/Files/SftpHostKey.html +1 -1
  312. data/doc/Files/ShareGroup.html +1 -1
  313. data/doc/Files/ShareGroupMember.html +1 -1
  314. data/doc/Files/SiemHttpDestination.html +1 -1
  315. data/doc/Files/SiemHttpDestinationEvent.html +1 -1
  316. data/doc/Files/Site.html +1 -1
  317. data/doc/Files/SiteAdminOrPartnerAdminPermissionRequiredError.html +1 -1
  318. data/doc/Files/SiteAdminOrWorkspaceAdminOrFolderAdminPermissionRequiredError.html +1 -1
  319. data/doc/Files/SiteAdminOrWorkspaceAdminOrPartnerAdminOrFolderAdminPermissionRequiredError.html +1 -1
  320. data/doc/Files/SiteAdminOrWorkspaceAdminOrPartnerAdminPermissionRequiredError.html +1 -1
  321. data/doc/Files/SiteAdminOrWorkspaceAdminPermissionRequiredError.html +1 -1
  322. data/doc/Files/SiteAdminRequiredError.html +1 -1
  323. data/doc/Files/SiteConfigurationError.html +1 -1
  324. data/doc/Files/SiteDisabledError.html +1 -1
  325. data/doc/Files/SiteFilesAreImmutableError.html +1 -1
  326. data/doc/Files/SiteNotFoundError.html +1 -1
  327. data/doc/Files/SiteSubdomainRedirect.html +1 -1
  328. data/doc/Files/SiteWasRemovedError.html +1 -1
  329. data/doc/Files/SizableIO.html +43 -41
  330. data/doc/Files/Snapshot.html +1 -1
  331. data/doc/Files/SsoEvent.html +1 -1
  332. data/doc/Files/SsoStrategy.html +1 -1
  333. data/doc/Files/Status.html +1 -1
  334. data/doc/Files/Style.html +1 -1
  335. data/doc/Files/SubfolderLockedError.html +1 -1
  336. data/doc/Files/Sync.html +1 -1
  337. data/doc/Files/SyncInProgressError.html +1 -1
  338. data/doc/Files/SyncLog.html +1 -1
  339. data/doc/Files/SyncRun.html +1 -1
  340. data/doc/Files/SyncRunLiveTransfer.html +1 -1
  341. data/doc/Files/SystemProfiler.html +1 -1
  342. data/doc/Files/TooManyConcurrentLoginsError.html +1 -1
  343. data/doc/Files/TooManyConcurrentRequestsError.html +1 -1
  344. data/doc/Files/TooManyLoginAttemptsError.html +1 -1
  345. data/doc/Files/TooManyRequestsError.html +1 -1
  346. data/doc/Files/TooManySharesError.html +1 -1
  347. data/doc/Files/Transfer/BufferLimitError.html +175 -0
  348. data/doc/Files/Transfer/ByteBudget/Reservation.html +301 -0
  349. data/doc/Files/Transfer/ByteBudget.html +748 -0
  350. data/doc/Files/Transfer/CancelledError.html +235 -0
  351. data/doc/Files/Transfer/Checkpoint.html +1371 -0
  352. data/doc/Files/Transfer/CheckpointError.html +173 -0
  353. data/doc/Files/Transfer/ClientTransport/ClientResponse.html +661 -0
  354. data/doc/Files/Transfer/ClientTransport/Reader.html +530 -0
  355. data/doc/Files/Transfer/ClientTransport.html +675 -0
  356. data/doc/Files/Transfer/Clock.html +189 -0
  357. data/doc/Files/Transfer/Control.html +667 -0
  358. data/doc/Files/Transfer/Controller/Attempt.html +465 -0
  359. data/doc/Files/Transfer/Controller.html +1166 -0
  360. data/doc/Files/Transfer/Coordinator/Timing.html +568 -0
  361. data/doc/Files/Transfer/Coordinator.html +534 -0
  362. data/doc/Files/Transfer/DownloadCoordinator/FileSink.html +976 -0
  363. data/doc/Files/Transfer/DownloadCoordinator/IoSink.html +1168 -0
  364. data/doc/Files/Transfer/DownloadCoordinator/Piece.html +875 -0
  365. data/doc/Files/Transfer/DownloadCoordinator.html +728 -0
  366. data/doc/Files/Transfer/FileSlots.html +480 -0
  367. data/doc/Files/Transfer/FrontendResponseError.html +235 -0
  368. data/doc/Files/Transfer/Holds.html +505 -0
  369. data/doc/Files/Transfer/HttpTransport.html +579 -0
  370. data/doc/Files/Transfer/Job.html +2338 -0
  371. data/doc/Files/Transfer/Options.html +924 -0
  372. data/doc/Files/Transfer/PausedError.html +319 -0
  373. data/doc/Files/Transfer/Policy.html +1642 -0
  374. data/doc/Files/Transfer/RequestPermits.html +160 -0
  375. data/doc/Files/Transfer/SourceChangedError.html +235 -0
  376. data/doc/Files/Transfer/StorageResponse.html +546 -0
  377. data/doc/Files/Transfer/StorageRetry.html +1098 -0
  378. data/doc/Files/Transfer/UploadCoordinator/Grant.html +1048 -0
  379. data/doc/Files/Transfer/UploadCoordinator/Part.html +1048 -0
  380. data/doc/Files/Transfer/UploadCoordinator/Region.html +295 -0
  381. data/doc/Files/Transfer/UploadCoordinator/Segments.html +299 -0
  382. data/doc/Files/Transfer/UploadCoordinator/Source.html +725 -0
  383. data/doc/Files/Transfer/UploadCoordinator/StreamSource.html +664 -0
  384. data/doc/Files/Transfer/UploadCoordinator.html +546 -0
  385. data/doc/Files/Transfer/UploadPlan.html +522 -0
  386. data/doc/Files/Transfer/VersionChangedError.html +235 -0
  387. data/doc/Files/Transfer.html +134 -0
  388. data/doc/Files/TrialExpiredError.html +1 -1
  389. data/doc/Files/TrialLockedError.html +1 -1
  390. data/doc/Files/TwoFactorAuthenticationCodeAlreadySentError.html +1 -1
  391. data/doc/Files/TwoFactorAuthenticationCountryBlacklistedError.html +1 -1
  392. data/doc/Files/TwoFactorAuthenticationErrorError.html +1 -1
  393. data/doc/Files/TwoFactorAuthenticationGeneralErrorError.html +1 -1
  394. data/doc/Files/TwoFactorAuthenticationMethodUnsupportedErrorError.html +1 -1
  395. data/doc/Files/TwoFactorAuthenticationRequiredError.html +1 -1
  396. data/doc/Files/TwoFactorAuthenticationSetupExpiredError.html +1 -1
  397. data/doc/Files/TwoFactorAuthenticationUnsubscribedRecipientError.html +1 -1
  398. data/doc/Files/URI.html +1 -1
  399. data/doc/Files/UnrecognizedSortIndexError.html +1 -1
  400. data/doc/Files/UnsupportedCurrencyError.html +1 -1
  401. data/doc/Files/UnsupportedHttpResponseFormatError.html +1 -1
  402. data/doc/Files/UnsupportedMediaTypeError.html +1 -1
  403. data/doc/Files/UpdatesNotAllowedForRemotesError.html +1 -1
  404. data/doc/Files/UploadsUnavailableError.html +1 -1
  405. data/doc/Files/UsageByTopLevelDir.html +1 -1
  406. data/doc/Files/UsageDailySnapshot.html +1 -1
  407. data/doc/Files/UsageSnapshot.html +1 -1
  408. data/doc/Files/User.html +1 -1
  409. data/doc/Files/UserAdditionalEmailRecipient.html +1 -1
  410. data/doc/Files/UserCipherUse.html +1 -1
  411. data/doc/Files/UserIdInvalidError.html +1 -1
  412. data/doc/Files/UserIdOnUserEndpointError.html +1 -1
  413. data/doc/Files/UserIdWithoutSiteAdminError.html +1 -1
  414. data/doc/Files/UserLifecycleRule.html +1 -1
  415. data/doc/Files/UserNotFoundError.html +1 -1
  416. data/doc/Files/UserRequest.html +1 -1
  417. data/doc/Files/UserRequestsEnabledRequiredError.html +1 -1
  418. data/doc/Files/UserRequiredError.html +1 -1
  419. data/doc/Files/UserSecurityEvent.html +1 -1
  420. data/doc/Files/UserSftpClientUse.html +1 -1
  421. data/doc/Files/Util/LogSettings.html +1 -1
  422. data/doc/Files/Util.html +1 -1
  423. data/doc/Files/WebDavActionLog.html +1 -1
  424. data/doc/Files/WebhookTest.html +1 -1
  425. data/doc/Files/Workspace.html +1 -1
  426. data/doc/Files/WriteAndBundlePermissionRequiredError.html +1 -1
  427. data/doc/Files/WritePermissionRequiredError.html +1 -1
  428. data/doc/Files/ZipListEntry.html +1 -1
  429. data/doc/Files.html +116 -116
  430. data/doc/_index.html +314 -4
  431. data/doc/class_list.html +1 -1
  432. data/doc/file.README.html +1 -1
  433. data/doc/index.html +1 -1
  434. data/doc/method_list.html +4703 -2415
  435. data/doc/top-level-namespace.html +1 -1
  436. data/docs/file.md +346 -0
  437. data/lib/files.com/api.rb +6 -1
  438. data/lib/files.com/api_client.rb +220 -32
  439. data/lib/files.com/models/file.rb +124 -17
  440. data/lib/files.com/sizable_io.rb +1 -0
  441. data/lib/files.com/transfer/budgets.rb +123 -0
  442. data/lib/files.com/transfer/checkpoint.rb +113 -0
  443. data/lib/files.com/transfer/clock.rb +14 -0
  444. data/lib/files.com/transfer/controller.rb +549 -0
  445. data/lib/files.com/transfer/download_coordinator.rb +906 -0
  446. data/lib/files.com/transfer/errors.rb +100 -0
  447. data/lib/files.com/transfer/http_transport.rb +337 -0
  448. data/lib/files.com/transfer/job.rb +778 -0
  449. data/lib/files.com/transfer/options.rb +126 -0
  450. data/lib/files.com/transfer/policy.rb +91 -0
  451. data/lib/files.com/transfer/storage_retry.rb +169 -0
  452. data/lib/files.com/transfer/upload_coordinator.rb +657 -0
  453. data/lib/files.com/transfer/upload_plan.rb +157 -0
  454. data/lib/files.com.rb +13 -0
  455. data/spec/lib/api_client_spec.rb +125 -3
  456. data/spec/models/file_transfer_spec.rb +70 -4
  457. data/spec/support/transfer_server.rb +113 -14
  458. data/spec/transfer/adaptive_download_spec.rb +1102 -0
  459. data/spec/transfer/adaptive_upload_spec.rb +1602 -0
  460. data/spec/transfer/concurrency_controller_spec.rb +382 -0
  461. data/spec/workspace_id_spec.rb +19 -14
  462. metadata +59 -1
@@ -0,0 +1,906 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module Files
6
+ module Transfer
7
+ # Downloads one remote file through a Job, in ranges fetched concurrently when storage binds a version.
8
+ #
9
+ # Its API requests, the download URL's lookup and its renewals, use the context the caller's thread fixed from the
10
+ # record's options as the download began (Coordinator.api_context), carried on as a lookup moves to a regional
11
+ # origin. Its content requests go through ApiClient.download_client as the caller's thread resolves it when the
12
+ # download begins: over the job's own connections when that client is the SDK's own with its default connection
13
+ # unchanged, otherwise through the client itself, one request at a time, its responses streamed (see
14
+ # ClientTransport); a client whose adapter cannot stream is refused before anything is requested or written. On the
15
+ # caller's thread it first asks for the opening range (8 MiB, or a quarter of buffer_bytes when that is less) with
16
+ # Accept-Encoding: identity. A 206 with a strong ETag and an exact Content-Range and Content-Length binds the
17
+ # download to that version: every later range is requested with If-Match and must come back as a 206 with the
18
+ # exact Content-Range, total, length and ETag, and a 412, or a whole response with any other ETag, stops it with
19
+ # VersionChangedError before anything of it is written. Ranges complete in any order, each read straight into one
20
+ # String of its length, wait in memory within buffer_bytes, and are written to the destination in order on the
21
+ # caller's thread, which progress, when given, is then called on. A range is on the network, for its controller,
22
+ # until its whole body has arrived and been checked. Every request, the opening one and a proof included, waits
23
+ # while a throttle's Retry-After holds its destination back for the job (Holds), and publishes one it gets before
24
+ # its request permit is given back. A range goes to the URL it was admitted for, even once that has been renewed,
25
+ # and a 403 for it is judged by that URL's own expiry.
26
+ #
27
+ # Without a strong ETag, storage's whole response is streamed instead, as one request. A retry of it starts again
28
+ # from the first byte: the destination, a local file or an IO that can seek back, is truncated to where it started
29
+ # only as the retry's first bytes are written; any other IO gets the failure instead of the same bytes twice. A
30
+ # cancellation or pause keeps what was written. Bytes are written exactly as stored, never decompressed.
31
+ #
32
+ # A resumed download first checks, before it sends or changes anything, that its destination is the same one and
33
+ # still holds exactly the bytes it wrote, by reading them back, a slice at a time charged to buffer_bytes, and
34
+ # comparing their SHA-256 digest; a destination that cannot give them back refuses the resume. Anything after those
35
+ # bytes stays until storage's response has proved its version: until the first new bytes are written, or, when every
36
+ # byte was written before the pause, until storage answers for the file's last byte with that version.
37
+ #
38
+ # A response carrying X-Files-Frontend-App came from the Files.com web app, not storage, and stops the download
39
+ # with FrontendResponseError before any of it is written. A refused transfer or a failed write closes a caller's
40
+ # IO, as without transfer:; other failures leave it open, holding what was written.
41
+ class DownloadCoordinator < Coordinator
42
+ RENEWALS = 2
43
+ MISMATCH = "Transfer response did not match the request"
44
+ EXPIRED = "The download URL the API returned has expired, and renewing it did not give one still valid"
45
+ # The most of a resumed destination's bytes read back at once to check them; never more than a range.
46
+ VERIFY_CHUNK_BYTES = 1024 * 1024
47
+ ControlStop = Class.new(StandardError)
48
+
49
+ Piece = Struct.new(:number, :offset, :bytesize, :reservation, :attempts, :renewals, :ready_at, :data, :generation, keyword_init: true)
50
+
51
+ # A SHA-256 digest of the next bytes of io, read chunk bytes at a time, into one buffer when io takes one, or false
52
+ # when io ends before them.
53
+ def self.digest_of(io, bytes, chunk)
54
+ digest = OpenSSL::Digest.new('SHA256')
55
+ buffer = String.new(capacity: chunk, encoding: Encoding::BINARY) if takes_buffer?(io)
56
+ remaining = bytes
57
+ while remaining.positive?
58
+ wanted = [ remaining, chunk ].min
59
+ data = buffer ? io.read(wanted, buffer) : io.read(wanted)
60
+ return false if data.nil? || data.empty?
61
+
62
+ digest.update(data)
63
+ remaining -= data.bytesize
64
+ end
65
+ digest
66
+ end
67
+
68
+ # Whether io's read and pread take a buffer to read into, as an IO's, a StringIO's and a Tempfile's do.
69
+ def self.takes_buffer?(io)
70
+ io.is_a?(::IO) || io.is_a?(StringIO) || (defined?(::Tempfile) && io.is_a?(::Tempfile))
71
+ end
72
+
73
+ # The local file download_file opened, or reopened to resume. It can always start over. A restart or resume only
74
+ # positions it: what they drop is truncated as the next bytes are written, or as the download completes.
75
+ class FileSink
76
+ attr_reader :path
77
+
78
+ def initialize(file, path)
79
+ @file = file
80
+ @path = path
81
+ @pending = nil
82
+ end
83
+
84
+ def write(data)
85
+ settle!
86
+ @file.write(data)
87
+ end
88
+
89
+ def restartable?
90
+ true
91
+ end
92
+
93
+ def restart!
94
+ @pending = 0
95
+ end
96
+
97
+ def verifiable?
98
+ true
99
+ end
100
+
101
+ def identity
102
+ Checkpoint.identity(@file.stat, contents: false)
103
+ end
104
+
105
+ def holds?(bytes)
106
+ @file.size >= bytes
107
+ end
108
+
109
+ # A digest of the file's first bytes, read chunk bytes at a time, or false when it is shorter.
110
+ def prefix_digest(bytes, chunk)
111
+ @file.rewind
112
+ DownloadCoordinator.digest_of(@file, bytes, chunk)
113
+ end
114
+
115
+ # Writing continues after the first bytes; what follows them is dropped only as the next bytes are written.
116
+ def resume_at!(bytes)
117
+ @file.seek(bytes)
118
+ @pending = bytes
119
+ end
120
+
121
+ # Drops what a restart or resume left after where writing continues.
122
+ def settle!
123
+ return unless @pending
124
+
125
+ @file.truncate(@pending)
126
+ @file.seek(@pending)
127
+ @pending = nil
128
+ end
129
+
130
+ def close
131
+ @file.close unless @file.closed?
132
+ end
133
+
134
+ def close_after_failure(_error); end
135
+ end
136
+
137
+ # The caller's IO, which stays open. It can start over only when it reports its position and can seek back and
138
+ # truncate there, which it does as the next bytes are written, or as the download completes.
139
+ class IoSink
140
+ attr_reader :start
141
+
142
+ def initialize(io, start: :current)
143
+ @io = io
144
+ @start = start == :current ? position : start
145
+ @pending = false
146
+ end
147
+
148
+ def path
149
+ nil
150
+ end
151
+
152
+ def write(data)
153
+ settle!
154
+ @io << data
155
+ end
156
+
157
+ def position
158
+ @io.pos if @io.respond_to?(:pos)
159
+ rescue IOError, SystemCallError, NotImplementedError
160
+ nil
161
+ end
162
+
163
+ def restartable?
164
+ !@start.nil? && @io.respond_to?(:seek) && @io.respond_to?(:truncate)
165
+ end
166
+
167
+ def restart!
168
+ @pending = true
169
+ end
170
+
171
+ def settle!
172
+ return unless @pending
173
+
174
+ @io.seek(@start)
175
+ @io.truncate(@start)
176
+ @pending = false
177
+ end
178
+
179
+ # Whether a resume could check the bytes written: only when the IO said where they begin.
180
+ def verifiable?
181
+ !@start.nil?
182
+ end
183
+
184
+ # The IO object itself, never a number another object could reuse.
185
+ def identity
186
+ [ :io, @io, @start ]
187
+ end
188
+
189
+ def matches?(io)
190
+ io.equal?(@io)
191
+ end
192
+
193
+ def holds?(bytes)
194
+ !@start.nil? && position == @start + bytes
195
+ end
196
+
197
+ # A digest of the bytes written from start, read back chunk bytes at a time without moving where the IO writes
198
+ # next: from a StringIO's string, with pread, by seeking back and reading, or, for a File it cannot read, from its
199
+ # path while that is still the same file. A Tempfile and other delegators read back as what they delegate to.
200
+ # false when it holds fewer bytes; nil when it cannot give them back at all.
201
+ def prefix_digest(bytes, chunk)
202
+ return if @start.nil?
203
+ return string_digest(bytes, chunk) if @io.is_a?(StringIO)
204
+
205
+ digest = read_back_digest(bytes, chunk)
206
+ digest.nil? ? path_digest(bytes, chunk) : digest
207
+ end
208
+
209
+ def resume_at!(_bytes); end
210
+
211
+ def close; end
212
+
213
+ private def string_digest(bytes, chunk)
214
+ string = @io.string
215
+ return false if string.bytesize < @start + bytes
216
+
217
+ digest = OpenSSL::Digest.new('SHA256')
218
+ (0...bytes).step(chunk) { |offset| digest.update(string.byteslice(@start + offset, [ chunk, bytes - offset ].min)) }
219
+ digest
220
+ end
221
+
222
+ private def read_back_digest(bytes, chunk)
223
+ if @io.respond_to?(:pread)
224
+ pread_digest(bytes, chunk)
225
+ elsif @io.respond_to?(:seek) && @io.respond_to?(:read)
226
+ seek_digest(bytes, chunk)
227
+ end
228
+ rescue IOError, SystemCallError, NotImplementedError
229
+ nil
230
+ end
231
+
232
+ private def pread_digest(bytes, chunk)
233
+ digest = OpenSSL::Digest.new('SHA256')
234
+ buffer = String.new(capacity: chunk, encoding: Encoding::BINARY) if DownloadCoordinator.takes_buffer?(@io)
235
+ offset = 0
236
+ while offset < bytes
237
+ wanted = [ bytes - offset, chunk ].min
238
+ data = buffer ? @io.pread(wanted, @start + offset, buffer) : @io.pread(wanted, @start + offset)
239
+ return false if data.empty?
240
+
241
+ digest.update(data)
242
+ offset += data.bytesize
243
+ end
244
+ digest
245
+ rescue EOFError
246
+ false
247
+ end
248
+
249
+ private def seek_digest(bytes, chunk)
250
+ resume_from = @io.pos
251
+ begin
252
+ @io.seek(@start)
253
+ DownloadCoordinator.digest_of(@io, bytes, chunk)
254
+ ensure
255
+ @io.seek(resume_from)
256
+ end
257
+ end
258
+
259
+ private def path_digest(bytes, chunk)
260
+ return unless @io.respond_to?(:path) && @io.respond_to?(:stat) && @io.path
261
+
262
+ ::File.open(@io.path, "rb") do |file|
263
+ next unless Checkpoint.identity(file.stat, contents: false) == Checkpoint.identity(@io.stat, contents: false)
264
+
265
+ file.seek(@start)
266
+ DownloadCoordinator.digest_of(file, bytes, chunk)
267
+ end
268
+ rescue IOError, SystemCallError
269
+ nil
270
+ end
271
+
272
+ # Pipe readers learn why the download failed before the pipe closes, and a failure to close never replaces the
273
+ # download's own error.
274
+ def close_after_failure(error)
275
+ @io.do_set_error(error) if @io.respond_to?(:do_set_error)
276
+ @io.close
277
+ rescue StandardError
278
+ nil
279
+ end
280
+ end
281
+
282
+ # Opens local_path only once the job gives the download a file slot.
283
+ def self.download_file(job, file, local_path, progress: nil)
284
+ new(job, file, -> { FileSink.new(::File.open(local_path, "wb"), local_path) }, progress: progress).run
285
+ end
286
+
287
+ def self.download_content(job, file, io, progress: nil)
288
+ new(job, file, -> { IoSink.new(io) }, progress: progress).run
289
+ end
290
+
291
+ def self.resume(job, checkpoint, destination, options, progress: nil)
292
+ checkpoint.resuming(:download) do |lease|
293
+ remote = Files::File.new(checkpoint.path, options)
294
+ open_sink = if checkpoint.local_path
295
+ -> { FileSink.new(::File.open(checkpoint.local_path, "r+b"), checkpoint.local_path) }
296
+ else
297
+ _, io, start = checkpoint.sink
298
+ raise CheckpointError, "Bad parameter: resume the download into the IO it paused in" unless destination.equal?(io)
299
+
300
+ -> { IoSink.new(destination, start: start) }
301
+ end
302
+ new(job, remote, open_sink, checkpoint: checkpoint, lease: lease, progress: progress).run
303
+ end
304
+ end
305
+
306
+ # Fixes, on the caller's thread, the file's path and any download URL its record holds, the API context from its
307
+ # options, and the transport for its content requests, which refuses a client whose adapter cannot stream.
308
+ def initialize(job, file, open_sink, checkpoint: nil, lease: nil, progress: nil)
309
+ super(job)
310
+ @path = Checkpoint.frozen(file.path)
311
+ @download_uri = Checkpoint.frozen(file.download_uri)
312
+ @options = Coordinator.api_context(file.options)
313
+ @open_sink = open_sink
314
+ @sink = nil
315
+ @lease = lease
316
+ @progress = progress
317
+ @transport = job.transport_for(Files::ApiClient.download_client, download: true)
318
+ @range_bytes = [ Policy::DOWNLOAD_RANGE_BYTES, job.options.buffer_bytes / 4 ].min
319
+ @mode = :probe
320
+ @written = 0
321
+ @counted = 0
322
+ @digest = OpenSSL::Digest.new('SHA256')
323
+ @total = nil
324
+ @version = nil
325
+ @generation = 0
326
+ @open = {}
327
+ @next_number = 0
328
+ @next_write = 0
329
+ @refusals = {}.compare_by_identity
330
+ @write_failure = nil
331
+ @origin = checkpoint
332
+ return unless checkpoint&.version
333
+
334
+ @written = checkpoint.completed_bytes
335
+ @counted = @written
336
+ @version = checkpoint.version
337
+ @total = checkpoint.total_bytes
338
+ end
339
+
340
+ private
341
+
342
+ # A destination that cannot be opened, or a resume's that changed or cannot be checked, stops the transfer as it is
343
+ # found (stop!), so a cancellation or pause after it cannot come first, and one before it leaves the checkpoint
344
+ # unspent and the destination as it was.
345
+ def open_resources
346
+ @sink = @open_sink.call
347
+ resume_sink(@origin) if @origin
348
+ rescue StandardError => e
349
+ raise stop!(e)
350
+ end
351
+
352
+ def close_resources
353
+ @sink&.close
354
+ end
355
+
356
+ # Before anything is sent or written: the destination must still be the one the paused download wrote to,
357
+ # holding exactly the bytes it wrote, read back a slice at a time into scratch the buffer budget holds, and
358
+ # compared by digest; one that cannot give them back refuses. A pause or cancellation while it waits for that
359
+ # budget leaves the checkpoint unspent. Only then is the checkpoint spent. An unbound download starts its
360
+ # destination over instead, as its first new bytes are written.
361
+ def resume_sink(checkpoint)
362
+ changed = @sink.path ? "Bad parameter: the local file changed after the download paused" : "Bad parameter: the IO changed after the download paused"
363
+ raise CheckpointError, changed unless @sink.identity == checkpoint.sink
364
+
365
+ if checkpoint.state[:restart]
366
+ @lease&.start!
367
+ return restart_sink
368
+ end
369
+
370
+ completed = checkpoint.completed_bytes
371
+ raise CheckpointError, changed unless @sink.holds?(completed)
372
+
373
+ chunk = [ VERIFY_CHUNK_BYTES, @range_bytes ].min
374
+ scratch = reserve(chunk)
375
+ begin
376
+ digest = @sink.prefix_digest(completed, chunk)
377
+ ensure
378
+ release(scratch)
379
+ end
380
+ raise CheckpointError, "Bad parameter: the bytes already downloaded cannot be read back to check them" if digest.nil?
381
+ raise CheckpointError, changed unless digest && digest.hexdigest == checkpoint.state[:digest]
382
+
383
+ @lease&.start!
384
+ @digest = digest
385
+ @sink.resume_at!(completed)
386
+ end
387
+
388
+ # The download, from its URL's lookup to its last write. Any error on the way is raised only when it came before
389
+ # any cancellation or pause (Coordinator#run), and a cancellation or pause, such as one from the last progress
390
+ # call, is raised in place of a success.
391
+ def transfer
392
+ @url = @download_uri || load_url
393
+ raise stop!(Files::APIError.new("The download URL the API returned is not usable")) unless http_url?(@url)
394
+
395
+ @current = [ @url, @generation, StorageRetry.expires_at(@url) ].freeze
396
+ @started_at = Clock.now
397
+ @start_written = @written
398
+ # Every byte of a resumed download was written before its pause: storage proves its version with its last byte.
399
+ @mode = :proof if @version && @written >= @total
400
+ if fetch_head == :bound
401
+ bind
402
+ @next_offset = @written
403
+ pump
404
+ end
405
+ @job.synchronize { observe_stops }
406
+ raise_outcome
407
+ @sink.settle!
408
+ nil
409
+ end
410
+
411
+ # A refused transfer or a failed write that ended the download first closes a caller's IO.
412
+ def after_failure(error)
413
+ @sink&.close_after_failure(error) if closes_destination?(error)
414
+ end
415
+
416
+ # The opening range, or the whole response, on the caller's thread, with its own retries and renewals, each
417
+ # request under one of the job's request permits and only once no throttle holds its destination back. A
418
+ # throttle it gets holds the destination back for every transfer of the job, from before its permit is given back
419
+ # (fetch_once). An expired URL is renewed before it is sent, at most twice; a renewal after storage refused an
420
+ # expired URL is one of the retries. Returns :bound when ranges remain, or :done. What ends it stops the transfer
421
+ # as it is found (stop!, refusal).
422
+ def fetch_head
423
+ attempts = 0
424
+ renewals = 0
425
+ loop do
426
+ check_control!
427
+ if StorageRetry.expired?(@current[2])
428
+ raise stop!(Files::APIError.new(EXPIRED, http_status: 403)) if renewals >= RENEWALS
429
+
430
+ renewals += 1
431
+ renew_url
432
+ next
433
+ end
434
+ begin
435
+ result = fetch_once
436
+ rescue NetworkFailure => e
437
+ raise stop!(e.original) unless e.retryable? && attempts < @max_retries && (@written == @attempt_start || @sink.restartable?)
438
+
439
+ attempts += 1
440
+ retry_head(StorageRetry::RETRY_DELAY)
441
+ restart_sink unless @written == @attempt_start
442
+ next
443
+ rescue Passthrough => e
444
+ raise stop!(e.error) unless e.error.is_a?(ControlStop)
445
+
446
+ # A cancellation or pause keeps what was written; a resumed unbound download starts over.
447
+ check_control!
448
+ raise Files::Error, "The transfer stopped"
449
+ end
450
+ next @mode = :whole if result == :unbound
451
+ return result if result.is_a?(Symbol)
452
+
453
+ response = result
454
+ raise refusal(FrontendResponseError.new) if StorageRetry.frontend?(response.headers)
455
+ return :done if response.status == 416 && @written.zero? && response.header("content-range").to_s.strip == "bytes */0"
456
+ raise refusal(VersionChangedError.new) if response.status == 412
457
+
458
+ decision, delay = if response.status == 403 && StorageRetry.expired?(@current[2])
459
+ [ :renew, 0.0 ]
460
+ else
461
+ StorageRetry.classify(response.status, response.headers, response.body, truncated: response.truncated)
462
+ end
463
+ raise refusal(refused(response)) if decision == :refuse || attempts >= @max_retries
464
+
465
+ attempts += 1
466
+ retry_head(delay)
467
+ end
468
+ end
469
+
470
+ def retry_head(delay)
471
+ @job.synchronize { @job.count_retry }
472
+ wait_before_retry(delay)
473
+ end
474
+
475
+ # One request for the head, under a request permit it holds only while the request is in flight: the opening
476
+ # range, the whole file, or, for a proof, the file's last byte. A throttle it gets holds the destination back as
477
+ # the permit is given back, under the same lock, so a request waiting for the permit is never sent before it.
478
+ def fetch_once
479
+ @attempt_start = @written
480
+ headers = { "Accept-Encoding" => "identity" }
481
+ unless @mode == :whole
482
+ first = @mode == :proof ? @total - 1 : @written
483
+ headers["Range"] = "bytes=#{first}-#{@mode == :proof ? first : first + @range_bytes - 1}"
484
+ end
485
+ headers["If-Match"] = @version if @version
486
+ reservation = reserve(@mode == :proof ? 1 : @range_bytes) unless @mode == :whole
487
+ begin
488
+ check_control! unless @job.acquire_request(@control, @url) == :running
489
+ begin
490
+ outcome = nil
491
+ response = @transport.perform(:get, @url, headers, @connections) { |http| outcome = head_response(http) }
492
+ throttle = head_throttle(response)
493
+ ensure
494
+ @job.release_request(@url, throttle)
495
+ end
496
+ return response unless response.status.between?(200, 299)
497
+
498
+ case outcome[0]
499
+ when :bound, :complete
500
+ _, data, total, etag = outcome
501
+ @version ||= etag if outcome[0] == :bound
502
+ @total = total
503
+ write!(data)
504
+ wrote(data.bytesize)
505
+ @written < @total ? :bound : :done
506
+ when :streamed, :proven then :done
507
+ else :unbound
508
+ end
509
+ ensure
510
+ release(reservation)
511
+ end
512
+ end
513
+
514
+ # The seconds a head response's throttle holds its destination back, as fetch_head classifies it, or nil: never
515
+ # for a success, nor for the web app's response, which fetch_head refuses instead.
516
+ def head_throttle(response)
517
+ return if response.status.between?(200, 299) || StorageRetry.frontend?(response.headers)
518
+
519
+ decision, delay = StorageRetry.classify(response.status, response.headers, response.body, truncated: response.truncated)
520
+ delay if decision == :throttle
521
+ end
522
+
523
+ # Inside the response, before anything of it is written.
524
+ def head_response(http)
525
+ raise refusal(FrontendResponseError.new) if http.key?(StorageRetry::FRONTEND_HEADER)
526
+
527
+ case http.code
528
+ when "206"
529
+ raise refusal(mismatch) if @mode == :whole
530
+
531
+ first, last, total = content_range(http["content-range"])
532
+ return proof(http, first, last, total) if @mode == :proof
533
+
534
+ length = last && (last - first + 1)
535
+ unless first == @written && length && last < total && length == [ @range_bytes, total - first ].min && http.content_length == length &&
536
+ (@total.nil? || total == @total)
537
+ raise refusal(mismatch)
538
+ end
539
+
540
+ etag = http["etag"]
541
+ raise refusal(VersionChangedError.new) if @version && etag && etag != @version
542
+ return [ :bound, read_exact(http, length), total, etag || @version ] if @version || strong?(etag)
543
+ return [ :complete, read_exact(http, length), total ] if first.zero? && last == total - 1
544
+
545
+ [ :unbound ]
546
+ when "200"
547
+ # A download bound to a version takes a whole response only of that version, and never as its proof.
548
+ raise refusal(VersionChangedError.new) if @version && http["etag"] != @version
549
+ raise refusal(mismatch) if @mode == :proof
550
+
551
+ stream_whole(http)
552
+ [ :streamed ]
553
+ else
554
+ raise refusal(mismatch)
555
+ end
556
+ end
557
+
558
+ # A resumed download's proof that storage still has its version: the file's last byte, answered 206 under
559
+ # If-Match with the exact Content-Range and length and no other ETag. Nothing of it is written.
560
+ def proof(http, first, last, total)
561
+ raise refusal(mismatch) unless first == @total - 1 && last == first && total == @total && http.content_length == 1
562
+
563
+ etag = http["etag"]
564
+ raise refusal(VersionChangedError.new) if etag && etag != @version
565
+
566
+ read_exact(http, 1)
567
+ [ :proven ]
568
+ end
569
+
570
+ # A whole response, written as it arrives from its first byte.
571
+ def stream_whole(http)
572
+ raise refusal(mismatch) if @written.positive? && !@sink.restartable?
573
+
574
+ restart_sink if @written.positive?
575
+ expected = http.chunked? ? nil : http.content_length
576
+ received = 0
577
+ http.read_body do |chunk|
578
+ raise Passthrough.new(ControlStop.new), cause: nil unless @control.nil? || @control.state == :running
579
+
580
+ begin
581
+ write!(chunk)
582
+ wrote(chunk.bytesize)
583
+ rescue StandardError => e
584
+ raise Passthrough.new(e), cause: nil
585
+ end
586
+ received += chunk.bytesize
587
+ end
588
+ raise EOFError, "Transfer response ended after #{received} of #{expected} bytes" if expected && received != expected
589
+
590
+ @total = received
591
+ end
592
+
593
+ # Starts the destination over, just before a request that writes it from the first byte again.
594
+ def restart_sink
595
+ @sink.restart!
596
+ @written = 0
597
+ @digest = OpenSSL::Digest.new('SHA256')
598
+ end
599
+
600
+ def bind
601
+ host = ::URI.parse(@url).host
602
+ options = @job.options
603
+ key, policy = if @transport.serialized?
604
+ [ [ :download, :client, host ], Policy.fixed(1) ]
605
+ elsif options.automatic?
606
+ [ [ :download, host ], Policy.download(max_concurrency: options.max_concurrency) ]
607
+ else
608
+ [ [ :download, :fixed, host ], Policy.fixed(options.concurrency) ]
609
+ end
610
+ @controller = @job.synchronize { @job.controller(key) { policy } }
611
+ end
612
+
613
+ def plan
614
+ renew_pieces
615
+ while @stop.nil? && @next_offset < @total && @queue.size < @controller.limit
616
+ length = [ @range_bytes, @total - @next_offset ].min
617
+ piece = Piece.new(number: @next_number, offset: @next_offset, bytesize: length, attempts: 0, renewals: 0, ready_at: 0, generation: @generation)
618
+ @open[piece.number] = piece
619
+ enqueue(piece)
620
+ @next_offset += length
621
+ @next_number += 1
622
+ end
623
+ rescue StandardError => e
624
+ stop!(e)
625
+ end
626
+
627
+ def wants_planning?
628
+ !@renewals.empty? || (@next_offset < @total && @queue.size < @controller.limit)
629
+ end
630
+
631
+ def finished?
632
+ @next_offset >= @total && @open.empty?
633
+ end
634
+
635
+ # Renews the URL once for the ranges that found theirs expired, then sends them again. A range that already
636
+ # used a newer URL just goes again. A range whose renewals still give an expired URL stops the download unsent.
637
+ def renew_pieces
638
+ pieces = @renewals.dup
639
+ @renewals.clear
640
+ current = pieces.select { |piece| piece.generation >= @generation }
641
+ exhausted = current.find { |piece| piece.renewals >= RENEWALS }
642
+ if exhausted
643
+ fail_item(exhausted, refusal(Files::APIError.new("Transfer request failed", http_status: 403)))
644
+ elsif !current.empty?
645
+ current.each { |piece| piece.renewals += 1 }
646
+ renew_url
647
+ end
648
+ pieces.each do |piece|
649
+ next if piece.equal?(exhausted)
650
+
651
+ piece.ready_at = 0
652
+ enqueue(piece)
653
+ end
654
+ end
655
+
656
+ # The new target, its URL, generation and expiry, is made whole and then published as one reference, so an
657
+ # attempt that looks at it for a newer generation never sees part of it beside part of the target before it.
658
+ def renew_url
659
+ url = load_url
660
+ raise stop!(Files::APIError.new("The download URL the API returned is not usable")) unless http_url?(url)
661
+
662
+ target = [ url, @generation + 1, StorageRetry.expires_at(url) ].freeze
663
+ @url, @generation = target
664
+ @current = target
665
+ end
666
+
667
+ # The download URL the API gives for the file now, asked with the context the download began with; the answer's
668
+ # own context, such as the regional origin a lockout region mismatch moved it to, carries on to the lookups after.
669
+ # The lookup's failure stops the transfer as it arrives (stop!).
670
+ def load_url
671
+ remote = begin
672
+ Files::File.download(@path, {}, @options)
673
+ rescue StandardError => e
674
+ raise stop!(e)
675
+ end
676
+ adopt_context(remote)
677
+ remote.download_uri
678
+ end
679
+
680
+ def reservation_bytes(piece)
681
+ piece.bytesize
682
+ end
683
+
684
+ # An expired URL is never sent; renew_pieces stops the download once a range's renewals run out.
685
+ def expiring?(piece)
686
+ return false unless StorageRetry.expired?(@current[2])
687
+
688
+ piece.generation = @generation
689
+ true
690
+ end
691
+
692
+ # Every range goes to the URL the download has now, with its generation and expiry.
693
+ def request_target(_piece)
694
+ @current
695
+ end
696
+
697
+ # On a worker thread, to the URL and generation the range was admitted for (target), even when the URL has been
698
+ # renewed since. A 403 renews it when that URL's own expiry has passed, or when a newer URL has been published
699
+ # since; any other 403 refuses it. timing starts with the send and ends once the range's whole body has arrived and
700
+ # been checked; the range's bytes are charged to the buffer budget already, and read straight into one String of
701
+ # its length.
702
+ def attempt_item(piece, target, connections, timing = nil)
703
+ url, generation, expires_at = target
704
+ piece.generation = generation
705
+ headers = { "Range" => "bytes=#{piece.offset}-#{piece.offset + piece.bytesize - 1}", "Accept-Encoding" => "identity", "If-Match" => @version }
706
+ response = @transport.perform(:get, url, headers, connections, timing: timing) { |http| range_body(http, piece) }
707
+ return [ :ok, response.body, piece.bytesize ] if response.status.between?(200, 299)
708
+ return [ :fail, refusal(FrontendResponseError.new) ] if StorageRetry.frontend?(response.headers)
709
+ return [ :fail, refusal(VersionChangedError.new) ] if response.status == 412
710
+ return [ :renew, refusal(refused(response)) ] if response.status == 403 && (StorageRetry.expired?(expires_at) || generation < @current[1])
711
+
712
+ decision, delay = StorageRetry.classify(response.status, response.headers, response.body, truncated: response.truncated)
713
+ case decision
714
+ when :throttle then [ :retry, delay, :throttle, refusal(refused(response)) ]
715
+ when :retry then [ :retry, delay, :failure, refusal(refused(response)) ]
716
+ else [ :fail, refusal(refused(response)) ]
717
+ end
718
+ end
719
+
720
+ def range_body(http, piece)
721
+ raise refusal(FrontendResponseError.new) if http.key?(StorageRetry::FRONTEND_HEADER)
722
+ raise refusal(mismatch) unless http.code == "206"
723
+
724
+ first, last, total = content_range(http["content-range"])
725
+ raise refusal(mismatch) unless first == piece.offset && last == piece.offset + piece.bytesize - 1 && total == @total && http.content_length == piece.bytesize
726
+
727
+ etag = http["etag"]
728
+ raise refusal(VersionChangedError.new) if etag && etag != @version
729
+
730
+ read_exact(http, piece.bytesize)
731
+ end
732
+
733
+ def read_exact(http, length)
734
+ data = String.new(capacity: length, encoding: Encoding::BINARY)
735
+ http.read_body do |chunk|
736
+ raise refusal(mismatch) if data.bytesize + chunk.bytesize > length
737
+
738
+ data << chunk
739
+ end
740
+ raise EOFError, "Transfer response ended after #{data.bytesize} of #{length} bytes" if data.bytesize < length
741
+
742
+ data
743
+ end
744
+
745
+ # With the lock held: keeps a range until the ones before it are written.
746
+ def accept(piece, data)
747
+ piece.data = data
748
+ piece
749
+ end
750
+
751
+ # On the caller's thread: writes the ranges that are next in order. They are still written after a pause, so
752
+ # its checkpoint keeps them, but not after a failure or cancellation.
753
+ def deliver(_completed)
754
+ while @stop.nil? || @stop == :paused
755
+ piece = @open[@next_write]
756
+ break unless piece&.data
757
+
758
+ write!(piece.data)
759
+ @open.delete(piece.number)
760
+ @job.post { discard(piece) }
761
+ @next_write += 1
762
+ wrote(piece.bytesize)
763
+ end
764
+ rescue StandardError => e
765
+ stop!(e)
766
+ end
767
+
768
+ # On the caller's thread, after bytes were written: counts those not written before, and reports progress with
769
+ # total_bytes (nil until known), written_bytes, elapsed_seconds of this call and bytes_per_second, its written
770
+ # bytes over them. Anything progress raises stops the transfer as it is raised (stop!).
771
+ def wrote(bytes)
772
+ @written += bytes
773
+ if @written > @counted
774
+ fresh = @written - @counted
775
+ @counted = @written
776
+ @job.synchronize { @job.count_accepted(fresh) }
777
+ end
778
+ return unless @progress
779
+
780
+ elapsed = Clock.now - @started_at
781
+ begin
782
+ @progress.call({ total_bytes: @total, written_bytes: @written, elapsed_seconds: elapsed,
783
+ bytes_per_second: elapsed.positive? ? [ @written - @start_written, 0 ].max / elapsed : 0.0 }
784
+ )
785
+ rescue StandardError => e
786
+ raise stop!(e)
787
+ end
788
+ end
789
+
790
+ def all_items
791
+ @open.values
792
+ end
793
+
794
+ # A range is on the network until its whole body has arrived and been checked.
795
+ def measured_through_body?
796
+ true
797
+ end
798
+
799
+ # Keeps a digest of the bytes written, so resuming can tell they are unchanged, never the bytes themselves. nil
800
+ # before the destination is open, as when a pause comes while it opens, and when resuming could neither start
801
+ # over nor check those bytes.
802
+ def checkpoint
803
+ return if @sink.nil?
804
+
805
+ state = {}
806
+ if @version.nil?
807
+ return unless @sink.restartable?
808
+
809
+ state = { restart: true }
810
+ else
811
+ return unless @sink.verifiable? && @digest
812
+
813
+ state = { digest: @digest.hexdigest }
814
+ end
815
+ Checkpoint.new(direction: :download, path: @path, local_path: @sink.path, total_bytes: @total, completed_bytes: @version ? @written : 0,
816
+ sink: @sink.identity, version: @version, state: state
817
+ )
818
+ end
819
+
820
+ def check_control!
821
+ @job.synchronize { observe_stops }
822
+ raise_outcome
823
+ end
824
+
825
+ def wait_before_retry(seconds)
826
+ deadline = Clock.now + seconds
827
+ @job.synchronize do
828
+ loop do
829
+ observe_stops
830
+ remaining = deadline - Clock.now
831
+ break if @stop || !remaining.positive?
832
+
833
+ @job.wait(remaining)
834
+ end
835
+ end
836
+ raise_outcome
837
+ end
838
+
839
+ # Waits for the buffer budget to hold bytes on the caller's thread, unless the transfer stops first.
840
+ def reserve(bytes)
841
+ @job.synchronize do
842
+ loop do
843
+ observe_stops
844
+ break if @stop
845
+ return @job.budget.reserve(bytes) if @job.budget.available?(bytes)
846
+
847
+ @job.wait
848
+ end
849
+ end
850
+ raise_outcome
851
+ end
852
+
853
+ def release(reservation)
854
+ @job.post { @job.budget.release(reservation) } if reservation
855
+ end
856
+
857
+ # On the caller's thread. A failed write stops the transfer as it fails (stop!).
858
+ def write!(data)
859
+ @sink.write(data)
860
+ @digest&.update(data)
861
+ rescue StandardError => e
862
+ @write_failure = e
863
+ raise stop!(e)
864
+ end
865
+
866
+ # Marks error as the kind that closes a caller's IO: storage refused, or answered something other than asked. On
867
+ # the caller's thread, where it always ends the transfer, it also stops it as it is found (stop!); a worker's goes
868
+ # back through its request's outcome instead.
869
+ def refusal(error)
870
+ @job.synchronize { @refusals[error] = true }
871
+ Thread.current.equal?(@thread) ? stop!(error) : error
872
+ end
873
+
874
+ def closes_destination?(error)
875
+ error.equal?(@write_failure) || @job.synchronize { @refusals.key?(error) }
876
+ end
877
+
878
+ def public_error(error)
879
+ error.is_a?(NetworkFailure) ? error.original : error
880
+ end
881
+
882
+ def refused(response)
883
+ Files::APIError.new("Transfer request failed", http_status: response.status)
884
+ end
885
+
886
+ def mismatch
887
+ Files::APIError.new(MISMATCH)
888
+ end
889
+
890
+ def strong?(etag)
891
+ etag.is_a?(String) && etag.match?(/\A"[^"]*"\z/)
892
+ end
893
+
894
+ def content_range(value)
895
+ match = value.to_s.match(/\Abytes (\d+)-(\d+)\/(\d+)\z/)
896
+ match ? match.captures.map(&:to_i) : []
897
+ end
898
+
899
+ def http_url?(url)
900
+ url.is_a?(String) && %w[http https].include?(::URI.parse(url).scheme)
901
+ rescue ::URI::InvalidURIError
902
+ false
903
+ end
904
+ end
905
+ end
906
+ end