using System.IO; using System.Reflection; using System.Security.Cryptography; namespace AmiReel.Services; public sealed record ToolPaths(string Ffmpeg, string Ffprobe); public static class ToolExtractor { public static async Task ResolveAsync(string ffmpegPath, string ffprobePath, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(ffmpegPath) || !string.IsNullOrWhiteSpace(ffprobePath)) { if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath)) throw new FileNotFoundException("Configured ffmpeg.exe was not found."); if (string.IsNullOrWhiteSpace(ffprobePath) || !File.Exists(ffprobePath)) throw new FileNotFoundException("Configured ffprobe.exe was not found."); return new ToolPaths(ffmpegPath, ffprobePath); } if (TryResolveFromSystemPath(out ToolPaths? systemTools) && systemTools is not null) return systemTools; return await ExtractAsync(cancellationToken); } public static bool TryResolveFromSystemPath(out ToolPaths? tools) { tools = null; string? ffmpeg = FindOnPath("ffmpeg.exe"); string? ffprobe = FindOnPath("ffprobe.exe"); if (ffmpeg is null || ffprobe is null) return false; tools = new ToolPaths(ffmpeg, ffprobe); return true; } private static async Task ExtractAsync(CancellationToken cancellationToken = default) { string toolDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AmiReel", "tools", "0.1.0"); Directory.CreateDirectory(toolDir); string ffmpeg = await ExtractOneAsync("ffmpeg.exe", toolDir, cancellationToken); string ffprobe = await ExtractOneAsync("ffprobe.exe", toolDir, cancellationToken); return new ToolPaths(ffmpeg, ffprobe); } private static async Task ExtractOneAsync(string fileName, string destination, CancellationToken token) { Assembly assembly = Assembly.GetExecutingAssembly(); string resourceName = $"AmiReel.Tools.{fileName}"; await using Stream source = assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException( $"Embedded {fileName} was not found. Add it to ThirdParty and publish the application again."); string target = Path.Combine(destination, fileName); string temporary = target + ".new"; await using (FileStream output = File.Create(temporary)) await source.CopyToAsync(output, token); if (File.Exists(target) && FilesMatch(target, temporary)) { File.Delete(temporary); return target; } File.Move(temporary, target, true); return target; } private static bool FilesMatch(string first, string second) { using SHA256 sha = SHA256.Create(); using FileStream a = File.OpenRead(first); byte[] aHash = sha.ComputeHash(a); using FileStream b = File.OpenRead(second); byte[] bHash = sha.ComputeHash(b); return aHash.AsSpan().SequenceEqual(bHash); } private static string? FindOnPath(string fileName) { string? path = Environment.GetEnvironmentVariable("PATH"); if (string.IsNullOrWhiteSpace(path)) return null; foreach (string directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { try { string candidate = Path.Combine(directory, fileName); if (File.Exists(candidate)) return candidate; } catch { } } return null; } }