├── Examples └── UnitTests.cs ├── README.md └── Runtime ├── DownloadFulfillerFactory.cs ├── IDownloadFulfiller.cs ├── IDownloader.cs ├── UWRFulfiller.cs ├── UnityFileDownloader.cs └── utils ├── CommonExtensions.cs ├── HTTPHelper.cs ├── SemaphoreLocker.cs └── UnityWebRequestExtensions.cs /Examples/UnitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UFD; 5 | using System; 6 | using System.IO; 7 | 8 | /// 9 | /// Unit-Tests the behaviours in 'UnityFileDownloader'. 10 | /// 11 | public class UnitTests : MonoBehaviour 12 | { 13 | async void Download() 14 | { 15 | UnityFileDownloader ufd = new UnityFileDownloader(new string[] { 16 | "https://wallpaperaccess.com/full/87731.jpg", 17 | "https://wallpaperaccess.com/full/1126085.jpg", 18 | "https://wallpaperaccess.com/full/1308917.jpg", 19 | "https://wallpaperaccess.com/full/281585.jpg", 20 | }); 21 | ufd.RequestHeaders = new Dictionary{ 22 | ["key"] = "value" 23 | }; 24 | ufd.AbandonOnFailure = true; 25 | ufd.ContinueAfterFailure = false; 26 | ufd.MaxConcurrency = 3; // amount of threads to process concurently 27 | ufd.DownloadPath = "C://"; 28 | Debug.Log($"Downloading chunks of {ufd.MultipartChunkSize} bytes."); 29 | ufd.TryMultipartDownload = true; // false to disable multipart 30 | ufd.OnDownloadSuccess += (string uri) => { 31 | Debug.Log("Downloaded " + uri + "! Total progress is " + ufd.Progress + "%"); 32 | IDownloadFulfiller idf = ufd.GetFulfiller(uri); 33 | Debug.Log("This download was " + (ufd.MultipartDownload ? "" : "NOT ") + "downloaded in multiparts."); 34 | 35 | if (true) { // dummy 36 | ufd.Cancel(); 37 | } 38 | }; 39 | // only if multipart is enabled for this uri 40 | ufd.OnDownloadChunkedSucces += (uri) { 41 | Debug.Log("Progress for " + uri + " is " + ufd.GetProgress(uri)); 42 | }; 43 | ufd.OnDownloadsSuccess += () => { 44 | Debug.Log("Downloaded all files. (inline func)"); 45 | }; 46 | ufd.OnDownloadError += (string uri, int errorCode, string errorMsg) => 47 | { 48 | Debug.Log($"ErrorCode={errorCode}, EM={errorMsg}, URU={uri}"); 49 | }; 50 | await ufd.Download(); 51 | Debug.Log("Downloaded all files. (post-awaitable Download invokation)"); 52 | Debug.Log("MB/S = " + ufd.MegabytesDownloadedPerSecond); 53 | } 54 | 55 | void Start() { 56 | Download(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

UnityFileDownloader☁️

2 |

3 | 4 | downloads 5 | 6 | 7 | License: MIT 8 | 9 |

10 | 11 | A file downloader that handles asyncronous and concurrent file downloading, implicit multi-part file downloading, pausing/restarting, properties with per-file granularity and more. 12 | 13 | ## Appendix 14 | 15 | This file downloader was built because UnityWebRequest and recent solutions built by Unity and third-parties are subpar and bare of features. While this project is written 100% in C#, this downloader should support all platforms that Unity does. If you have any feedback, please reach out to me at jpgordon00@gmail.com, or submit an issue or pull request. Collaboration is encouraged! 16 | 17 | ## Features 18 | 19 | - Concurrently download any amount of files while adhering to a fixed number of asyncronous tasks 20 | - Implicitly handles multi-part file downloading, including pausing/restarting of chunked downloads 21 | - Provides both task-based asyncronous methods and standard callbacks for errors, individual file completion, all file completion, and more 22 | - Granularity of properties can be on a per-file basis, providing the ability to pause/restart, and to get/set progress, timeouts, headers, callbacks and more 23 | - Optionally handle "atomic" file downloading by deleting all downloaded (partially or impartially) files and ability to continue downloading through HTTP/S errors 24 | - Code structure that allows for modularity of both download fulfillment (such as UnityWebRequest) and dispatching of downloads. 25 | 26 | ## Usage/Examples 27 | 28 | ```javascript 29 | using UFD; 30 | using System; 31 | 32 | // ... in some class 33 | public class Example 34 | { 35 | async void Download() 36 | { 37 | UnityFileDownloader ufd = new UnityFileDownloader(new string[] { 38 | "https://wallpaperaccess.com/full/87731.jpg", 39 | }); 40 | await ufd.Download(); 41 | } 42 | 43 | } 44 | ``` 45 | 46 | ## Issues 47 | - Unity Editor termination and application lifecycle methods do not stop this downloader from functioning. 48 | 49 | ## TODO 50 | The following features are some of which I deem to be deseriable and not currently implemented: 51 | 52 | - Dynamic chunk sizing based on download speeds 53 | - Cleanup of class structure to provide complete modularity of download fulfillment and downloader. 54 | - Provide alternative fulfillers to UnityWebRequest, perhaps unlocking better platform specific performance. 55 | -------------------------------------------------------------------------------- /Runtime/DownloadFulfillerFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Threading; 3 | using System.Collections.Generic; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace UFD 8 | { 9 | 10 | /// 11 | /// Contains functions that create 'IDownloadFulfiller' children from a class name of one of its children 12 | /// 13 | public static class DownloadFulfillerFactory 14 | { 15 | /// 16 | /// Stores all the 'IDownloadFulfiller' children types. 17 | /// 18 | /// 19 | /// 20 | internal static List < Type > _Types = new List < Type > (); 21 | 22 | /// 23 | /// Searches through assembly to find all children of 'IDownloadFulfiller' and store it in '_Types' 24 | /// 25 | static DownloadFulfillerFactory() 26 | { 27 | _Types = typeof (IDownloadFulfiller).Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof (IDownloadFulfiller)) && !t.IsAbstract).Select(t => { 28 | if (t == typeof (IDownloadFulfiller)) return null; // ignore base types 29 | return t; //(IDownloadFulfiller) Activator.CreateInstance(t); 30 | }).ToList(); 31 | } 32 | 33 | /// 34 | /// Given a string matching the complete classname of some 'IDownloadFulfiller' child, return a new instance of that class. 35 | /// 36 | /// 37 | /// 38 | public static IDownloadFulfiller CreateFromClassName(string classname) { 39 | return (IDownloadFulfiller) Activator.CreateInstance(_Types.Find(t => t.Name == classname)); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Runtime/IDownloadFulfiller.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Threading; 3 | using System; 4 | using UnityEngine.Networking; 5 | using System.IO; 6 | using System.Collections.Generic; 7 | 8 | namespace UFD 9 | { 10 | 11 | /// 12 | /// Represents a class that should be capable of downloading files from a public endpoint. 13 | /// 14 | public abstract class IDownloadFulfiller 15 | { 16 | /// 17 | /// Amount of bytes to chunk each request by 18 | /// 19 | public int IntitialChunkSize = 200000; 20 | 21 | public bool _CompletedMultipartDownload = false; 22 | public bool _DidHeadReq = false; 23 | 24 | 25 | /// 26 | /// Returns true when this fulfiller has completed. 27 | /// 28 | public bool Completed => Progress == 1.0f; 29 | 30 | /// 31 | /// Returns the progress, if any, on this download, from 0f to 1f. 32 | /// 33 | /// 34 | public abstract float Progress 35 | { 36 | get; 37 | } 38 | 39 | /// 40 | /// Returns the URI associated with this fulfiller, if its not set upon 'Download'. 41 | /// 42 | /// 43 | public abstract string Uri 44 | { 45 | get; set; 46 | } 47 | 48 | public abstract int BytesDownloaded 49 | { 50 | get; 51 | } 52 | 53 | /// 54 | /// Headers to be used for any dispatched requests. 55 | /// 56 | public Dictionary RequestHeaders = null; 57 | 58 | /// 59 | /// The download path to download to, excluding the file name. 60 | /// 61 | /// 62 | public abstract string DownloadPath 63 | { 64 | get; set; 65 | } 66 | 67 | public string DownloadResultPath => (Uri == null || DownloadPath == null) ? null : Path.Combine(DownloadPath, HTTPHelper.GetFilenameFromUriNaively(Uri)).Replace("/", Path.DirectorySeparatorChar.ToString()); 68 | 69 | /// 70 | /// Returns true if this fulfiller can expect to download this file in chunks. 71 | /// This field is determined implicitly when downloading the file based on the 'ChunkSize'. 72 | /// 73 | /// 74 | public abstract bool MultipartDownload 75 | { 76 | get; set; 77 | } 78 | 79 | /// 80 | /// Explicitly checks for Multipart downloads only if this bool is true. 81 | /// 82 | public bool TryMultipartDownload = true; 83 | 84 | /// 85 | /// Set to true via the 'Pause' function and set to false by 'Download'. 86 | /// 87 | /// 88 | public abstract bool Paused 89 | { 90 | get; 91 | } 92 | 93 | /// 94 | /// True to remove the entire file upon a network error, even if it was partially downloaded. 95 | /// 96 | /// 97 | public abstract bool AbandonOnFailure 98 | { 99 | get; set; 100 | } 101 | 102 | public abstract bool DidError 103 | { 104 | get; 105 | } 106 | 107 | public abstract int StartTime 108 | { 109 | get; 110 | } 111 | 112 | public bool Downloading => StartTime != 0; 113 | 114 | public abstract int EndTime 115 | { 116 | get; 117 | } 118 | 119 | public abstract int Timeout 120 | { 121 | get; set; 122 | } 123 | 124 | public int ElapsedTime => Math.Abs(StartTime == 0 ? 0 : (EndTime == 0 ? DateTime.Now.Millisecond - StartTime : EndTime - StartTime)); 125 | 126 | public float MegabytesDownloadedPerSecond => (BytesDownloaded / 1000) / ((ElapsedTime / 1000) == 0 ? 1 : (ElapsedTime / 1000)); 127 | 128 | /// 129 | /// If this fulfiller has `MultipartDownload` set to true, then pause the download. 130 | /// Returns true if this downloader was succesfully paused. 131 | /// 132 | /// 133 | public abstract bool Cancel(); 134 | 135 | 136 | /// 137 | /// Initiates the download (or unpauses it), given the existing 'URI' property 138 | /// 139 | /// 140 | /// 141 | public abstract UnityWebRequestAsyncOperation Download(); 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /Runtime/IDownloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace UFD 7 | { 8 | public abstract class IDownloader 9 | { 10 | /// 11 | /// Constructs a IDownloader-child object with the given URIs. 12 | /// 13 | /// 14 | public IDownloader(params string[] uris) 15 | { 16 | Uris = uris; 17 | } 18 | 19 | /// 20 | /// Stores the Uri's that this downloader will attempt to download. 21 | /// 22 | protected string[] _Uris; 23 | 24 | /// 25 | /// Stores the URI's that this downloader succesfully downloaded. 26 | /// 27 | protected string[] _DownloadedUris; 28 | 29 | /// 30 | /// Stores the Uri's that this downloader has failed to download. 31 | /// 32 | protected string[] _IncompletedUris; 33 | 34 | /// 35 | /// Headers to be used for any dispatched requests. 36 | /// 37 | public Dictionary RequestHeaders = null; 38 | 39 | public bool TryMultipartDownload = true; 40 | 41 | /// 42 | /// Stores the actual IDownloadFulfiller's associated with each file that is to be downloaded. 43 | /// 44 | protected IDownloadFulfiller[] _Fulfillers = new IDownloadFulfiller[0]; 45 | 46 | protected IDownloadFulfiller[] _FulfillersOld = new IDownloadFulfiller[0]; 47 | 48 | /// 49 | /// Using the 'IDownloadFulfillerFactory', this string will be passed into the 'CreateFromClassName' function to construct 'Fulfillers' upon 'Uris' to assignment. 50 | /// 51 | public virtual string IDownloadFulfillerClassName => "UWRFulfiller"; 52 | 53 | /// 54 | /// Invoked upon error, where the first parameter is the uri, the second is the error code, and the third is the error message. 55 | /// 56 | public event Action OnDownloadError; 57 | 58 | /// 59 | /// Invoked when a file has been downloaded, with param name being the uri 60 | /// 61 | public event Action OnDownloadSuccess, OnDownloadChunkedSucces; 62 | 63 | /// 64 | /// Initiates a download from 'Uris'. 65 | /// 66 | public abstract Task Download(); 67 | 68 | /// 69 | /// Initiates a download for a specific URI 70 | /// This is used to either target additional URI's or to uncancel a specific URI that was downloading. 71 | /// 72 | /// 73 | public abstract Task Download(string uri); 74 | 75 | /// 76 | /// Pauses the downloader for a specific URI, if applicable. 77 | /// 78 | /// 79 | /// 80 | public abstract Task Cancel(string uri); 81 | 82 | /// 83 | /// Cancels this downloader, if applicable. 84 | /// 85 | /// 86 | public abstract Task Cancel(); 87 | 88 | 89 | 90 | public abstract void Reset(); 91 | 92 | /// 93 | /// Calculates progress based on 'Fulfillers' from 0f to 1f. 94 | /// 95 | /// 96 | public float Progress 97 | { 98 | get { 99 | // progress = (allProg) / numFiles 100 | float prog = 0; 101 | float num = NumFilesTotal; 102 | foreach (var idf in _Fulfillers) prog += idf.Progress; 103 | return prog; 104 | } 105 | } 106 | 107 | /// 108 | /// Calculates the total amount of files to download based on 'Fulfillers'. 109 | /// 110 | /// 111 | public int NumFilesTotal => _Fulfillers.Length; 112 | 113 | /// 114 | /// Returns true when this downloader has completed. 115 | /// 116 | public bool Completed => Progress == 1.0f; 117 | 118 | public int MultipartChunkSize = 200000; 119 | 120 | public abstract string DownloadPath 121 | { 122 | get; set; 123 | } 124 | 125 | /// 126 | /// Gets progress associated with a specific URI, if it exists. 127 | /// 128 | /// 129 | /// 130 | public float GetProgress(string uri) 131 | { 132 | if (!_Uris.ToList().Contains(uri)) return 0f; 133 | return _Fulfillers.ToList().Where(idf => idf.Uri == uri).ToArray().Length == 1 ? _Fulfillers.ToList().Find(idf => idf.Uri == uri).Progress : _FulfillersOld.ToList().Find(idf => idf.Uri == uri).Progress; 134 | } 135 | 136 | /// 137 | /// Set upon construction or manually, but a 'Download' invokation must follow. 138 | /// Checks URI for basic parsing before allowing. 139 | /// 140 | public string[] Uris { 141 | get => _Uris; 142 | set { 143 | List list = new List(); 144 | //foreach (var idf in _Fulfillers) idf.Dispose(); 145 | _Fulfillers = null; 146 | List fulfillers = new List(); 147 | foreach (var str in value) 148 | { 149 | try { 150 | Uri uri = new Uri(str); // dummy way of checking for valid URIs 151 | } catch (Exception ex) 152 | { 153 | throw new Exception($"URI {str} cannot be fed into {GetType().Name}.Uris"); 154 | return; 155 | } 156 | IDownloadFulfiller idf = DownloadFulfillerFactory.CreateFromClassName(IDownloadFulfillerClassName); 157 | idf.Uri = str; 158 | idf.DownloadPath = DownloadPath; 159 | idf.AbandonOnFailure = AbandonOnFailure; 160 | idf.Timeout = Timeout; 161 | idf.RequestHeaders = RequestHeaders; 162 | idf.TryMultipartDownload = TryMultipartDownload; 163 | idf.IntitialChunkSize = MultipartChunkSize; 164 | fulfillers.Add(idf); 165 | PendingURIS.Add(str); 166 | 167 | /// 168 | /// Invok action on parent 169 | /// 170 | /// 171 | ((UWRFulfiller) idf).OnDownloadChunkedSucces += () => { 172 | OnDownloadChunkedSucces?.Invoke(idf.Uri); 173 | }; 174 | 175 | /// 176 | /// Handle AbandonOnFailure, updating 'DidError' and dispatching 'OnError', and IncompleteUris 177 | /// 178 | /// 179 | /// 180 | /// 181 | ((UWRFulfiller) idf).OnDownloadError += async (int errorCode, string errorMsg) => { 182 | DidError = true; 183 | OnDownloadError?.Invoke(idf.Uri, errorCode, errorMsg); 184 | _IncompletedUris.Add(new string[]{idf.Uri}); 185 | }; 186 | 187 | /// 188 | /// Updates 'DownloadedURIS' asyncronously 189 | /// TODO: avoid casting by keeping event in parent class 190 | /// 191 | /// 192 | ((UWRFulfiller) idf).OnDownloadSuccess += () => { 193 | _DownloadedUris.Add(new string[]{idf.Uri}); 194 | OnDownloadSuccess?.Invoke(idf.Uri); 195 | }; 196 | list.Add(str); 197 | } 198 | _Uris = list.ToArray(); 199 | _Fulfillers = fulfillers.ToArray(); 200 | } 201 | } 202 | 203 | public abstract int Timeout 204 | { 205 | get; set; 206 | } 207 | 208 | public abstract int MaxConcurrency 209 | { 210 | get; set; 211 | } 212 | 213 | /// 214 | /// Returns the amount of files this downloader processes per second, only counting time after a Download initiates and before it begins. 215 | /// 216 | /// 217 | public abstract float NumFilesPerSecond 218 | { 219 | get; 220 | } 221 | 222 | /// 223 | /// If true, then this downloader will delete all partially downloaded files. 224 | /// 225 | /// 226 | public abstract bool AbandonOnFailure 227 | { 228 | get; set; 229 | } 230 | 231 | /// 232 | /// If true, then this downloader will continue attempting to download files post-error. 233 | /// 234 | /// 235 | public abstract bool ContinueAfterFailure 236 | { 237 | get; set; 238 | } 239 | 240 | 241 | public abstract bool Downloading 242 | { 243 | get; 244 | } 245 | 246 | public abstract bool Paused 247 | { 248 | get; 249 | } 250 | 251 | public abstract bool DidError 252 | { 253 | get; protected set; 254 | } 255 | 256 | public abstract int NumFilesRemaining 257 | { 258 | get; 259 | } 260 | 261 | public abstract int StartTime 262 | { 263 | get; 264 | } 265 | 266 | public abstract int EndTime 267 | { 268 | get; 269 | } 270 | 271 | public int ElapsedTime => Math.Abs(StartTime == 0 ? 0 : (EndTime == 0 ? DateTime.Now.Millisecond - StartTime : EndTime - StartTime)); 272 | 273 | public abstract string[] PendingURIS 274 | { 275 | get; 276 | } 277 | 278 | public string[] DownloadedUris => _DownloadedUris; 279 | 280 | public abstract string[] IncompletedURIS 281 | { 282 | get; 283 | } 284 | } 285 | 286 | } -------------------------------------------------------------------------------- /Runtime/UWRFulfiller.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using UnityEngine; 3 | using System; 4 | using System.IO; 5 | using System.Collections.Generic; 6 | using UnityEngine.Networking; 7 | 8 | namespace UFD 9 | { 10 | 11 | public class UWRFulfiller : IDownloadFulfiller 12 | { 13 | 14 | protected int _ExpectedSize = 0; 15 | protected int _ChunkSize = 0; 16 | 17 | 18 | 19 | internal int _StartTime = 0, _EndTime = 0; 20 | internal int _BytesDownloaded; 21 | internal float _Progress = 0f; 22 | internal int _Timeout = 6; 23 | internal string _Uri = null; 24 | internal string _DownloadPath = Application.persistentDataPath; 25 | internal bool _MultipartDownload = false; // set implicitly post-head request 26 | internal bool _AbandonOnFailure = true; 27 | internal bool _Paused = false; 28 | 29 | public event Action OnDownloadSuccess, OnCancel, OnDownloadChunkedSucces; 30 | 31 | public bool Completed => Progress == 1.0f; 32 | 33 | public override float Progress 34 | { 35 | get => _Progress; 36 | } 37 | 38 | public override int BytesDownloaded => _BytesDownloaded; 39 | 40 | public override string Uri 41 | { 42 | get => _Uri; 43 | set { 44 | _Uri = value; 45 | } 46 | } 47 | 48 | public override string DownloadPath 49 | { 50 | get => _DownloadPath; 51 | set { 52 | _DownloadPath = value; 53 | } 54 | } 55 | 56 | public override bool MultipartDownload 57 | { 58 | get => _MultipartDownload; 59 | set { 60 | _MultipartDownload = value; 61 | } 62 | } 63 | 64 | public override bool AbandonOnFailure 65 | { 66 | get => _AbandonOnFailure; 67 | set { 68 | _AbandonOnFailure = value; 69 | } 70 | } 71 | 72 | public override int Timeout { 73 | get => _Timeout; 74 | set 75 | { 76 | _Timeout = value; 77 | } 78 | } 79 | 80 | public override bool Paused => _Paused; 81 | 82 | internal bool _DidError = false; 83 | public override bool DidError => _DidError; 84 | 85 | public override int StartTime => _StartTime; 86 | public override int EndTime => _EndTime; 87 | 88 | public event Action OnDownloadError; 89 | 90 | /// 91 | /// TODO: Implement 92 | /// 93 | /// 94 | public override bool Cancel() { 95 | OnCancel?.Invoke(); 96 | if (_AbandonOnFailure && File.Exists(DownloadResultPath)) { 97 | File.Delete(DownloadResultPath); 98 | } 99 | return false; 100 | } 101 | 102 | /// 103 | /// Submits a head request to the URI to determine if a multipart download is possible. 104 | /// 105 | /// 106 | public UnityWebRequestAsyncOperation _HeadRequest() 107 | { 108 | // case: didn't submit a head request yet, and we should 109 | UnityWebRequest uwr = null; 110 | UnityWebRequestAsyncOperation hreq = HTTPHelper.Head(ref uwr, _Uri, RequestHeaders, _Timeout); 111 | _DidHeadReq = true; 112 | hreq.completed += (resp) => { 113 | if (uwr.isNetworkError || uwr.isHttpError) { 114 | UnityEngine.Debug.LogError($"URI {_Uri} does not support HEAD requests and therefore Multipart downloading."); 115 | MultipartDownload = false; 116 | return; 117 | } 118 | if (!uwr.GetResponseHeaders().ContainsKey("Content-Length") || (!uwr.GetResponseHeaders().ContainsKey("Accept-Ranges") ? true : uwr.GetResponseHeaders()["Accept-Ranges"] != "bytes")) 119 | { 120 | UnityEngine.Debug.LogError($"URI {_Uri} does not support Multipart downloading."); 121 | return; 122 | } 123 | try { 124 | _ExpectedSize = Int32.Parse(uwr.GetResponseHeaders()["Content-Length"]); 125 | } catch (Exception ex) { 126 | UnityEngine.Debug.LogError($"URI {_Uri} does not support Multipart downloading."); 127 | return; 128 | } 129 | _ChunkSize = IntitialChunkSize; 130 | // download not multipart if size is below chunksize 131 | MultipartDownload = _ExpectedSize > _ChunkSize; 132 | }; 133 | return hreq; 134 | } 135 | 136 | /// 137 | /// Downloads the given URI in either a single or multi-part download. 138 | /// 139 | /// 140 | public override UnityWebRequestAsyncOperation Download() { 141 | if (_CompletedMultipartDownload) return null; 142 | if (_Uri == null) return null; 143 | if (_DownloadPath == null) return null; 144 | _StartTime = DateTime.Now.Millisecond; 145 | UnityWebRequestAsyncOperation resp = null; 146 | UnityWebRequest uwr = null; 147 | if (!MultipartDownload) { 148 | resp = HTTPHelper.Download(ref uwr, Uri, _DownloadPath, AbandonOnFailure, false, RequestHeaders, _Timeout); 149 | resp.completed += (obj) => { 150 | if (!File.Exists(DownloadResultPath)) return; 151 | _Progress = 1.0f; 152 | OnDownloadSuccess?.Invoke(); 153 | _EndTime = DateTime.Now.Millisecond; 154 | _BytesDownloaded = (int) new FileInfo(DownloadResultPath).Length; 155 | }; // invoke completed event when it actually happens 156 | } else { 157 | try { 158 | int fileSize = 0; 159 | if (File.Exists(DownloadResultPath)) { 160 | try { 161 | fileSize = (int) (new FileInfo(DownloadResultPath).Length); 162 | } catch (Exception ex) { 163 | Debug.LogError(ex.ToString()); 164 | return null; 165 | } 166 | } 167 | int remaining = _ExpectedSize - fileSize; 168 | if (remaining == 0) return null; // case: no need to download 169 | int reqChunkSize = _ChunkSize >= remaining ? remaining : _ChunkSize; 170 | if (fileSize + reqChunkSize >= _ExpectedSize) reqChunkSize = remaining; // case: _ChunkSize is smaller than remaining but greater than needed 171 | if (RequestHeaders == null) RequestHeaders = new Dictionary(); 172 | //"Range: bytes=0-1023" 173 | if (RequestHeaders.ContainsKey("Range")) RequestHeaders.Remove("Range"); 174 | string str = ""; 175 | RequestHeaders.Add("Range", str = $"bytes={fileSize}-{fileSize + reqChunkSize}"); 176 | resp = HTTPHelper.Download(ref uwr, Uri, _DownloadPath, AbandonOnFailure, true, RequestHeaders, _Timeout); 177 | resp.completed -= _OnCompleteMulti; 178 | resp.completed += _OnCompleteMulti; 179 | } catch (Exception e) { 180 | Debug.LogError(e.ToString()); 181 | } 182 | } 183 | resp.completed += (obj) => { 184 | if (uwr.isHttpError || uwr.isNetworkError) 185 | { 186 | _DidError = true; 187 | OnDownloadError?.Invoke(0, uwr.error); 188 | Cancel(); 189 | } 190 | }; 191 | return resp; 192 | } 193 | 194 | 195 | /// 196 | /// This function is called when a request ( multi-chunk only) has completed 197 | /// 198 | /// 199 | internal void _OnCompleteMulti(AsyncOperation obj) 200 | { 201 | if (!File.Exists(DownloadResultPath)) return; 202 | int fileSize = 0; 203 | if (File.Exists(DownloadResultPath)) fileSize = (int) (new FileInfo(DownloadResultPath).Length); 204 | OnDownloadChunkedSucces?.Invoke(); 205 | int remaining = _ExpectedSize - fileSize; 206 | int reqChunkSize = _ChunkSize > remaining ? remaining : _ChunkSize; 207 | _Progress = reqChunkSize / _ExpectedSize; 208 | _BytesDownloaded = (int) new FileInfo(DownloadResultPath).Length; 209 | 210 | if (new FileInfo(DownloadResultPath).Length == _ExpectedSize) { 211 | // case: complete! 212 | OnDownloadSuccess?.Invoke(); 213 | _EndTime = DateTime.Now.Millisecond; 214 | _CompletedMultipartDownload = true; 215 | } else { 216 | // case: not complete! 217 | // Download is invoke recursively 218 | } 219 | } 220 | } 221 | 222 | 223 | } 224 | -------------------------------------------------------------------------------- /Runtime/UnityFileDownloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using System.Threading; 6 | using System.IO; 7 | using UnityEngine; 8 | using UnityEngine.Networking; 9 | 10 | namespace UFD 11 | { 12 | /// 13 | /// Represents the top-level object responsible for handling file downloading in Untiy. 14 | /// 15 | public class UnityFileDownloader : IDownloader 16 | { 17 | public UnityFileDownloader() 18 | { 19 | 20 | } 21 | 22 | /// 23 | /// Constructor to allow the setting of URIs 24 | /// 25 | /// 26 | public UnityFileDownloader(IEnumerable strings) 27 | { 28 | List uris = new List(); 29 | foreach (var str in strings) uris.Add(str); 30 | Uris = uris.ToArray(); 31 | } 32 | 33 | internal readonly static SemaphoreLocker _Locker = new SemaphoreLocker(); 34 | 35 | internal int _InitialCount; 36 | internal int _Timeout = 6; 37 | internal int _MaxConcurrency = 4; 38 | internal bool _AbandonOnFailure = true; 39 | internal bool _ContinueAfterFailure = false; 40 | internal bool _Downloading = false; 41 | internal bool _Paused = false; 42 | internal bool _DidError = false; 43 | internal int _NumFilesRemaining = 0; 44 | internal int _StartTime = 0, _EndTime = 0; 45 | internal string _DownloadPath = UnityEngine.Application.persistentDataPath; 46 | internal string[] _PendingUris = null; 47 | internal string[] _DownloadedUris = null; 48 | 49 | #region Events/Actions 50 | 51 | /// 52 | /// Invoked when all files are downloaded 53 | /// 54 | public event Action OnDownloadsSuccess; 55 | 56 | /// 57 | /// Invoked implicitly upon download and cancel. 58 | /// 59 | public event Action OnDownloadInvoked, OnCancelInvoked; 60 | 61 | /// 62 | /// Invoked implicitly when a pause or unpause initiates. 63 | /// 64 | public event Action OnCancelIndividual, OnDownloadIndividualInvoked; 65 | 66 | public event Action OnCancel; 67 | 68 | #endregion 69 | 70 | public override string IDownloadFulfillerClassName => "UWRFulfiller"; // for reundancy / an example 71 | 72 | public override int StartTime => _StartTime; 73 | public override int EndTime => _EndTime; 74 | 75 | /// 76 | /// Calculates the amount of files-per-second this downloder processed. 77 | /// 78 | /// 79 | public override float NumFilesPerSecond 80 | { 81 | get 82 | { 83 | if (ElapsedTime == 0) return 0f; 84 | return (ElapsedTime / 1000) / (DownloadedUris.Length); 85 | } 86 | } 87 | 88 | public float MegabytesDownloadedPerSecond => BytesDownloadedPerSecond == 0 ? 0 : BytesDownloadedPerSecond / 1000; 89 | 90 | public float BytesDownloadedPerSecond 91 | { 92 | get 93 | { 94 | float tb = 0; 95 | float elapsed = 0; 96 | foreach (var idf in _FulfillersOld) { 97 | tb += idf.BytesDownloaded; 98 | elapsed += idf.ElapsedTime; 99 | } 100 | foreach (var idf in _Fulfillers) { 101 | tb += idf.BytesDownloaded; 102 | elapsed += idf.ElapsedTime; 103 | } 104 | return (tb / 1000) / (elapsed / 1000f); 105 | } 106 | } 107 | 108 | public override int Timeout 109 | { 110 | get => _Timeout; 111 | set { 112 | _Timeout = value; 113 | } 114 | } 115 | 116 | public override int MaxConcurrency 117 | { 118 | get => _MaxConcurrency; set { 119 | _MaxConcurrency = value; 120 | } 121 | } 122 | 123 | public override string DownloadPath 124 | { 125 | get => _DownloadPath; set { 126 | _DownloadPath = value; 127 | } 128 | } 129 | 130 | /// 131 | /// If true, then this downloader will delete all partially downloaded files. 132 | /// 133 | /// 134 | public override bool AbandonOnFailure 135 | { 136 | get => _AbandonOnFailure; set { 137 | _AbandonOnFailure = value; 138 | } 139 | } 140 | 141 | /// 142 | /// If true, then this downloader will continue attempting to download files post-error. 143 | /// 144 | /// 145 | public override bool ContinueAfterFailure 146 | { 147 | get => _ContinueAfterFailure; set { 148 | _ContinueAfterFailure = value; 149 | } 150 | } 151 | 152 | 153 | public override bool Downloading => _Downloading; 154 | 155 | public override bool DidError { 156 | get => _DidError; 157 | protected set { 158 | _DidError = value; 159 | } 160 | } 161 | 162 | public override bool Paused => _Paused; 163 | 164 | public override int NumFilesRemaining => _NumFilesRemaining; 165 | 166 | public override string[] PendingURIS => _PendingUris; 167 | 168 | public override string[] IncompletedURIS => IncompletedURIS; 169 | 170 | /// 171 | /// Current number of concurrent WebRequest's being processed in the moment. 172 | /// 173 | public int NumThreads => _N; 174 | 175 | internal int _N = 0; 176 | 177 | /// 178 | /// TODO: Return task after complete download 179 | /// 180 | /// 181 | public override async Task Download() 182 | { 183 | if (Downloading || Uris == null ? true : Uris.Length == 0) { 184 | throw new Exception($"{GetType().FullName}.Download() cannot be invoked with property Uris set null or empty."); 185 | return false; 186 | }; 187 | OnDownloadInvoked?.Invoke(); 188 | _PendingUris = _Uris; 189 | _NumFilesRemaining = Uris.Length; 190 | _StartTime = DateTime.Now.Millisecond; 191 | _Downloading = true; 192 | // dispatch the appropriate amount of files depending on concurrency and amnt of files 193 | _InitialCount = Uris.Length; 194 | int n = _InitialCount >= MaxConcurrency ? MaxConcurrency : _NumFilesRemaining; 195 | if (n <= 0) { 196 | throw new Exception($"{GetType().FullName}.Download expects MaxConcurrency to be a non-negative integer."); 197 | return false; 198 | } 199 | List> rvs = new List>(); 200 | for (int i = 0; i < n; i++) rvs.Add(_Dispatch()); 201 | var resp = await Task.WhenAll(rvs); 202 | // wait if download is in process or if an error occured and you do not continue post-failure 203 | while (Downloading && !(DidError && !ContinueAfterFailure)) await Task.Delay(75); 204 | return true; // TODO: create return response from fulfiller response 205 | } 206 | 207 | /// 208 | /// if URI is a fulfiller, then do nothing 209 | /// if URI is an old fulfiller, move to Fulfiller/add pending UR 210 | /// if URI has never been fulfilled, do normal fulfillment process 211 | /// 212 | /// 213 | /// 214 | public override async Task Download(string uri) 215 | { 216 | if (!Downloading) { 217 | _Downloading = true; 218 | _StartTime = DateTime.Now.Millisecond; 219 | _InitialCount = 1; 220 | }; 221 | if (_Fulfillers.ToList().Select(idf => idf.Uri == uri).ToList().Count == 1) { 222 | // case: queued (to be fulfilled) 223 | } else if (_FulfillersOld.ToList().Select(idf => idf.Uri == uri).ToList().Count == 0) { 224 | // case: has yet to be fulfilled 225 | IDownloadFulfiller idf = DownloadFulfillerFactory.CreateFromClassName(IDownloadFulfillerClassName); 226 | idf.Uri = uri; 227 | idf.DownloadPath = DownloadPath; 228 | idf.AbandonOnFailure = AbandonOnFailure; 229 | idf.Timeout = Timeout; 230 | _PendingUris.Add(new string[]{uri}); 231 | _Fulfillers.Add(new IDownloadFulfiller[]{idf}); 232 | _NumFilesRemaining += 1; 233 | } else { 234 | // case: has been fulfilled already 235 | // move existing fulfiller 236 | IDownloadFulfiller idf = _FulfillersOld.ToList().Where(idf => idf.Uri == uri).ToArray()[0]; 237 | var v = _FulfillersOld.ToList(); 238 | v.Remove(idf); 239 | _FulfillersOld = v.ToArray(); 240 | _PendingUris.Add(new string[]{uri}); 241 | _Fulfillers.Add(new IDownloadFulfiller[]{idf}); 242 | } 243 | OnDownloadIndividualInvoked?.Invoke(uri); 244 | return true; 245 | } 246 | 247 | internal async Task _ReturnFalseAsync() { 248 | return false; 249 | } 250 | 251 | /// 252 | /// Syncronously dispatches a file downloader for any remaining Uris. 253 | /// 254 | internal Task _Dispatch() 255 | { 256 | if (_PendingUris == null ? true : _PendingUris.Length == 0) return _ReturnFalseAsync(); 257 | string uri = _PendingUris[0]; 258 | IDownloadFulfiller idf = _Fulfillers[0]; 259 | _PendingUris = _PendingUris.Dequeue(); 260 | _Fulfillers = _Fulfillers.Dequeue(); 261 | _FulfillersOld = _FulfillersOld.Add(new IDownloadFulfiller[]{idf}); // add to old 262 | if (idf._CompletedMultipartDownload) return _ReturnFalseAsync(); 263 | if (!idf._DidHeadReq && idf.TryMultipartDownload) { 264 | // case: didn't do a head request yet and we should, submit one 265 | UnityWebRequestAsyncOperation treq = ((UWRFulfiller)idf)._HeadRequest(); 266 | _N++; 267 | treq.completed += (obj) => { 268 | UnityWebRequestAsyncOperation rv = idf.Download(); 269 | rv.completed += resp => { 270 | _N--; 271 | _DispatchCompletion(idf); 272 | }; 273 | }; 274 | return _ReturnFalseAsync(); 275 | } 276 | UnityWebRequestAsyncOperation rv = idf.Download(); 277 | if (rv == null) { // case: either multi-part completed or file is fully downloaded already 278 | _DispatchCompletion(); 279 | return _ReturnFalseAsync(); 280 | } 281 | _N++; 282 | rv.completed += resp => { 283 | _N--; 284 | _DispatchCompletion(idf); 285 | }; 286 | return _ReturnFalseAsync(); 287 | } 288 | 289 | /// 290 | /// Dispatches a given IDF, for multi-part downloads. 291 | /// 292 | /// 293 | /// 294 | internal Task _Dispatch(IDownloadFulfiller idf) 295 | { 296 | string uri = idf.Uri; 297 | UnityWebRequestAsyncOperation rv = idf.Download(); 298 | if (rv == null) { 299 | _DispatchCompletion(); 300 | return _ReturnFalseAsync(); 301 | } 302 | _N++; 303 | rv.completed += resp => { 304 | _N--; 305 | _DispatchCompletion(idf); 306 | }; 307 | return _ReturnFalseAsync(); 308 | } 309 | 310 | internal async Task _DispatchCompletion() 311 | { 312 | await _Locker.LockAsync(async () => { 313 | if (!Downloading) { 314 | return; 315 | } 316 | if (_PendingUris.Length > 0) 317 | { 318 | _Dispatch(); 319 | } else if (NumThreads == 0) { 320 | // case: download completion 321 | OnDownloadsSuccess?.Invoke(); 322 | _EndTime = DateTime.Now.Millisecond; 323 | _Downloading = false; 324 | } 325 | }); 326 | } 327 | 328 | /// 329 | /// Handles dispatch completion in a syncronous fashion (with allowable awaiting) 330 | /// 331 | /// 332 | /// 333 | internal async Task _DispatchCompletion(IDownloadFulfiller idf) 334 | { 335 | await _Locker.LockAsync(async () => { 336 | if (!Downloading) { 337 | // case: download pause occured after this IDF has been fulfilled 338 | idf.Cancel(); 339 | return; 340 | } 341 | // handle idf error and 'ContiueAfterError' 342 | if (idf.DidError) 343 | { 344 | if (_ContinueAfterFailure && _PendingUris.Length > 0) { 345 | // case: can continue 346 | _Dispatch(); 347 | } else if (!ContinueAfterFailure) { 348 | // case: can't continue, cancel 349 | await Cancel(); 350 | } else { 351 | await Cancel(); 352 | } 353 | } else 354 | { 355 | if (!idf._CompletedMultipartDownload && idf.MultipartDownload) { 356 | // case: continue multi-part download instead of dispatchal 357 | _Dispatch(idf); 358 | return; 359 | } else { 360 | //_FulfillersOld = _FulfillersOld.Add(new IDownloadFulfiller[]{idf}); // add to old 361 | } 362 | if (_PendingUris.Length > 0) 363 | { 364 | _Dispatch(); 365 | } else if (NumThreads == 0) { 366 | // case: download completion 367 | OnDownloadsSuccess?.Invoke(); 368 | _EndTime = DateTime.Now.Millisecond; 369 | _Downloading = false; 370 | } 371 | } 372 | }); 373 | } 374 | 375 | 376 | public IDownloadFulfiller GetFulfiller(string uri) 377 | { 378 | return _Fulfillers.Where(idf => idf.Uri == uri).ToArray().Length == 0 ? _FulfillersOld.Where(idf => idf.Uri == uri).ToArray()[0] : _Fulfillers.Where(idf => idf.Uri == uri).ToArray()[0]; 379 | } 380 | 381 | /// 382 | /// Cancel all takss for this downloader asyncronously. 383 | /// 384 | /// 385 | public override async Task Cancel() 386 | { 387 | _Downloading = false; 388 | // invoke event 389 | OnCancel?.Invoke(); 390 | 391 | _EndTime = DateTime.Now.Millisecond; 392 | _HandleAbandonOnFailure(); 393 | return true; 394 | } 395 | 396 | /// 397 | /// Moves fulfiller for given URI to old if applicable 398 | /// Invokes cancel on fulfiller 399 | /// /// 400 | /// 401 | public override async Task Cancel(string uri) 402 | { 403 | OnCancelIndividual?.Invoke(uri); 404 | 405 | if (_Fulfillers.ToList().Select(idf => idf.Uri == uri).ToList().Count == 1) { 406 | // case: queued (to be fulfilled) 407 | IDownloadFulfiller idf = _Fulfillers.ToList().Where(idf => idf.Uri == uri).ToArray()[0]; 408 | idf.Cancel(); 409 | var v = _Fulfillers.ToList(); 410 | v.Remove(idf); 411 | _Fulfillers = v.ToArray(); 412 | _FulfillersOld = _FulfillersOld.Add(new IDownloadFulfiller[]{idf}); 413 | } else if (_FulfillersOld.ToList().Select(idf => idf.Uri == uri).ToList().Count == 0) { 414 | // case: has never been fulfilled 415 | throw new Exception("Cancelation invoked for a URI that was never invoked " + uri); 416 | return false; 417 | } else { 418 | // case: has been fulfilled 419 | Debug.LogError("Cancelation invoked for a URI that has completed."); 420 | } 421 | return true; 422 | } 423 | 424 | /// 425 | /// Deletes all files if AbandonOnFailure is true. 426 | /// 427 | internal void _HandleAbandonOnFailure() 428 | { 429 | if (AbandonOnFailure) foreach (var idf in _Fulfillers) 430 | { 431 | idf.Cancel(); 432 | } 433 | if (AbandonOnFailure) foreach (var idf in _FulfillersOld) 434 | { 435 | idf.Cancel(); 436 | } 437 | } 438 | 439 | /// 440 | /// Resets all properties relating to this downloader. 441 | /// 442 | public override void Reset() 443 | { 444 | if (Downloading) { 445 | throw new Exception("Reset for UnityFileDownloader cannot be invoked while still Downloading. Invoke Cancel first."); 446 | return; 447 | } 448 | _Downloading = false; 449 | _Timeout = 6; 450 | _MaxConcurrency = 4; 451 | _AbandonOnFailure = true; 452 | _ContinueAfterFailure = false; 453 | _Downloading = false; 454 | _Paused = false; 455 | _DidError = false; 456 | _NumFilesRemaining = 0; 457 | _StartTime = 0; 458 | _EndTime = 0; 459 | _PendingUris = null; 460 | _DownloadedUris = null; 461 | _IncompletedUris = null; 462 | _Fulfillers = new IDownloadFulfiller[0]; 463 | _FulfillersOld = new IDownloadFulfiller[0]; 464 | _Uris = new string[0]; 465 | } 466 | 467 | 468 | } 469 | } -------------------------------------------------------------------------------- /Runtime/utils/CommonExtensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine.Networking; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System; 8 | namespace UFD 9 | { 10 | 11 | public static class TaskExtensions 12 | { 13 | public static async Task Then(this Task task, Func then) 14 | { 15 | var result = await task; 16 | return then(result); 17 | } 18 | } 19 | 20 | public static class IDownloadFulfillerExtensions 21 | { 22 | 23 | public static IDownloadFulfiller[] Add(this IDownloadFulfiller[] arr1, IDownloadFulfiller[] arr2) 24 | { 25 | if (arr1 == null) arr1 = new IDownloadFulfiller[0]; 26 | if (arr2 == null) arr2 = new IDownloadFulfiller[0]; 27 | var pp = arr1.ToList(); 28 | pp.AddRange(arr2.ToList()); 29 | return pp.ToArray(); 30 | } 31 | 32 | public static IDownloadFulfiller[] Dequeue(this IDownloadFulfiller[] arr) 33 | { 34 | if (arr == null) arr = new IDownloadFulfiller[0]; 35 | List strs = new List(); 36 | int i = 0; 37 | foreach (var str in arr) { 38 | if (i++ == 0) continue; 39 | strs.Add(str); 40 | } 41 | arr = strs.ToArray(); 42 | return arr; 43 | } 44 | 45 | } 46 | 47 | 48 | /// 49 | /// Provides the utility to add string arrays together (using Linq) and to Dequeue an array 50 | /// 51 | public static class Extensions { 52 | public static string[] Add(this string[] arr1, string arr2) 53 | { 54 | return Add(arr1, new string[]{arr2}); 55 | } 56 | 57 | public static string[] Add(this string[] arr1, string[] arr2) 58 | { 59 | if (arr1 == null) arr1 = new string[0]; 60 | if (arr2 == null) arr2 = new string[0]; 61 | arr1.ToList().AddRange(arr2.ToList()); 62 | return arr1.ToArray(); 63 | } 64 | 65 | public static string[] Dequeue(this string[] arr) 66 | { 67 | if (arr == null) arr = new string[0]; 68 | List strs = new List(); 69 | string v = arr[0]; 70 | int i = 0; 71 | foreach (var str in arr) { 72 | if (i++ == 0) continue; 73 | strs.Add(str); 74 | } 75 | arr = strs.ToArray(); 76 | return arr; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Runtime/utils/HTTPHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using System; 6 | using UnityEngine; 7 | using UnityEngine.Networking; 8 | using System.Text; 9 | using System.Security.Cryptography; 10 | using System.Collections; 11 | using System.Collections.Generic; 12 | using System.IO; 13 | using System; 14 | using UnityEngine; 15 | using System.Threading.Tasks; 16 | using System.Threading; 17 | using UnityEngine.Networking; 18 | using System.Runtime.CompilerServices; 19 | 20 | 21 | namespace UFD 22 | { 23 | /// 24 | /// Defines a valid or invalid HTTP/HTTPS response. 25 | /// 26 | public class HTTPResponse { 27 | public string ResponseText; 28 | public int ResponseCode; 29 | public int DownloadedBytes = 0; 30 | 31 | public Dictionary Headers; 32 | 33 | /// 34 | /// This property is determined by UWR and is true incase of an error. 35 | /// 36 | public bool DidError; 37 | 38 | public string ToString() { 39 | return $"ResponseText={ResponseText}, ResponseCode={ResponseCode}, DidError={DidError}"; 40 | } 41 | } 42 | 43 | /// 44 | /// Contains basic utilities for HTTP communication via UnityWebRequest. 45 | /// 46 | public static class HTTPHelper 47 | { 48 | 49 | /// 50 | /// Submits an async HTTP Head request. 51 | /// 52 | /// 53 | /// 54 | /// 55 | public static async Task < HTTPResponse > Get(string uri, Dictionary < string, string > headers = null, int timeoutSeconds = 3) { 56 | HTTPResponse resp = new HTTPResponse(); 57 | 58 | UnityWebRequest req = UnityWebRequest.Get(uri); 59 | req.timeout = timeoutSeconds; 60 | if (headers != null) { 61 | foreach(var kvp in headers) req.SetRequestHeader(kvp.Key, kvp.Value); 62 | } 63 | UnityWebRequest.Result result = await req.SendWebRequest(); 64 | 65 | if (result == UnityWebRequest.Result.Success) { 66 | resp.ResponseText = req.downloadHandler.text; 67 | resp.DidError = false; 68 | } else { 69 | resp.ResponseText = req.error; 70 | resp.DidError = true; 71 | } 72 | resp.ResponseCode = (int) req.responseCode; 73 | return resp; 74 | } 75 | 76 | /// 77 | /// Given a URI, naively extracts the file and type it is pointing to. 78 | /// 79 | /// 80 | /// 81 | public static string GetFilenameFromUriNaively(string uri) 82 | { 83 | string[] arr = uri.Split("/"); 84 | string v = arr[arr.Length - 1]; 85 | if (v.Contains("%")) { 86 | arr = v.Split("%"); 87 | v = arr[arr.Length - 1]; 88 | } 89 | return v; 90 | } 91 | 92 | 93 | 94 | /// 95 | /// Submits a basic HEAD HTTP/HTTPS REST call. 96 | /// 97 | /// 98 | /// 99 | /// 100 | /// 101 | /* 102 | public static async Task < HTTPResponse > Head(string uri, Dictionary < string, string > headers = null, int timeoutSeconds = 3) { 103 | HTTPResponse resp = new HTTPResponse(); 104 | 105 | UnityWebRequest req = new UnityWebRequest(); 106 | req.method = UnityWebRequest.kHttpVerbHEAD; 107 | req.url = uri; 108 | req.timeout = timeoutSeconds; 109 | if (headers != null) { 110 | foreach(var kvp in headers) req.SetRequestHeader(kvp.Key, kvp.Value); 111 | } 112 | 113 | Debug.Log($"Head URI={uri}"); 114 | if (headers != null) foreach (var str in headers) Debug.Log($"[{str.Key}={str.Value}"); 115 | UnityWebRequest.Result result = await req.SendWebRequest(); 116 | 117 | if (result == UnityWebRequest.Result.Success) { 118 | resp.ResponseText = req.downloadHandler.text; 119 | resp.DidError = false; 120 | } else { 121 | resp.ResponseText = req.error; 122 | resp.DidError = true; 123 | } 124 | resp.ResponseCode = (int) req.responseCode; 125 | resp.Headers = req.GetResponseHeaders(); 126 | req.Dispose(); 127 | return resp; 128 | } 129 | */ 130 | 131 | public static UnityWebRequestAsyncOperation Head(ref UnityWebRequest req, string uri, Dictionary < string, string > headers = null, int timeoutSeconds = 3) { 132 | 133 | req = new UnityWebRequest(); 134 | req.method = UnityWebRequest.kHttpVerbHEAD; 135 | req.url = uri; 136 | req.timeout = timeoutSeconds; 137 | if (headers != null) { 138 | foreach(var kvp in headers) req.SetRequestHeader(kvp.Key, kvp.Value); 139 | } 140 | 141 | Debug.Log($"Head URI={uri}"); 142 | if (headers != null) foreach (var str in headers) Debug.Log($"[{str.Key}={str.Value}"); 143 | return req.SendWebRequest(); 144 | } 145 | 146 | 147 | public static UnityWebRequestAsyncOperation Download(ref UnityWebRequest req, string uri, String path = null, bool abandonOnFailure = false, bool append = false, Dictionary < string, string > headers = null, int timeoutSeconds = 3) { 148 | if (path == null) path = Application.persistentDataPath; // c# does not support non-const defaults 149 | if (headers != null) foreach (var str in headers) Debug.Log($"[{str.Key}={str.Value}"); 150 | HTTPResponse resp = null; 151 | req = new UnityWebRequest(uri); 152 | req.method = UnityWebRequest.kHttpVerbGET; 153 | string filename = GetFilenameFromUriNaively(uri); 154 | string _path = Path.Combine(path, filename); 155 | _path = _path.Replace("/", Path.DirectorySeparatorChar.ToString()); 156 | if (_path.Contains("%")) { 157 | var arr = _path.Split("%"); 158 | _path = arr[arr.Length - 1]; 159 | } 160 | req.downloadHandler = new DownloadHandlerFile(_path, append); 161 | ((DownloadHandlerFile)req.downloadHandler).removeFileOnAbort = abandonOnFailure; 162 | req.timeout = timeoutSeconds; 163 | if (headers != null) { 164 | foreach(var kvp in headers) req.SetRequestHeader(kvp.Key, kvp.Value); 165 | } 166 | return req.SendWebRequest(); 167 | } 168 | 169 | } 170 | } -------------------------------------------------------------------------------- /Runtime/utils/SemaphoreLocker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace UFD 6 | { 7 | 8 | /// 9 | /// Used to handle async locking while being able to await 10 | /// 11 | public class SemaphoreLocker 12 | { 13 | protected readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); 14 | 15 | public async Task LockAsync(Func worker, object args = null) 16 | { 17 | await _semaphore.WaitAsync(); 18 | try 19 | { 20 | await worker(); 21 | } 22 | finally 23 | { 24 | _semaphore.Release(); 25 | } 26 | } 27 | 28 | // overloading variant for non-void methods with return type (generic T) 29 | public async Task LockAsync(Func> worker, object args = null) 30 | { 31 | await _semaphore.WaitAsync(); 32 | try 33 | { 34 | return await worker(); 35 | } 36 | finally 37 | { 38 | _semaphore.Release(); 39 | } 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Runtime/utils/UnityWebRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine.Networking; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using UnityEngine; 5 | using UnityEngine.Networking; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace UFD 9 | { 10 | /// 11 | /// Contains the following utilities relating to UnityWebRequest: 12 | /// - Provides awaitability for the 'SendRequest' func 13 | /// - Provides a stringification function for debugging purposes 14 | /// 15 | public static class UnityWebRequestExtensions { 16 | 17 | /// 18 | /// TODO: complete 19 | /// 20 | /// The web request to stringify. 21 | /// 22 | public static string ToString(this UnityWebRequest uwr) { 23 | string fstr = ""; 24 | return $"TYPE: {uwr.method}\nURL: {uwr.url}\nURI: {uwr.uri} FORM: {fstr}"; 25 | } 26 | 27 | /// 28 | /// Provides an awaiter for UnityWebRequest's to be used in an asyncronous fashion. 29 | /// 30 | /// 31 | /// 32 | public static TaskAwaiter < UnityWebRequest.Result > GetAwaiter(this UnityWebRequestAsyncOperation reqOp) { 33 | TaskCompletionSource < UnityWebRequest.Result > tsc = new(); 34 | reqOp.completed += asyncOp => tsc.TrySetResult(reqOp.webRequest.result); 35 | 36 | if (reqOp.isDone) 37 | tsc.TrySetResult(reqOp.webRequest.result); 38 | 39 | return tsc.Task.GetAwaiter(); 40 | } 41 | } 42 | } --------------------------------------------------------------------------------