Files
DCIT/DCIT.Core/Services/FileScanService.cs

342 lines
14 KiB
C#
Raw Normal View History

2026-07-13 10:04:36 +08:00
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<FileScanItem> ScanReplaceCandidates(string sourceDirectory, string outputDirectory)
{
return ScanReplaceCandidates(sourceDirectory, outputDirectory, null);
}
public IList<FileScanItem> ScanReplaceCandidates(string sourceDirectory, string outputDirectory, RuleFile ruleFile)
{
var items = new List<FileScanItem>();
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<FileScanItem> 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<string>();
var diffLookup = selectedDiffs.ToDictionary(
path => BuildRestoreKey(replaceDirectory, path),
StringComparer.OrdinalIgnoreCase);
var wordFiles = EnumerateWordFiles(replaceDirectory);
var items = new List<FileScanItem>();
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<string> ValidateReplaceDirectories(string sourceDirectory, string outputDirectory)
{
return PathUtility.ValidateDirectoryPair("原始文件夹", sourceDirectory, "替换文件夹", outputDirectory).ToList();
}
public IList<string> ValidateRestoreDirectories(string replaceDirectory, string restoreDirectory)
{
return PathUtility.ValidateDirectoryPair("替换文件夹", replaceDirectory, "恢复文件夹", restoreDirectory).ToList();
}
private static IEnumerable<string> EnumerateWordFiles(string rootDirectory)
{
if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory))
{
return Enumerable.Empty<string>();
}
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<RuleDefinition> GetEnabledRules(RuleFile ruleFile, RuleTarget target)
{
if (ruleFile == null || ruleFile.Rules == null)
{
return new List<RuleDefinition>();
}
return ruleFile.Rules
.Where(rule => rule != null && rule.IsEnabled && !string.IsNullOrEmpty(rule.SearchText) && rule.Target == target)
.ToList();
}
public static string ApplyRulesToFileName(string fileNameWithoutExtension, IList<RuleDefinition> 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);
}
}
}