diff --git a/Documentation~/README.md b/Documentation~/README.md index 14c762e..d154b02 100644 --- a/Documentation~/README.md +++ b/Documentation~/README.md @@ -6,6 +6,10 @@ A Utilities.Rest package for the [Unity](https://unity.com/) Game Engine. ## Installing +Requires Unity 2021.3 LTS or higher. + +The recommended installation method is though the unity package manager and [OpenUPM](https://openupm.com/packages/com.utilities.rest). + ### Via Unity Package Manager and OpenUPM - Open your Unity project settings @@ -77,7 +81,9 @@ var restParameters = new RestParameters( disposeDownloadHandler, // Optional, dispose the DownloadHandler. Default is true. disposeUploadHandler, // Optional, dispose the UploadHandler. Default is true. certificateHandler, // Optional, certificate handler for the request. - disposeCertificateHandler); // Optional, dispose the CertificateHandler. Default is true. + disposeCertificateHandler, // Optional, dispose the CertificateHandler. Default is true. + cacheDownloads, // Optional, cache downloaded content. Default is true + debug); // Optional, enable debug output of the request. Default is false. var response = await Rest.GetAsync("www.your.api/endpoint", restParameters); ``` @@ -154,9 +160,15 @@ response.Validate(debug: true); ```csharp // cache directory defaults to {Application.temporaryCachePath}/download_cache/ -// it is currently not possible to set this value, but is likely a nice feature request. Debug.Log(Rest.DownloadCacheDirectory); +// cache directory can be set to one of: +// Application.temporaryCachePath, +// Application.persistentDataPath, +// Application.dataPath, +// Application.streamingAssetsPath +Rest.DownloadCacheDirectory = Application.dataPath; + var uri = "www.url.to/remote/resource"; if (Rest.TryGetDownloadCacheItem(uri, out var cachedFilePath)) @@ -177,6 +189,8 @@ Rest.DeleteDownloadCache(); #### Files +Download a file. + ```csharp var downloadedFilePath = await Rest.DownloadFileAsync("www.your.api/your_file.pdf"); @@ -186,6 +200,30 @@ if (!string.IsNullOrWhiteSpace(downloadedFilePath)) } ``` +Download a file, and get the raw byte data. + +```csharp +var downloadedBytes = await Rest.DownloadFileBytesAsync("www.your.api/your_file.pdf"); + +if (downloadedBytes != null && downloadedBytes.Length > 0) +{ + Debug.Log(downloadedBytes.Length); +} +``` + +Download raw file bytes. + +> This request does not cache data + +```csharp +var downloadedBytes = await Rest.DownloadBytesAsync("www.your.api/your_file.pdf"); + +if (downloadedBytes != null && downloadedBytes.Length > 0) +{ + Debug.Log(downloadedBytes.Length); +} +``` + #### Textures > Pro Tip: This also works with local file paths to load textures async at runtime! diff --git a/Runtime/AbstractAuthentication.cs b/Runtime/AbstractAuthentication.cs index ea180e8..e06f42b 100644 --- a/Runtime/AbstractAuthentication.cs +++ b/Runtime/AbstractAuthentication.cs @@ -46,9 +46,6 @@ public virtual TAuthentication LoadDefaultsReversed() LoadFromDirectory() ?? LoadFromAsset(); - [Obsolete("Use LoadFromAsset (remove angle bracket type specification)")] - public TAuthentication LoadFromAsset() => LoadFromAsset(); - /// /// Attempts to load the authentication from a asset that implements . /// diff --git a/Runtime/Rest.cs b/Runtime/Rest.cs index 5a73422..fdfa253 100644 --- a/Runtime/Rest.cs +++ b/Runtime/Rest.cs @@ -26,6 +26,10 @@ public static class Rest private const string kHttpVerbPATCH = "PATCH"; private const string eventDelimiter = "data: "; private const string stopEventDelimiter = "[DONE]"; + private const string content_type = "Content-Type"; + private const string content_length = "Content-Length"; + private const string application_json = "application/json"; + private const string application_octet_stream = "application/octet-stream"; #region Authentication @@ -132,11 +136,7 @@ public static string GetBearerOAuthToken(string authToken) RestParameters parameters = null, CancellationToken cancellationToken = default) { -#if UNITY_2022_2_OR_NEWER using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#else - using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#endif return await webRequest.SendAsync(parameters, cancellationToken); } @@ -172,17 +172,13 @@ public static string GetBearerOAuthToken(string authToken) RestParameters parameters = null, CancellationToken cancellationToken = default) { -#if UNITY_2022_2_OR_NEWER - using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#else using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#endif var data = new UTF8Encoding().GetBytes(jsonData); using var uploadHandler = new UploadHandlerRaw(data); webRequest.uploadHandler = uploadHandler; using var downloadHandler = new DownloadHandlerBuffer(); webRequest.downloadHandler = downloadHandler; - webRequest.SetRequestHeader("Content-Type", "application/json"); + webRequest.SetRequestHeader(content_type, application_json); return await webRequest.SendAsync(parameters, cancellationToken); } @@ -202,17 +198,13 @@ public static string GetBearerOAuthToken(string authToken) RestParameters parameters = null, CancellationToken cancellationToken = default) { -#if UNITY_2022_2_OR_NEWER - using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#else using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#endif var data = new UTF8Encoding().GetBytes(jsonData); using var uploadHandler = new UploadHandlerRaw(data); webRequest.uploadHandler = uploadHandler; using var downloadHandler = new DownloadHandlerBuffer(); webRequest.downloadHandler = downloadHandler; - webRequest.SetRequestHeader("Content-Type", "application/json"); + webRequest.SetRequestHeader(content_type, application_json); return await webRequest.SendAsync(parameters, serverSentEventCallback, cancellationToken); } @@ -234,11 +226,7 @@ public static string GetBearerOAuthToken(string authToken) RestParameters parameters = null, CancellationToken cancellationToken = default) { -#if UNITY_2022_2_OR_NEWER - using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#else using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#endif var data = new UTF8Encoding().GetBytes(jsonData); using var uploadHandler = new UploadHandlerRaw(data); webRequest.uploadHandler = uploadHandler; @@ -247,7 +235,7 @@ public static string GetBearerOAuthToken(string authToken) : new DownloadHandlerCallback(webRequest); downloadHandler.OnDataReceived += dataReceivedEventCallback; webRequest.downloadHandler = downloadHandler; - webRequest.SetRequestHeader("Content-Type", "application/json"); + webRequest.SetRequestHeader(content_type, application_json); try { @@ -273,16 +261,12 @@ public static string GetBearerOAuthToken(string authToken) RestParameters parameters = null, CancellationToken cancellationToken = default) { -#if UNITY_2022_2_OR_NEWER using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#else - using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#endif using var uploadHandler = new UploadHandlerRaw(bodyData); webRequest.uploadHandler = uploadHandler; using var downloadHandler = new DownloadHandlerBuffer(); webRequest.downloadHandler = downloadHandler; - webRequest.SetRequestHeader("Content-Type", "application/octet-stream"); + webRequest.SetRequestHeader(content_type, application_octet_stream); return await webRequest.SendAsync(parameters, cancellationToken); } @@ -300,11 +284,7 @@ public static string GetBearerOAuthToken(string authToken) RestParameters parameters = null, CancellationToken cancellationToken = default) { -#if UNITY_2022_2_OR_NEWER - using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#else using var webRequest = new UnityWebRequest(query, UnityWebRequest.kHttpVerbPOST); -#endif var boundary = UnityWebRequest.GenerateBoundary(); var formSections = UnityWebRequest.SerializeFormSections(form, boundary); using var uploadHandler = new UploadHandlerRaw(formSections); @@ -334,7 +314,7 @@ public static string GetBearerOAuthToken(string authToken) CancellationToken cancellationToken = default) { using var webRequest = UnityWebRequest.Put(query, jsonData); - webRequest.SetRequestHeader("Content-Type", "application/json"); + webRequest.SetRequestHeader(content_type, application_json); return await webRequest.SendAsync(parameters, cancellationToken); } @@ -353,7 +333,7 @@ public static string GetBearerOAuthToken(string authToken) CancellationToken cancellationToken = default) { using var webRequest = UnityWebRequest.Put(query, bodyData); - webRequest.SetRequestHeader("Content-Type", "application/octet-stream"); + webRequest.SetRequestHeader(content_type, application_octet_stream); return await webRequest.SendAsync(parameters, cancellationToken); } @@ -377,7 +357,7 @@ public static string GetBearerOAuthToken(string authToken) { using var webRequest = UnityWebRequest.Put(query, jsonData); webRequest.method = kHttpVerbPATCH; - webRequest.SetRequestHeader("Content-Type", "application/json"); + webRequest.SetRequestHeader(content_type, application_json); return await webRequest.SendAsync(parameters, cancellationToken); } @@ -397,7 +377,7 @@ public static string GetBearerOAuthToken(string authToken) { using var webRequest = UnityWebRequest.Put(query, bodyData); webRequest.method = kHttpVerbPATCH; - webRequest.SetRequestHeader("Content-Type", "application/octet-stream"); + webRequest.SetRequestHeader(content_type, application_octet_stream); return await webRequest.SendAsync(parameters, cancellationToken); } @@ -440,11 +420,37 @@ private static IDownloadCache Cache _ => new DiskDownloadCache() }; + private static readonly List allowedDownloadLocations = new() + { + Application.temporaryCachePath, + Application.persistentDataPath, + Application.dataPath, + Application.streamingAssetsPath + }; + + private static string downloadLocation = Application.temporaryCachePath; + + public static string DownloadLocation + { + get => downloadLocation; + set + { + if (allowedDownloadLocations.Contains(value)) + { + downloadLocation = value; + } + else + { + Debug.LogError($"Invalid Download location specified. Must be one of: {string.Join(", ", allowedDownloadLocations)}"); + } + } + } + /// /// The download cache directory.
///
public static string DownloadCacheDirectory - => Path.Combine(Application.temporaryCachePath, download_cache); + => Path.Combine(DownloadLocation, download_cache); /// /// Creates the if it doesn't exist. @@ -498,31 +504,19 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) #endregion Download Cache - [Obsolete("use new overload with debug support")] - public static async Task DownloadTextureAsync( - string url, - string fileName = null, - RestParameters parameters = null, - CancellationToken cancellationToken = default) - { - return await DownloadTextureAsync(url, fileName, parameters, false, cancellationToken); - } - /// /// Download a from the provided . /// /// The url to download the from. /// Optional, file name to download (including extension). /// Optional, . - /// Optional, debug http request. /// Optional, . /// A new instance. public static async Task DownloadTextureAsync( - string url, - string fileName = null, - RestParameters parameters = null, - bool debug = false, - CancellationToken cancellationToken = default) + string url, + string fileName = null, + RestParameters parameters = null, + CancellationToken cancellationToken = default) { await Awaiters.UnityMainThread; @@ -533,6 +527,7 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) bool isCached; string cachePath; + parameters ??= new RestParameters(); if (url.Contains(FileUriPrefix)) { @@ -541,7 +536,7 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) } else { - isCached = TryGetDownloadCacheItem(fileName, out cachePath); + isCached = TryGetDownloadCacheItem(fileName, out cachePath) && parameters.CacheDownloads; } if (isCached) @@ -550,16 +545,15 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) } Texture2D texture; - parameters ??= new RestParameters(); parameters.DisposeDownloadHandler = true; using var webRequest = UnityWebRequestTexture.GetTexture(url); try { var response = await webRequest.SendAsync(parameters, cancellationToken); - response.Validate(debug); + response.Validate(parameters.Debug); - if (!isCached) + if (!isCached && parameters.CacheDownloads) { await Cache.WriteCacheItemAsync(webRequest.downloadHandler.data, cachePath, cancellationToken).ConfigureAwait(true); } @@ -580,16 +574,6 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) return texture; } - [Obsolete("Use new overload with debug support")] - public static async Task DownloadAudioClipAsync( - string url, - AudioType audioType, - RestParameters parameters = null, - CancellationToken cancellationToken = default) - { - return await DownloadAudioClipAsync(url, audioType, httpMethod: UnityWebRequest.kHttpVerbGET, parameters: parameters, cancellationToken: cancellationToken); - } - /// /// Download a from the provided . /// @@ -600,7 +584,6 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) /// Optional, json payload. Only OR can be supplied. /// Optional, raw byte payload. Only OR can be supplied. /// Optional, . - /// Optional, debug http request. /// Optional, . /// A new instance. public static async Task DownloadAudioClipAsync( @@ -611,7 +594,6 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) string jsonData = null, byte[] payload = null, RestParameters parameters = null, - bool debug = false, CancellationToken cancellationToken = default) { await Awaiters.UnityMainThread; @@ -623,6 +605,7 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) bool isCached; string cachePath; + parameters ??= new RestParameters(); if (url.Contains(FileUriPrefix)) { @@ -631,7 +614,7 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) } else { - isCached = TryGetDownloadCacheItem(fileName, out cachePath); + isCached = TryGetDownloadCacheItem(fileName, out cachePath) && parameters.CacheDownloads; } if (isCached) @@ -655,28 +638,21 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) var jsonHeaders = new Dictionary { - { "Content-Type", "application/json" } + { content_type, application_json } }; - if (parameters is { Headers: not null }) + foreach (var header in parameters.Headers) { - foreach (var header in parameters.Headers) - { - jsonHeaders.Add(header.Key, header.Value); - } + jsonHeaders.Add(header.Key, header.Value); } - if (parameters != null) - { - parameters.Headers = jsonHeaders; - } + parameters.Headers = jsonHeaders; } uploadHandler = new UploadHandlerRaw(payload); } AudioClip clip; - parameters ??= new RestParameters(); parameters.DisposeUploadHandler = false; parameters.DisposeDownloadHandler = false; using var webRequest = new UnityWebRequest(url, httpMethod, downloadHandler, uploadHandler); @@ -684,9 +660,9 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) try { var response = await webRequest.SendAsync(parameters, cancellationToken); - response.Validate(debug); + response.Validate(parameters.Debug); - if (!isCached) + if (!isCached && parameters.CacheDownloads) { await Cache.WriteCacheItemAsync(downloadHandler.data, cachePath, cancellationToken); } @@ -721,7 +697,6 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) /// Optional, file name to download (including extension). /// Optional, the amount of data to to download before signaling that streaming is ready. /// Optional, . - /// Optional, debug http request. /// Optional, . /// A new instance. public static async Task StreamAudioAsync( @@ -734,7 +709,6 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) byte[] payload = null, ulong playbackAmountThreshold = 10000, RestParameters parameters = null, - bool debug = false, CancellationToken cancellationToken = default) { await Awaiters.UnityMainThread; @@ -767,7 +741,7 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) var jsonHeaders = new Dictionary { - { "Content-Type", "application/json" } + { content_type, application_json } }; if (parameters is { Headers: not null }) @@ -828,7 +802,7 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) var response = await webRequest.SendAsync(parameters, cancellationToken); uploadHandler?.Dispose(); - response.Validate(debug); + response.Validate(parameters.Debug); var loadedClip = downloadHandler.audioClip; @@ -906,61 +880,40 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) using (webRequest) { - Response response; + parameters ??= new RestParameters(); + parameters.Timeout = options?.Timeout ?? -1; + parameters.DisposeDownloadHandler = false; + var response = await webRequest.SendAsync(parameters, cancellationToken); + response.Validate(parameters.Debug); - try - { - parameters ??= new RestParameters(); - parameters.Timeout = options?.Timeout ?? -1; - parameters.DisposeDownloadHandler = false; - response = await webRequest.SendAsync(parameters, cancellationToken); - } - catch (Exception e) - { - Debug.LogError(e); - throw; - } + var downloadHandler = (DownloadHandlerAssetBundle)webRequest.downloadHandler; + var assetBundle = downloadHandler.assetBundle; + downloadHandler.Dispose(); - if (!response.Successful) + if (assetBundle == null) { - Debug.LogError($"Failed to download asset bundle from \"{url}\"!\n{response.Code}:{response.Body}"); - return null; + throw new RestException(response, $"Failed to download asset bundle from \"{url}\"!"); } - var downloadHandler = (DownloadHandlerAssetBundle)webRequest.downloadHandler; - var assetBundle = downloadHandler.assetBundle; - downloadHandler.Dispose(); return assetBundle; } } #endif // UNITY_ADDRESSABLES - [Obsolete("use new overload with debug support")] - public static async Task DownloadFileAsync( - string url, - string fileName = null, - RestParameters parameters = null, - CancellationToken cancellationToken = default) - { - return await DownloadFileAsync(url, fileName, parameters, false, cancellationToken); - } - /// /// Download a file from the provided . /// /// The url to download the file from. /// Optional, file name to download (including extension). /// Optional, . - /// Optional, debug http request. /// Optional, . /// The path to the downloaded file. public static async Task DownloadFileAsync( - string url, - string fileName = null, - RestParameters parameters = null, - bool debug = false, - CancellationToken cancellationToken = default) + string url, + string fileName = null, + RestParameters parameters = null, + CancellationToken cancellationToken = default) { await Awaiters.UnityMainThread; @@ -969,7 +922,8 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) TryGetFileNameFromUrl(url, out fileName); } - if (TryGetDownloadCacheItem(fileName, out var filePath)) + if (TryGetDownloadCacheItem(fileName, out var filePath) && + (parameters?.CacheDownloads ?? true)) { return filePath; } @@ -979,10 +933,59 @@ public static bool TryGetFileNameFromUrl(string url, out string fileName) fileDownloadHandler.removeFileOnAbort = true; webRequest.downloadHandler = fileDownloadHandler; var response = await webRequest.SendAsync(parameters, cancellationToken); - response.Validate(debug); + response.Validate(parameters?.Debug ?? false); return filePath; } + /// + /// Download a file from the provided and return the contents as bytes. + /// + /// The url to download the file from. + /// Optional, file name to download (including extension). + /// Optional, . + /// Optional, . + /// The bytes of the downloaded file. + public static async Task DownloadFileBytesAsync( + string url, + string fileName = null, + RestParameters parameters = null, + CancellationToken cancellationToken = default) + { + await Awaiters.UnityMainThread; + byte[] bytes = null; + var filePath = await DownloadFileAsync(url, fileName, parameters, cancellationToken); + var localPath = filePath.Replace("file://", string.Empty); + + if (File.Exists(localPath)) + { + bytes = await File.ReadAllBytesAsync(localPath, cancellationToken).ConfigureAwait(true); + } + + return bytes; + } + + /// + /// Download raw file contents from the provided . + /// + /// The url to download from. + /// Optional, . + /// Optional, . + /// The bytes downloaded from the server. + /// This request does not cache results. + public static async Task DownloadBytesAsync( + string url, + RestParameters parameters = null, + CancellationToken cancellationToken = default) + { + await Awaiters.UnityMainThread; + using var webRequest = UnityWebRequest.Get(url); + using var downloadHandlerBuffer = new DownloadHandlerBuffer(); + webRequest.downloadHandler = downloadHandlerBuffer; + var response = await webRequest.SendAsync(parameters, cancellationToken); + response.Validate(parameters?.Debug ?? false); + return response.Data; + } + #endregion Get Multimedia Content /// @@ -1035,13 +1038,12 @@ UnityWebRequest.kHttpVerbPUT or // HACK: Workaround for extra quotes around boundary. if (hasUpload) { - const string CONTENT_TYPE = "Content-Type"; - var contentType = webRequest.GetRequestHeader(CONTENT_TYPE); + var contentType = webRequest.GetRequestHeader(content_type); if (!string.IsNullOrWhiteSpace(contentType)) { contentType = contentType.Replace("\"", string.Empty); - webRequest.SetRequestHeader(CONTENT_TYPE, contentType); + webRequest.SetRequestHeader(content_type, contentType); } } @@ -1101,10 +1103,9 @@ async void CallbackThread() var percentage = hasUpload && webRequest.uploadProgress > 1f ? webRequest.uploadProgress : webRequest.downloadProgress; - // Get the content length of the download - const string CONTENT_LENGTH = "Content-Length"; - if (!ulong.TryParse(webRequest.GetResponseHeader(CONTENT_LENGTH), out var length)) + // Get the content length of the download + if (!ulong.TryParse(webRequest.GetResponseHeader(content_length), out var length)) { length = webRequest.downloadedBytes; } @@ -1127,13 +1128,18 @@ async void CallbackThread() } } -#pragma warning disable CS4014 +#pragma warning disable CS4014 // We purposefully don't await this task so it will run on a background thread. Task.Run(CallbackThread, cancellationToken); #pragma warning restore CS4014 } try { + if (parameters is { Debug: true }) + { + Debug.Log($"[{webRequest.method}] {webRequest.url}"); + } + await webRequest.SendWebRequest(); } catch (Exception e) diff --git a/Runtime/RestParameters.cs b/Runtime/RestParameters.cs index 92e0815..486b520 100644 --- a/Runtime/RestParameters.cs +++ b/Runtime/RestParameters.cs @@ -21,6 +21,8 @@ public class RestParameters /// Optional, dispose the ?
Default is true. /// Optional, certificate handler for the request. /// Optional, dispose the ?
Default is true. + /// Optional, cache downloaded content.
Default is true. + /// Optional, enable debug output of the request.
Default is false. public RestParameters( IReadOnlyDictionary headers = null, IProgress progress = null, @@ -28,7 +30,9 @@ public class RestParameters bool disposeDownloadHandler = true, bool disposeUploadHandler = true, CertificateHandler certificateHandler = null, - bool disposeCertificateHandler = true) + bool disposeCertificateHandler = true, + bool cacheDownloads = true, + bool debug = false) { Headers = headers; Progress = progress; @@ -37,6 +41,8 @@ public class RestParameters DisposeUploadHandler = disposeUploadHandler; CertificateHandler = certificateHandler; DisposeCertificateHandler = disposeCertificateHandler; + CacheDownloads = cacheDownloads; + Debug = debug; } /// @@ -78,5 +84,16 @@ public class RestParameters public bool DisposeUploadHandler { get; internal set; } internal int ServerSentEventCount { get; set; } + + /// + /// Cache downloaded content.
+ /// Default is true. + ///
+ public bool CacheDownloads { get; set; } + + /// + /// Enable debug output of the request. + /// + public bool Debug { get; set; } } } diff --git a/package.json b/package.json index 6a9847a..424979d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Utilities.Rest", "description": "This package contains useful RESTful utilities for the Unity Game Engine.", "keywords": [], - "version": "2.4.6", + "version": "2.5.0", "unity": "2021.3", "documentationUrl": "https://github.com/RageAgainstThePixel/com.utilities.rest#documentation", "changelogUrl": "https://github.com/RageAgainstThePixel/com.utilities.rest/releases",