corrade-vassal – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 vero 1 /*
2 * Copyright (c) 2006-2014, openmetaverse.org
3 * All rights reserved.
4 *
5 * - Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * - Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 * - Neither the name of the openmetaverse.org nor the names
11 * of its contributors may be used to endorse or promote products derived from
12 * this software without specific prior written permission.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
18 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24 * POSSIBILITY OF SUCH DAMAGE.
25 */
26  
27 using System;
28 using System.Collections.Generic;
29 using System.IO;
30 using System.Threading;
31  
32 namespace OpenMetaverse
33 {
34 /// <summary>
35 /// Class that handles the local asset cache
36 /// </summary>
37 public class AssetCache
38 {
39 // User can plug in a routine to compute the asset cache location
40 public delegate string ComputeAssetCacheFilenameDelegate(string cacheDir, UUID assetID);
41  
42 public ComputeAssetCacheFilenameDelegate ComputeAssetCacheFilename = null;
43  
44 private GridClient Client;
45 private Thread cleanerThread;
46 private System.Timers.Timer cleanerTimer;
47 private double pruneInterval = 1000 * 60 * 5;
48 private bool autoPruneEnabled = true;
49  
50 /// <summary>
51 /// Allows setting weather to periodicale prune the cache if it grows too big
52 /// Default is enabled, when caching is enabled
53 /// </summary>
54 public bool AutoPruneEnabled
55 {
56 set
57 {
58 autoPruneEnabled = value;
59  
60 if (autoPruneEnabled)
61 {
62 SetupTimer();
63 }
64 else
65 {
66 DestroyTimer();
67 }
68 }
69 get { return autoPruneEnabled; }
70 }
71  
72 /// <summary>
73 /// How long (in ms) between cache checks (default is 5 min.)
74 /// </summary>
75 public double AutoPruneInterval
76 {
77 set
78 {
79 pruneInterval = value;
80 SetupTimer();
81 }
82 get { return pruneInterval; }
83 }
84  
85 /// <summary>
86 /// Default constructor
87 /// </summary>
88 /// <param name="client">A reference to the GridClient object</param>
89 public AssetCache(GridClient client)
90 {
91 Client = client;
92 Client.Network.LoginProgress += delegate(object sender, LoginProgressEventArgs e)
93 {
94 if (e.Status == LoginStatus.Success)
95 {
96 SetupTimer();
97 }
98 };
99  
100 Client.Network.Disconnected += delegate(object sender, DisconnectedEventArgs e) { DestroyTimer(); };
101 }
102  
103  
104 /// <summary>
105 /// Disposes cleanup timer
106 /// </summary>
107 private void DestroyTimer()
108 {
109 if (cleanerTimer != null)
110 {
111 cleanerTimer.Dispose();
112 cleanerTimer = null;
113 }
114 }
115  
116 /// <summary>
117 /// Only create timer when needed
118 /// </summary>
119 private void SetupTimer()
120 {
121 if (Operational() && autoPruneEnabled && Client.Network.Connected)
122 {
123 if (cleanerTimer == null)
124 {
125 cleanerTimer = new System.Timers.Timer(pruneInterval);
126 cleanerTimer.Elapsed += new System.Timers.ElapsedEventHandler(cleanerTimer_Elapsed);
127 }
128 cleanerTimer.Interval = pruneInterval;
129 cleanerTimer.Enabled = true;
130 }
131 }
132  
133 /// <summary>
134 /// Return bytes read from the local asset cache, null if it does not exist
135 /// </summary>
136 /// <param name="assetID">UUID of the asset we want to get</param>
137 /// <returns>Raw bytes of the asset, or null on failure</returns>
138 public byte[] GetCachedAssetBytes(UUID assetID)
139 {
140 if (!Operational())
141 {
142 return null;
143 }
144 try
145 {
146 byte[] data;
147  
148 if (File.Exists(FileName(assetID)))
149 {
150 DebugLog("Reading " + FileName(assetID) + " from asset cache.");
151 data = File.ReadAllBytes(FileName(assetID));
152 }
153 else
154 {
155 DebugLog("Reading " + StaticFileName(assetID) + " from static asset cache.");
156 data = File.ReadAllBytes(StaticFileName(assetID));
157  
158 }
159 return data;
160 }
161 catch (Exception ex)
162 {
163 DebugLog("Failed reading asset from cache (" + ex.Message + ")");
164 return null;
165 }
166 }
167  
168 /// <summary>
169 /// Returns ImageDownload object of the
170 /// image from the local image cache, null if it does not exist
171 /// </summary>
172 /// <param name="imageID">UUID of the image we want to get</param>
173 /// <returns>ImageDownload object containing the image, or null on failure</returns>
174 public ImageDownload GetCachedImage(UUID imageID)
175 {
176 if (!Operational())
177 return null;
178  
179 byte[] imageData = GetCachedAssetBytes(imageID);
180 if (imageData == null)
181 return null;
182 ImageDownload transfer = new ImageDownload();
183 transfer.AssetType = AssetType.Texture;
184 transfer.ID = imageID;
185 transfer.Simulator = Client.Network.CurrentSim;
186 transfer.Size = imageData.Length;
187 transfer.Success = true;
188 transfer.Transferred = imageData.Length;
189 transfer.AssetData = imageData;
190 return transfer;
191 }
192  
193 /// <summary>
194 /// Constructs a file name of the cached asset
195 /// </summary>
196 /// <param name="assetID">UUID of the asset</param>
197 /// <returns>String with the file name of the cahced asset</returns>
198 private string FileName(UUID assetID)
199 {
200 if (ComputeAssetCacheFilename != null)
201 {
202 return ComputeAssetCacheFilename(Client.Settings.ASSET_CACHE_DIR, assetID);
203 }
204 return Client.Settings.ASSET_CACHE_DIR + Path.DirectorySeparatorChar + assetID.ToString();
205 }
206  
207 /// <summary>
208 /// Constructs a file name of the static cached asset
209 /// </summary>
210 /// <param name="assetID">UUID of the asset</param>
211 /// <returns>String with the file name of the static cached asset</returns>
212 private string StaticFileName(UUID assetID)
213 {
214 return Settings.RESOURCE_DIR + Path.DirectorySeparatorChar + "static_assets" + Path.DirectorySeparatorChar + assetID.ToString();
215 }
216  
217 /// <summary>
218 /// Saves an asset to the local cache
219 /// </summary>
220 /// <param name="assetID">UUID of the asset</param>
221 /// <param name="assetData">Raw bytes the asset consists of</param>
222 /// <returns>Weather the operation was successfull</returns>
223 public bool SaveAssetToCache(UUID assetID, byte[] assetData)
224 {
225 if (!Operational())
226 {
227 return false;
228 }
229  
230 try
231 {
232 DebugLog("Saving " + FileName(assetID) + " to asset cache.");
233  
234 if (!Directory.Exists(Client.Settings.ASSET_CACHE_DIR))
235 {
236 Directory.CreateDirectory(Client.Settings.ASSET_CACHE_DIR);
237 }
238  
239 File.WriteAllBytes(FileName(assetID), assetData);
240 }
241 catch (Exception ex)
242 {
243 Logger.Log("Failed saving asset to cache (" + ex.Message + ")", Helpers.LogLevel.Warning, Client);
244 return false;
245 }
246  
247 return true;
248 }
249  
250 private void DebugLog(string message)
251 {
252 if (Client.Settings.LOG_DISKCACHE) Logger.DebugLog(message, Client);
253 }
254  
255 /// <summary>
256 /// Get the file name of the asset stored with gived UUID
257 /// </summary>
258 /// <param name="assetID">UUID of the asset</param>
259 /// <returns>Null if we don't have that UUID cached on disk, file name if found in the cache folder</returns>
260 public string AssetFileName(UUID assetID)
261 {
262 if (!Operational())
263 {
264 return null;
265 }
266  
267 string fileName = FileName(assetID);
268  
269 if (File.Exists(fileName))
270 return fileName;
271 else
272 return null;
273 }
274  
275 /// <summary>
276 /// Checks if the asset exists in the local cache
277 /// </summary>
278 /// <param name="assetID">UUID of the asset</param>
279 /// <returns>True is the asset is stored in the cache, otherwise false</returns>
280 public bool HasAsset(UUID assetID)
281 {
282 if (!Operational())
283 return false;
284 else
285 if (File.Exists(FileName(assetID)))
286 return true;
287 else
288 return File.Exists(StaticFileName(assetID));
289  
290 }
291  
292 /// <summary>
293 /// Wipes out entire cache
294 /// </summary>
295 public void Clear()
296 {
297 string cacheDir = Client.Settings.ASSET_CACHE_DIR;
298 if (!Directory.Exists(cacheDir))
299 {
300 return;
301 }
302  
303 DirectoryInfo di = new DirectoryInfo(cacheDir);
304 // We save file with UUID as file name, only delete those
305 FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly);
306  
307 int num = 0;
308 foreach (FileInfo file in files)
309 {
310 file.Delete();
311 ++num;
312 }
313  
314 DebugLog("Wiped out " + num + " files from the cache directory.");
315 }
316  
317 /// <summary>
318 /// Brings cache size to the 90% of the max size
319 /// </summary>
320 public void Prune()
321 {
322 string cacheDir = Client.Settings.ASSET_CACHE_DIR;
323 if (!Directory.Exists(cacheDir))
324 {
325 return;
326 }
327 DirectoryInfo di = new DirectoryInfo(cacheDir);
328 // We save file with UUID as file name, only count those
329 FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly);
330  
331 long size = GetFileSize(files);
332  
333 if (size > Client.Settings.ASSET_CACHE_MAX_SIZE)
334 {
335 Array.Sort(files, new SortFilesByAccesTimeHelper());
336 long targetSize = (long)(Client.Settings.ASSET_CACHE_MAX_SIZE * 0.9);
337 int num = 0;
338 foreach (FileInfo file in files)
339 {
340 ++num;
341 size -= file.Length;
342 file.Delete();
343 if (size < targetSize)
344 {
345 break;
346 }
347 }
348 DebugLog(num + " files deleted from the cache, cache size now: " + NiceFileSize(size));
349 }
350 else
351 {
352 DebugLog("Cache size is " + NiceFileSize(size) + ", file deletion not needed");
353 }
354  
355 }
356  
357 /// <summary>
358 /// Asynchronously brings cache size to the 90% of the max size
359 /// </summary>
360 public void BeginPrune()
361 {
362 // Check if the background cache cleaning thread is active first
363 if (cleanerThread != null && cleanerThread.IsAlive)
364 {
365 return;
366 }
367  
368 lock (this)
369 {
370 cleanerThread = new Thread(new ThreadStart(this.Prune));
371 cleanerThread.IsBackground = true;
372 cleanerThread.Start();
373 }
374 }
375  
376 /// <summary>
377 /// Adds up file sizes passes in a FileInfo array
378 /// </summary>
379 long GetFileSize(FileInfo[] files)
380 {
381 long ret = 0;
382 foreach (FileInfo file in files)
383 {
384 ret += file.Length;
385 }
386 return ret;
387 }
388  
389 /// <summary>
390 /// Checks whether caching is enabled
391 /// </summary>
392 private bool Operational()
393 {
394 return Client.Settings.USE_ASSET_CACHE;
395 }
396  
397 /// <summary>
398 /// Periodically prune the cache
399 /// </summary>
400 private void cleanerTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
401 {
402 BeginPrune();
403 }
404  
405 /// <summary>
406 /// Nicely formats file sizes
407 /// </summary>
408 /// <param name="byteCount">Byte size we want to output</param>
409 /// <returns>String with humanly readable file size</returns>
410 private string NiceFileSize(long byteCount)
411 {
412 string size = "0 Bytes";
413 if (byteCount >= 1073741824)
414 size = String.Format("{0:##.##}", byteCount / 1073741824) + " GB";
415 else if (byteCount >= 1048576)
416 size = String.Format("{0:##.##}", byteCount / 1048576) + " MB";
417 else if (byteCount >= 1024)
418 size = String.Format("{0:##.##}", byteCount / 1024) + " KB";
419 else if (byteCount > 0 && byteCount < 1024)
420 size = byteCount.ToString() + " Bytes";
421  
422 return size;
423 }
424  
425 /// <summary>
426 /// Helper class for sorting files by their last accessed time
427 /// </summary>
428 private class SortFilesByAccesTimeHelper : IComparer<FileInfo>
429 {
430 int IComparer<FileInfo>.Compare(FileInfo f1, FileInfo f2)
431 {
432 if (f1.LastAccessTime > f2.LastAccessTime)
433 return 1;
434 if (f1.LastAccessTime < f2.LastAccessTime)
435 return -1;
436 else
437 return 0;
438 }
439 }
440 }
441 }