Skip to content

Commit 52ffc60

Browse files
authored
Add Legacy FileLocationOption for cache directory selection (#1117)
* Add FileLocationOption for cache directory selection Introduces the FileLocationOption enum to allow selection between default and legacy cache directory locations. Updates AkavacheBuilder, IAkavacheBuilder, and related initialization methods to support this option. Adjusts cache directory resolution logic in both core and Sqlite3 extensions to respect the selected file location option. * Add FileLocationOption support to AkavacheBuilder AkavacheBuilder now accepts a FileLocationOption parameter to control cache directory selection. The SettingsCachePath property uses this option to determine whether to use the legacy or isolated cache directory. Exposes the selected FileLocationOption via a new property.
1 parent d07e90a commit 52ffc60

File tree

6 files changed

+198
-28
lines changed

6 files changed

+198
-28
lines changed

src/Akavache.Core/AkavacheBuilderExtensions.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
#if NET6_0_OR_GREATER
77
using System.Diagnostics.CodeAnalysis;
8+
using System.IO;
9+
810
#endif
911
using System.IO.IsolatedStorage;
1012
using System.Reflection;
@@ -412,4 +414,129 @@ public static IAkavacheBuilder WithInMemory(this IAkavacheBuilder builder)
412414

413415
return cachePath;
414416
}
417+
418+
/// <summary>
419+
/// Gets the legacy cache directory.
420+
/// </summary>
421+
/// <param name="builder">The builder.</param>
422+
/// <param name="cacheName">Name of the cache.</param>
423+
/// <returns>The Legacy cache path.</returns>
424+
/// <exception cref="System.ArgumentNullException">builder.</exception>
425+
/// <exception cref="System.ArgumentException">
426+
/// Cache name cannot be null or empty. - cacheName
427+
/// or
428+
/// Application name cannot be null or empty. - ApplicationName.
429+
/// </exception>
430+
public static string? GetLegacyCacheDirectory(this IAkavacheInstance builder, string cacheName)
431+
{
432+
if (builder == null)
433+
{
434+
throw new ArgumentNullException(nameof(builder));
435+
}
436+
437+
if (string.IsNullOrWhiteSpace(cacheName))
438+
{
439+
throw new ArgumentException("Cache name cannot be null or empty.", nameof(cacheName));
440+
}
441+
442+
if (string.IsNullOrWhiteSpace(builder.ApplicationName))
443+
{
444+
throw new ArgumentException("Application name cannot be null or empty.", nameof(builder.ApplicationName));
445+
}
446+
447+
#if ANDROID
448+
switch (cacheName)
449+
{
450+
case "LocalMachine":
451+
return Application.Context.CacheDir?.AbsolutePath;
452+
case "Secure":
453+
var path = Application.Context.FilesDir?.AbsolutePath;
454+
455+
if (path is null)
456+
{
457+
return null;
458+
}
459+
460+
var di = new DirectoryInfo(Path.Combine(path, "Secret"));
461+
if (!di.Exists)
462+
{
463+
di.CreateRecursive();
464+
}
465+
466+
return di.FullName;
467+
default:
468+
// Use the cache directory for UserAccount and SettingsCache caches
469+
return Application.Context.FilesDir?.AbsolutePath;
470+
}
471+
#elif IOS || MACCATALYST
472+
return cacheName switch
473+
{
474+
"LocalMachine" => (string)CreateAppDirectory(NSSearchPathDirectory.CachesDirectory, builder.ApplicationName, "BlobCache"),
475+
"Secure" => (string)CreateAppDirectory(NSSearchPathDirectory.ApplicationSupportDirectory, builder.ApplicationName, "SecretCache"),
476+
_ => (string)CreateAppDirectory(NSSearchPathDirectory.ApplicationSupportDirectory, builder.ApplicationName, "BlobCache"),
477+
};
478+
#else
479+
return cacheName switch
480+
{
481+
"LocalMachine" => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), builder.ApplicationName, "BlobCache"),
482+
"Secure" => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), builder.ApplicationName, "SecretCache"),
483+
_ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), builder.ApplicationName, "BlobCache"),
484+
};
485+
#endif
486+
}
487+
488+
internal static void CreateRecursive(this DirectoryInfo directoryInfo) =>
489+
_ = directoryInfo.SplitFullPath().Aggregate((parent, dir) =>
490+
{
491+
var path = Path.Combine(parent, dir);
492+
493+
if (!Directory.Exists(path))
494+
{
495+
Directory.CreateDirectory(path);
496+
}
497+
498+
return path;
499+
});
500+
501+
internal static IEnumerable<string> SplitFullPath(this DirectoryInfo directoryInfo)
502+
{
503+
var root = Path.GetPathRoot(directoryInfo.FullName);
504+
var components = new List<string>();
505+
for (var path = directoryInfo.FullName; path != root && path is not null; path = Path.GetDirectoryName(path))
506+
{
507+
var filename = Path.GetFileName(path);
508+
if (string.IsNullOrEmpty(filename))
509+
{
510+
continue;
511+
}
512+
513+
components.Add(filename);
514+
}
515+
516+
if (root is not null)
517+
{
518+
components.Add(root);
519+
}
520+
521+
components.Reverse();
522+
return components;
523+
}
524+
525+
#if IOS || MACCATALYST
526+
private static string CreateAppDirectory(NSSearchPathDirectory targetDir, string applicationName, string subDir = "BlobCache")
527+
{
528+
using var fm = new NSFileManager();
529+
var url = fm.GetUrl(targetDir, NSSearchPathDomain.All, null, true, out _) ?? throw new DirectoryNotFoundException();
530+
var rp = url.RelativePath ?? throw new DirectoryNotFoundException();
531+
var ret = Path.Combine(rp, applicationName, subDir);
532+
533+
var di = new DirectoryInfo(ret);
534+
if (!di.Exists)
535+
{
536+
di.CreateRecursive();
537+
}
538+
539+
return ret;
540+
}
541+
#endif
415542
}

src/Akavache.Core/CacheDatabase.cs

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,14 @@ public static IObservable<Unit> Shutdown()
142142
/// </summary>
143143
/// <typeparam name="T">The serializer.</typeparam>
144144
/// <param name="applicationName">The application name for cache directories. If null, uses the current ApplicationName.</param>
145+
/// <param name="fileLocationOption">The file location option.</param>
145146
/// <exception cref="InvalidOperationException">Failed to create AkavacheBuilder instance.</exception>
146147
#if NET6_0_OR_GREATER
147-
148148
[RequiresUnreferencedCode("Serializers require types to be preserved for serialization.")]
149-
public static void Initialize<T>(string? applicationName = null)
150-
#else
151-
public static void Initialize<T>(string? applicationName = null)
152149
#endif
153-
where T : ISerializer, new() => SetBuilder(CreateBuilder()
150+
public static void Initialize<T>(string? applicationName = null, FileLocationOption fileLocationOption = FileLocationOption.Default)
151+
152+
where T : ISerializer, new() => SetBuilder(CreateBuilder(fileLocationOption)
154153
.WithApplicationName(applicationName)
155154
.WithSerializer<T>()
156155
.WithInMemoryDefaults()
@@ -163,15 +162,12 @@ public static void Initialize<T>(string? applicationName = null)
163162
/// <typeparam name="T">The serializer.</typeparam>
164163
/// <param name="configureSerializer">The Serializer configuration.</param>
165164
/// <param name="applicationName">The application name for cache directories. If null, uses the current ApplicationName.</param>
166-
/// <exception cref="InvalidOperationException">Failed to create AkavacheBuilder instance.</exception>
165+
/// <param name="fileLocationOption">The file location option.</param>
167166
#if NET6_0_OR_GREATER
168-
169167
[RequiresUnreferencedCode("Serializers require types to be preserved for serialization.")]
170-
public static void Initialize<T>(Func<T> configureSerializer, string? applicationName = null)
171-
#else
172-
public static void Initialize<T>(Func<T> configureSerializer, string? applicationName = null)
173168
#endif
174-
where T : ISerializer, new() => SetBuilder(CreateBuilder()
169+
public static void Initialize<T>(Func<T> configureSerializer, string? applicationName = null, FileLocationOption fileLocationOption = FileLocationOption.Default)
170+
where T : ISerializer, new() => SetBuilder(CreateBuilder(fileLocationOption)
175171
.WithApplicationName(applicationName)
176172
.WithSerializer(configureSerializer)
177173
.WithInMemoryDefaults()
@@ -183,22 +179,19 @@ public static void Initialize<T>(Func<T> configureSerializer, string? applicatio
183179
/// <typeparam name="T">The serializer.</typeparam>
184180
/// <param name="configure">An action to configure the Akavache builder.</param>
185181
/// <param name="applicationName">Name of the application.</param>
186-
/// <exception cref="ArgumentNullException">configure.</exception>
182+
/// <param name="fileLocationOption">The file location option.</param>
187183
#if NET6_0_OR_GREATER
188-
189184
[RequiresUnreferencedCode("Serializers require types to be preserved for serialization.")]
190-
public static void Initialize<T>(Action<IAkavacheBuilder> configure, string? applicationName = null)
191-
#else
192-
public static void Initialize<T>(Action<IAkavacheBuilder> configure, string? applicationName = null)
193185
#endif
186+
public static void Initialize<T>(Action<IAkavacheBuilder> configure, string? applicationName = null, FileLocationOption fileLocationOption = FileLocationOption.Default)
194187
where T : ISerializer, new()
195188
{
196189
if (configure == null)
197190
{
198191
throw new ArgumentNullException(nameof(configure));
199192
}
200193

201-
var builder = CreateBuilder()
194+
var builder = CreateBuilder(fileLocationOption)
202195
.WithApplicationName(applicationName)
203196
.WithSerializer<T>();
204197

@@ -214,22 +207,19 @@ public static void Initialize<T>(Action<IAkavacheBuilder> configure, string? app
214207
/// <param name="configureSerializer">The Serializer configuration.</param>
215208
/// <param name="configure">An action to configure the Akavache builder.</param>
216209
/// <param name="applicationName">Name of the application.</param>
217-
/// <exception cref="ArgumentNullException">configure.</exception>
210+
/// <param name="fileLocationOption">The file location option.</param>
218211
#if NET6_0_OR_GREATER
219-
220212
[RequiresUnreferencedCode("Serializers require types to be preserved for serialization.")]
221-
public static void Initialize<T>(Func<T> configureSerializer, Action<IAkavacheBuilder> configure, string? applicationName = null)
222-
#else
223-
public static void Initialize<T>(Func<T> configureSerializer, Action<IAkavacheBuilder> configure, string? applicationName = null)
224213
#endif
214+
public static void Initialize<T>(Func<T> configureSerializer, Action<IAkavacheBuilder> configure, string? applicationName = null, FileLocationOption fileLocationOption = FileLocationOption.Default)
225215
where T : ISerializer, new()
226216
{
227217
if (configure == null)
228218
{
229219
throw new ArgumentNullException(nameof(configure));
230220
}
231221

232-
var builder = CreateBuilder()
222+
var builder = CreateBuilder(fileLocationOption)
233223
.WithApplicationName(applicationName)
234224
.WithSerializer(configureSerializer);
235225

@@ -241,8 +231,11 @@ public static void Initialize<T>(Func<T> configureSerializer, Action<IAkavacheBu
241231
/// <summary>
242232
/// Creates a new Akavache builder for configuration.
243233
/// </summary>
244-
/// <returns>A new Akavache builder instance.</returns>
245-
public static IAkavacheBuilder CreateBuilder() => new AkavacheBuilder();
234+
/// <param name="fileLocationOption">The file location option.</param>
235+
/// <returns>
236+
/// A new Akavache builder instance.
237+
/// </returns>
238+
public static IAkavacheBuilder CreateBuilder(FileLocationOption fileLocationOption = FileLocationOption.Default) => new AkavacheBuilder(fileLocationOption);
246239

247240
/// <summary>
248241
/// Internal method to set the builder instance. Used by the builder pattern.

src/Akavache.Core/Core/AkavacheBuilder.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ internal class AkavacheBuilder : IAkavacheBuilder
1818
{
1919
private static readonly object _lock = new();
2020
private string? _settingsCachePath;
21+
private FileLocationOption _fileLocationOption;
2122

2223
[SuppressMessage("ExecutingAssembly.Location", "IL3000:String may be null", Justification = "Handled.")]
23-
public AkavacheBuilder()
24+
public AkavacheBuilder(FileLocationOption fileLocationOption = FileLocationOption.Default)
2425
{
26+
_fileLocationOption = fileLocationOption;
2527
try
2628
{
2729
ExecutingAssemblyName = ExecutingAssembly.FullName!.Split(',')[0];
@@ -73,7 +75,11 @@ public string? SettingsCachePath
7375
// Lazy computation to ensure ApplicationName is properly set via WithApplicationName()
7476
if (_settingsCachePath == null)
7577
{
76-
_settingsCachePath = this.GetIsolatedCacheDirectory("SettingsCache");
78+
_settingsCachePath = _fileLocationOption switch
79+
{
80+
FileLocationOption.Legacy => this.GetLegacyCacheDirectory("SettingsCache"),
81+
_ => this.GetIsolatedCacheDirectory("SettingsCache"),
82+
};
7783
}
7884

7985
return _settingsCachePath;
@@ -119,6 +125,14 @@ public string? SettingsCachePath
119125
/// </value>
120126
public string? SerializerTypeName { get; internal set; }
121127

128+
/// <summary>
129+
/// Gets the file location option.
130+
/// </summary>
131+
/// <value>
132+
/// The file location option.
133+
/// </value>
134+
public FileLocationOption FileLocationOption => _fileLocationOption;
135+
122136
internal static Dictionary<string, IBlobCache?>? BlobCaches { get; set; } = [];
123137

124138
internal static Dictionary<string, ISettingsStorage?>? SettingsStores { get; set; } = [];
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
namespace Akavache.Core
7+
{
8+
/// <summary>
9+
/// File Location Option.
10+
/// </summary>
11+
public enum FileLocationOption
12+
{
13+
/// <summary>
14+
/// Use the default location for the platform.
15+
/// </summary>
16+
Default,
17+
/// <summary>
18+
/// Use the legacy location, if available on the platform.
19+
/// </summary>
20+
Legacy,
21+
}
22+
}

src/Akavache.Core/IAkavacheBuilder.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// The .NET Foundation licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
55

6+
using Akavache.Core;
67
#if NET6_0_OR_GREATER
78
using System.Diagnostics.CodeAnalysis;
89
#endif
@@ -14,6 +15,14 @@ namespace Akavache;
1415
/// </summary>
1516
public interface IAkavacheBuilder : IAkavacheInstance
1617
{
18+
/// <summary>
19+
/// Gets the file location option.
20+
/// </summary>
21+
/// <value>
22+
/// The file location option.
23+
/// </value>
24+
FileLocationOption FileLocationOption { get; }
25+
1726
/// <summary>
1827
/// Sets the application name for cache directory paths.
1928
/// </summary>

src/Akavache.Sqlite3/AkavacheBuilderExtensions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,12 @@ private static SqliteBlobCache CreateSqliteCache(string name, IAkavacheBuilder b
150150
// Validate cache name to prevent path traversal attacks
151151
var validatedName = SecurityUtilities.ValidateCacheName(name, nameof(name));
152152

153-
var directory = builder.GetIsolatedCacheDirectory(validatedName);
153+
// Determine the cache directory
154+
var directory = builder.FileLocationOption switch
155+
{
156+
FileLocationOption.Legacy => builder.GetLegacyCacheDirectory(validatedName),
157+
_ => builder.GetIsolatedCacheDirectory(validatedName),
158+
};
154159
if (string.IsNullOrWhiteSpace(directory))
155160
{
156161
throw new InvalidOperationException("Failed to determine a valid cache directory.");

0 commit comments

Comments
 (0)