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