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