From 0d242e556cbda6ddefb1bc53c390f34328002315 Mon Sep 17 00:00:00 2001 From: ClemensFischer Date: Wed, 14 Aug 2024 17:27:14 +0200 Subject: [PATCH] UWP ImageFileCache and Package References --- MBTiles/Avalonia/MBTiles.Avalonia.csproj | 2 +- MBTiles/UWP/MBTiles.UWP.csproj | 2 +- MBTiles/WinUI/MBTiles.WinUI.csproj | 2 +- .../Avalonia/MapControl.Avalonia.csproj | 2 +- .../Avalonia/TileImageLoader.Avalonia.cs | 1 - MapControl/Shared/ImageFileCache.cs | 14 +- MapControl/UWP/ImageFileCache.UWP.cs | 272 ++++++++++++++++++ MapControl/UWP/MapControl.UWP.csproj | 6 +- MapControl/UWP/TileImageLoader.UWP.cs | 3 +- MapControl/WPF/TileImageLoader.WPF.cs | 1 - MapControl/WinUI/MapControl.WinUI.csproj | 2 +- MapControl/WinUI/TileImageLoader.WinUI.cs | 1 - .../Avalonia/MapProjections.Avalonia.csproj | 2 +- MapProjections/UWP/MapProjections.UWP.csproj | 2 +- .../WinUI/MapProjections.WinUI.csproj | 2 +- .../Avalonia/MapUiTools.Avalonia.csproj | 2 +- MapUiTools/UWP/MapUiTools.UWP.csproj | 2 +- MapUiTools/WinUI/MapUiTools.WinUI.csproj | 2 +- SampleApps/AvaloniaApp/AvaloniaApp.csproj | 10 +- SampleApps/UniversalApp/UniversalApp.csproj | 2 +- SampleApps/WinUiApp/WinUiApp.csproj | 2 +- 21 files changed, 301 insertions(+), 33 deletions(-) create mode 100644 MapControl/UWP/ImageFileCache.UWP.cs diff --git a/MBTiles/Avalonia/MBTiles.Avalonia.csproj b/MBTiles/Avalonia/MBTiles.Avalonia.csproj index fe132b41..51422504 100644 --- a/MBTiles/Avalonia/MBTiles.Avalonia.csproj +++ b/MBTiles/Avalonia/MBTiles.Avalonia.csproj @@ -15,7 +15,7 @@ - + diff --git a/MBTiles/UWP/MBTiles.UWP.csproj b/MBTiles/UWP/MBTiles.UWP.csproj index cf23e721..d3ba3b8d 100644 --- a/MBTiles/UWP/MBTiles.UWP.csproj +++ b/MBTiles/UWP/MBTiles.UWP.csproj @@ -11,7 +11,7 @@ MBTiles.UWP en-US UAP - 10.0.22000.0 + 10.0.22621.0 10.0.17763.0 14 512 diff --git a/MBTiles/WinUI/MBTiles.WinUI.csproj b/MBTiles/WinUI/MBTiles.WinUI.csproj index 8cc4f389..ceb5497c 100644 --- a/MBTiles/WinUI/MBTiles.WinUI.csproj +++ b/MBTiles/WinUI/MBTiles.WinUI.csproj @@ -18,7 +18,7 @@ - + diff --git a/MapControl/Avalonia/MapControl.Avalonia.csproj b/MapControl/Avalonia/MapControl.Avalonia.csproj index 34ccf0ac..3a5231b1 100644 --- a/MapControl/Avalonia/MapControl.Avalonia.csproj +++ b/MapControl/Avalonia/MapControl.Avalonia.csproj @@ -16,7 +16,7 @@ - + diff --git a/MapControl/Avalonia/TileImageLoader.Avalonia.cs b/MapControl/Avalonia/TileImageLoader.Avalonia.cs index 37b35c39..225803b1 100644 --- a/MapControl/Avalonia/TileImageLoader.Avalonia.cs +++ b/MapControl/Avalonia/TileImageLoader.Avalonia.cs @@ -15,7 +15,6 @@ namespace MapControl public static string DefaultCacheFolder => System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache"); - private static async Task LoadTileAsync(Tile tile, Func> loadImageFunc) { var image = await loadImageFunc().ConfigureAwait(false); diff --git a/MapControl/Shared/ImageFileCache.cs b/MapControl/Shared/ImageFileCache.cs index a1bde7b5..4c9aae2b 100644 --- a/MapControl/Shared/ImageFileCache.cs +++ b/MapControl/Shared/ImageFileCache.cs @@ -8,11 +8,11 @@ using Microsoft.Extensions.Options; using System; using System.Diagnostics; using System.IO; -using Path = System.IO.Path; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Path = System.IO.Path; namespace MapControl.Caching { @@ -269,11 +269,13 @@ namespace MapControl.Caching try { var hasExpired = false; - var buffer = new byte[16]; using (var stream = file.OpenRead()) { stream.Seek(-16, SeekOrigin.End); + + var buffer = new byte[16]; + hasExpired = stream.Read(buffer, 0, 16) == 16 && GetExpirationTicks(buffer, out long expiration) && expiration <= DateTimeOffset.UtcNow.Ticks; @@ -377,10 +379,11 @@ namespace MapControl.Caching private static Task WriteAsync(Stream stream, byte[] bytes) => stream.WriteAsync(bytes, 0, bytes.Length); - static partial void SetAccessControl(string path); -#if !UWP && !AVALONIA - static partial void SetAccessControl(string path) + private static void SetAccessControl(string path) { +#if AVALONIA + if (!OperatingSystem.IsWindows()) return; +#endif var fileInfo = new FileInfo(path); var fileSecurity = fileInfo.GetAccessControl(); var fullControlRule = new System.Security.AccessControl.FileSystemAccessRule( @@ -392,6 +395,5 @@ namespace MapControl.Caching fileSecurity.AddAccessRule(fullControlRule); fileInfo.SetAccessControl(fileSecurity); } -#endif } } diff --git a/MapControl/UWP/ImageFileCache.UWP.cs b/MapControl/UWP/ImageFileCache.UWP.cs new file mode 100644 index 00000000..b227df58 --- /dev/null +++ b/MapControl/UWP/ImageFileCache.UWP.cs @@ -0,0 +1,272 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// Copyright © 2024 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.Streams; +using Buffer = Windows.Storage.Streams.Buffer; + +namespace MapControl.Caching +{ + /// + /// IDistributedCache implementation based on local image files. + /// + public partial class ImageFileCache : IDistributedCache + { + private static readonly byte[] expirationTag = Encoding.ASCII.GetBytes("EXPIRES:"); + + private readonly MemoryDistributedCache memoryCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); + private readonly StorageFolder rootFolder; + + public ImageFileCache(StorageFolder folder) + { + rootFolder = folder ?? throw new ArgumentException($"The {nameof(folder)} argument must not be null or empty.", nameof(folder)); + + Debug.WriteLine($"ImageFileCache: {rootFolder}"); + + _ = Task.Factory.StartNew(CleanAsync, TaskCreationOptions.LongRunning); + } + + public byte[] Get(string key) + { + throw new NotSupportedException(); + } + + public void Set(string key, byte[] buffer, DistributedCacheEntryOptions options) + { + throw new NotSupportedException(); + } + + public void Remove(string key) + { + throw new NotSupportedException(); + } + + public Task RemoveAsync(string key, CancellationToken token = default) + { + throw new NotSupportedException(); + } + + public void Refresh(string key) + { + throw new NotSupportedException(); + } + + public Task RefreshAsync(string key, CancellationToken token = default) + { + throw new NotSupportedException(); + } + + public async Task GetAsync(string key, CancellationToken token = default) + { + var buffer = await memoryCache.GetAsync(key, token).ConfigureAwait(false); + + if (buffer == null) + { + try + { + var item = await rootFolder.TryGetItemAsync(key.Replace('/', '\\')); + + if (item is StorageFile file) + { + buffer = (await FileIO.ReadBufferAsync(file)).ToArray(); + + if (CheckExpiration(ref buffer, out DistributedCacheEntryOptions options)) + { + await memoryCache.SetAsync(key, buffer, options, token).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"ImageFileCache: Failed reading {key}: {ex.Message}"); + } + } + + return buffer; + } + + public async Task SetAsync(string key, byte[] buffer, DistributedCacheEntryOptions options, CancellationToken token = default) + { + await memoryCache.SetAsync(key, buffer, options, token).ConfigureAwait(false); + + if (buffer?.Length > 0) + { + try + { + var keyComponents = key.Split('/'); + var folder = rootFolder; + + for (int i = 0; i < keyComponents.Length - 1; i++) + { + folder = await folder.CreateFolderAsync(keyComponents[i], CreationCollisionOption.OpenIfExists); + } + + var file = await folder.CreateFileAsync(keyComponents[keyComponents.Length - 1], CreationCollisionOption.OpenIfExists); + + using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite)) + { + await stream.WriteAsync(buffer.AsBuffer()); + + if (GetExpirationBytes(options, out byte[] expiration)) + { + await stream.WriteAsync(expiration.AsBuffer()); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"ImageFileCache: Failed writing {key}: {ex.Message}"); + } + } + } + + public async Task CleanAsync() + { + var deletedFileCount = await CleanFolder(rootFolder); + + if (deletedFileCount > 0) + { + Debug.WriteLine($"ImageFileCache: Deleted {deletedFileCount} expired files."); + } + } + + private static async Task CleanFolder(StorageFolder folder) + { + var deletedFileCount = 0; + + try + { + foreach (var f in await folder.GetFoldersAsync()) + { + deletedFileCount += await CleanFolder(f); + } + + foreach (var f in await folder.GetFilesAsync()) + { + deletedFileCount += await CleanFile(f); + } + + if ((await folder.GetItemsAsync()).Count == 0) + { + await folder.DeleteAsync(); + } + } + catch (Exception ex) + { + Debug.WriteLine($"ImageFileCache: Failed cleaning {folder.Path}: {ex.Message}"); + } + + return deletedFileCount; + } + + private static async Task CleanFile(StorageFile file) + { + var deletedFileCount = 0; + var size = (await file.GetBasicPropertiesAsync()).Size; + + if (size > 16) + { + try + { + var hasExpired = false; + + using (var stream = await file.OpenReadAsync()) + { + stream.Seek(size - 16); + + var buffer = await stream.ReadAsync(new Buffer(16), 16, InputStreamOptions.None); + + hasExpired = buffer.Length == 16 + && GetExpirationTicks(buffer.ToArray(), out long expiration) + && expiration <= DateTimeOffset.UtcNow.Ticks; + } + + if (hasExpired) + { + await file.DeleteAsync(); + deletedFileCount = 1; + } + } + catch (Exception ex) + { + Debug.WriteLine($"ImageFileCache: Failed cleaning {file.Path}: {ex.Message}"); + } + } + + return deletedFileCount; + } + + private static bool CheckExpiration(ref byte[] buffer, out DistributedCacheEntryOptions options) + { + if (GetExpirationTicks(buffer, out long expiration)) + { + if (expiration > DateTimeOffset.UtcNow.Ticks) + { + Array.Resize(ref buffer, buffer.Length - 16); + + options = new DistributedCacheEntryOptions + { + AbsoluteExpiration = new DateTimeOffset(expiration, TimeSpan.Zero) + }; + + return true; + } + + buffer = null; // buffer has expired + } + + options = null; + return false; + } + + private static bool GetExpirationTicks(byte[] buffer, out long expirationTicks) + { + if (buffer.Length >= 16 && + expirationTag.SequenceEqual(buffer.Skip(buffer.Length - 16).Take(8))) + { + expirationTicks = BitConverter.ToInt64(buffer, buffer.Length - 8); + return true; + } + + expirationTicks = 0; + return false; + } + + private static bool GetExpirationBytes(DistributedCacheEntryOptions options, out byte[] expirationBytes) + { + long expirationTicks; + + if (options.AbsoluteExpiration.HasValue) + { + expirationTicks = options.AbsoluteExpiration.Value.Ticks; + } + else if (options.AbsoluteExpirationRelativeToNow.HasValue) + { + expirationTicks = DateTimeOffset.UtcNow.Add(options.AbsoluteExpirationRelativeToNow.Value).Ticks; + } + else if (options.SlidingExpiration.HasValue) + { + expirationTicks = DateTimeOffset.UtcNow.Add(options.SlidingExpiration.Value).Ticks; + } + else + { + expirationBytes = null; + return false; + } + + expirationBytes = expirationTag.Concat(BitConverter.GetBytes(expirationTicks)).ToArray(); + return true; + } + } +} diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index 24bf291b..1a2f5e5b 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -11,7 +11,7 @@ MapControl.UWP en-US UAP - 10.0.22000.0 + 10.0.22621.0 10.0.17763.0 14 512 @@ -83,9 +83,6 @@ GroundOverlay.cs - - ImageFileCache.cs - ImageLoader.cs @@ -275,6 +272,7 @@ Tile.WinUI.cs + diff --git a/MapControl/UWP/TileImageLoader.UWP.cs b/MapControl/UWP/TileImageLoader.UWP.cs index c57b82ed..ee27b03d 100644 --- a/MapControl/UWP/TileImageLoader.UWP.cs +++ b/MapControl/UWP/TileImageLoader.UWP.cs @@ -15,8 +15,7 @@ namespace MapControl /// /// Default folder where the Cache instance may save data. /// - public static string DefaultCacheFolder => ApplicationData.Current.TemporaryFolder.Path; - + public static StorageFolder DefaultCacheFolder => ApplicationData.Current.LocalCacheFolder; private static async Task LoadTileAsync(Tile tile, Func> loadImageFunc) { diff --git a/MapControl/WPF/TileImageLoader.WPF.cs b/MapControl/WPF/TileImageLoader.WPF.cs index 0bac2ee2..0aacd1a0 100644 --- a/MapControl/WPF/TileImageLoader.WPF.cs +++ b/MapControl/WPF/TileImageLoader.WPF.cs @@ -17,7 +17,6 @@ namespace MapControl public static string DefaultCacheFolder => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache"); - private static async Task LoadTileAsync(Tile tile, Func> loadImageFunc) { var image = await loadImageFunc().ConfigureAwait(false); diff --git a/MapControl/WinUI/MapControl.WinUI.csproj b/MapControl/WinUI/MapControl.WinUI.csproj index 8768f4fe..c4153771 100644 --- a/MapControl/WinUI/MapControl.WinUI.csproj +++ b/MapControl/WinUI/MapControl.WinUI.csproj @@ -21,7 +21,7 @@ - + diff --git a/MapControl/WinUI/TileImageLoader.WinUI.cs b/MapControl/WinUI/TileImageLoader.WinUI.cs index 3109f0d7..135f0909 100644 --- a/MapControl/WinUI/TileImageLoader.WinUI.cs +++ b/MapControl/WinUI/TileImageLoader.WinUI.cs @@ -18,7 +18,6 @@ namespace MapControl public static string DefaultCacheFolder => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl", "TileCache"); - private static Task LoadTileAsync(Tile tile, Func> loadImageFunc) { var tcs = new TaskCompletionSource(); diff --git a/MapProjections/Avalonia/MapProjections.Avalonia.csproj b/MapProjections/Avalonia/MapProjections.Avalonia.csproj index 70d83ccf..9c471e32 100644 --- a/MapProjections/Avalonia/MapProjections.Avalonia.csproj +++ b/MapProjections/Avalonia/MapProjections.Avalonia.csproj @@ -19,7 +19,7 @@ - + diff --git a/MapProjections/UWP/MapProjections.UWP.csproj b/MapProjections/UWP/MapProjections.UWP.csproj index 872c94b7..8500d368 100644 --- a/MapProjections/UWP/MapProjections.UWP.csproj +++ b/MapProjections/UWP/MapProjections.UWP.csproj @@ -11,7 +11,7 @@ MapProjections.UWP en-US UAP - 10.0.22000.0 + 10.0.22621.0 10.0.17763.0 14 512 diff --git a/MapProjections/WinUI/MapProjections.WinUI.csproj b/MapProjections/WinUI/MapProjections.WinUI.csproj index 35f202d1..3d41cafc 100644 --- a/MapProjections/WinUI/MapProjections.WinUI.csproj +++ b/MapProjections/WinUI/MapProjections.WinUI.csproj @@ -22,7 +22,7 @@ - + diff --git a/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj b/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj index 62ea2731..0d5ba966 100644 --- a/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj +++ b/MapUiTools/Avalonia/MapUiTools.Avalonia.csproj @@ -15,6 +15,6 @@ - + diff --git a/MapUiTools/UWP/MapUiTools.UWP.csproj b/MapUiTools/UWP/MapUiTools.UWP.csproj index 1d0c5852..8cf295b6 100644 --- a/MapUiTools/UWP/MapUiTools.UWP.csproj +++ b/MapUiTools/UWP/MapUiTools.UWP.csproj @@ -11,7 +11,7 @@ MapUiTools.UWP en-US UAP - 10.0.22000.0 + 10.0.22621.0 10.0.17763.0 14 512 diff --git a/MapUiTools/WinUI/MapUiTools.WinUI.csproj b/MapUiTools/WinUI/MapUiTools.WinUI.csproj index 56034d7f..f22e058d 100644 --- a/MapUiTools/WinUI/MapUiTools.WinUI.csproj +++ b/MapUiTools/WinUI/MapUiTools.WinUI.csproj @@ -18,7 +18,7 @@ - + diff --git a/SampleApps/AvaloniaApp/AvaloniaApp.csproj b/SampleApps/AvaloniaApp/AvaloniaApp.csproj index 871c2479..04ba67be 100644 --- a/SampleApps/AvaloniaApp/AvaloniaApp.csproj +++ b/SampleApps/AvaloniaApp/AvaloniaApp.csproj @@ -32,11 +32,11 @@ - - - - - + + + + + diff --git a/SampleApps/UniversalApp/UniversalApp.csproj b/SampleApps/UniversalApp/UniversalApp.csproj index c3440fe5..4d2e26c9 100644 --- a/SampleApps/UniversalApp/UniversalApp.csproj +++ b/SampleApps/UniversalApp/UniversalApp.csproj @@ -11,7 +11,7 @@ UniversalApp en-US UAP - 10.0.22000.0 + 10.0.22621.0 10.0.17763.0 14 true diff --git a/SampleApps/WinUiApp/WinUiApp.csproj b/SampleApps/WinUiApp/WinUiApp.csproj index 01983a6a..0ba425d2 100644 --- a/SampleApps/WinUiApp/WinUiApp.csproj +++ b/SampleApps/WinUiApp/WinUiApp.csproj @@ -37,7 +37,7 @@ - +