Skip to content

Commit 09ac57a

Browse files
author
ArthurHub
committed
* Improve image downloading handling
- Fix collision of downloading the same image - Don't cache failed or partial image downloads responses
1 parent a1f4bd9 commit 09ac57a

File tree

5 files changed

+306
-112
lines changed

5 files changed

+306
-112
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
using System;
14+
using System.Collections.Generic;
15+
using System.ComponentModel;
16+
using System.IO;
17+
using System.Net;
18+
using System.Threading;
19+
using TheArtOfDev.HtmlRenderer.Core.Utils;
20+
21+
namespace TheArtOfDev.HtmlRenderer.Core.Handlers
22+
{
23+
/// <summary>
24+
/// On download file async complete, success or fail.
25+
/// </summary>
26+
/// <param name="imageUri">The online image uri</param>
27+
/// <param name="filePath">the path to the downloaded file</param>
28+
/// <param name="error">the error if download failed</param>
29+
/// <param name="canceled">is the file download request was canceled</param>
30+
public delegate void DownloadFileAsyncCallback(Uri imageUri, string filePath, Exception error, bool canceled);
31+
32+
/// <summary>
33+
///
34+
/// </summary>
35+
internal sealed class ImageDownloader : IDisposable
36+
{
37+
/// <summary>
38+
/// the web client used to download image from URL (to cancel on dispose)
39+
/// </summary>
40+
private readonly List<WebClient> _clients = new List<WebClient>();
41+
42+
/// <summary>
43+
/// dictionary of image cache path to callbacks of download to handle multiple requests to download the same image
44+
/// </summary>
45+
private readonly Dictionary<string, List<DownloadFileAsyncCallback>> _imageDownloadCallbacks = new Dictionary<string, List<DownloadFileAsyncCallback>>();
46+
47+
/// <summary>
48+
/// Makes a request to download the image from the server and raises the <see cref="cachedFileCallback"/> when it's down.<br/>
49+
/// </summary>
50+
/// <param name="imageUri">The online image uri</param>
51+
/// <param name="filePath">the path on disk to download the file to</param>
52+
/// <param name="async">is to download the file sync or async (true-async)</param>
53+
/// <param name="cachedFileCallback">This callback will be called with local file path. If something went wrong in the download it will return null.</param>
54+
public void DownloadImage(Uri imageUri, string filePath, bool async, DownloadFileAsyncCallback cachedFileCallback)
55+
{
56+
ArgChecker.AssertArgNotNull(imageUri, "imageUri");
57+
ArgChecker.AssertArgNotNull(cachedFileCallback, "cachedFileCallback");
58+
59+
// to handle if the file is already been downloaded
60+
bool download = true;
61+
lock (_imageDownloadCallbacks)
62+
{
63+
if (_imageDownloadCallbacks.ContainsKey(filePath))
64+
{
65+
download = false;
66+
_imageDownloadCallbacks[filePath].Add(cachedFileCallback);
67+
}
68+
else
69+
{
70+
_imageDownloadCallbacks[filePath] = new List<DownloadFileAsyncCallback> { cachedFileCallback };
71+
}
72+
}
73+
74+
if (download)
75+
{
76+
var tempPath = Path.GetTempFileName();
77+
if (async)
78+
ThreadPool.QueueUserWorkItem(DownloadImageFromUrlAsync, new DownloadData(imageUri, tempPath, filePath));
79+
else
80+
DownloadImageFromUrl(imageUri, tempPath, filePath);
81+
}
82+
}
83+
84+
/// <summary>
85+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
86+
/// </summary>
87+
public void Dispose()
88+
{
89+
ReleaseObjects();
90+
}
91+
92+
93+
#region Private/Protected methods
94+
95+
/// <summary>
96+
/// Download the requested file in the URI to the given file path.<br/>
97+
/// Use async sockets API to download from web, <see cref="OnDownloadImageAsyncCompleted"/>.
98+
/// </summary>
99+
private void DownloadImageFromUrl(Uri source, string tempPath, string filePath)
100+
{
101+
try
102+
{
103+
using (var client = new WebClient())
104+
{
105+
_clients.Add(client);
106+
client.DownloadFile(source, tempPath);
107+
OnDownloadImageCompleted(client, source, tempPath, filePath, null, false);
108+
}
109+
}
110+
catch (Exception ex)
111+
{
112+
OnDownloadImageCompleted(null, source, tempPath, filePath, ex, false);
113+
}
114+
}
115+
116+
/// <summary>
117+
/// Download the requested file in the URI to the given file path.<br/>
118+
/// Use async sockets API to download from web, <see cref="OnDownloadImageAsyncCompleted"/>.
119+
/// </summary>
120+
/// <param name="data">key value pair of URL and file info to download the file to</param>
121+
private void DownloadImageFromUrlAsync(object data)
122+
{
123+
var downloadData = (DownloadData)data;
124+
try
125+
{
126+
var client = new WebClient();
127+
_clients.Add(client);
128+
client.DownloadFileCompleted += OnDownloadImageAsyncCompleted;
129+
client.DownloadFileAsync(downloadData._uri, downloadData._tempPath, downloadData);
130+
}
131+
catch (Exception ex)
132+
{
133+
OnDownloadImageCompleted(null, downloadData._uri, downloadData._tempPath, downloadData._filePath, ex, false);
134+
}
135+
}
136+
137+
/// <summary>
138+
/// On download image complete to local file.<br/>
139+
/// If the download canceled do nothing, if failed report error.
140+
/// </summary>
141+
private void OnDownloadImageAsyncCompleted(object sender, AsyncCompletedEventArgs e)
142+
{
143+
var downloadData = (DownloadData)e.UserState;
144+
try
145+
{
146+
using (var client = (WebClient)sender)
147+
{
148+
client.DownloadFileCompleted -= OnDownloadImageAsyncCompleted;
149+
OnDownloadImageCompleted(client, downloadData._uri, downloadData._tempPath, downloadData._filePath, e.Error, e.Cancelled);
150+
}
151+
}
152+
catch (Exception ex)
153+
{
154+
OnDownloadImageCompleted(null, downloadData._uri, downloadData._tempPath, downloadData._filePath, ex, false);
155+
}
156+
}
157+
158+
/// <summary>
159+
/// Checks if the file was downloaded and raises the cachedFileCallback from <see cref="_imageDownloadCallbacks"/>
160+
/// </summary>
161+
private void OnDownloadImageCompleted(WebClient client, Uri source, string tempPath, string filePath, Exception error, bool cancelled)
162+
{
163+
if (!cancelled)
164+
{
165+
if (error == null)
166+
{
167+
var contentType = CommonUtils.GetResponseContentType(client);
168+
if (contentType == null || !contentType.StartsWith("image", StringComparison.OrdinalIgnoreCase))
169+
{
170+
error = new Exception("Failed to load image, not image content type: " + contentType);
171+
}
172+
173+
}
174+
175+
if (error == null)
176+
{
177+
if (File.Exists(tempPath))
178+
{
179+
try
180+
{
181+
File.Move(tempPath, filePath);
182+
}
183+
catch (Exception ex)
184+
{
185+
error = new Exception("Failed to move downloaded image from temp to cache location", ex);
186+
}
187+
}
188+
189+
error = File.Exists(filePath) ? null : (error ?? new Exception("Failed to download image, unknown error"));
190+
}
191+
}
192+
193+
List<DownloadFileAsyncCallback> callbacksList;
194+
lock (_imageDownloadCallbacks)
195+
{
196+
callbacksList = _imageDownloadCallbacks[filePath];
197+
_imageDownloadCallbacks.Remove(filePath);
198+
}
199+
200+
foreach (var cachedFileCallback in callbacksList)
201+
{
202+
try
203+
{
204+
cachedFileCallback(source, filePath, error, cancelled);
205+
}
206+
catch
207+
{ }
208+
}
209+
}
210+
211+
/// <summary>
212+
/// Release the image and client objects.
213+
/// </summary>
214+
private void ReleaseObjects()
215+
{
216+
_imageDownloadCallbacks.Clear();
217+
while (_clients.Count > 0)
218+
{
219+
try
220+
{
221+
var client = _clients[0];
222+
client.CancelAsync();
223+
client.Dispose();
224+
_clients.RemoveAt(0);
225+
}
226+
catch
227+
{ }
228+
}
229+
}
230+
231+
#endregion
232+
233+
234+
#region Inner class: DownloadData
235+
236+
private sealed class DownloadData
237+
{
238+
public readonly Uri _uri;
239+
public readonly string _tempPath;
240+
public readonly string _filePath;
241+
242+
public DownloadData(Uri uri, string tempPath, string filePath)
243+
{
244+
_uri = uri;
245+
_tempPath = tempPath;
246+
_filePath = filePath;
247+
}
248+
}
249+
250+
#endregion
251+
}
252+
}

0 commit comments

Comments
 (0)