{
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 |
5 |
6 |
7 |
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 | }
--------------------------------------------------------------------------------