Skip to content

Commit

Permalink
com.utilities.rest 2.2.0 (#45)
Browse files Browse the repository at this point in the history
- Added dataReceivedEventCallback overloads to capture streamed data
- Added GetUrl eueryParamters overload
- Added missing docs
- Formalized error responses and logging
- Updated deps
  • Loading branch information
StephenHodgson authored Oct 25, 2023
1 parent 3f637b9 commit 28b5b3a
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 17 deletions.
22 changes: 20 additions & 2 deletions Runtime/BaseEndPoint.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Collections.Generic;
using System.Linq;
using Utilities.WebRequestRest.Interfaces;

namespace Utilities.WebRequestRest
Expand Down Expand Up @@ -35,7 +37,23 @@ public abstract class BaseEndPoint<TClient, TAuthentication, TSettings>
/// Gets the full formatted url for the API endpoint.
/// </summary>
/// <param name="endpoint">The endpoint url.</param>
protected string GetUrl(string endpoint = "")
=> string.Format(client.Settings.BaseRequestUrlFormat, $"{Root}{endpoint}");
/// <param name="queryParameters">Optional, parameters to add to the endpoint.</param>
protected string GetUrl(string endpoint = "", Dictionary<string, string> queryParameters = null)
{
var result = string.Format(client.Settings.BaseRequestUrlFormat, $"{Root}{endpoint}");

if (queryParameters is { Count: not 0 })
{
result += $"?{string.Join("&", queryParameters.Select(parameter => $"{parameter.Key}={parameter.Value}"))}";
}

return result;
}

/// <summary>
/// Enables or disables the logging of all http responses of header and body information for this endpoint.<br/>
/// WARNING! Enabling this in your production build, could potentially leak sensitive information!
/// </summary>
public bool EnableDebug { get; set; }
}
}
16 changes: 13 additions & 3 deletions Runtime/DiskDownloadCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ private static Guid GenerateGuid(string @string)
return new Guid(md5.ComputeHash(Encoding.Default.GetBytes(@string)));
}

/// <inheritdoc />
public void ValidateCacheDirectory()
{
if (!Directory.Exists(Rest.DownloadCacheDirectory))
Expand All @@ -33,12 +34,14 @@ public void ValidateCacheDirectory()
}
}

/// <inheritdoc />
public async Task ValidateCacheDirectoryAsync()
{
await Awaiters.UnityMainThread;
ValidateCacheDirectory();
}

/// <inheritdoc />
public bool TryGetDownloadCacheItem(string uri, out string filePath)
{
ValidateCacheDirectory();
Expand Down Expand Up @@ -69,6 +72,7 @@ public bool TryGetDownloadCacheItem(string uri, out string filePath)
return exists;
}

/// <inheritdoc />
public bool TryDeleteCacheItem(string uri)
{
if (!TryGetDownloadCacheItem(uri, out var filePath))
Expand All @@ -88,6 +92,7 @@ public bool TryDeleteCacheItem(string uri)
return !File.Exists(filePath);
}

/// <inheritdoc />
public void DeleteDownloadCache()
{
if (Directory.Exists(Rest.DownloadCacheDirectory))
Expand All @@ -96,26 +101,31 @@ public void DeleteDownloadCache()
}
}

/// <inheritdoc />
public async Task WriteCacheItemAsync(byte[] data, string cachePath, CancellationToken cancellationToken)
{
if (File.Exists(cachePath))
{
return;
}

var fileStream = File.OpenWrite(cachePath);
FileStream fileStream = null;

try
{
await fileStream.WriteAsync(data, 0, data.Length, cancellationToken);
fileStream = new FileStream(cachePath, FileMode.CreateNew, FileAccess.Read);
await fileStream.WriteAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(true);
}
catch (Exception e)
{
Debug.LogError($"Failed to write asset to disk! {e}");
}
finally
{
await fileStream.DisposeAsync();
if (fileStream != null)
{
await fileStream.DisposeAsync().ConfigureAwait(true);
}
}
}
}
Expand Down
96 changes: 96 additions & 0 deletions Runtime/DownloadHandlerCallback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using Utilities.Async;

namespace Utilities.WebRequestRest
{
internal class DownloadHandlerCallback : DownloadHandlerScript
{
internal const int kEventChunkSize = 512;

private readonly MemoryStream stream = new MemoryStream();

private long streamPosition;

private long StreamOffset => stream.Length - streamPosition;

private int eventChunkSize = kEventChunkSize;

public int EventChunkSize
{
get => eventChunkSize;
set
{
if (value < 1)
{
throw new InvalidOperationException($"{nameof(EventChunkSize)} must be greater than 1!");
}

eventChunkSize = value;
}
}

public UnityWebRequest UnityWebRequest { get; set; }

public Action<UnityWebRequest, byte[]> OnDataReceived { get; set; }

protected override bool ReceiveData(byte[] unprocessedData, int dataLength)
{
var offset = unprocessedData.Length - dataLength;

try
{
stream.Position = stream.Length;
stream.Write(unprocessedData, offset, dataLength);

if (StreamOffset >= EventChunkSize)
{
var multiplier = StreamOffset / EventChunkSize;
var bytesToRead = EventChunkSize * multiplier;
stream.Position = streamPosition;
var buffer = new byte[bytesToRead];
var bytesRead = stream.Read(buffer, 0, (int)bytesToRead);
streamPosition += bytesRead;
OnDataReceived?.Invoke(UnityWebRequest, buffer);
}
}
catch (Exception e)
{
Debug.LogError(e);
}

return base.ReceiveData(unprocessedData, dataLength);
}

protected override void CompleteContent()
{
try
{
if (StreamOffset > 0)
{
stream.Position = streamPosition;
var buffer = new byte[StreamOffset];
var bytesRead = stream.Read(buffer);
streamPosition += bytesRead;
OnDataReceived.Invoke(UnityWebRequest, buffer);
}
}
catch (Exception e)
{
Debug.LogError(e);
}

base.CompleteContent();
}

public override void Dispose()
{
stream.Dispose();
base.Dispose();
}
}
}
11 changes: 11 additions & 0 deletions Runtime/DownloadHandlerCallback.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 92 additions & 10 deletions Runtime/Rest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ public static async Task<Response> GetAsync(
return await webRequest.SendAsync(parameters, serverSentEventCallback, cancellationToken);
}

/// <summary>
/// Rest GET.
/// </summary>
/// <param name="query">Finalized Endpoint Query with parameters.</param>
/// <param name="dataReceivedEventCallback"><see cref="Action{T}"/> data received event callback.</param>
/// <param name="parameters">Optional, <see cref="RestParameters"/>.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns>The response data.</returns>
public static async Task<Response> GetAsync(
string query,
Action<UnityWebRequest, byte[]> dataReceivedEventCallback,
RestParameters parameters = null,
CancellationToken cancellationToken = default)
{
using var webRequest = UnityWebRequest.Get(query);
using var downloadHandler = new DownloadHandlerCallback();
downloadHandler.OnDataReceived += dataReceivedEventCallback;

try
{
return await webRequest.SendAsync(parameters, cancellationToken);
}
finally
{
downloadHandler.OnDataReceived -= dataReceivedEventCallback;
}
}

#endregion GET

#region POST
Expand Down Expand Up @@ -184,6 +212,54 @@ public static async Task<Response> PostAsync(
return await webRequest.SendAsync(parameters, serverSentEventCallback, cancellationToken);
}

/// <summary>
/// Rest POST.
/// </summary>
/// <param name="query">Finalized Endpoint Query with parameters.</param>
/// <param name="jsonData">JSON data for the request.</param>
/// <param name="dataReceivedEventCallback"><see cref="Action{T}"/> data received event callback.</param>
/// <param name="eventChunkSize">Optional, <see cref="dataReceivedEventCallback"/> event chunk size in bytes (Defaults to 512 bytes).</param>
/// <param name="parameters">Optional, <see cref="RestParameters"/>.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns>The response data.</returns>
public static async Task<Response> PostAsync(
string query,
string jsonData,
Action<UnityWebRequest, byte[]> dataReceivedEventCallback,
int? eventChunkSize = null,
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 DownloadHandlerCallback();
downloadHandler.UnityWebRequest = webRequest;

if (eventChunkSize.HasValue)
{
downloadHandler.EventChunkSize = eventChunkSize.Value;
}

downloadHandler.OnDataReceived += dataReceivedEventCallback;
webRequest.downloadHandler = downloadHandler;
webRequest.SetRequestHeader("Content-Type", "application/json");

try
{
return await webRequest.SendAsync(parameters, null, cancellationToken);
}
finally
{
downloadHandler.OnDataReceived -= dataReceivedEventCallback;
}
}

/// <summary>
/// Rest POST.
/// </summary>
Expand Down Expand Up @@ -211,6 +287,14 @@ public static async Task<Response> PostAsync(
return await webRequest.SendAsync(parameters, cancellationToken);
}

/// <summary>
/// Rest POST.
/// </summary>
/// <param name="query">Finalized Endpoint Query with parameters.</param>
/// <param name="form">The <see cref="IMultipartFormSection"/> to post.</param>
/// <param name="parameters">Optional, <see cref="RestParameters"/>.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns>The response data.</returns>
public static async Task<Response> PostAsync(
string query,
List<IMultipartFormSection> form,
Expand Down Expand Up @@ -473,8 +557,7 @@ public static async Task<Texture2D> DownloadTextureAsync(

if (!response.Successful)
{
Debug.LogError($"Failed to download texture from \"{url}\"!\n[{response.Code}] {response.Body}");
return null;
throw new RestException(response, $"Failed to download texture from \"{url}\"!");
}

var downloadHandler = (DownloadHandlerTexture)webRequest.downloadHandler;
Expand All @@ -490,8 +573,7 @@ public static async Task<Texture2D> DownloadTextureAsync(

if (texture == null)
{
Debug.LogError($"Failed to download texture from \"{url}\"!\n[{response.Code}] {response.Body}");
return null;
throw new RestException(response, $"Failed to download texture from \"{url}\"!");
}

texture.name = Path.GetFileNameWithoutExtension(cachePath);
Expand Down Expand Up @@ -546,7 +628,7 @@ public static async Task<AudioClip> DownloadAudioClipAsync(

if (!response.Successful)
{
throw new RestException(response, $"Failed to download audio clip from \"{url}\"!\n{response.ToString(nameof(StreamAudioAsync))}");
throw new RestException(response, $"Failed to download audio clip from \"{url}\"!");
}

var downloadHandler = (DownloadHandlerAudioClip)webRequest.downloadHandler;
Expand Down Expand Up @@ -575,6 +657,7 @@ public static async Task<AudioClip> DownloadAudioClipAsync(
/// <param name="fileName">Optional, file name to download (including extension).</param>
/// <param name="playbackAmountThreshold">Optional, the amount of data to to download before signaling that streaming is ready.</param>
/// <param name="parameters">Optional, <see cref="RestParameters"/>.</param>
/// <param name="debug">Optional, debug http request.</param>
/// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
/// <returns>Raw downloaded bytes from the stream.</returns>
public static async Task<AudioClip> StreamAudioAsync(
Expand All @@ -587,6 +670,7 @@ public static async Task<AudioClip> StreamAudioAsync(
byte[] payload = null,
ulong playbackAmountThreshold = 10000,
RestParameters parameters = null,
bool debug = false,
CancellationToken cancellationToken = default)
{
await Awaiters.UnityMainThread;
Expand Down Expand Up @@ -643,8 +727,7 @@ public static async Task<AudioClip> StreamAudioAsync(
parameters.DisposeUploadHandler = false;
parameters.DisposeDownloadHandler = false;
using var downloadHandler = new DownloadHandlerAudioClip(url, audioType);
downloadHandler.streamAudio = true; // Due to a Unity bug this is actually totally non-functional... https://forum.unity.com/threads/downloadhandleraudioclip-streamaudio-is-ignored.699908/

downloadHandler.streamAudio = true; // BUG: Due to a Unity bug this is actually totally non-functional... https://forum.unity.com/threads/downloadhandleraudioclip-streamaudio-is-ignored.699908/
using var webRequest = new UnityWebRequest(url, httpMethod, downloadHandler, uploadHandler);

IProgress<Progress> progress = null;
Expand Down Expand Up @@ -682,7 +765,7 @@ public static async Task<AudioClip> StreamAudioAsync(

var response = await webRequest.SendAsync(parameters, cancellationToken);
uploadHandler?.Dispose();
response.Validate(true);
response.Validate(debug);

var loadedClip = downloadHandler.audioClip;

Expand Down Expand Up @@ -824,8 +907,7 @@ public static async Task<string> DownloadFileAsync(

if (!response.Successful)
{
Debug.LogError($"Failed to download file from \"{url}\"!\n[{response.Code}] {response.Body}");
return null;
throw new RestException(response, $"Failed to download file from \"{url}\"!");
}

return filePath;
Expand Down
Loading

0 comments on commit 28b5b3a

Please sign in to comment.