using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using DCIT.Core.Models; using DCIT.Core.Utilities; namespace DCIT.Core.Services { public class FileScanService { private static readonly TimeSpan RuleMatchTimeout = TimeSpan.FromSeconds(1); private readonly DiffSerializationService _diffSerializationService; public FileScanService() : this(new DiffSerializationService(new BmpDiffCodec())) { } public FileScanService(DiffSerializationService diffSerializationService) { if (diffSerializationService == null) { throw new ArgumentNullException("diffSerializationService"); } _diffSerializationService = diffSerializationService; } public IList ScanReplaceCandidates(string sourceDirectory, string outputDirectory) { return ScanReplaceCandidates(sourceDirectory, outputDirectory, null); } public IList ScanReplaceCandidates(string sourceDirectory, string outputDirectory, RuleFile ruleFile) { var items = new List(); var hasOutputDirectory = !string.IsNullOrWhiteSpace(outputDirectory); var rules = GetEnabledRules(ruleFile, RuleTarget.FileName); var files = EnumerateWordFiles(sourceDirectory); foreach (var file in files) { var relativePath = PathUtility.GetRelativePath(sourceDirectory, file); var relativeDirectory = Path.GetDirectoryName(relativePath) ?? string.Empty; var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); var extension = Path.GetExtension(file); var replacedFileNameWithoutExtension = ApplyRulesToFileName(fileNameWithoutExtension, rules); ValidateReplacementFileName(file, replacedFileNameWithoutExtension); var datedFileName = string.Format("{0}-{1:yyyyMMdd}{2}", replacedFileNameWithoutExtension, DateTime.Today, extension); var outputPath = hasOutputDirectory ? Path.Combine(outputDirectory, relativeDirectory, datedFileName) : string.Empty; var diffPath = hasOutputDirectory ? Path.Combine(outputDirectory, relativeDirectory, string.Format("{0}-{1:yyyyMMdd}.diff", replacedFileNameWithoutExtension, DateTime.Today)) : string.Empty; items.Add(new FileScanItem { IsSelected = true, SourcePath = file, OutputPath = outputPath, DiffPath = diffPath, }); } return items; } public IList ScanRestoreCandidates(string replaceDirectory, string restoreDirectory, string diffInputFormat) { var hasRestoreDirectory = !string.IsNullOrWhiteSpace(restoreDirectory); var normalizedFormat = string.Equals(diffInputFormat, ".bmp", StringComparison.OrdinalIgnoreCase) ? ".bmp" : ".diff"; var selectedDiffs = Directory.Exists(replaceDirectory) ? Directory.EnumerateFiles(replaceDirectory, "*" + normalizedFormat, SearchOption.AllDirectories).ToList() : new List(); var diffLookup = selectedDiffs.ToDictionary( path => BuildRestoreKey(replaceDirectory, path), StringComparer.OrdinalIgnoreCase); var wordFiles = EnumerateWordFiles(replaceDirectory); var items = new List(); foreach (var wordFile in wordFiles) { var key = BuildRestoreKey(replaceDirectory, wordFile); if (!diffLookup.ContainsKey(key)) { continue; } var relativePath = PathUtility.GetRelativePath(replaceDirectory, wordFile); var relativeDirectory = Path.GetDirectoryName(relativePath) ?? string.Empty; var extension = Path.GetExtension(wordFile); var restoreFileName = ResolveRestoreFileName(diffLookup[key], wordFile); var restorePath = hasRestoreDirectory ? Path.Combine(restoreDirectory, relativeDirectory, restoreFileName ?? Path.GetFileNameWithoutExtension(wordFile) + "-rcv" + extension) : string.Empty; items.Add(new FileScanItem { IsSelected = true, SourcePath = wordFile, OutputPath = restorePath, DiffPath = diffLookup[key], }); } return items; } public IList ValidateReplaceDirectories(string sourceDirectory, string outputDirectory) { return PathUtility.ValidateDirectoryPair("原始文件夹", sourceDirectory, "替换文件夹", outputDirectory).ToList(); } public IList ValidateRestoreDirectories(string replaceDirectory, string restoreDirectory) { return PathUtility.ValidateDirectoryPair("替换文件夹", replaceDirectory, "恢复文件夹", restoreDirectory).ToList(); } private static IEnumerable EnumerateWordFiles(string rootDirectory) { if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) { return Enumerable.Empty(); } return Directory.EnumerateFiles(rootDirectory, "*.*", SearchOption.AllDirectories) .Where(path => { var fileName = Path.GetFileName(path); if (fileName != null && fileName.StartsWith("~$", StringComparison.Ordinal)) { return false; } var extension = Path.GetExtension(path); return extension.Equals(".doc", StringComparison.OrdinalIgnoreCase) || extension.Equals(".docx", StringComparison.OrdinalIgnoreCase); }) .OrderBy(path => path, StringComparer.OrdinalIgnoreCase) .ToList(); } private string ResolveRestoreFileName(string diffPath, string wordFile) { try { var document = _diffSerializationService.ReadMetadata(diffPath); var sourceFileName = document == null || document.SourceDocument == null ? null : document.SourceDocument.FileName; if (!string.IsNullOrWhiteSpace(sourceFileName)) { var safeFileName = Path.GetFileName(sourceFileName); if (!string.IsNullOrWhiteSpace(safeFileName) && string.Equals(safeFileName, sourceFileName, StringComparison.Ordinal) && !ContainsInvalidFileNameCharacter(safeFileName)) { return safeFileName; } } } catch { // Legacy or placeholder diff files are still paired by basename and use the old preview naming. } return Path.GetFileNameWithoutExtension(wordFile) + "-rcv" + Path.GetExtension(wordFile); } private static IList GetEnabledRules(RuleFile ruleFile, RuleTarget target) { if (ruleFile == null || ruleFile.Rules == null) { return new List(); } return ruleFile.Rules .Where(rule => rule != null && rule.IsEnabled && !string.IsNullOrEmpty(rule.SearchText) && rule.Target == target) .ToList(); } public static string ApplyRulesToFileName(string fileNameWithoutExtension, IList rules) { var result = fileNameWithoutExtension ?? string.Empty; if (rules == null || rules.Count == 0 || string.IsNullOrEmpty(result)) { return result; } foreach (var rule in rules) { result = ApplyRuleToFileName(result, rule); } return result; } private static string ApplyRuleToFileName(string text, RuleDefinition rule) { if (string.IsNullOrEmpty(text) || rule == null || string.IsNullOrEmpty(rule.SearchText)) { return text; } if (rule.MatchMode == RuleMatchMode.RegularExpression) { var regex = new Regex(rule.SearchText, GetRegexOptions(rule), RuleMatchTimeout); return regex.Replace(text, match => { if (!match.Success || match.Length <= 0) { return match.Value; } return match.Result(rule.ReplacementText ?? string.Empty); }); } var escapedPattern = Regex.Escape(rule.SearchText); var plainRegex = new Regex(escapedPattern, GetRegexOptions(rule), RuleMatchTimeout); return plainRegex.Replace(text, match => rule.ReplacementText ?? string.Empty); } private static string ApplyRuleToText(string text, RuleDefinition rule) { if (string.IsNullOrEmpty(text) || rule == null || string.IsNullOrEmpty(rule.SearchText)) { return text; } if (rule.MatchMode == RuleMatchMode.RegularExpression) { var regex = new Regex(rule.SearchText, GetRegexOptions(rule), RuleMatchTimeout); return regex.Replace(text, match => { if (!match.Success || match.Length <= 0) { return match.Value; } if (rule.IsWholeWord && !IsWholeWord(text, match.Index, match.Length)) { return match.Value; } return match.Result(rule.ReplacementText ?? string.Empty); }); } var escapedPattern = Regex.Escape(rule.SearchText); var plainRegex = new Regex(escapedPattern, GetRegexOptions(rule), RuleMatchTimeout); return plainRegex.Replace(text, match => { if (rule.IsWholeWord && !IsWholeWord(text, match.Index, match.Length)) { return match.Value; } return rule.ReplacementText ?? string.Empty; }); } private static RegexOptions GetRegexOptions(RuleDefinition rule) { var options = RegexOptions.CultureInvariant; if (!rule.IsCaseSensitive) { options |= RegexOptions.IgnoreCase; } return options; } private static bool IsWholeWord(string text, int start, int length) { var leftOk = start <= 0 || GetWordCharClass(text[start - 1]) != GetWordCharClass(text[start]); var rightIndex = start + length; var rightOk = rightIndex >= text.Length || GetWordCharClass(text[rightIndex - 1]) != GetWordCharClass(text[rightIndex]); return leftOk && rightOk; } private static int GetWordCharClass(char character) { if ((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_') { return 1; } if (character >= 0x4E00 && character <= 0x9FFF) { return 2; } return 0; } private static void ValidateReplacementFileName(string sourcePath, string fileNameWithoutExtension) { if (string.IsNullOrWhiteSpace(fileNameWithoutExtension)) { throw new InvalidDataException("文件名规则替换结果为空,无法生成输出文件名。源文件: " + sourcePath); } if (ContainsInvalidFileNameCharacter(fileNameWithoutExtension)) { throw new InvalidDataException("文件名规则替换结果包含非法文件名字符,无法生成输出文件名。源文件: " + sourcePath + ";替换后文件名: " + fileNameWithoutExtension); } } private static bool ContainsInvalidFileNameCharacter(string fileName) { if (fileName == null) { return true; } return fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || fileName.IndexOf(Path.DirectorySeparatorChar) >= 0 || fileName.IndexOf(Path.AltDirectorySeparatorChar) >= 0; } private static string BuildRestoreKey(string rootDirectory, string path) { var relativePath = PathUtility.GetRelativePath(rootDirectory, path); var directory = Path.GetDirectoryName(relativePath) ?? string.Empty; var fileName = Path.GetFileNameWithoutExtension(relativePath); return Path.Combine(directory, fileName); } } }