|
using System.Text.RegularExpressions; |
|
|
|
using Microsoft.Extensions.FileSystemGlobbing; |
|
using Microsoft.Extensions.FileSystemGlobbing.Abstractions; |
|
|
|
namespace FubarDev.ArtifactResolver; |
|
|
|
public class ArtifactResolver |
|
{ |
|
private static readonly Regex _moveRegex = new Regex(@"^\s*(?<mode>[+-]:)?\s*(?<source>(?![+-]:).+?)(\s*=>\s*(?<target>.+?))?\s*$", RegexOptions.Compiled); |
|
private readonly string[] _excludePatterns; |
|
private readonly List<ArtifactInclude> _includes; |
|
|
|
public ArtifactResolver(IEnumerable<ArtifactConfigurationItem> configuration) |
|
{ |
|
var includes = new List<ArtifactInclude>(); |
|
var excludes = new List<ArtifactExclude>(); |
|
foreach (var item in configuration) |
|
{ |
|
switch (item) |
|
{ |
|
case ArtifactInclude include: |
|
includes.Add(include); |
|
break; |
|
case ArtifactExclude exclude: |
|
excludes.Add(exclude); |
|
break; |
|
} |
|
} |
|
|
|
_includes = includes; |
|
_excludePatterns = excludes.Select(x => x.Pattern).ToArray(); |
|
} |
|
|
|
public static IEnumerable<ArtifactConfigurationItem> LoadConfiguration(IEnumerable<string> lines) |
|
{ |
|
foreach (var line in lines) |
|
{ |
|
var match = _moveRegex.Match(line); |
|
if (!match.Success) |
|
{ |
|
throw new InvalidOperationException($"Ungültige Struktur: {line}"); |
|
} |
|
|
|
var mode = match.Groups["mode"].Value; |
|
var source = match.Groups["source"].Value.Trim(); |
|
var target = match.Groups["target"].Value.Trim(); |
|
if (source.EndsWith("=>")) |
|
{ |
|
throw new InvalidOperationException($"Ungültige Struktur: {line}"); |
|
} |
|
|
|
if (mode == "-:") |
|
{ |
|
if (!string.IsNullOrEmpty(target)) |
|
{ |
|
throw new InvalidOperationException($"Ungültige Struktur: {line}"); |
|
} |
|
|
|
yield return new ArtifactExclude(source); |
|
} |
|
else |
|
{ |
|
var targetDir = string.IsNullOrEmpty(target) ? null : target; |
|
if (targetDir != null) |
|
{ |
|
targetDir = targetDir.Replace('\\', '/'); |
|
if (targetDir.StartsWith("./")) |
|
{ |
|
targetDir = targetDir[1..]; |
|
} |
|
|
|
if (!targetDir.EndsWith("/")) |
|
{ |
|
targetDir += "/"; |
|
} |
|
|
|
if (!targetDir.StartsWith("/")) |
|
{ |
|
targetDir = "/" + targetDir; |
|
} |
|
} |
|
|
|
yield return new ArtifactInclude(source) |
|
{ |
|
TargetDirectory = targetDir, |
|
}; |
|
} |
|
} |
|
} |
|
|
|
public DirectoryInfoBase Transform(DirectoryInfoBase sourceDirectory) |
|
{ |
|
var result = new InMemoryDir(string.Empty, string.Empty); |
|
foreach (var (matcher, targetDir) in CreateMatchers(sourceDirectory)) |
|
{ |
|
var matchResult = matcher.Execute(sourceDirectory); |
|
var transfer = new List<(string Source, string Target)>(); |
|
if (string.IsNullOrEmpty(targetDir)) |
|
{ |
|
foreach (var match in matchResult.Files) |
|
{ |
|
transfer.Add((match.Path, match.Path)); |
|
} |
|
} |
|
else |
|
{ |
|
foreach (var match in matchResult.Files) |
|
{ |
|
var relativePath = match.Stem ?? Path.GetFileName(match.Path); |
|
var targetPath = targetDir + relativePath; |
|
transfer.Add((match.Path, targetPath)); |
|
} |
|
} |
|
|
|
foreach (var (source, target) in transfer) |
|
{ |
|
var sourceInfo = FindFile(sourceDirectory, source); |
|
result.AddFile(target, sourceInfo.FullName); |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
|
|
public static void CopyTo(DirectoryInfoBase root, string target) |
|
{ |
|
Directory.CreateDirectory(target); |
|
foreach (var item in root.EnumerateFileSystemInfos()) |
|
{ |
|
if (item is DirectoryInfoBase dirItem) |
|
{ |
|
CopyTo(dirItem, $"{target}/{dirItem.Name}"); |
|
} |
|
else if (item is FileInfoBase fileItem) |
|
{ |
|
System.IO.File.Copy(fileItem.FullName, $"{target}/{fileItem.Name}", true); |
|
} |
|
else |
|
{ |
|
throw new NotSupportedException(); |
|
} |
|
} |
|
} |
|
|
|
private static FileInfoBase FindFile(DirectoryInfoBase root, string path) |
|
{ |
|
return (FileInfoBase?) Find(root, path) ?? throw new InvalidOperationException(); |
|
} |
|
|
|
private static FileSystemInfoBase? Find(DirectoryInfoBase root, string path) |
|
{ |
|
var parts = path.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); |
|
var dir = root; |
|
foreach (var part in parts.Take(parts.Length - 1)) |
|
{ |
|
dir = dir.GetDirectory(part) ?? throw new InvalidOperationException(); |
|
} |
|
|
|
var lastPart = parts[^1]; |
|
return dir.EnumerateFileSystemInfos() |
|
.FirstOrDefault(x => string.Equals(x.Name, lastPart, StringComparison.OrdinalIgnoreCase)); |
|
} |
|
|
|
private IEnumerable<(Matcher Matcher, string? TargetDir)> CreateMatchers(DirectoryInfoBase sourceDirectory) |
|
{ |
|
var includesByTargetDirs = _includes |
|
.GroupBy(x => x.TargetDirectory ?? string.Empty); |
|
foreach (var group in includesByTargetDirs) |
|
{ |
|
var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); |
|
foreach (var include in group) |
|
{ |
|
var hasGlob = include.Pattern.Contains('*') || include.Pattern.Contains('?'); |
|
if (!hasGlob && Find(sourceDirectory, include.Pattern) is DirectoryInfoBase) |
|
{ |
|
matcher.AddInclude(include.Pattern + "/**"); |
|
} |
|
else |
|
{ |
|
matcher.AddInclude(include.Pattern); |
|
} |
|
} |
|
|
|
matcher.AddExcludePatterns(_excludePatterns); |
|
var targetDir = string.IsNullOrEmpty(group.Key) ? null : group.Key; |
|
yield return (matcher, targetDir); |
|
} |
|
} |
|
|
|
private class InMemoryFile(string name, string fullName, InMemoryDir parentDir) : FileInfoBase |
|
{ |
|
public override string Name => name; |
|
public override string FullName => fullName; |
|
public override DirectoryInfoBase ParentDirectory => parentDir; |
|
} |
|
|
|
private class InMemoryDir(string name, string fullName) : DirectoryInfoBase |
|
{ |
|
private readonly InMemoryDir? _parentDirectory; |
|
private readonly List<FileSystemInfoBase?> _items = []; |
|
|
|
private readonly Dictionary<string, (InMemoryDir Info, int Index)> _subDirectories = |
|
new(StringComparer.OrdinalIgnoreCase); |
|
|
|
private readonly Dictionary<string, (FileInfoBase Info, int Index)> _files = |
|
new(StringComparer.OrdinalIgnoreCase); |
|
|
|
public InMemoryDir(string name, string fullName, InMemoryDir? parentDir) |
|
: this(name, fullName) |
|
{ |
|
_parentDirectory = parentDir; |
|
} |
|
|
|
public override string Name => name; |
|
public override string FullName => fullName; |
|
|
|
public override DirectoryInfoBase? ParentDirectory => _parentDirectory; |
|
|
|
internal void AddItem(FileSystemInfoBase item) |
|
{ |
|
if (item is FileInfoBase file) |
|
{ |
|
if (_files.TryGetValue(file.Name, out var oldItem)) |
|
{ |
|
_items[oldItem.Index] = null; |
|
} |
|
|
|
_files[file.Name] = (file, _items.Count); |
|
_items.Add(file); |
|
} |
|
else if (item is InMemoryDir dir) |
|
{ |
|
if (_subDirectories.TryGetValue(dir.Name, out var oldItem)) |
|
{ |
|
_items[oldItem.Index] = null; |
|
} |
|
|
|
_subDirectories[dir.Name] = (dir, _items.Count); |
|
_items.Add(dir); |
|
} |
|
else |
|
{ |
|
throw new NotSupportedException(); |
|
} |
|
} |
|
|
|
internal void AddFile(string targetPath, string sourcePath) |
|
{ |
|
if (string.IsNullOrEmpty(targetPath)) |
|
{ |
|
throw new InvalidOperationException(); |
|
} |
|
|
|
var dir = this; |
|
var parts = targetPath.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries); |
|
var basePath = string.IsNullOrEmpty(dir.FullName) ? string.Empty : dir.FullName + "/"; |
|
foreach (var part in parts.Take(parts.Length - 1)) |
|
{ |
|
if (dir.GetDirectory(part) is not InMemoryDir subDir) |
|
{ |
|
subDir = new InMemoryDir(part, basePath + part, dir); |
|
dir.AddItem(subDir); |
|
} |
|
|
|
dir = subDir; |
|
basePath += part + "/"; |
|
} |
|
|
|
var fileName = parts[^1]; |
|
dir.AddItem(new InMemoryFile(fileName, sourcePath, dir)); |
|
} |
|
|
|
public override IEnumerable<FileSystemInfoBase> EnumerateFileSystemInfos() |
|
{ |
|
return _items.Where(x => x != null)!; |
|
} |
|
|
|
public override DirectoryInfoBase? GetDirectory(string path) |
|
{ |
|
return _subDirectories.TryGetValue(path, out var item) ? item.Info : null; |
|
} |
|
|
|
public override FileInfoBase? GetFile(string path) |
|
{ |
|
return _files.TryGetValue(path, out var item) ? item.Info : null; |
|
} |
|
} |
|
} |