From 15633f5a3aec6cb492434c1ec2d97620efb419ed Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 15 Jan 2025 08:20:58 -0800 Subject: [PATCH 01/10] chore: Update .codegen.json with commit hash of codegen and openapi spec --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index 1f3308c8..d5beef02 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "ab51050", "specHash": "00c93fe", "version": "1.5.0" } +{ "engineHash": "ab51050", "specHash": "139b256", "version": "1.5.0" } From 82d05d4f21e30d81923f644494f570d5c74c8be8 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 15 Jan 2025 08:22:15 -0800 Subject: [PATCH 02/10] feat: Support chunked upload in Java (box/box-codegen#640) --- .codegen.json | 2 +- Box.Sdk.Gen/Managers/ChunkedUploads/PartAccumulator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.codegen.json b/.codegen.json index d5beef02..68a79fbd 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "ab51050", "specHash": "139b256", "version": "1.5.0" } +{ "engineHash": "622e4e3", "specHash": "139b256", "version": "1.5.0" } diff --git a/Box.Sdk.Gen/Managers/ChunkedUploads/PartAccumulator.cs b/Box.Sdk.Gen/Managers/ChunkedUploads/PartAccumulator.cs index c29730eb..a681e434 100644 --- a/Box.Sdk.Gen/Managers/ChunkedUploads/PartAccumulator.cs +++ b/Box.Sdk.Gen/Managers/ChunkedUploads/PartAccumulator.cs @@ -1,8 +1,8 @@ using System; using System.Collections.ObjectModel; using System.Collections.Generic; -using Box.Sdk.Gen.Schemas; using Box.Sdk.Gen.Internal; +using Box.Sdk.Gen.Schemas; namespace Box.Sdk.Gen.Managers { internal class PartAccumulator { From 272aa289104aa741fb8ecaa8b17e55300bb827df Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 15 Jan 2025 08:23:34 -0800 Subject: [PATCH 03/10] chore: Update .codegen.json with commit hash of codegen and openapi spec --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index 68a79fbd..4e219644 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "622e4e3", "specHash": "139b256", "version": "1.5.0" } +{ "engineHash": "622e4e3", "specHash": "6e37ae3", "version": "1.5.0" } From 3599da383bfbe2a65fa094881e3b7211491f570f Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 15 Jan 2025 08:43:55 -0800 Subject: [PATCH 04/10] chore: Update .codegen.json with commit hash of codegen and openapi spec --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index 4e219644..45f45415 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "622e4e3", "specHash": "6e37ae3", "version": "1.5.0" } +{ "engineHash": "b2c3124", "specHash": "90b039e", "version": "1.5.0" } From 770057ebb23ee13256642c62b93f8d00f6bee85d Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 15 Jan 2025 08:45:12 -0800 Subject: [PATCH 05/10] chore: Update .codegen.json with commit hash of codegen and openapi spec --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index 45f45415..2576fc0e 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "b2c3124", "specHash": "90b039e", "version": "1.5.0" } +{ "engineHash": "b2c3124", "specHash": "652aa18", "version": "1.5.0" } From caba490d8b8e9f64933b589645c2fe36cade9e49 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 15 Jan 2025 08:46:30 -0800 Subject: [PATCH 06/10] fix: order of fields in the IntegrationMapping schema (box/box-openapi#497) --- .codegen.json | 2 +- .../IntegrationMapping/IntegrationMapping.cs | 46 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.codegen.json b/.codegen.json index 2576fc0e..90f2b03f 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "b2c3124", "specHash": "652aa18", "version": "1.5.0" } +{ "engineHash": "b2c3124", "specHash": "0a89b2b", "version": "1.5.0" } diff --git a/Box.Sdk.Gen/Schemas/IntegrationMapping/IntegrationMapping.cs b/Box.Sdk.Gen/Schemas/IntegrationMapping/IntegrationMapping.cs index c847e94d..0fdbcea0 100644 --- a/Box.Sdk.Gen/Schemas/IntegrationMapping/IntegrationMapping.cs +++ b/Box.Sdk.Gen/Schemas/IntegrationMapping/IntegrationMapping.cs @@ -6,25 +6,6 @@ namespace Box.Sdk.Gen.Schemas { public class IntegrationMapping : IntegrationMappingBase, ISerializable { - /// - /// The Box folder, to which the object from the - /// partner app domain (referenced in `partner_item_id`) is mapped - /// - [JsonPropertyName("box_item")] - public FolderMini BoxItem { get; } - - /// - /// When the integration mapping object was created - /// - [JsonPropertyName("created_at")] - public System.DateTimeOffset? CreatedAt { get; init; } - - /// - /// When the integration mapping object was last modified - /// - [JsonPropertyName("modified_at")] - public System.DateTimeOffset? ModifiedAt { get; init; } - /// /// Identifies the Box partner app, /// with which the mapping is associated. @@ -66,15 +47,34 @@ public class IntegrationMapping : IntegrationMappingBase, ISerializable { [JsonPropertyName("partner_item")] public IntegrationMappingPartnerItemSlackUnion PartnerItem { get; } - public IntegrationMapping(string id, FolderMini boxItem, IntegrationMappingPartnerItemSlackUnion partnerItem, IntegrationMappingBaseTypeField type = IntegrationMappingBaseTypeField.IntegrationMapping) : base(id, type) { - BoxItem = boxItem; + /// + /// The Box folder, to which the object from the + /// partner app domain (referenced in `partner_item_id`) is mapped + /// + [JsonPropertyName("box_item")] + public FolderMini BoxItem { get; } + + /// + /// When the integration mapping object was created + /// + [JsonPropertyName("created_at")] + public System.DateTimeOffset? CreatedAt { get; init; } + + /// + /// When the integration mapping object was last modified + /// + [JsonPropertyName("modified_at")] + public System.DateTimeOffset? ModifiedAt { get; init; } + + public IntegrationMapping(string id, IntegrationMappingPartnerItemSlackUnion partnerItem, FolderMini boxItem, IntegrationMappingBaseTypeField type = IntegrationMappingBaseTypeField.IntegrationMapping) : base(id, type) { PartnerItem = partnerItem; + BoxItem = boxItem; } [JsonConstructorAttribute] - internal IntegrationMapping(string id, FolderMini boxItem, IntegrationMappingPartnerItemSlackUnion partnerItem, StringEnum type) : base(id, type ?? new StringEnum(IntegrationMappingBaseTypeField.IntegrationMapping)) { - BoxItem = boxItem; + internal IntegrationMapping(string id, IntegrationMappingPartnerItemSlackUnion partnerItem, FolderMini boxItem, StringEnum type) : base(id, type ?? new StringEnum(IntegrationMappingBaseTypeField.IntegrationMapping)) { PartnerItem = partnerItem; + BoxItem = boxItem; } internal new string? RawJson { get; set; } = default; From fde894e1755f063837ff53bafa91ccf9134844d5 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 15 Jan 2025 08:47:46 -0800 Subject: [PATCH 07/10] chore: Update .codegen.json with commit hash of codegen and openapi spec --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index 90f2b03f..ec1deaa0 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "b2c3124", "specHash": "0a89b2b", "version": "1.5.0" } +{ "engineHash": "6eb2c9c", "specHash": "0a89b2b", "version": "1.5.0" } From 28ab8c6e6ce2aaf06f3e181409532448745e89b4 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Thu, 16 Jan 2025 07:43:09 -0800 Subject: [PATCH 08/10] chore: Update .codegen.json with commit hash of codegen and openapi spec --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index ec1deaa0..b0964571 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "6eb2c9c", "specHash": "0a89b2b", "version": "1.5.0" } +{ "engineHash": "458e7a8", "specHash": "0a89b2b", "version": "1.5.0" } From 1108607ae11fb1961e20b1ebe0c83d2311108582 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Thu, 16 Jan 2025 10:37:30 -0800 Subject: [PATCH 09/10] chore: Update .codegen.json with commit hash of codegen and openapi spec --- .codegen.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codegen.json b/.codegen.json index b0964571..288bc86d 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "458e7a8", "specHash": "0a89b2b", "version": "1.5.0" } +{ "engineHash": "458e7a8", "specHash": "cf0a265", "version": "1.5.0" } From 98faee50e58aa1e00b766127454bea26e07f7e33 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Fri, 17 Jan 2025 03:39:24 -0800 Subject: [PATCH 10/10] feat: support getDownloadUrl in C# (box/box-codegen#642) --- .codegen.json | 2 +- .../Test/Downloads/DownloadsManagerTests.cs | 9 ++++ .../Test/Files/FilesManagerTests.cs | 11 ++++ Box.Sdk.Gen/Box/JwtAuth/JwtConfig.cs | 2 +- Box.Sdk.Gen/Client/BoxClient.cs | 2 +- .../Managers/Downloads/DownloadsManager.cs | 37 ++++++++++++++ .../Downloads/GetDownloadFileUrlHeaders.cs | 40 +++++++++++++++ .../GetDownloadFileUrlQueryParams.cs | 24 +++++++++ .../Managers/Downloads/IDownloadsManager.cs | 24 +++++++++ Box.Sdk.Gen/Managers/Files/FilesManager.cs | 50 +++++++++++++++++++ .../Files/GetFileThumbnailUrlExtension.cs | 12 +++++ .../Files/GetFileThumbnailUrlHeaders.cs | 20 ++++++++ .../Files/GetFileThumbnailUrlQueryParams.cs | 35 +++++++++++++ Box.Sdk.Gen/Managers/Files/IFilesManager.cs | 37 ++++++++++++++ .../BoxNetworkClient/BoxNetworkClient.cs | 46 ++++++++++++++--- .../DefaultNetworkClient.cs | 45 ++++++++++++++--- .../Networking/FetchOptions/FetchOptions.cs | 5 ++ docs/Downloads.md | 44 ++++++++++++++++ docs/Files.md | 44 ++++++++++++++++ 19 files changed, 474 insertions(+), 15 deletions(-) create mode 100644 Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlHeaders.cs create mode 100644 Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlQueryParams.cs create mode 100644 Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlExtension.cs create mode 100644 Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlHeaders.cs create mode 100644 Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlQueryParams.cs diff --git a/.codegen.json b/.codegen.json index 288bc86d..cdf6ef76 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "458e7a8", "specHash": "cf0a265", "version": "1.5.0" } +{ "engineHash": "553616e", "specHash": "cf0a265", "version": "1.5.0" } diff --git a/Box.Sdk.Gen.Tests.Integration/Test/Downloads/DownloadsManagerTests.cs b/Box.Sdk.Gen.Tests.Integration/Test/Downloads/DownloadsManagerTests.cs index 01b3dd6d..e51113e8 100644 --- a/Box.Sdk.Gen.Tests.Integration/Test/Downloads/DownloadsManagerTests.cs +++ b/Box.Sdk.Gen.Tests.Integration/Test/Downloads/DownloadsManagerTests.cs @@ -24,5 +24,14 @@ public async System.Threading.Tasks.Task TestDownloadFile() { await client.Files.DeleteFileByIdAsync(fileId: uploadedFile.Id); } + [TestMethod] + public async System.Threading.Tasks.Task TestGetDownloadUrl() { + FileFull uploadedFile = await new CommonsManager().UploadNewFileAsync(); + string downloadUrl = await client.Downloads.GetDownloadFileUrlAsync(fileId: uploadedFile.Id); + Assert.IsTrue(downloadUrl != null); + Assert.IsTrue(downloadUrl.Contains("https://")); + await client.Files.DeleteFileByIdAsync(fileId: uploadedFile.Id); + } + } } \ No newline at end of file diff --git a/Box.Sdk.Gen.Tests.Integration/Test/Files/FilesManagerTests.cs b/Box.Sdk.Gen.Tests.Integration/Test/Files/FilesManagerTests.cs index 61f8eed3..c34609b5 100644 --- a/Box.Sdk.Gen.Tests.Integration/Test/Files/FilesManagerTests.cs +++ b/Box.Sdk.Gen.Tests.Integration/Test/Files/FilesManagerTests.cs @@ -20,6 +20,17 @@ public async System.Threading.Tasks.Task UploadFileAsync(string fileNa return NullableUtils.Unwrap(uploadedFiles.Entries)[0]; } + [TestMethod] + public async System.Threading.Tasks.Task TestGetFileThumbnailUrl() { + string thumbnailFileName = Utils.GetUUID(); + System.IO.Stream thumbnailContentStream = Utils.GenerateByteStream(size: 1024 * 1024); + FileFull thumbnailFile = await UploadFileAsync(fileName: thumbnailFileName, fileStream: thumbnailContentStream); + string downloadUrl = await client.Files.GetFileThumbnailUrlAsync(fileId: thumbnailFile.Id, extension: GetFileThumbnailUrlExtension.Png); + Assert.IsTrue(downloadUrl != null); + Assert.IsTrue(downloadUrl.Contains("https://")); + await client.Files.DeleteFileByIdAsync(fileId: thumbnailFile.Id); + } + [TestMethod] public async System.Threading.Tasks.Task TestGetFileThumbnail() { string thumbnailFileName = Utils.GetUUID(); diff --git a/Box.Sdk.Gen/Box/JwtAuth/JwtConfig.cs b/Box.Sdk.Gen/Box/JwtAuth/JwtConfig.cs index e9e7c8f0..41f719f8 100644 --- a/Box.Sdk.Gen/Box/JwtAuth/JwtConfig.cs +++ b/Box.Sdk.Gen/Box/JwtAuth/JwtConfig.cs @@ -42,7 +42,7 @@ public class JwtConfig { /// public string? UserId { get; init; } - internal JwtAlgorithm? Algorithm { get; } + internal JwtAlgorithm? Algorithm { get; init; } = JwtAlgorithm.Rs256; public ITokenStorage TokenStorage { get; } diff --git a/Box.Sdk.Gen/Client/BoxClient.cs b/Box.Sdk.Gen/Client/BoxClient.cs index a15822b1..ff632700 100644 --- a/Box.Sdk.Gen/Client/BoxClient.cs +++ b/Box.Sdk.Gen/Client/BoxClient.cs @@ -232,7 +232,7 @@ public BoxClient(IAuthentication auth, NetworkSession? networkSession = default) public async System.Threading.Tasks.Task MakeRequestAsync(FetchOptions fetchOptions) { IAuthentication auth = fetchOptions.Auth == null ? this.Auth : NullableUtils.Unwrap(fetchOptions.Auth); NetworkSession networkSession = fetchOptions.NetworkSession == null ? this.NetworkSession : NullableUtils.Unwrap(fetchOptions.NetworkSession); - FetchOptions enrichedFetchOptions = new FetchOptions(url: fetchOptions.Url, method: fetchOptions.Method, contentType: fetchOptions.ContentType, responseFormat: fetchOptions.ResponseFormat) { Auth = auth, NetworkSession = networkSession, Parameters = fetchOptions.Parameters, Headers = fetchOptions.Headers, Data = fetchOptions.Data, FileStream = fetchOptions.FileStream, MultipartData = fetchOptions.MultipartData }; + FetchOptions enrichedFetchOptions = new FetchOptions(url: fetchOptions.Url, method: fetchOptions.Method, contentType: fetchOptions.ContentType, responseFormat: fetchOptions.ResponseFormat) { Auth = auth, NetworkSession = networkSession, Parameters = fetchOptions.Parameters, Headers = fetchOptions.Headers, Data = fetchOptions.Data, FileStream = fetchOptions.FileStream, MultipartData = fetchOptions.MultipartData, FollowRedirects = fetchOptions.FollowRedirects }; return await networkSession.NetworkClient.FetchAsync(options: enrichedFetchOptions).ConfigureAwait(false); } diff --git a/Box.Sdk.Gen/Managers/Downloads/DownloadsManager.cs b/Box.Sdk.Gen/Managers/Downloads/DownloadsManager.cs index 93a3e7f7..b20f272b 100644 --- a/Box.Sdk.Gen/Managers/Downloads/DownloadsManager.cs +++ b/Box.Sdk.Gen/Managers/Downloads/DownloadsManager.cs @@ -13,6 +13,43 @@ public class DownloadsManager : IDownloadsManager { public DownloadsManager(NetworkSession? networkSession = default) { NetworkSession = networkSession ?? new NetworkSession(); } + /// + /// Returns the contents of a file in binary format. + /// + /// + /// The unique identifier that represents a file. + /// + /// The ID for any file can be determined + /// by visiting a file in the web application + /// and copying the ID from the URL. For example, + /// for the URL `https://*.app.box.com/files/123` + /// the `file_id` is `123`. + /// Example: "12345" + /// + /// + /// Query parameters of downloadFile method + /// + /// + /// Headers of downloadFile method + /// + /// + /// Token used for request cancellation. + /// + public async System.Threading.Tasks.Task GetDownloadFileUrlAsync(string fileId, GetDownloadFileUrlQueryParams? queryParams = default, GetDownloadFileUrlHeaders? headers = default, System.Threading.CancellationToken? cancellationToken = null) { + queryParams = queryParams ?? new GetDownloadFileUrlQueryParams(); + headers = headers ?? new GetDownloadFileUrlHeaders(); + Dictionary queryParamsMap = Utils.PrepareParams(map: new Dictionary() { { "version", StringUtils.ToStringRepresentation(queryParams.Version) }, { "access_token", StringUtils.ToStringRepresentation(queryParams.AccessTokenField) } }); + Dictionary headersMap = Utils.PrepareParams(map: DictionaryUtils.MergeDictionaries(new Dictionary() { { "range", StringUtils.ToStringRepresentation(headers.Range) }, { "boxapi", StringUtils.ToStringRepresentation(headers.Boxapi) } }, headers.ExtraHeaders)); + FetchResponse response = await this.NetworkSession.NetworkClient.FetchAsync(options: new FetchOptions(url: string.Concat(this.NetworkSession.BaseUrls.BaseUrl, "/2.0/files/", StringUtils.ToStringRepresentation(fileId), "/content"), method: "GET", responseFormat: Box.Sdk.Gen.ResponseFormat.NoContent) { Parameters = queryParamsMap, Headers = headersMap, Auth = this.Auth, NetworkSession = this.NetworkSession, CancellationToken = cancellationToken, FollowRedirects = false }).ConfigureAwait(false); + if (response.Headers.ContainsKey("location")) { + return response.Headers["location"]; + } + if (response.Headers.ContainsKey("Location")) { + return response.Headers["Location"]; + } + throw new BoxSdkException(message: "No location header in response"); + } + /// /// Returns the contents of a file in binary format. /// diff --git a/Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlHeaders.cs b/Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlHeaders.cs new file mode 100644 index 00000000..acd3111a --- /dev/null +++ b/Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlHeaders.cs @@ -0,0 +1,40 @@ +using Box.Sdk.Gen; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Box.Sdk.Gen.Internal; +using Box.Sdk.Gen.Schemas; + +namespace Box.Sdk.Gen.Managers { + public class GetDownloadFileUrlHeaders { + /// + /// The byte range of the content to download. + /// + /// The format `bytes={start_byte}-{end_byte}` can be used to specify + /// what section of the file to download. + /// + public string? Range { get; init; } + + /// + /// The URL, and optional password, for the shared link of this item. + /// + /// This header can be used to access items that have not been + /// explicitly shared with a user. + /// + /// Use the format `shared_link=[link]` or if a password is required then + /// use `shared_link=[link]&shared_link_password=[password]`. + /// + /// This header can be used on the file or folder shared, as well as on any files + /// or folders nested within the item. + /// + public string? Boxapi { get; init; } + + /// + /// Extra headers that will be included in the HTTP request. + /// + public Dictionary ExtraHeaders { get; } + + public GetDownloadFileUrlHeaders(Dictionary? extraHeaders = default) { + ExtraHeaders = extraHeaders ?? new Dictionary() { }; + } + } +} \ No newline at end of file diff --git a/Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlQueryParams.cs b/Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlQueryParams.cs new file mode 100644 index 00000000..c447552c --- /dev/null +++ b/Box.Sdk.Gen/Managers/Downloads/GetDownloadFileUrlQueryParams.cs @@ -0,0 +1,24 @@ +using Box.Sdk.Gen; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Box.Sdk.Gen.Internal; +using Box.Sdk.Gen.Schemas; + +namespace Box.Sdk.Gen.Managers { + public class GetDownloadFileUrlQueryParams { + /// + /// The file version to download + /// + public string? Version { get; init; } + + /// + /// An optional access token that can be used to pre-authenticate this request, which means that a download link can be shared with a browser or a third party service without them needing to know how to handle the authentication. + /// When using this parameter, please make sure that the access token is sufficiently scoped down to only allow read access to that file and no other files or folders. + /// + public string? AccessTokenField { get; init; } + + public GetDownloadFileUrlQueryParams() { + + } + } +} \ No newline at end of file diff --git a/Box.Sdk.Gen/Managers/Downloads/IDownloadsManager.cs b/Box.Sdk.Gen/Managers/Downloads/IDownloadsManager.cs index 5e0f7ef9..1a2992f0 100644 --- a/Box.Sdk.Gen/Managers/Downloads/IDownloadsManager.cs +++ b/Box.Sdk.Gen/Managers/Downloads/IDownloadsManager.cs @@ -28,6 +28,30 @@ public interface IDownloadsManager { /// /// Token used for request cancellation. /// + public System.Threading.Tasks.Task GetDownloadFileUrlAsync(string fileId, GetDownloadFileUrlQueryParams? queryParams = default, GetDownloadFileUrlHeaders? headers = default, System.Threading.CancellationToken? cancellationToken = null) => throw new System.NotImplementedException("This method needs to be implemented by the derived class before calling it."); + + /// + /// Returns the contents of a file in binary format. + /// + /// + /// The unique identifier that represents a file. + /// + /// The ID for any file can be determined + /// by visiting a file in the web application + /// and copying the ID from the URL. For example, + /// for the URL `https://*.app.box.com/files/123` + /// the `file_id` is `123`. + /// Example: "12345" + /// + /// + /// Query parameters of downloadFile method + /// + /// + /// Headers of downloadFile method + /// + /// + /// Token used for request cancellation. + /// public System.Threading.Tasks.Task DownloadFileAsync(string fileId, DownloadFileQueryParams? queryParams = default, DownloadFileHeaders? headers = default, System.Threading.CancellationToken? cancellationToken = null) => throw new System.NotImplementedException("This method needs to be implemented by the derived class before calling it."); } diff --git a/Box.Sdk.Gen/Managers/Files/FilesManager.cs b/Box.Sdk.Gen/Managers/Files/FilesManager.cs index 52d317be..fb9a996f 100644 --- a/Box.Sdk.Gen/Managers/Files/FilesManager.cs +++ b/Box.Sdk.Gen/Managers/Files/FilesManager.cs @@ -145,6 +145,56 @@ public async System.Threading.Tasks.Task CopyFileAsync(string fileId, return SimpleJsonSerializer.Deserialize(NullableUtils.Unwrap(response.Data)); } + /// + /// Retrieves a thumbnail, or smaller image representation, of a file. + /// + /// Sizes of `32x32`,`64x64`, `128x128`, and `256x256` can be returned in + /// the `.png` format and sizes of `32x32`, `160x160`, and `320x320` + /// can be returned in the `.jpg` format. + /// + /// Thumbnails can be generated for the image and video file formats listed + /// [found on our community site][1]. + /// + /// [1]: https://community.box.com/t5/Migrating-and-Previewing-Content/File-Types-and-Fonts-Supported-in-Box-Content-Preview/ta-p/327 + /// + /// + /// The unique identifier that represents a file. + /// + /// The ID for any file can be determined + /// by visiting a file in the web application + /// and copying the ID from the URL. For example, + /// for the URL `https://*.app.box.com/files/123` + /// the `file_id` is `123`. + /// Example: "12345" + /// + /// + /// The file format for the thumbnail + /// Example: "png" + /// + /// + /// Query parameters of getFileThumbnailById method + /// + /// + /// Headers of getFileThumbnailById method + /// + /// + /// Token used for request cancellation. + /// + public async System.Threading.Tasks.Task GetFileThumbnailUrlAsync(string fileId, GetFileThumbnailUrlExtension extension, GetFileThumbnailUrlQueryParams? queryParams = default, GetFileThumbnailUrlHeaders? headers = default, System.Threading.CancellationToken? cancellationToken = null) { + queryParams = queryParams ?? new GetFileThumbnailUrlQueryParams(); + headers = headers ?? new GetFileThumbnailUrlHeaders(); + Dictionary queryParamsMap = Utils.PrepareParams(map: new Dictionary() { { "min_height", StringUtils.ToStringRepresentation(queryParams.MinHeight) }, { "min_width", StringUtils.ToStringRepresentation(queryParams.MinWidth) }, { "max_height", StringUtils.ToStringRepresentation(queryParams.MaxHeight) }, { "max_width", StringUtils.ToStringRepresentation(queryParams.MaxWidth) } }); + Dictionary headersMap = Utils.PrepareParams(map: DictionaryUtils.MergeDictionaries(new Dictionary() { }, headers.ExtraHeaders)); + FetchResponse response = await this.NetworkSession.NetworkClient.FetchAsync(options: new FetchOptions(url: string.Concat(this.NetworkSession.BaseUrls.BaseUrl, "/2.0/files/", StringUtils.ToStringRepresentation(fileId), "/thumbnail.", StringUtils.ToStringRepresentation(extension)), method: "GET", responseFormat: Box.Sdk.Gen.ResponseFormat.NoContent) { Parameters = queryParamsMap, Headers = headersMap, Auth = this.Auth, NetworkSession = this.NetworkSession, CancellationToken = cancellationToken, FollowRedirects = false }).ConfigureAwait(false); + if (response.Headers.ContainsKey("location")) { + return response.Headers["location"]; + } + if (response.Headers.ContainsKey("Location")) { + return response.Headers["Location"]; + } + throw new BoxSdkException(message: "No location header in response"); + } + /// /// Retrieves a thumbnail, or smaller image representation, of a file. /// diff --git a/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlExtension.cs b/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlExtension.cs new file mode 100644 index 00000000..64014242 --- /dev/null +++ b/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlExtension.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using Box.Sdk.Gen.Internal; +using Box.Sdk.Gen.Schemas; + +namespace Box.Sdk.Gen.Managers { + public enum GetFileThumbnailUrlExtension { + [Description("png")] + Png, + [Description("jpg")] + Jpg + } +} \ No newline at end of file diff --git a/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlHeaders.cs b/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlHeaders.cs new file mode 100644 index 00000000..b6197b65 --- /dev/null +++ b/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlHeaders.cs @@ -0,0 +1,20 @@ +using Box.Sdk.Gen; +using System; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Box.Sdk.Gen.Internal; +using Box.Sdk.Gen.Schemas; + +namespace Box.Sdk.Gen.Managers { + public class GetFileThumbnailUrlHeaders { + /// + /// Extra headers that will be included in the HTTP request. + /// + public Dictionary ExtraHeaders { get; } + + public GetFileThumbnailUrlHeaders(Dictionary? extraHeaders = default) { + ExtraHeaders = extraHeaders ?? new Dictionary() { }; + } + } +} \ No newline at end of file diff --git a/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlQueryParams.cs b/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlQueryParams.cs new file mode 100644 index 00000000..4f56f8ab --- /dev/null +++ b/Box.Sdk.Gen/Managers/Files/GetFileThumbnailUrlQueryParams.cs @@ -0,0 +1,35 @@ +using Box.Sdk.Gen; +using System; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Box.Sdk.Gen.Internal; +using Box.Sdk.Gen.Schemas; + +namespace Box.Sdk.Gen.Managers { + public class GetFileThumbnailUrlQueryParams { + /// + /// The minimum height of the thumbnail + /// + public long? MinHeight { get; init; } + + /// + /// The minimum width of the thumbnail + /// + public long? MinWidth { get; init; } + + /// + /// The maximum height of the thumbnail + /// + public long? MaxHeight { get; init; } + + /// + /// The maximum width of the thumbnail + /// + public long? MaxWidth { get; init; } + + public GetFileThumbnailUrlQueryParams() { + + } + } +} \ No newline at end of file diff --git a/Box.Sdk.Gen/Managers/Files/IFilesManager.cs b/Box.Sdk.Gen/Managers/Files/IFilesManager.cs index 7f765982..1fd13076 100644 --- a/Box.Sdk.Gen/Managers/Files/IFilesManager.cs +++ b/Box.Sdk.Gen/Managers/Files/IFilesManager.cs @@ -147,6 +147,43 @@ public interface IFilesManager { /// /// Token used for request cancellation. /// + public System.Threading.Tasks.Task GetFileThumbnailUrlAsync(string fileId, GetFileThumbnailUrlExtension extension, GetFileThumbnailUrlQueryParams? queryParams = default, GetFileThumbnailUrlHeaders? headers = default, System.Threading.CancellationToken? cancellationToken = null) => throw new System.NotImplementedException("This method needs to be implemented by the derived class before calling it."); + + /// + /// Retrieves a thumbnail, or smaller image representation, of a file. + /// + /// Sizes of `32x32`,`64x64`, `128x128`, and `256x256` can be returned in + /// the `.png` format and sizes of `32x32`, `160x160`, and `320x320` + /// can be returned in the `.jpg` format. + /// + /// Thumbnails can be generated for the image and video file formats listed + /// [found on our community site][1]. + /// + /// [1]: https://community.box.com/t5/Migrating-and-Previewing-Content/File-Types-and-Fonts-Supported-in-Box-Content-Preview/ta-p/327 + /// + /// + /// The unique identifier that represents a file. + /// + /// The ID for any file can be determined + /// by visiting a file in the web application + /// and copying the ID from the URL. For example, + /// for the URL `https://*.app.box.com/files/123` + /// the `file_id` is `123`. + /// Example: "12345" + /// + /// + /// The file format for the thumbnail + /// Example: "png" + /// + /// + /// Query parameters of getFileThumbnailById method + /// + /// + /// Headers of getFileThumbnailById method + /// + /// + /// Token used for request cancellation. + /// public System.Threading.Tasks.Task GetFileThumbnailByIdAsync(string fileId, GetFileThumbnailByIdExtension extension, GetFileThumbnailByIdQueryParams? queryParams = default, GetFileThumbnailByIdHeaders? headers = default, System.Threading.CancellationToken? cancellationToken = null) => throw new System.NotImplementedException("This method needs to be implemented by the derived class before calling it."); } diff --git a/Box.Sdk.Gen/Networking/BoxNetworkClient/BoxNetworkClient.cs b/Box.Sdk.Gen/Networking/BoxNetworkClient/BoxNetworkClient.cs index bb665ff4..16b23a90 100644 --- a/Box.Sdk.Gen/Networking/BoxNetworkClient/BoxNetworkClient.cs +++ b/Box.Sdk.Gen/Networking/BoxNetworkClient/BoxNetworkClient.cs @@ -1,3 +1,4 @@ +using Box.Sdk.Gen.Schemas; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -22,7 +23,14 @@ static BoxNetworkClient() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddHttpClient(); + serviceCollection.AddHttpClient("DefaultHttpClient") + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + AllowAutoRedirect = false, + }; + }); var httpClientFactory = serviceCollection.BuildServiceProvider().GetService(); if (httpClientFactory == null) @@ -72,9 +80,7 @@ async Task INetworkClient.FetchAsync(FetchOptions options) if (response.IsSuccessStatusCode && (!isRetryAfterWithAcceptedPresent || attempt >= networkSession.RetryAttempts)) { seekableStream?.Dispose(); - return isStreamResponse ? - new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false) } : - new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Data = JsonUtils.JsonToSerializedData(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)) }; + return await ReadResponse(isStreamResponse, response, statusCode, url, cancellationToken).ConfigureAwait(false); } if (attempt >= networkSession.RetryAttempts) @@ -83,6 +89,26 @@ async Task INetworkClient.FetchAsync(FetchOptions options) throw new BoxSdkException($"Max retry attempts excedeed.", DateTimeOffset.UtcNow); } + if (statusCode >= 300 & statusCode < 400) + { + if (options.FollowRedirects == false) + { + seekableStream?.Dispose(); + return await ReadResponse(isStreamResponse, response, statusCode, url, cancellationToken).ConfigureAwait(false); + } + + var locationHeader = response.Headers.FirstOrDefault( + h => string.Equals(h.Key, "location", StringComparison.OrdinalIgnoreCase)); + + if (locationHeader.Key == null) + { + throw new BoxSdkException($"Redirect response missing Location header for: {options.Url}"); + } + + return await ((INetworkClient)this).FetchAsync(new FetchOptions(locationHeader.Value.First(), "GET", options.ContentType, options.ResponseFormat) + { Auth = options.Auth, NetworkSession = networkSession }).ConfigureAwait(false); + } + if (statusCode == 401) { if (options.Auth != null) @@ -142,7 +168,7 @@ private static HttpClient GetOrCreateHttpClient(NetworkSession networkSession) { if (networkSession.proxyConfig == null) { - return _clientFactory.CreateClient(); + return _clientFactory.CreateClient("DefaultHttpClient"); } else { @@ -177,7 +203,8 @@ private static HttpClient CreateProxyClient(ProxyConfig proxyConfig) { Proxy = webProxy, UseProxy = true, - PreAuthenticate = true + PreAuthenticate = true, + AllowAutoRedirect = false }; return new HttpClient(handler, disposeHandler: true); @@ -410,5 +437,12 @@ public ProxyClient(string proxyKey, ProxyConfig proxyConfig) { "HEAD", HttpMethod.Head }, { "TRACE", HttpMethod.Trace }, }; + + private static async Task ReadResponse(bool isStreamResponse, HttpResponseMessage response, int statusCode, string url, CancellationToken cancellationToken) + { + return isStreamResponse ? + new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false) } : + new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Data = JsonUtils.JsonToSerializedData(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)) }; + } } } diff --git a/Box.Sdk.Gen/Networking/DefaultNetworkClient/DefaultNetworkClient.cs b/Box.Sdk.Gen/Networking/DefaultNetworkClient/DefaultNetworkClient.cs index c4ded749..b1764d25 100644 --- a/Box.Sdk.Gen/Networking/DefaultNetworkClient/DefaultNetworkClient.cs +++ b/Box.Sdk.Gen/Networking/DefaultNetworkClient/DefaultNetworkClient.cs @@ -32,7 +32,14 @@ static DefaultNetworkClient() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddHttpClient(); + serviceCollection.AddHttpClient("DefaultHttpClient") + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + AllowAutoRedirect = false, + }; + }); var httpClientFactory = serviceCollection.BuildServiceProvider().GetService(); if (httpClientFactory == null) @@ -82,9 +89,7 @@ async Task INetworkClient.FetchAsync(FetchOptions options) if (response.IsSuccessStatusCode && (!isRetryAfterWithAcceptedPresent || attempt >= networkSession.RetryAttempts)) { seekableStream?.Dispose(); - return isStreamResponse ? - new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false) } : - new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Data = JsonUtils.JsonToSerializedData(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)) }; + return await ReadResponse(isStreamResponse, response, statusCode, url, cancellationToken).ConfigureAwait(false); } if (attempt >= networkSession.RetryAttempts) @@ -93,6 +98,26 @@ async Task INetworkClient.FetchAsync(FetchOptions options) throw new BoxSdkException($"Max retry attempts excedeed.", DateTimeOffset.UtcNow); } + if (statusCode >= 300 & statusCode < 400) + { + if (options.FollowRedirects == false) + { + seekableStream?.Dispose(); + return await ReadResponse(isStreamResponse, response, statusCode, url, cancellationToken).ConfigureAwait(false); + } + + var locationHeader = response.Headers.FirstOrDefault( + h => string.Equals(h.Key, "location", StringComparison.OrdinalIgnoreCase)); + + if (locationHeader.Key == null) + { + throw new BoxSdkException($"Redirect response missing Location header for: {options.Url}"); + } + + return await ((INetworkClient)this).FetchAsync(new FetchOptions(locationHeader.Value.First(), "GET", options.ContentType, options.ResponseFormat) + { Auth = options.Auth, NetworkSession = networkSession }).ConfigureAwait(false); + } + if (statusCode == 401) { if (options.Auth != null) @@ -152,7 +177,7 @@ private static HttpClient GetOrCreateHttpClient(NetworkSession networkSession) { if (networkSession.proxyConfig == null) { - return _clientFactory.CreateClient(); + return _clientFactory.CreateClient("DefaultHttpClient"); } else { @@ -187,7 +212,8 @@ private static HttpClient CreateProxyClient(ProxyConfig proxyConfig) { Proxy = webProxy, UseProxy = true, - PreAuthenticate = true + PreAuthenticate = true, + AllowAutoRedirect = false }; return new HttpClient(handler, disposeHandler: true); @@ -415,5 +441,12 @@ public ProxyClient(string proxyKey, ProxyConfig proxyConfig) { "HEAD", HttpMethod.Head }, { "TRACE", HttpMethod.Trace }, }; + + private static async Task ReadResponse(bool isStreamResponse, HttpResponseMessage response, int statusCode, string url, CancellationToken cancellationToken) + { + return isStreamResponse ? + new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false) } : + new FetchResponse(status: statusCode, headers: response.Headers.ToDictionary(x => x.Key, x => x.Value.First())) { Url = url, Data = JsonUtils.JsonToSerializedData(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)) }; + } } } diff --git a/Box.Sdk.Gen/Networking/FetchOptions/FetchOptions.cs b/Box.Sdk.Gen/Networking/FetchOptions/FetchOptions.cs index 9caceb18..5d1ab752 100644 --- a/Box.Sdk.Gen/Networking/FetchOptions/FetchOptions.cs +++ b/Box.Sdk.Gen/Networking/FetchOptions/FetchOptions.cs @@ -66,6 +66,11 @@ public class FetchOptions { /// public System.Threading.CancellationToken? CancellationToken { get; init; } + /// + /// A boolean value indicate if the request should follow redirects. Defaults to True. Not supported in Browser environment. + /// + public bool? FollowRedirects { get; init; } = true; + public FetchOptions(string url, string method, string contentType = "application/json", Box.Sdk.Gen.ResponseFormat responseFormat = Box.Sdk.Gen.ResponseFormat.Json) { Url = url; Method = method; diff --git a/docs/Downloads.md b/docs/Downloads.md index 95aa4b7a..d61f1147 100644 --- a/docs/Downloads.md +++ b/docs/Downloads.md @@ -1,8 +1,52 @@ # IDownloadsManager +- [Download file URL](#download-file-url) - [Download file](#download-file) +## Download file URL + +Get the download URL without downloading the content. + +This operation is performed by calling function `GetDownloadFileUrl`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference/get-files-id-content/). + + +``` +await client.Downloads.GetDownloadFileUrlAsync(fileId: uploadedFile.Id); +``` + +### Arguments + +- fileId `string` + - The unique identifier that represents a file. The ID for any file can be determined by visiting a file in the web application and copying the ID from the URL. For example, for the URL `https://*.app.box.com/files/123` the `file_id` is `123`. Example: "12345" +- queryParams `GetDownloadFileUrlQueryParams` + - Query parameters of downloadFile method +- headers `GetDownloadFileUrlHeaders` + - Headers of downloadFile method +- cancellationToken `System.Threading.CancellationToken?` + - Token used for request cancellation. + + +### Returns + +This function returns a value of type `string`. + +Returns the requested file if the client has the **follow +redirects** setting enabled to automatically +follow HTTP `3xx` responses as redirects. If not, the request +will return `302` instead. +For details, see +the [download file guide](g://downloads/file#download-url).If the file is not ready to be downloaded yet `Retry-After` header will +be returned indicating the time in seconds after which the file will +be available for the client to download. + +This response can occur when the file was uploaded immediately before the +download request. + + ## Download file Returns the contents of a file in binary format. diff --git a/docs/Files.md b/docs/Files.md index 43a025b5..3f79bed8 100644 --- a/docs/Files.md +++ b/docs/Files.md @@ -5,6 +5,7 @@ - [Update file](#update-file) - [Delete file](#delete-file) - [Copy file](#copy-file) +- [Get file thumbnail URL](#get-file-thumbnail-url) - [Get file thumbnail](#get-file-thumbnail) ## Get file information @@ -159,6 +160,49 @@ Not all available fields are returned by default. Use the any specific fields. +## Get file thumbnail URL + +Get the download URL without downloading the content. + +This operation is performed by calling function `GetFileThumbnailUrl`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference/get-files-id-thumbnail-id/). + + +``` +await client.Files.GetFileThumbnailUrlAsync(fileId: thumbnailFile.Id, extension: GetFileThumbnailUrlExtension.Png); +``` + +### Arguments + +- fileId `string` + - The unique identifier that represents a file. The ID for any file can be determined by visiting a file in the web application and copying the ID from the URL. For example, for the URL `https://*.app.box.com/files/123` the `file_id` is `123`. Example: "12345" +- extension `GetFileThumbnailUrlExtension` + - The file format for the thumbnail Example: "png" +- queryParams `GetFileThumbnailUrlQueryParams` + - Query parameters of getFileThumbnailById method +- headers `GetFileThumbnailUrlHeaders` + - Headers of getFileThumbnailById method +- cancellationToken `System.Threading.CancellationToken?` + - Token used for request cancellation. + + +### Returns + +This function returns a value of type `string`. + +When a thumbnail can be created the thumbnail data will be +returned in the body of the response.Sometimes generating a thumbnail can take a few seconds. In these +situations the API returns a `Location`-header pointing to a +placeholder graphic for this file type. + +The placeholder graphic can be used in a user interface until the +thumbnail generation has completed. The `Retry-After`-header indicates +when to the thumbnail will be ready. At that time, retry this endpoint +to retrieve the thumbnail. + + ## Get file thumbnail Retrieves a thumbnail, or smaller image representation, of a file.