/* * Copyright (c) 2006-2016, openmetaverse.co * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.co nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.Threading; namespace OpenMetaverse { /// /// Class that handles the local asset cache /// public class AssetCache { // User can plug in a routine to compute the asset cache location public delegate string ComputeAssetCacheFilenameDelegate(string cacheDir, UUID assetID); public ComputeAssetCacheFilenameDelegate ComputeAssetCacheFilename = null; private GridClient Client; private ManualResetEventSlim cleanerEvent = new ManualResetEventSlim(); private System.Timers.Timer cleanerTimer; private double pruneInterval = 1000 * 60 * 5; private bool autoPruneEnabled = true; /// /// Allows setting weather to periodicale prune the cache if it grows too big /// Default is enabled, when caching is enabled /// public bool AutoPruneEnabled { set { autoPruneEnabled = value; if (autoPruneEnabled) { SetupTimer(); } else { DestroyTimer(); } } get { return autoPruneEnabled; } } /// /// How long (in ms) between cache checks (default is 5 min.) /// public double AutoPruneInterval { set { pruneInterval = value; SetupTimer(); } get { return pruneInterval; } } /// /// Default constructor /// /// A reference to the GridClient object public AssetCache(GridClient client) { Client = client; Client.Network.LoginProgress += delegate(object sender, LoginProgressEventArgs e) { if (e.Status == LoginStatus.Success) { SetupTimer(); } }; Client.Network.Disconnected += delegate(object sender, DisconnectedEventArgs e) { DestroyTimer(); }; } /// /// Disposes cleanup timer /// private void DestroyTimer() { if (cleanerTimer != null) { cleanerTimer.Dispose(); cleanerTimer = null; } } /// /// Only create timer when needed /// private void SetupTimer() { if (Operational() && autoPruneEnabled && Client.Network.Connected) { if (cleanerTimer == null) { cleanerTimer = new System.Timers.Timer(pruneInterval); cleanerTimer.Elapsed += new System.Timers.ElapsedEventHandler(cleanerTimer_Elapsed); } cleanerTimer.Interval = pruneInterval; cleanerTimer.Enabled = true; } } /// /// Return bytes read from the local asset cache, null if it does not exist /// /// UUID of the asset we want to get /// Raw bytes of the asset, or null on failure public byte[] GetCachedAssetBytes(UUID assetID) { if (!Operational()) { return null; } try { byte[] data; if (File.Exists(FileName(assetID))) { DebugLog("Reading " + FileName(assetID) + " from asset cache."); data = File.ReadAllBytes(FileName(assetID)); } else { DebugLog("Reading " + StaticFileName(assetID) + " from static asset cache."); data = File.ReadAllBytes(StaticFileName(assetID)); } return data; } catch (Exception ex) { DebugLog("Failed reading asset from cache (" + ex.Message + ")"); return null; } } /// /// Returns ImageDownload object of the /// image from the local image cache, null if it does not exist /// /// UUID of the image we want to get /// ImageDownload object containing the image, or null on failure public ImageDownload GetCachedImage(UUID imageID) { if (!Operational()) return null; byte[] imageData = GetCachedAssetBytes(imageID); if (imageData == null) return null; ImageDownload transfer = new ImageDownload(); transfer.AssetType = AssetType.Texture; transfer.ID = imageID; transfer.Simulator = Client.Network.CurrentSim; transfer.Size = imageData.Length; transfer.Success = true; transfer.Transferred = imageData.Length; transfer.AssetData = imageData; return transfer; } /// /// Constructs a file name of the cached asset /// /// UUID of the asset /// String with the file name of the cahced asset private string FileName(UUID assetID) { if (ComputeAssetCacheFilename != null) { return ComputeAssetCacheFilename(Client.Settings.ASSET_CACHE_DIR, assetID); } return Client.Settings.ASSET_CACHE_DIR + Path.DirectorySeparatorChar + assetID.ToString(); } /// /// Constructs a file name of the static cached asset /// /// UUID of the asset /// String with the file name of the static cached asset private string StaticFileName(UUID assetID) { return Settings.RESOURCE_DIR + Path.DirectorySeparatorChar + "static_assets" + Path.DirectorySeparatorChar + assetID.ToString(); } /// /// Saves an asset to the local cache /// /// UUID of the asset /// Raw bytes the asset consists of /// Weather the operation was successfull public bool SaveAssetToCache(UUID assetID, byte[] assetData) { if (!Operational()) { return false; } try { DebugLog("Saving " + FileName(assetID) + " to asset cache."); if (!Directory.Exists(Client.Settings.ASSET_CACHE_DIR)) { Directory.CreateDirectory(Client.Settings.ASSET_CACHE_DIR); } File.WriteAllBytes(FileName(assetID), assetData); } catch (Exception ex) { Logger.Log("Failed saving asset to cache (" + ex.Message + ")", Helpers.LogLevel.Warning, Client); return false; } return true; } private void DebugLog(string message) { if (Client.Settings.LOG_DISKCACHE) Logger.DebugLog(message, Client); } /// /// Get the file name of the asset stored with gived UUID /// /// UUID of the asset /// Null if we don't have that UUID cached on disk, file name if found in the cache folder public string AssetFileName(UUID assetID) { if (!Operational()) { return null; } string fileName = FileName(assetID); if (File.Exists(fileName)) return fileName; else return null; } /// /// Checks if the asset exists in the local cache /// /// UUID of the asset /// True is the asset is stored in the cache, otherwise false public bool HasAsset(UUID assetID) { if (!Operational()) return false; else if (File.Exists(FileName(assetID))) return true; else return File.Exists(StaticFileName(assetID)); } /// /// Wipes out entire cache /// public void Clear() { string cacheDir = Client.Settings.ASSET_CACHE_DIR; if (!Directory.Exists(cacheDir)) { return; } DirectoryInfo di = new DirectoryInfo(cacheDir); // We save file with UUID as file name, only delete those FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly); int num = 0; foreach (FileInfo file in files) { file.Delete(); ++num; } DebugLog("Wiped out " + num + " files from the cache directory."); } /// /// Brings cache size to the 90% of the max size /// public void Prune() { string cacheDir = Client.Settings.ASSET_CACHE_DIR; if (!Directory.Exists(cacheDir)) { return; } DirectoryInfo di = new DirectoryInfo(cacheDir); // We save file with UUID as file name, only count those FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly); long size = GetFileSize(files); if (size > Client.Settings.ASSET_CACHE_MAX_SIZE) { Array.Sort(files, new SortFilesByAccesTimeHelper()); long targetSize = (long)(Client.Settings.ASSET_CACHE_MAX_SIZE * 0.9); int num = 0; foreach (FileInfo file in files) { ++num; size -= file.Length; file.Delete(); if (size < targetSize) { break; } } DebugLog(num + " files deleted from the cache, cache size now: " + NiceFileSize(size)); } else { DebugLog("Cache size is " + NiceFileSize(size) + ", file deletion not needed"); } cleanerEvent.Reset(); } /// /// Asynchronously brings cache size to the 90% of the max size /// public void BeginPrune() { // Check if the background cache cleaning thread is active first if (!cleanerEvent.IsSet) { cleanerEvent.Set(); ThreadPool.QueueUserWorkItem((_) => Prune()); } } /// /// Adds up file sizes passes in a FileInfo array /// long GetFileSize(FileInfo[] files) { long ret = 0; foreach (FileInfo file in files) { ret += file.Length; } return ret; } /// /// Checks whether caching is enabled /// private bool Operational() { return Client.Settings.USE_ASSET_CACHE; } /// /// Periodically prune the cache /// private void cleanerTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { BeginPrune(); } /// /// Nicely formats file sizes /// /// Byte size we want to output /// String with humanly readable file size private string NiceFileSize(long byteCount) { string size = "0 Bytes"; if (byteCount >= 1073741824) size = String.Format("{0:##.##}", byteCount / 1073741824) + " GB"; else if (byteCount >= 1048576) size = String.Format("{0:##.##}", byteCount / 1048576) + " MB"; else if (byteCount >= 1024) size = String.Format("{0:##.##}", byteCount / 1024) + " KB"; else if (byteCount > 0 && byteCount < 1024) size = byteCount.ToString() + " Bytes"; return size; } /// /// Helper class for sorting files by their last accessed time /// private class SortFilesByAccesTimeHelper : IComparer { int IComparer.Compare(FileInfo f1, FileInfo f2) { if (f1.LastAccessTime > f2.LastAccessTime) return 1; if (f1.LastAccessTime < f2.LastAccessTime) return -1; else return 0; } } } }