/* * 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 image cache /// public class TextureCache { // User can plug in a routine to compute the texture cache location public delegate string ComputeTextureCacheFilenameDelegate(string cacheDir, UUID textureID); public ComputeTextureCacheFilenameDelegate ComputeTextureCacheFilename = null; private GridClient Client; private Thread cleanerThread; private System.Timers.Timer cleanerTimer; private double pruneInterval = 1000 * 60 * 5; /// /// Allows setting weather to periodicale prune the cache if it grows too big /// Default is enabled, when caching is enabled /// public bool AutoPruneEnabled { set { if (!Operational()) { return; } else { cleanerTimer.Enabled = value; } } get { return cleanerTimer.Enabled; } } /// /// How long (in ms) between cache checks (default is 5 min.) /// public double AutoPruneInterval { get { return pruneInterval; } set { pruneInterval = value; cleanerTimer.Interval = pruneInterval; } } /// /// Default constructor /// /// A reference to the GridClient object public TextureCache(GridClient client) { Client = client; cleanerTimer = new System.Timers.Timer(pruneInterval); cleanerTimer.Elapsed += new System.Timers.ElapsedEventHandler(cleanerTimer_Elapsed); if (Operational()) { cleanerTimer.Enabled = true; } else { cleanerTimer.Enabled = false; } } /// /// Return bytes read from the local image cache, null if it does not exist /// /// UUID of the image we want to get /// Raw bytes of the image, or null on failure public byte[] GetCachedImageBytes(UUID imageID) { if (!Operational()) { return null; } try { Logger.DebugLog("Reading " + FileName(imageID) + " from texture cache."); byte[] data = File.ReadAllBytes(FileName(imageID)); return data; } catch (Exception ex) { Logger.Log("Failed reading image 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 = GetCachedImageBytes(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 image /// /// UUID of the image /// String with the file name of the cahced image private string FileName(UUID imageID) { if (ComputeTextureCacheFilename != null) { return ComputeTextureCacheFilename(Client.Settings.TEXTURE_CACHE_DIR, imageID); } return Client.Settings.TEXTURE_CACHE_DIR + Path.DirectorySeparatorChar + imageID.ToString(); } /// /// Saves an image to the local cache /// /// UUID of the image /// Raw bytes the image consists of /// Weather the operation was successfull public bool SaveImageToCache(UUID imageID, byte[] imageData) { if (!Operational()) { return false; } try { Logger.DebugLog("Saving " + FileName(imageID) + " to texture cache.", Client); if (!Directory.Exists(Client.Settings.TEXTURE_CACHE_DIR)) { Directory.CreateDirectory(Client.Settings.TEXTURE_CACHE_DIR); } File.WriteAllBytes(FileName(imageID), imageData); } catch (Exception ex) { Logger.Log("Failed saving image 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 image /// Null if we don't have that UUID cached on disk, file name if found in the cache folder public string ImageFileName(UUID imageID) { if (!Operational()) { return null; } string fileName = FileName(imageID); if (File.Exists(fileName)) return fileName; else return null; } /// /// Checks if the image exists in the local cache /// /// UUID of the image /// True is the image is stored in the cache, otherwise false public bool HasImage(UUID imageID) { if (!Operational()) return false; else return File.Exists(FileName(imageID)); } /// /// Wipes out entire cache /// public void Clear() { string cacheDir = Client.Settings.TEXTURE_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.TEXTURE_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.TEXTURE_CACHE_MAX_SIZE) { Array.Sort(files, new SortFilesByAccesTimeHelper()); long targetSize = (long)(Client.Settings.TEXTURE_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_TEXTURE_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; } } } }