/*
* 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;
}
}
}
}