Skip to content

Commit f56763b

Browse files
committed
Create C# version of CompactGUI.Core
1 parent ede9dbd commit f56763b

31 files changed

+1035
-13
lines changed

CompactGUI.Core/Analyser.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System.Collections.Concurrent;
2+
using System.Diagnostics;
3+
4+
namespace CompactGUI.Core;
5+
6+
public class Analyser
7+
{
8+
9+
public string FolderName { get; set; }
10+
public long UncompressedBytes { get; set; }
11+
public long CompressedBytes { get; set; }
12+
public bool ContainsCompressedFiles { get; set; }
13+
public List<AnalysedFileDetails> FileCompressionDetailsList { get; set; } = new List<AnalysedFileDetails>();
14+
15+
16+
public Analyser(string folder)
17+
{
18+
FolderName = folder;
19+
UncompressedBytes = 0;
20+
CompressedBytes = 0;
21+
ContainsCompressedFiles = false;
22+
}
23+
24+
25+
public async Task<Boolean?> AnalyseFolder(CancellationToken cancellationToken)
26+
{
27+
try
28+
{
29+
var allFiles = await Task.Run(() => Directory.EnumerateFiles(FolderName, "*", new EnumerationOptions { RecurseSubdirectories = true, IgnoreInaccessible = true }).AsShortPathNames(), cancellationToken).ConfigureAwait(false);
30+
var fileDetails = allFiles
31+
.AsParallel()
32+
.WithCancellation(cancellationToken)
33+
.Select(AnalyseFile)
34+
.Where(details => details != null)
35+
.Cast<AnalysedFileDetails>()
36+
.ToList<AnalysedFileDetails>();
37+
38+
CompressedBytes = fileDetails.Sum(f => f.CompressedSize);
39+
UncompressedBytes = fileDetails.Sum(f => f.UncompressedSize);
40+
ContainsCompressedFiles = fileDetails.Any(f => f.CompressionMode != WOFCompressionAlgorithm.NO_COMPRESSION);
41+
42+
FileCompressionDetailsList = fileDetails;
43+
44+
return ContainsCompressedFiles;
45+
}
46+
catch (Exception ex)
47+
{
48+
Debug.WriteLine(ex.Message);
49+
return null;
50+
}
51+
52+
53+
54+
}
55+
56+
57+
private AnalysedFileDetails? AnalyseFile(string file)
58+
{
59+
try
60+
{
61+
FileInfo fileInfo = new FileInfo(file);
62+
long uncompressedSize = fileInfo.Length;
63+
long compressedSize = SharedMethods.GetFileSizeOnDisk(file);
64+
compressedSize = compressedSize < 0 ? 0 : compressedSize;
65+
WOFCompressionAlgorithm compressionMode = (compressedSize == uncompressedSize)
66+
? WOFCompressionAlgorithm.NO_COMPRESSION
67+
: WOFHelper.DetectCompression(fileInfo);
68+
69+
return new AnalysedFileDetails { FileName = file, CompressedSize = compressedSize, UncompressedSize = uncompressedSize, CompressionMode = compressionMode, FileInfo = fileInfo };
70+
}
71+
catch (IOException)
72+
{
73+
return null;
74+
}
75+
}
76+
77+
78+
public async Task<List<ExtensionResult>> GetPoorlyCompressedExtensions()
79+
{
80+
81+
var extRes = new ConcurrentDictionary<string, ExtensionResult>();
82+
83+
await Task.Run(() =>
84+
{
85+
Parallel.ForEach(
86+
FileCompressionDetailsList, fl =>
87+
{
88+
if (fl.UncompressedSize == 0) return;
89+
90+
string ext = Path.GetExtension(fl.FileName); //should probably use ToLowerInvariant() here
91+
92+
extRes.AddOrUpdate(
93+
ext,
94+
key => new ExtensionResult
95+
{
96+
Extension = ext,
97+
TotalFiles = 1,
98+
CompressedBytes = fl.CompressedSize,
99+
UncompressedBytes = fl.UncompressedSize
100+
},
101+
(key, existing) =>
102+
{
103+
existing.TotalFiles++;
104+
existing.CompressedBytes += fl.CompressedSize;
105+
existing.UncompressedBytes += fl.UncompressedSize;
106+
return existing;
107+
});
108+
});
109+
});
110+
111+
return extRes.Values.Where(r => r.CRatio > 0.95).ToList();
112+
113+
114+
}
115+
116+
117+
118+
119+
120+
}
121+
122+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0-windows</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" />
11+
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
12+
<PrivateAssets>all</PrivateAssets>
13+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
14+
</PackageReference>
15+
</ItemGroup>
16+
17+
</Project>

CompactGUI.Core/Compactor.cs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+

2+
using Microsoft.Win32.SafeHandles;
3+
using System.Collections.Concurrent;
4+
using System.Diagnostics;
5+
using System.IO.Enumeration;
6+
using System.Runtime.InteropServices;
7+
using Windows.Win32;
8+
9+
namespace CompactGUI.Core;
10+
11+
public class Compactor : ICompressor, IDisposable
12+
{
13+
14+
private readonly string workingDirectory;
15+
private readonly string[] excludedFileExtensions;
16+
private readonly WOFCompressionAlgorithm wofCompressionAlgorithm;
17+
18+
19+
private IntPtr compressionInfoPtr;
20+
private UInt32 compressionInfoSize;
21+
22+
private long totalProcessedBytes = 0;
23+
private readonly SemaphoreSlim pauseSemaphore = new SemaphoreSlim(1, 2);
24+
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
25+
26+
27+
public Compactor(string folderPath, WOFCompressionAlgorithm compressionLevel, string[] excludedFileTypes)
28+
{
29+
workingDirectory = folderPath;
30+
excludedFileExtensions = excludedFileTypes;
31+
wofCompressionAlgorithm = compressionLevel;
32+
33+
InitializeCompressionInfoPointer();
34+
}
35+
36+
37+
private void InitializeCompressionInfoPointer()
38+
{
39+
var _EFInfo = new WOFHelper.WOF_FILE_COMPRESSION_INFO_V1 { Algorithm = (UInt32)wofCompressionAlgorithm, Flags = 0 };
40+
compressionInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_EFInfo));
41+
compressionInfoSize = (UInt32)Marshal.SizeOf(_EFInfo);
42+
Marshal.StructureToPtr(_EFInfo, compressionInfoPtr, true);
43+
44+
}
45+
46+
public async Task<bool> RunAsync(List<string> filesList, IProgress<CompressionProgress> progressMonitor = null, int maxParallelism = 1)
47+
{
48+
if(cancellationTokenSource.IsCancellationRequested) { return false; }
49+
50+
var workingFiles = await BuildWorkingFilesList().ConfigureAwait(false);
51+
long totalFilesSize = workingFiles.Sum((f) => f.UncompressedSize);
52+
53+
totalProcessedBytes = 0;
54+
55+
if(maxParallelism <= 0) maxParallelism = Environment.ProcessorCount;
56+
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxParallelism };
57+
58+
try
59+
{
60+
await Parallel.ForEachAsync(workingFiles, parallelOptions,
61+
(file, ctx) =>
62+
{
63+
ctx.ThrowIfCancellationRequested();
64+
return new ValueTask(PauseAndProcessFile(file, totalFilesSize, cancellationTokenSource.Token, progressMonitor));
65+
}).ConfigureAwait(false);
66+
}
67+
catch (Exception)
68+
{
69+
70+
return false;
71+
}
72+
return true;
73+
}
74+
75+
private async Task PauseAndProcessFile(FileDetails file, long totalFilesSize, CancellationToken token, IProgress<CompressionProgress> progressMonitor)
76+
{
77+
try
78+
{
79+
await pauseSemaphore.WaitAsync(token).ConfigureAwait(false);
80+
pauseSemaphore.Release();
81+
}
82+
catch (Exception)
83+
{
84+
throw;
85+
}
86+
87+
token.ThrowIfCancellationRequested();
88+
89+
var res = WOFCompressFile(file.FileName);
90+
Interlocked.Add(ref totalProcessedBytes, file.UncompressedSize);
91+
progressMonitor?.Report(new CompressionProgress((int)((double)totalProcessedBytes / totalFilesSize * 100.0), file.FileName));
92+
93+
}
94+
95+
private unsafe int? WOFCompressFile(string filePath)
96+
{
97+
try
98+
{
99+
using (SafeFileHandle fs = File.OpenHandle(filePath))
100+
{
101+
return PInvoke.WofSetFileDataLocation(fs, (uint)WOFHelper.WOF_PROVIDER_FILE, compressionInfoPtr.ToPointer(), compressionInfoSize);
102+
}
103+
}
104+
catch (Exception ex)
105+
{
106+
Debug.WriteLine(ex.Message);
107+
return null;
108+
}
109+
}
110+
111+
private async Task<IEnumerable<FileDetails>> BuildWorkingFilesList()
112+
{
113+
uint clusterSize = SharedMethods.GetClusterSize(workingDirectory);
114+
115+
var filesList = new ConcurrentBag<FileDetails>();
116+
117+
var analyser = new Analyser(workingDirectory);
118+
var ret = await analyser.AnalyseFolder(cancellationTokenSource.Token);
119+
120+
Parallel.ForEach(analyser.FileCompressionDetailsList, (fl) =>
121+
{
122+
var ft = fl.FileInfo;
123+
if ((!excludedFileExtensions.Contains(ft?.Extension) || excludedFileExtensions.Contains(fl.FileName))
124+
&& ft.Length > clusterSize
125+
&& fl.CompressionMode != wofCompressionAlgorithm)
126+
{
127+
filesList.Add(new FileDetails { FileName = fl.FileName, UncompressedSize = fl.UncompressedSize });
128+
}
129+
});
130+
131+
return filesList.ToList();
132+
}
133+
134+
135+
public void Pause()
136+
{
137+
pauseSemaphore.Wait(cancellationTokenSource.Token);
138+
}
139+
140+
141+
public void Resume()
142+
{
143+
if (pauseSemaphore.CurrentCount == 0) pauseSemaphore.Release();
144+
}
145+
146+
147+
public void Cancel()
148+
{
149+
Resume();
150+
cancellationTokenSource.Cancel();
151+
}
152+
153+
154+
public void Dispose()
155+
{
156+
cancellationTokenSource?.Dispose();
157+
pauseSemaphore?.Dispose();
158+
if (compressionInfoPtr != IntPtr.Zero)
159+
{
160+
Marshal.FreeHGlobal(compressionInfoPtr);
161+
compressionInfoPtr = IntPtr.Zero;
162+
}
163+
}
164+
165+
166+
private readonly record struct FileDetails(string FileName, long UncompressedSize);
167+
168+
169+
}

0 commit comments

Comments
 (0)