553 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			553 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using Dalamud.Utility;
 | |
| using K4os.Compression.LZ4.Streams;
 | |
| using MareSynchronos.Interop.Ipc;
 | |
| using MareSynchronos.MareConfiguration;
 | |
| using MareSynchronos.Services.Mediator;
 | |
| using MareSynchronos.Utils;
 | |
| using Microsoft.Extensions.Hosting;
 | |
| using Microsoft.Extensions.Logging;
 | |
| using System.Collections.Concurrent;
 | |
| using System.Globalization;
 | |
| using System.Text;
 | |
| 
 | |
| namespace MareSynchronos.FileCache;
 | |
| 
 | |
| public sealed class FileCacheManager : IHostedService
 | |
| {
 | |
|     public const string CachePrefix = "{cache}";
 | |
|     public const string CsvSplit = "|";
 | |
|     public const string PenumbraPrefix = "{penumbra}";
 | |
|     public const string SubstPrefix = "{subst}";
 | |
|     public const string SubstPath = "subst";
 | |
|     public string CacheFolder => _configService.Current.CacheFolder;
 | |
|     public string SubstFolder => CacheFolder.IsNullOrEmpty() ? string.Empty : CacheFolder.ToLowerInvariant().TrimEnd('\\') + "\\" + SubstPath;
 | |
|     private readonly MareConfigService _configService;
 | |
|     private readonly MareMediator _mareMediator;
 | |
|     private readonly string _csvPath;
 | |
|     private readonly ConcurrentDictionary<string, List<FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
 | |
|     private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
 | |
|     private readonly object _fileWriteLock = new();
 | |
|     private readonly IpcManager _ipcManager;
 | |
|     private readonly ILogger<FileCacheManager> _logger;
 | |
| 
 | |
|     public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, MareConfigService configService, MareMediator mareMediator)
 | |
|     {
 | |
|         _logger = logger;
 | |
|         _ipcManager = ipcManager;
 | |
|         _configService = configService;
 | |
|         _mareMediator = mareMediator;
 | |
|         _csvPath = Path.Combine(configService.ConfigurationDirectory, "FileCache.csv");
 | |
|     }
 | |
| 
 | |
|     private string CsvBakPath => _csvPath + ".bak";
 | |
| 
 | |
|     public FileCacheEntity? CreateCacheEntry(string path)
 | |
|     {
 | |
|         FileInfo fi = new(path);
 | |
|         if (!fi.Exists) return null;
 | |
|         _logger.LogTrace("Creating cache entry for {path}", path);
 | |
|         var fullName = fi.FullName.ToLowerInvariant();
 | |
|         if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null;
 | |
|         string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
 | |
|         return CreateFileCacheEntity(fi, prefixedPath);
 | |
|     }
 | |
| 
 | |
|     public FileCacheEntity? CreateSubstEntry(string path)
 | |
|     {
 | |
|         FileInfo fi = new(path);
 | |
|         if (!fi.Exists) return null;
 | |
|         _logger.LogTrace("Creating substitute entry for {path}", path);
 | |
|         var fullName = fi.FullName.ToLowerInvariant();
 | |
|         if (!fullName.Contains(SubstFolder, StringComparison.Ordinal)) return null;
 | |
|         string prefixedPath = fullName.Replace(SubstFolder, SubstPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
 | |
|         var fakeHash = Path.GetFileNameWithoutExtension(fi.FullName).ToUpperInvariant();
 | |
|         var result = CreateFileCacheEntity(fi, prefixedPath, fakeHash);
 | |
|         return result;
 | |
|     }
 | |
| 
 | |
|     public FileCacheEntity? CreateFileEntry(string path)
 | |
|     {
 | |
|         FileInfo fi = new(path);
 | |
|         if (!fi.Exists) return null;
 | |
|         _logger.LogTrace("Creating file entry for {path}", path);
 | |
|         var fullName = fi.FullName.ToLowerInvariant();
 | |
|         if (!fullName.Contains(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
 | |
|         string prefixedPath = fullName.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
 | |
|         return CreateFileCacheEntity(fi, prefixedPath);
 | |
|     }
 | |
| 
 | |
|     public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList();
 | |
| 
 | |
|     public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
 | |
|     {
 | |
|         List<FileCacheEntity> output = [];
 | |
|         if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
 | |
|         {
 | |
|             foreach (var fileCache in fileCacheEntities.Where(c => ignoreCacheEntries ? (!c.IsCacheEntry && !c.IsSubstEntry) : true).ToList())
 | |
|             {
 | |
|                 if (!validate) output.Add(fileCache);
 | |
|                 else
 | |
|                 {
 | |
|                     var validated = GetValidatedFileCache(fileCache);
 | |
|                     if (validated != null) output.Add(validated);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return output;
 | |
|     }
 | |
| 
 | |
|     public Task<List<FileCacheEntity>> ValidateLocalIntegrity(IProgress<(int, int, FileCacheEntity)> progress, CancellationToken cancellationToken)
 | |
|     {
 | |
|         _mareMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
 | |
|         _logger.LogInformation("Validating local storage");
 | |
|         var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList();
 | |
|         List<FileCacheEntity> brokenEntities = [];
 | |
|         int i = 0;
 | |
|         foreach (var fileCache in cacheEntries)
 | |
|         {
 | |
|             if (cancellationToken.IsCancellationRequested) break;
 | |
|             if (fileCache.IsSubstEntry) continue;
 | |
| 
 | |
|             _logger.LogInformation("Validating {file}", fileCache.ResolvedFilepath);
 | |
| 
 | |
|             progress.Report((i, cacheEntries.Count, fileCache));
 | |
|             i++;
 | |
|             if (!File.Exists(fileCache.ResolvedFilepath))
 | |
|             {
 | |
|                 brokenEntities.Add(fileCache);
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             try
 | |
|             {
 | |
|                 var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
 | |
|                 if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal))
 | |
|                 {
 | |
|                     _logger.LogInformation("Failed to validate {file}, got hash {hash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash);
 | |
|                     brokenEntities.Add(fileCache);
 | |
|                 }
 | |
|             }
 | |
|             catch (Exception e)
 | |
|             {
 | |
|                 _logger.LogWarning(e, "Error during validation of {file}", fileCache.ResolvedFilepath);
 | |
|                 brokenEntities.Add(fileCache);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         foreach (var brokenEntity in brokenEntities)
 | |
|         {
 | |
|             RemoveHashedFile(brokenEntity.Hash, brokenEntity.PrefixedFilePath);
 | |
| 
 | |
|             try
 | |
|             {
 | |
|                 File.Delete(brokenEntity.ResolvedFilepath);
 | |
|             }
 | |
|             catch (Exception ex)
 | |
|             {
 | |
|                 _logger.LogWarning(ex, "Could not delete {file}", brokenEntity.ResolvedFilepath);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         _mareMediator.Publish(new ResumeScanMessage(nameof(ValidateLocalIntegrity)));
 | |
|         return Task.FromResult(brokenEntities);
 | |
|     }
 | |
| 
 | |
|     public string GetCacheFilePath(string hash, string extension)
 | |
|     {
 | |
|         return Path.Combine(_configService.Current.CacheFolder, hash + "." + extension);
 | |
|     }
 | |
| 
 | |
|     public string GetSubstFilePath(string hash, string extension)
 | |
|     {
 | |
|         return Path.Combine(SubstFolder, hash + "." + extension);
 | |
|     }
 | |
| 
 | |
|     public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
 | |
|     {
 | |
|         var fileCache = GetFileCacheByHash(fileHash)!;
 | |
|         using var fs = File.OpenRead(fileCache.ResolvedFilepath);
 | |
|         var ms = new MemoryStream(64 * 1024);
 | |
|         using var encstream = LZ4Stream.Encode(ms, new LZ4EncoderSettings(){CompressionLevel=K4os.Compression.LZ4.LZ4Level.L09_HC});
 | |
|         await fs.CopyToAsync(encstream, uploadToken).ConfigureAwait(false);
 | |
|         encstream.Close();
 | |
|         fileCache.CompressedSize = encstream.Length;
 | |
|         return (fileHash, ms.ToArray());
 | |
|     }
 | |
| 
 | |
|     public FileCacheEntity? GetFileCacheByHash(string hash, bool preferSubst = false)
 | |
|     {
 | |
|         var caches = GetFileCachesByHash(hash);
 | |
|         if (preferSubst && caches.Subst != null)
 | |
|             return caches.Subst;
 | |
|         return caches.Penumbra ?? caches.Cache;
 | |
|     }
 | |
| 
 | |
|     public (FileCacheEntity? Penumbra, FileCacheEntity? Cache, FileCacheEntity? Subst) GetFileCachesByHash(string hash)
 | |
|     {
 | |
|         (FileCacheEntity? Penumbra, FileCacheEntity? Cache, FileCacheEntity? Subst) result = (null, null, null);
 | |
|         if (_fileCaches.TryGetValue(hash, out var hashes))
 | |
|         {
 | |
|             result.Penumbra = hashes.Where(p => p.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault();
 | |
|             result.Cache = hashes.Where(p => p.PrefixedFilePath.StartsWith(CachePrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault();
 | |
|             result.Subst = hashes.Where(p => p.PrefixedFilePath.StartsWith(SubstPrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault();
 | |
|         }
 | |
|         return result;
 | |
|     }
 | |
| 
 | |
|     private FileCacheEntity? GetFileCacheByPath(string path)
 | |
|     {
 | |
|         var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant()
 | |
|             .Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
 | |
|         var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
 | |
| 
 | |
|         if (entry == null)
 | |
|         {
 | |
|             _logger.LogDebug("Found no entries for {path}", cleanedPath);
 | |
|             return CreateFileEntry(path);
 | |
|         }
 | |
| 
 | |
|         var validatedCacheEntry = GetValidatedFileCache(entry);
 | |
| 
 | |
|         return validatedCacheEntry;
 | |
|     }
 | |
| 
 | |
|     public Dictionary<string, FileCacheEntity?> GetFileCachesByPaths(string[] paths)
 | |
|     {
 | |
|         _getCachesByPathsSemaphore.Wait();
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var cleanedPaths = paths.Distinct(StringComparer.OrdinalIgnoreCase).ToDictionary(p => p,
 | |
|                 p => p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase)
 | |
|                     .Replace(_ipcManager.Penumbra.ModDirectory!, _ipcManager.Penumbra.ModDirectory!.EndsWith('\\') ? PenumbraPrefix + '\\' : PenumbraPrefix, StringComparison.OrdinalIgnoreCase)
 | |
|                     .Replace(SubstFolder, SubstPrefix, StringComparison.OrdinalIgnoreCase)
 | |
|                     .Replace(_configService.Current.CacheFolder, _configService.Current.CacheFolder.EndsWith('\\') ? CachePrefix + '\\' : CachePrefix, StringComparison.OrdinalIgnoreCase)
 | |
|                     .Replace("\\\\", "\\", StringComparison.Ordinal),
 | |
|                 StringComparer.OrdinalIgnoreCase);
 | |
| 
 | |
|             Dictionary<string, FileCacheEntity?> result = new(StringComparer.OrdinalIgnoreCase);
 | |
| 
 | |
|             var dict = _fileCaches.SelectMany(f => f.Value)
 | |
|                 .ToDictionary(d => d.PrefixedFilePath, d => d, StringComparer.OrdinalIgnoreCase);
 | |
| 
 | |
|             foreach (var entry in cleanedPaths)
 | |
|             {
 | |
|                 //_logger.LogDebug("Checking {path}", entry.Value);
 | |
| 
 | |
|                 if (dict.TryGetValue(entry.Value, out var entity))
 | |
|                 {
 | |
|                     var validatedCache = GetValidatedFileCache(entity);
 | |
|                     result.Add(entry.Key, validatedCache);
 | |
|                 }
 | |
|                 else
 | |
|                 {
 | |
|                     if (entry.Value.StartsWith(PenumbraPrefix, StringComparison.Ordinal))
 | |
|                         result.Add(entry.Key, CreateFileEntry(entry.Key));
 | |
|                     else if (entry.Value.StartsWith(SubstPrefix, StringComparison.Ordinal))
 | |
|                         result.Add(entry.Key, CreateSubstEntry(entry.Key));
 | |
|                     else if (entry.Value.StartsWith(CachePrefix, StringComparison.Ordinal))
 | |
|                         result.Add(entry.Key, CreateCacheEntry(entry.Key));
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             return result;
 | |
|         }
 | |
|         finally
 | |
|         {
 | |
|             _getCachesByPathsSemaphore.Release();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public void RemoveHashedFile(string hash, string prefixedFilePath)
 | |
|     {
 | |
|         if (_fileCaches.TryGetValue(hash, out var caches))
 | |
|         {
 | |
|             var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal));
 | |
|             _logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath);
 | |
| 
 | |
|             if (caches?.Count == 0)
 | |
|             {
 | |
|                 _fileCaches.Remove(hash, out var entity);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
 | |
|     {
 | |
|         _logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath);
 | |
|         var oldHash = fileCache.Hash;
 | |
|         var prefixedPath = fileCache.PrefixedFilePath;
 | |
|         if (computeProperties)
 | |
|         {
 | |
|             var fi = new FileInfo(fileCache.ResolvedFilepath);
 | |
|             fileCache.Size = fi.Length;
 | |
|             fileCache.CompressedSize = null;
 | |
|             fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
 | |
|             fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
 | |
|         }
 | |
|         RemoveHashedFile(oldHash, prefixedPath);
 | |
|         AddHashedFile(fileCache);
 | |
|     }
 | |
| 
 | |
|     public (FileState State, FileCacheEntity FileCache) ValidateFileCacheEntity(FileCacheEntity fileCache)
 | |
|     {
 | |
|         fileCache = ReplacePathPrefixes(fileCache);
 | |
|         FileInfo fi = new(fileCache.ResolvedFilepath);
 | |
|         if (!fi.Exists)
 | |
|         {
 | |
|             return (FileState.RequireDeletion, fileCache);
 | |
|         }
 | |
|         if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
 | |
|         {
 | |
|             return (FileState.RequireUpdate, fileCache);
 | |
|         }
 | |
| 
 | |
|         return (FileState.Valid, fileCache);
 | |
|     }
 | |
| 
 | |
|     public void WriteOutFullCsv()
 | |
|     {
 | |
|         lock (_fileWriteLock)
 | |
|         {
 | |
|             StringBuilder sb = new();
 | |
|             foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
 | |
|             {
 | |
|                 sb.AppendLine(entry.CsvEntry);
 | |
|             }
 | |
| 
 | |
|             if (File.Exists(_csvPath))
 | |
|             {
 | |
|                 File.Copy(_csvPath, CsvBakPath, overwrite: true);
 | |
|             }
 | |
| 
 | |
|             try
 | |
|             {
 | |
|                 File.WriteAllText(_csvPath, sb.ToString());
 | |
|                 File.Delete(CsvBakPath);
 | |
|             }
 | |
|             catch
 | |
|             {
 | |
|                 File.WriteAllText(CsvBakPath, sb.ToString());
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
 | |
|     {
 | |
|         try
 | |
|         {
 | |
|             RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
 | |
|             var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
 | |
|             File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
 | |
|             var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
 | |
|             newHashedEntity.SetResolvedFilePath(extensionPath);
 | |
|             AddHashedFile(newHashedEntity);
 | |
|             _logger.LogTrace("Migrated from {oldPath} to {newPath}", fileCache.ResolvedFilepath, newHashedEntity.ResolvedFilepath);
 | |
|             return newHashedEntity;
 | |
|         }
 | |
|         catch (Exception ex)
 | |
|         {
 | |
|             AddHashedFile(fileCache);
 | |
|             _logger.LogWarning(ex, "Failed to migrate entity {entity}", fileCache.PrefixedFilePath);
 | |
|             return fileCache;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private void AddHashedFile(FileCacheEntity fileCache)
 | |
|     {
 | |
|         if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null)
 | |
|         {
 | |
|             _fileCaches[fileCache.Hash] = entries = [];
 | |
|         }
 | |
| 
 | |
|         if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
 | |
|         {
 | |
|             //_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
 | |
|             entries.Add(fileCache);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
 | |
|     {
 | |
|         hash ??= Crypto.GetFileHash(fileInfo.FullName);
 | |
|         var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length);
 | |
|         entity = ReplacePathPrefixes(entity);
 | |
|         AddHashedFile(entity);
 | |
|         lock (_fileWriteLock)
 | |
|         {
 | |
|             File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
 | |
|         }
 | |
|         var result = GetFileCacheByPath(fileInfo.FullName);
 | |
|         _logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
 | |
|         return result;
 | |
|     }
 | |
| 
 | |
|     private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
 | |
|     {
 | |
|         var resultingFileCache = ReplacePathPrefixes(fileCache);
 | |
|         //_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
 | |
|         resultingFileCache = Validate(resultingFileCache);
 | |
|         return resultingFileCache;
 | |
|     }
 | |
| 
 | |
|     private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache)
 | |
|     {
 | |
|         if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
 | |
|         {
 | |
|             fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(PenumbraPrefix, _ipcManager.Penumbra.ModDirectory, StringComparison.Ordinal));
 | |
|         }
 | |
|         else if (fileCache.PrefixedFilePath.StartsWith(SubstPrefix, StringComparison.OrdinalIgnoreCase))
 | |
|         {
 | |
|             fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(SubstPrefix, SubstFolder, StringComparison.Ordinal));
 | |
|         }
 | |
|         else if (fileCache.PrefixedFilePath.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase))
 | |
|         {
 | |
|             fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(CachePrefix, _configService.Current.CacheFolder, StringComparison.Ordinal));
 | |
|         }
 | |
| 
 | |
|         return fileCache;
 | |
|     }
 | |
| 
 | |
|     private FileCacheEntity? Validate(FileCacheEntity fileCache)
 | |
|     {
 | |
|         var file = new FileInfo(fileCache.ResolvedFilepath);
 | |
|         if (!file.Exists)
 | |
|         {
 | |
|             RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
 | |
|             return null;
 | |
|         }
 | |
| 
 | |
|         if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
 | |
|         {
 | |
|             UpdateHashedFile(fileCache);
 | |
|         }
 | |
| 
 | |
|         return fileCache;
 | |
|     }
 | |
| 
 | |
|     public Task StartAsync(CancellationToken cancellationToken)
 | |
|     {
 | |
|         _logger.LogInformation("Starting FileCacheManager");
 | |
|         lock (_fileWriteLock)
 | |
|         {
 | |
|             try
 | |
|             {
 | |
|                 _logger.LogInformation("Checking for {bakPath}", CsvBakPath);
 | |
| 
 | |
|                 if (File.Exists(CsvBakPath))
 | |
|                 {
 | |
|                     _logger.LogInformation("{bakPath} found, moving to {csvPath}", CsvBakPath, _csvPath);
 | |
| 
 | |
|                     File.Move(CsvBakPath, _csvPath, overwrite: true);
 | |
|                 }
 | |
|             }
 | |
|             catch (Exception ex)
 | |
|             {
 | |
|                 _logger.LogWarning(ex, "Failed to move BAK to ORG, deleting BAK");
 | |
|                 try
 | |
|                 {
 | |
|                     if (File.Exists(CsvBakPath))
 | |
|                         File.Delete(CsvBakPath);
 | |
|                 }
 | |
|                 catch (Exception ex1)
 | |
|                 {
 | |
|                     _logger.LogWarning(ex1, "Could not delete bak file");
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (File.Exists(_csvPath))
 | |
|         {
 | |
|             if (!_ipcManager.Penumbra.APIAvailable || string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory))
 | |
|             {
 | |
|                 _mareMediator.Publish(new NotificationMessage("Penumbra not connected",
 | |
|                     "Could not load local file cache data. Penumbra is not connected or not properly set up. Please enable and/or configure Penumbra properly to use Loporrit. After, reload Loporrit in the Plugin installer.",
 | |
|                     MareConfiguration.Models.NotificationType.Error));
 | |
|             }
 | |
| 
 | |
|             _logger.LogInformation("{csvPath} found, parsing", _csvPath);
 | |
| 
 | |
|             bool success = false;
 | |
|             string[] entries = [];
 | |
|             int attempts = 0;
 | |
|             while (!success && attempts < 10)
 | |
|             {
 | |
|                 try
 | |
|                 {
 | |
|                     _logger.LogInformation("Attempting to read {csvPath}", _csvPath);
 | |
|                     entries = File.ReadAllLines(_csvPath);
 | |
|                     success = true;
 | |
|                 }
 | |
|                 catch (Exception ex)
 | |
|                 {
 | |
|                     attempts++;
 | |
|                     _logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
 | |
|                     Thread.Sleep(100);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (!entries.Any())
 | |
|             {
 | |
|                 _logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
 | |
|             }
 | |
| 
 | |
|             _logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
 | |
| 
 | |
|             Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
 | |
|             foreach (var entry in entries)
 | |
|             {
 | |
|                 var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
 | |
|                 try
 | |
|                 {
 | |
|                     var hash = splittedEntry[0];
 | |
|                     if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
 | |
|                     var path = splittedEntry[1];
 | |
|                     var time = splittedEntry[2];
 | |
| 
 | |
|                     if (processedFiles.ContainsKey(path))
 | |
|                     {
 | |
|                         _logger.LogWarning("Already processed {file}, ignoring", path);
 | |
|                         continue;
 | |
|                     }
 | |
| 
 | |
|                     processedFiles.Add(path, value: true);
 | |
| 
 | |
|                     long size = -1;
 | |
|                     long compressed = -1;
 | |
|                     if (splittedEntry.Length > 3)
 | |
|                     {
 | |
|                         if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
 | |
|                         {
 | |
|                             size = result;
 | |
|                         }
 | |
|                         if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
 | |
|                         {
 | |
|                             compressed = resultCompressed;
 | |
|                         }
 | |
|                     }
 | |
|                     AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
 | |
|                 }
 | |
|                 catch (Exception ex)
 | |
|                 {
 | |
|                     _logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (processedFiles.Count != entries.Length)
 | |
|             {
 | |
|                 WriteOutFullCsv();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         _logger.LogInformation("Started FileCacheManager");
 | |
| 
 | |
|         return Task.CompletedTask;
 | |
|     }
 | |
| 
 | |
|     public Task StopAsync(CancellationToken cancellationToken)
 | |
|     {
 | |
|         WriteOutFullCsv();
 | |
|         return Task.CompletedTask;
 | |
|     }
 | |
| } | 
