// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // © 2021 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MapControl.Caching { /// /// Image Cache implementation based on local image files. /// The only valid data type for cached values is Tuple. /// public partial class ImageFileCache { private const string expiresTag = "EXPIRES:"; private readonly string rootDirectory; public ImageFileCache(string directory) { if (string.IsNullOrEmpty(directory)) { throw new ArgumentException("The directory argument must not be null or empty.", nameof(directory)); } rootDirectory = directory; Debug.WriteLine("Created ImageFileCache in " + rootDirectory); } public Task Clean() { return Task.Factory.StartNew(CleanRootDirectory, TaskCreationOptions.LongRunning); } private string GetPath(string key) { try { return Path.Combine(rootDirectory, Path.Combine(key.Split('/', ':', ';', ','))); } catch (Exception ex) { Debug.WriteLine("ImageFileCache: Invalid key {0}/{1}: {2}", rootDirectory, key, ex.Message); } return null; } private void CleanRootDirectory() { try { foreach (var dir in new DirectoryInfo(rootDirectory).EnumerateDirectories()) { var deletedFileCount = CleanDirectory(dir); if (deletedFileCount > 0) { Debug.WriteLine("ImageFileCache: Cleaned {0} files in {1}", deletedFileCount, dir); } } } catch (Exception ex) { Debug.WriteLine("ImageFileCache: Failed enumerating directories in {0}: {1}", rootDirectory, ex.Message); } } private static int CleanDirectory(DirectoryInfo directory) { var deletedFileCount = 0; try { deletedFileCount += directory.EnumerateDirectories().Sum(dir => CleanDirectory(dir)); } catch (Exception ex) { Debug.WriteLine("ImageFileCache: Failed enumerating directories in {0}: {1}", directory.FullName, ex.Message); } try { deletedFileCount += directory.EnumerateFiles().Sum(file => CleanFile(file)); } catch (Exception ex) { Debug.WriteLine("ImageFileCache: Failed enumerating files in {0}: {1}", directory.FullName, ex.Message); } try { if (!directory.EnumerateFileSystemInfos().Any()) { directory.Delete(); } } catch (Exception ex) { Debug.WriteLine("ImageFileCache: Failed cleaning {0}: {1}", directory.FullName, ex.Message); } return deletedFileCount; } private static int CleanFile(FileInfo file) { var deletedFileCount = 0; try { if (ReadExpiration(file) < DateTime.UtcNow) { file.Delete(); deletedFileCount = 1; } } catch (Exception ex) { Debug.WriteLine("ImageFileCache: Failed cleaning {0}: {1}", file.FullName, ex.Message); } return deletedFileCount; } private static DateTime ReadExpiration(FileInfo file) { DateTime? expiration = null; if (file.Length > 16) { var buffer = new byte[16]; using (var stream = file.OpenRead()) { stream.Seek(-16, SeekOrigin.End); if (stream.Read(buffer, 0, 16) == 16) { expiration = ReadExpiration(buffer); } } } return expiration ?? DateTime.Today; } private static DateTime ReadExpiration(ref byte[] buffer) { DateTime? expiration = ReadExpiration(buffer); if (expiration.HasValue) { Array.Resize(ref buffer, buffer.Length - 16); return expiration.Value; } return DateTime.Today; } private static DateTime? ReadExpiration(byte[] buffer) { DateTime? expiration = null; if (buffer.Length >= 16 && Encoding.ASCII.GetString(buffer, buffer.Length - 16, 8) == expiresTag) { expiration = new DateTime(BitConverter.ToInt64(buffer, buffer.Length - 8), DateTimeKind.Utc); } return expiration; } private static void WriteExpiration(Stream stream, DateTime expiration) { stream.Write(Encoding.ASCII.GetBytes(expiresTag), 0, 8); stream.Write(BitConverter.GetBytes(expiration.Ticks), 0, 8); } private static async Task WriteExpirationAsync(Stream stream, DateTime expiration) { await stream.WriteAsync(Encoding.ASCII.GetBytes(expiresTag), 0, 8); await stream.WriteAsync(BitConverter.GetBytes(expiration.Ticks), 0, 8); } } }