diff --git a/.codegen.json b/.codegen.json index 90f2b03f..cdf6ef76 100644 --- a/.codegen.json +++ b/.codegen.json @@ -1 +1 @@ -{ "engineHash": "b2c3124", "specHash": "0a89b2b", "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.