Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CompactGUI.Core/Compactor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ namespace CompactGUI.Core;
public sealed class Compactor : ICompressor, IDisposable
{

private const int HResultCompressionNotBeneficial = unchecked((int)0x80070158);

private readonly string workingDirectory;
private readonly HashSet<string> excludedFileExtensions;
private readonly HashSet<string> resumeExcludedFiles;
private readonly WOFCompressionAlgorithm wofCompressionAlgorithm;
private readonly CompressionProgressBaseline progressBaseline;
private readonly CompressionNotBeneficialCache notBeneficialCache = CompressionNotBeneficialCache.Shared;


private IntPtr compressionInfoPtr;
Expand Down Expand Up @@ -273,6 +276,11 @@ private unsafe FileOperationResult WOFCompressFile(string filePath)

if (result == 0) return new FileOperationResult(true);

if (result == HResultCompressionNotBeneficial)
{
notBeneficialCache.Record(filePath, wofCompressionAlgorithm);
}

string failureReason = Marshal.GetExceptionForHR(result)?.Message
?? $"Windows returned HRESULT 0x{result:X8}.";
CompactorLog.FileCompressionFailed(_logger, filePath, failureReason);
Expand Down Expand Up @@ -305,6 +313,7 @@ public async Task<IEnumerable<FileDetails>> BuildWorkingFilesList(IReadOnlyColle
.Where(fl =>
fl.CompressionMode != wofCompressionAlgorithm
&& !resumeExcludedFiles.Contains(fl.FileName)
&& !notBeneficialCache.ShouldSkip(fl.FileName, wofCompressionAlgorithm)
&& fl.UncompressedSize > clusterSize
&& ((fl.FileInfo != null && !excludedFileExtensions.Contains(fl.FileInfo.Extension)) || excludedFileExtensions.Contains(fl.FileName))
)
Expand Down
186 changes: 186 additions & 0 deletions CompactGUI.Core/CompressionNotBeneficialCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
using System.Diagnostics;
using System.Text.Json;

namespace CompactGUI.Core;

/// <summary>
/// Remembers unchanged files for which Windows reported that WOF compression
/// would not save disk space. Entries are algorithm-specific and become stale
/// automatically when the file size or last-write timestamp changes.
/// </summary>
public sealed class CompressionNotBeneficialCache
{
private const string CacheFileName = "compression-not-beneficial.json";

private readonly object syncRoot = new();
private readonly FileInfo cacheFile;
private readonly JsonSerializerOptions jsonOptions = new() { WriteIndented = true };
private readonly Dictionary<string, CompressionNotBeneficialCacheEntry> entries;

public static CompressionNotBeneficialCache Shared { get; } = new();

private CompressionNotBeneficialCache()
{
DirectoryInfo dataFolder = new(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"IridiumIO",
"CompactGUI"));

cacheFile = new FileInfo(Path.Combine(dataFolder.FullName, CacheFileName));
entries = LoadEntries();
}

public bool ShouldSkip(string filePath, WOFCompressionAlgorithm algorithm)
{
string normalizedPath = NormalizePath(filePath);
string key = BuildKey(normalizedPath, algorithm);

lock (syncRoot)
{
if (!entries.TryGetValue(key, out CompressionNotBeneficialCacheEntry? entry))
return false;

try
{
FileInfo file = new(normalizedPath);
if (file.Exists
&& file.Length == entry.Length
&& file.LastWriteTimeUtc.Ticks == entry.LastWriteTimeUtcTicks)
{
return true;
}
}
catch (Exception ex)
{
Trace.WriteLine($"Unable to validate compression cache entry for '{normalizedPath}': {ex.Message}");
}

// The file disappeared or changed. Forget the old result so a future
// file at this path is allowed to try compression again.
if (entries.Remove(key))
WriteEntries();

return false;
}
}

public void Record(string filePath, WOFCompressionAlgorithm algorithm)
{
string normalizedPath = NormalizePath(filePath);

try
{
FileInfo file = new(normalizedPath);
if (!file.Exists) return;

CompressionNotBeneficialCacheEntry entry = new()
{
FilePath = normalizedPath,
Algorithm = algorithm,
Length = file.Length,
LastWriteTimeUtcTicks = file.LastWriteTimeUtc.Ticks
};

lock (syncRoot)
{
string key = BuildKey(normalizedPath, algorithm);
if (entries.TryGetValue(key, out CompressionNotBeneficialCacheEntry? existing)
&& existing.Length == entry.Length
&& existing.LastWriteTimeUtcTicks == entry.LastWriteTimeUtcTicks)
{
return;
}

entries[key] = entry;
WriteEntries();
}
}
catch (Exception ex)
{
Trace.WriteLine($"Unable to record non-beneficial compression result for '{normalizedPath}': {ex.Message}");
}
}

private Dictionary<string, CompressionNotBeneficialCacheEntry> LoadEntries()
{
Dictionary<string, CompressionNotBeneficialCacheEntry> loaded = new(StringComparer.OrdinalIgnoreCase);

try
{
if (!cacheFile.Directory!.Exists)
cacheFile.Directory.Create();

if (!cacheFile.Exists || cacheFile.Length == 0)
return loaded;

string json = File.ReadAllText(cacheFile.FullName);
List<CompressionNotBeneficialCacheEntry>? savedEntries =
JsonSerializer.Deserialize<List<CompressionNotBeneficialCacheEntry>>(json, jsonOptions);

if (savedEntries is null) return loaded;

foreach (CompressionNotBeneficialCacheEntry entry in savedEntries)
{
if (string.IsNullOrWhiteSpace(entry.FilePath)) continue;

string normalizedPath = NormalizePath(entry.FilePath);
entry.FilePath = normalizedPath;
loaded[BuildKey(normalizedPath, entry.Algorithm)] = entry;
}
}
catch (Exception ex)
{
Trace.WriteLine($"Unable to load compression-not-beneficial cache: {ex.Message}");
}

return loaded;
}

private void WriteEntries()
{
try
{
if (!cacheFile.Directory!.Exists)
cacheFile.Directory.Create();

string json = JsonSerializer.Serialize(
entries.Values
.OrderBy(entry => entry.FilePath, StringComparer.OrdinalIgnoreCase)
.ThenBy(entry => entry.Algorithm)
.ToList(),
jsonOptions);

string tempPath = cacheFile.FullName + ".tmp";
File.WriteAllText(tempPath, json);
File.Move(tempPath, cacheFile.FullName, true);
cacheFile.Refresh();
}
catch (Exception ex)
{
Trace.WriteLine($"Unable to save compression-not-beneficial cache: {ex.Message}");
}
}

private static string BuildKey(string normalizedPath, WOFCompressionAlgorithm algorithm)
=> $"{normalizedPath}|{(int)algorithm}";

private static string NormalizePath(string filePath)
{
try
{
return Path.GetFullPath(filePath);
}
catch
{
return filePath;
}
}
}

public sealed class CompressionNotBeneficialCacheEntry
{
public string FilePath { get; set; } = string.Empty;
public WOFCompressionAlgorithm Algorithm { get; set; }
public long Length { get; set; }
public long LastWriteTimeUtcTicks { get; set; }
}
Loading