using System.Formats.Tar; using System.IO.Compression; namespace ControlPlane.Api.Services; /// /// Creates a gzipped tar stream from a directory, respecting .dockerignore rules. /// Used to supply the Docker build context to the Docker SDK. /// internal static class TarHelper { private static readonly string[] DefaultIgnore = [ ".git", ".vs", ".vscode", "node_modules", "bin", "obj", "VaultData", "*.user", "*.suo", ]; public static void Pack(string root, Stream destination) { var ignorePatterns = LoadDockerIgnore(root); using var gz = new GZipStream(destination, CompressionLevel.Fastest, leaveOpen: true); using var tar = new TarWriter(gz, TarEntryFormat.Gnu, leaveOpen: false); foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) { var relative = Path.GetRelativePath(root, file).Replace('\\', '/'); if (ShouldIgnore(relative, ignorePatterns)) continue; var entry = new GnuTarEntry(TarEntryType.RegularFile, relative) { DataStream = File.OpenRead(file), }; tar.WriteEntry(entry); } } private static List LoadDockerIgnore(string root) { var path = Path.Combine(root, ".dockerignore"); var patterns = new List(DefaultIgnore); if (!File.Exists(path)) return patterns; foreach (var line in File.ReadAllLines(path)) { var trimmed = line.Trim(); if (!string.IsNullOrEmpty(trimmed) && !trimmed.StartsWith('#')) patterns.Add(trimmed); } return patterns; } private static bool ShouldIgnore(string relativePath, List patterns) { var segments = relativePath.Split('/'); foreach (var pattern in patterns) { var p = pattern.TrimStart('/').TrimEnd('/'); // Glob suffix match (e.g. *.user) if (p.StartsWith('*')) { if (relativePath.EndsWith(p[1..], StringComparison.OrdinalIgnoreCase)) return true; continue; } // Exact full-path match or root-anchored prefix (e.g. .git, .vs) if (relativePath.Equals(p, StringComparison.OrdinalIgnoreCase)) return true; if (relativePath.StartsWith(p + "/", StringComparison.OrdinalIgnoreCase)) return true; // Match any path segment so that nested bin/, obj/, node_modules/ etc. are caught // regardless of which project subdirectory they live in. if (segments.Any(seg => seg.Equals(p, StringComparison.OrdinalIgnoreCase))) return true; } return false; } }