/* * Copyright (c) 2008, openmetaverse.org * 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.org 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 Thread cleanerThread; 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.OnConnected += new NetworkManager.ConnectedCallback(Network_OnConnected); Client.Network.OnDisconnected += new NetworkManager.DisconnectedCallback(Network_OnDisconnected); } void Network_OnDisconnected(NetworkManager.DisconnectType reason, string message) { DestroyTimer(); } void Network_OnConnected(object sender) { SetupTimer(); } /// /// 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 { Logger.DebugLog("Reading " + FileName(assetID) + " from asset cache."); byte[] data = File.ReadAllBytes(FileName(assetID)); return data; } catch (Exception ex) { Logger.Log("Failed reading asset from cache (" + ex.Message + ")", Helpers.LogLevel.Warning, Client); 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(); } /// /// 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 { Logger.DebugLog("Saving " + FileName(assetID) + " to asset cache.", Client); 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; } /// /// 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 return File.Exists(FileName(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; } Logger.Log("Wiped out " + num + " files from the cache directory.", Helpers.LogLevel.Debug); } /// /// 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; } } Logger.Log(num + " files deleted from the cache, cache size now: " + NiceFileSize(size), Helpers.LogLevel.Debug); } else { Logger.Log("Cache size is " + NiceFileSize(size) + ", file deletion not needed", Helpers.LogLevel.Debug); } } /// /// 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 (cleanerThread != null && cleanerThread.IsAlive) { return; } lock (this) { cleanerThread = new Thread(new ThreadStart(this.Prune)); cleanerThread.IsBackground = true; cleanerThread.Start(); } } /// /// 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; } } } }