342 lines
14 KiB
C#
342 lines
14 KiB
C#
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|