commit e2bffc8b4968d516cd4cd9ff328ac19aea026b79 Author: Dvurechensky <46356631+Dvurechensky@users.noreply.github.com> Date: Sat Oct 5 10:06:04 2024 +0300 1.0 Main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c4efe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,261 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/FileSearch.sln b/FileSearch.sln new file mode 100644 index 0000000..6a504d1 --- /dev/null +++ b/FileSearch.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileSearch", "FileSearch\FileSearch.csproj", "{C62B3AF4-1119-4EDF-AEA5-3D00296EB8E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C62B3AF4-1119-4EDF-AEA5-3D00296EB8E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C62B3AF4-1119-4EDF-AEA5-3D00296EB8E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C62B3AF4-1119-4EDF-AEA5-3D00296EB8E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C62B3AF4-1119-4EDF-AEA5-3D00296EB8E1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CC28CDB4-BCDF-47E2-8527-C8F13943DECF} + EndGlobalSection +EndGlobal diff --git a/FileSearch/ExceptionsForm.Designer.cs b/FileSearch/ExceptionsForm.Designer.cs new file mode 100644 index 0000000..8c73de2 --- /dev/null +++ b/FileSearch/ExceptionsForm.Designer.cs @@ -0,0 +1,85 @@ +namespace FileSearch +{ + partial class ExceptionsForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + lstItems = new ListView(); + lstcFile = new ColumnHeader(); + lstcException = new ColumnHeader(); + SuspendLayout(); + // + // lstItems + // + lstItems.Columns.AddRange(new ColumnHeader[] { lstcFile, lstcException }); + lstItems.Dock = DockStyle.Fill; + lstItems.FullRowSelect = true; + lstItems.GridLines = true; + lstItems.HeaderStyle = ColumnHeaderStyle.Nonclickable; + lstItems.Location = new Point(0, 0); + lstItems.Margin = new Padding(4, 3, 4, 3); + lstItems.Name = "lstItems"; + lstItems.Size = new Size(740, 374); + lstItems.TabIndex = 2; + lstItems.UseCompatibleStateImageBehavior = false; + lstItems.View = View.Details; + lstItems.VirtualMode = true; + lstItems.RetrieveVirtualItem += lstItems_RetrieveVirtualItem; + // + // lstcFile + // + lstcFile.Text = "Файл"; + lstcFile.Width = 400; + // + // lstcException + // + lstcException.Text = "Ошибка"; + lstcException.Width = 200; + // + // ExceptionsForm + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(740, 374); + Controls.Add(lstItems); + Margin = new Padding(4, 3, 4, 3); + MinimizeBox = false; + Name = "ExceptionsForm"; + ShowIcon = false; + ShowInTaskbar = false; + StartPosition = FormStartPosition.CenterParent; + Text = "Ошибки"; + ResumeLayout(false); + } + + #endregion + + private ListView lstItems; + private ColumnHeader lstcFile; + private ColumnHeader lstcException; + } +} \ No newline at end of file diff --git a/FileSearch/ExceptionsForm.cs b/FileSearch/ExceptionsForm.cs new file mode 100644 index 0000000..2ba0f70 --- /dev/null +++ b/FileSearch/ExceptionsForm.cs @@ -0,0 +1,33 @@ +using FileSearch.Logic.Model.Engine; +using FileSearch.Logic.UI.Entries; + +namespace FileSearch +{ + public partial class ExceptionsForm : Form + { + private IList _exceptions; + private SearchExceptionEntry[] _exceptionEntries; + + public ExceptionsForm() + { + InitializeComponent(); + } + + public void SetContent(IList exceptions) + { + if (exceptions == null) throw new ArgumentNullException("exceptions"); + + _exceptions = exceptions; + _exceptionEntries = new SearchExceptionEntry[exceptions.Count]; + lstItems.VirtualListSize = exceptions.Count; + } + + private void lstItems_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) + { + var entry = _exceptionEntries[e.ItemIndex]; + if (entry == null) + _exceptionEntries[e.ItemIndex] = entry = new SearchExceptionEntry(_exceptions[e.ItemIndex]); + e.Item = entry; + } + } +} diff --git a/FileSearch/ExceptionsForm.resx b/FileSearch/ExceptionsForm.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/FileSearch/ExceptionsForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/FileSearch/FileSearch.csproj b/FileSearch/FileSearch.csproj new file mode 100644 index 0000000..7185440 --- /dev/null +++ b/FileSearch/FileSearch.csproj @@ -0,0 +1,31 @@ + + + + WinExe + net6.0-windows + enable + true + enable + + + + + + + + + + True + True + Resource.resx + + + + + + ResXFileCodeGenerator + Resource.Designer.cs + + + + \ No newline at end of file diff --git a/FileSearch/LargeListViewUserControl.Designer.cs b/FileSearch/LargeListViewUserControl.Designer.cs new file mode 100644 index 0000000..fee2693 --- /dev/null +++ b/FileSearch/LargeListViewUserControl.Designer.cs @@ -0,0 +1,66 @@ +namespace FileSearch +{ + partial class LargeListViewUserControl + { + /// + /// Обязательная переменная конструктора. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Освободить все используемые ресурсы. + /// + /// истинно, если управляемый ресурс должен быть удален; иначе ложно. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Код, автоматически созданный конструктором компонентов + + /// + /// Требуемый метод для поддержки конструктора — не изменяйте + /// содержимое этого метода с помощью редактора кода. + /// + private void InitializeComponent() + { + lstItems = new ListView(); + SuspendLayout(); + // + // lstItems + // + lstItems.Dock = System.Windows.Forms.DockStyle.Fill; + lstItems.FullRowSelect = true; + lstItems.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable; + lstItems.Location = new Point(0, 0); + lstItems.Name = "lstItems"; + lstItems.Size = new Size(483, 307); + lstItems.TabIndex = 0; + lstItems.UseCompatibleStateImageBehavior = false; + lstItems.View = System.Windows.Forms.View.Details; + lstItems.VirtualMode = true; + lstItems.RetrieveVirtualItem += new System.Windows.Forms.RetrieveVirtualItemEventHandler(this.RetrieveVirtualItem); + lstItems.KeyDown += new System.Windows.Forms.KeyEventHandler(this.lstItems_KeyDown); + lstItems.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.lstItems_MouseDoubleClick); + // + // LargeListViewUserControl + // + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + Controls.Add(lstItems); + Name = "LargeListViewUserControl"; + Size = new Size(483, 307); + SizeChanged += LargeListViewUserControl_SizeChanged; + Enter += LargeListViewUserControl_Enter; + ResumeLayout(false); + } + + #endregion + + private ListView lstItems; + } +} diff --git a/FileSearch/LargeListViewUserControl.cs b/FileSearch/LargeListViewUserControl.cs new file mode 100644 index 0000000..66fd8d8 --- /dev/null +++ b/FileSearch/LargeListViewUserControl.cs @@ -0,0 +1,152 @@ +using FileSearch.Logic.Model.Engine; +using FileSearch.Logic.UI.Entries; +using FileSearch.Logic.UI.ViewBuilders; + +namespace FileSearch +{ + internal partial class LargeListViewUserControl : UserControl + { + public event EventHandler FileOpened; + public event EventHandler DirectoryOpened; + + private readonly IList _collection; + private ListViewItem[] _cache; + private IViewBuilder _viewBuilder; + + public LargeListViewUserControl() + { + InitializeComponent(); + _collection = new List(); + } + + public int Count + { + get { return _cache.Length; } + } + + public void ClearContent() + { + _collection.Clear(); + VirtualListChanged(); + } + + public void ResetViewBuilder() + { + SetViewBuilder(new DefaultViewBuilder()); + } + + public void SetViewBuilder(IViewBuilder builder) + { + if (_viewBuilder != null && builder.GetType() == _viewBuilder.GetType()) + return; + + _viewBuilder = builder; + + lstItems.Columns.Clear(); + foreach (var c in builder.ColumnSizes) + { + lstItems.Columns.Add(new ColumnHeader { Text = c.Item1, Width = c.Item2 }); + } + LargeListViewUserControl_SizeChanged(this, EventArgs.Empty); + } + + public void AddSearchResults(IEnumerable searchResults) + { + if (searchResults == null) throw new ArgumentNullException("searchResults"); + + int index = 0; + var oldName = string.Empty; + + foreach (var result in searchResults) + { + foreach (var entry in _viewBuilder.Build(result, index)) + { + var directoryInfo = new DirectoryInfo(entry.FileSystemInfo.FullName); + if (directoryInfo.Parent?.FullName != oldName) + { + oldName = directoryInfo.Parent?.FullName; + _collection.Add(entry); + } + } + ++index; + } + VirtualListChanged(); + } + + private void VirtualListChanged() + { + lstItems.VirtualListSize = _collection.Count; + _cache = new ListViewItem[lstItems.VirtualListSize]; + if (lstItems.Items.Count > 0) + lstItems.Items[lstItems.Items.Count - 1].EnsureVisible(); + } + + private void RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) + { + e.Item = _cache[e.ItemIndex]; + if (e.Item == null) + { + var content = (PathEntry)_collection[e.ItemIndex]; + var item = content.BuildListViewItem(); + _cache[e.ItemIndex] = e.Item = item; + } + } + + private void LargeListViewUserControl_SizeChanged(object sender, EventArgs e) + { + if (lstItems.Columns.Count <= 0) + return; + + var fullWidth = this.Width - SystemInformation.VerticalScrollBarWidth - SystemInformation.FrameBorderSize.Width; + var fullColumns = lstItems.Columns.Cast().Where((h, i) => _viewBuilder.ColumnSizes[i].Item2 == -1).ToList(); + + var otherColumnWidth = 0; + foreach (ColumnHeader c in lstItems.Columns.Cast().Where(h => !fullColumns.Any(h2 => h2 != h))) + { + otherColumnWidth += c.Width; + } + + var partWidth = fullWidth / fullColumns.Count; + foreach (var c in fullColumns) + c.Width = partWidth; + } + + private void LargeListViewUserControl_Enter(object sender, EventArgs e) + { + lstItems.Focus(); + if (lstItems.VirtualListSize > 0 && lstItems.SelectedIndices.Count <= 0) + { + lstItems.SelectedIndices.Add(0); + } + } + + private void lstItems_MouseDoubleClick(object sender, MouseEventArgs e) + { + OpenExplorerIfPossible(); + } + + private void OpenExplorerIfPossible() + { + if (lstItems.SelectedIndices.Count <= 0 || _collection.Count <= 0) + return; + + var path = (PathEntry)(_collection[lstItems.SelectedIndices[0]]); + + if (path.IsDirectory) + { + if (DirectoryOpened != null) + DirectoryOpened(this, new PathEventArgs(path)); + } + else if (FileOpened != null) + FileOpened(this, new PathEventArgs(path)); + } + + private void lstItems_KeyDown(object sender, KeyEventArgs e) + { + if (!e.Shift && !e.Alt && !e.Control && (e.KeyCode == Keys.Enter || e.KeyCode == Keys.Return)) + { + OpenExplorerIfPossible(); + } + } + } +} diff --git a/FileSearch/LargeListViewUserControl.resx b/FileSearch/LargeListViewUserControl.resx new file mode 100644 index 0000000..af32865 --- /dev/null +++ b/FileSearch/LargeListViewUserControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/FileSearch/Logic/Extensions.cs b/FileSearch/Logic/Extensions.cs new file mode 100644 index 0000000..4520c58 --- /dev/null +++ b/FileSearch/Logic/Extensions.cs @@ -0,0 +1,23 @@ +using System.Globalization; + +namespace FileSearch.Logic +{ + internal static class Extensions + { + public static List ConvertTreeNodes(this string directoryPath) + { + if(directoryPath.Contains("\\")) + return directoryPath.Split(new char[] { '\\' }).ToList(); + if (directoryPath.Contains("/")) + return directoryPath.Split(new char[] { '/' }).ToList(); + return new List(); + } + + public static string GetFriendlyNotation(this TimeSpan timeSpan) + { + if (timeSpan.Minutes > 2) + return string.Format(CultureInfo.CurrentCulture, "{0:#,##0.0} мин.", timeSpan.TotalMinutes); + return string.Format(CultureInfo.CurrentCulture, "{0:#,##0.00} сек.", timeSpan.TotalSeconds); + } + } +} diff --git a/FileSearch/Logic/FileSearcher.cs b/FileSearch/Logic/FileSearcher.cs new file mode 100644 index 0000000..fa623e7 --- /dev/null +++ b/FileSearch/Logic/FileSearcher.cs @@ -0,0 +1,227 @@ +using FileSearch.Logic.Model.CriterionSchemas; +using FileSearch.Logic.Model.Engine; +using System.Collections.ObjectModel; + +namespace FileSearch.Logic +{ + internal class FileSearcher + { + private readonly IList _additionalCriterion; + private readonly EngineOptions _engineOptions; + private readonly IList _expections; + private bool _stop; + private bool _pause; + private Task> _currentTask; + private DateTime _startTime; + + public FileSearcher(EngineOptions engineOptions, IEnumerable additionalCriterion) + { + if (engineOptions == null) throw new ArgumentNullException("engineOptions"); + + _engineOptions = engineOptions; + _additionalCriterion = additionalCriterion.ToList(); + this.RefreshTimer = TimeSpan.FromSeconds(1); + _expections = new List(); + } + + /// + /// Получает или задает интервал времени для тайм-аута обратного вызова сопоставления. + /// + public TimeSpan RefreshTimer { get; set; } + + /// + /// Получает время, в течение которого поисковая система работала над последней операцией. + /// + public TimeSpan OperatingTime { get; private set; } + + /// + /// Получает время, в течение которого поисковая система работает. + /// + public TimeSpan CurrentTime { get; private set; } + + /// + /// Получает список всех исключений поиска последней операции. + /// + public IList Exceptions { get { return new ReadOnlyCollection(_expections); } } + + /// + /// Значение, указывающее, работает ли поисковая система. + /// + public bool IsRunning { get { return _currentTask != null; } } + + /// + /// Получает список всех критериев, которые использовались в текущей или последней операции. + /// + public IList UsedCriteria { get; private set; } + + /// + /// Запускает операцию поиска. + /// + /// Обратный вызов при обнаружении совпадений. + /// Обратный вызов после завершения поиска. + public void Start(Action> matchCallback, Action finishCallback) + { + this.OperatingTime = new TimeSpan(); + _startTime = DateTime.UtcNow; + _expections.Clear(); + + var timeout = new TimedCallback(this.RefreshTimer, matchCallback); + _stop = false; + + _currentTask = Task.Factory.StartNew>(() => Search(timeout)); + + _currentTask.ContinueWith(t => { + timeout.SetData(t.Result); + }) + .ContinueWith(t => { + _currentTask = null; + OperatingTime = DateTime.UtcNow - _startTime; + finishCallback(); + }); + } + + /// + /// Прерывает текущую операцию поиска. + /// + public void Stop() + { + if (IsRunning) + { + _pause = false; + _stop = true; + } + } + + /// + /// Приостанавливает текущую операцию поиска. + /// + public void Pause(Action state, bool update) + { + if (IsRunning) + _pause = update; + state(_pause); + } + + private IList BuildCriteria() + { + // Разрешить только один IPostProcessingCriterion. В противном случае результаты будут странными. + var criteria = CriteriaFactory.Build(_engineOptions).Union(_additionalCriterion).OrderBy(c => c is IPostProcessingCriterion).ThenBy(c => c.Weight).ToList(); + UsedCriteria = new ReadOnlyCollection(criteria); + return criteria; + } + + private IList Search(object state) + { + var timer = (TimedCallback)state; + + var criteria = BuildCriteria(); + var list = new List(64); + var requiresPostProcessing = criteria.Any(c => c is IPostProcessingCriterion); + + foreach (var rootDirectory in _engineOptions.RootDirectories) + { + + foreach (var fileSystemInfo in ListAllFileSystemInfo(rootDirectory, -1)) + { + var contexts = new Dictionary(); + var isDir = fileSystemInfo is DirectoryInfo; + var match = true; + this.CurrentTime = DateTime.UtcNow - _startTime; + + //Приостановить цикл + while (_pause) + { + Console.WriteLine(""); + } + + try + { + foreach (var c in criteria) + { + var context = c.BuildContext(); + + // Проверьте, поддерживает ли критерий тип целевой файловой системы. + if ((c.DirectorySupport && isDir) || (c.FileSupport && !isDir)) + { + if (c.IsMatch(fileSystemInfo, context)) + { + // Добавьте контекст, если он совпадает. + if (context != null) + contexts.Add(c.GetType(), context); + } + else + { + match = false; + break; + } + } + } + } + catch (Exception ex) + { + _expections.Add(SearchExceptionFactory.Build(fileSystemInfo, ex)); + match = false; + } + + if (match && !requiresPostProcessing) + list.Add(new SearchResult(fileSystemInfo) { Metadata = null }); + + // Есть верно, и результат не важен. + if (list.Count > 0 && !requiresPostProcessing && timer.DataNeeded) + { + timer.SetData(list); + list = new List(64); + } + + // Остановить цикл + if (_stop) + break; + } + // Остановить цикл + if (_stop) + break; + } + + + if (requiresPostProcessing) + { + // Выбираем последний, это самый интенсивный критерий. + var resultLists = criteria.OfType().Single(); + return resultLists.PostProcess().ToList(); + } + + return list; + } + + private IEnumerable ListAllFileSystemInfo(FileSystemInfo fileSystemInfo, int level) + { + var directoryInfo = fileSystemInfo as DirectoryInfo; + var isRoot = level == -1; + + // Возвращает папку или, если это файл, всегда. Пропускает корневой уровень. + if (!isRoot && (directoryInfo == null)) + yield return fileSystemInfo; + + if (directoryInfo != null || isRoot) + { + FileSystemInfo[] infos = null; + try + { + infos = directoryInfo.GetFileSystemInfos(); + } + catch (UnauthorizedAccessException ex) + { + _expections.Add(SearchExceptionFactory.Build(directoryInfo, ex)); + } + if (infos == null) + yield break; + + foreach (var item in infos.SelectMany(s => ListAllFileSystemInfo(s, level + 1))) + { + if (_stop) yield break; + yield return item; + } + } + } + } +} diff --git a/FileSearch/Logic/Model/Contexts/ZipCriterionContext.cs b/FileSearch/Logic/Model/Contexts/ZipCriterionContext.cs new file mode 100644 index 0000000..c5b240d --- /dev/null +++ b/FileSearch/Logic/Model/Contexts/ZipCriterionContext.cs @@ -0,0 +1,15 @@ +using FileSearch.Logic.Model.Engine; +using System.Collections.ObjectModel; + +namespace FileSearch.Logic.Model.Contexts +{ + internal sealed class ZipCriterionContext : ICriterionContext + { + public ZipCriterionContext() + { + this.Childs = new Collection(); + } + + public IList Childs { get; private set; } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/AttributeCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/AttributeCriterion.cs new file mode 100644 index 0000000..c710b99 --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/AttributeCriterion.cs @@ -0,0 +1,30 @@ +using FileSearch.Logic.Model.Engine; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class AttributeCriterion : CriterionBase, ICriterion + { + private readonly FileAttributes _includedAttributes; + private readonly FileAttributes _excludedAttributes; + + public AttributeCriterion(FileAttributes includedAttributes, FileAttributes excludedAttributes) + { + _includedAttributes = includedAttributes; + _excludedAttributes = excludedAttributes; + } + + public string Name { get { return "Attributes"; } } + + public CriterionWeight Weight { get { return CriterionWeight.Medium; } } + + public bool DirectorySupport { get { return true; } } + + public bool FileSupport { get { return true; } } + + public bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + return (fileSystemInfo.Attributes & _includedAttributes) == _includedAttributes + && (fileSystemInfo.Attributes & _excludedAttributes) == 0; + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/ContentCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/ContentCriterion.cs new file mode 100644 index 0000000..3975025 --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/ContentCriterion.cs @@ -0,0 +1,168 @@ +using FileSearch.Logic.Model.EncodingDetection; +using FileSearch.Logic.Model.Engine; +using System.Text; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class ContentCriterion : CriterionBase, ICriterion + { + private const int BufferSize = 32 * 1024; // 32KB + + private readonly string _text; + private readonly char[][] _textInChars; + private readonly bool _ignoreCase; + private readonly bool _matchFullWords; + private readonly IEncodingFactory _encodingFactory; + + public ContentCriterion(string text, bool ignoreCase, bool matchFullWords, IEncodingFactory encodingFactory) + { + if (text == null) throw new ArgumentNullException("text"); + if (encodingFactory == null) throw new ArgumentNullException("encodingFactory"); + + _text = text; + _ignoreCase = ignoreCase; + _matchFullWords = matchFullWords; + _encodingFactory = encodingFactory; + + _textInChars = StringToCharArrays(text, ignoreCase); + } + + public string Name { get { return "File content"; } } + + public CriterionWeight Weight { get { return CriterionWeight.Heavy; } } + + public bool DirectorySupport { get { return false; } } + + public bool FileSupport { get { return true; } } + + public bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + var fileInfo = (FileInfo)fileSystemInfo; + + var buffer = new byte[BufferSize]; + var textLength = _text.Length; + + Encoding[] encodings = new Encoding[1]; + + // Проверить несколько кодировок + for (int encodingIndex = 0; encodingIndex < encodings.Length; encodingIndex++) + { + // Кодирование текущего цикла. Первая попытка будет NULL. + Encoding encoding = encodings[encodingIndex]; + bool characterShouldBeNonWord = false; + char characterBefore = '\0'; + + using (var stream = fileInfo.OpenRead()) + { + int length; + int foundMatchingSymbols = 0; + while ((length = stream.Read(buffer, 0, BufferSize)) > 0) + { + // Если кодировка еще не определена, определите ее сейчас. + if (encoding == null) + { + encodings = _encodingFactory.DetectEncoding(buffer); + encoding = encodings[encodingIndex]; + } + + var currentString = encoding.GetString(buffer, 0, length); + var currentStringLength = currentString.Length; // Кэш + + bool startAtBegin = foundMatchingSymbols > 0; + var charIndex = 0; + + // Первый символ должен быть символом, отличным от слова, если предыдущие прочитанные байты заканчиваются соответствующей строкой. + if (_matchFullWords && characterShouldBeNonWord && currentStringLength > 0) + { + characterShouldBeNonWord = false; + if (!CharIsWordChar(currentString[0])) + return true; + } + + // Проверьте, находится ли первый или следующий совпадающий символ в текущей строке. + if ((charIndex = currentString.IndexOfAny(_textInChars[foundMatchingSymbols], charIndex)) >= 0) + { + do + { + // Назначьте персонажа заранее. + if (charIndex > 0 && foundMatchingSymbols == 0 && _matchFullWords) + characterBefore = currentString[charIndex - 1]; + + // Не первый символ _text, поэтому проверьте, находится ли второй символ в первой позиции. + if (startAtBegin) + { + startAtBegin = false; + if (charIndex > 0) // Буква должна быть на первой позиции! Если нет, начните заново с первого символа в _text. + { + foundMatchingSymbols = 0; + continue; + } + } + + // Скопируйте переменную, чтобы она не была изменена в приведенном ниже цикле. + var current = charIndex; + // Постарайтесь сопоставить как можно больше символов. + while (++foundMatchingSymbols < textLength && currentStringLength > ++current && (currentString[current] == _text[foundMatchingSymbols] || _ignoreCase && _textInChars[foundMatchingSymbols].Any(c => c == currentString[current]))) ; + // Нашел! + if (foundMatchingSymbols == textLength && (!_matchFullWords || !CharIsWordChar(characterBefore))) + { + if (_matchFullWords) + { + // Попытайтесь определить следующее чтение, заканчивается ли строка символом, отличным от слова. + if (current >= currentStringLength - 1) + characterShouldBeNonWord = true; + // Проверьте, не является ли следующая буква словом char. Если нет, верните true. В противном случае убедитесь, что индекс равен +1, чтобы он был сброшен в следующем операторе IF. + else if (!CharIsWordChar(currentString[++current])) + return true; + } + else + // Возвращает true, если не проверяется слово вместо части строки. + return true; + } + // Сбросить счетчик совпадающих символов, если конец текущей строки не достигнут. Если да, продолжайте тестирование при следующем чтении. + if (currentStringLength != current) + foundMatchingSymbols = 0; + // Проверьте, есть ли следующий соответствующий символ в текущей строке. + } while ((charIndex = currentString.IndexOfAny(_textInChars[foundMatchingSymbols], ++charIndex)) >= 0); + + // Назначьте последний символ, чтобы в следующем раунде он знал предыдущий символ. + if (foundMatchingSymbols == 0 && _matchFullWords) + characterBefore = currentString[currentStringLength - 1]; + } + else + { + foundMatchingSymbols = 0; + // Переназначьте предыдущий символ последнему символу в строке. + if (_matchFullWords) characterBefore = currentString[currentStringLength - 1]; + } + } + if (_matchFullWords && characterShouldBeNonWord) + return true; + } + } + return false; + } + + private static bool CharIsWordChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_'; + } + + private static char[][] StringToCharArrays(string input, bool ignoreCase) + { + var list = new List(); + foreach (var c in input) + { + if (!ignoreCase || !char.IsLetter(c)) + list.Add(new[] { c }); + else + { + var u = char.ToUpperInvariant(c); + var l = char.ToLowerInvariant(c); + list.Add(u != l ? new[] { u, l } : new[] { c }); + } + } + return list.ToArray(); + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/ContentRegexCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/ContentRegexCriterion.cs new file mode 100644 index 0000000..a199ae8 --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/ContentRegexCriterion.cs @@ -0,0 +1,32 @@ +using FileSearch.Logic.Model.Engine; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class ContentRegexCriterion : CriterionBase, ICriterion + { + private readonly string _regexText; + private readonly bool _ignoreCase; + + public ContentRegexCriterion(string regexText, bool ignoreCase) + { + if (regexText == null) throw new ArgumentNullException("regexText"); + + _regexText = regexText; + _ignoreCase = ignoreCase; + } + + public string Name { get { return "File content using regular expressions"; } } + + public CriterionWeight Weight { get { return CriterionWeight.Extreme; } } + + public bool DirectorySupport { get { return false; } } + + public bool FileSupport { get { return true; } } + + public bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + var fileInfo = (FileInfo)fileSystemInfo; + throw new NotImplementedException(); + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/CriteriaFactory.cs b/FileSearch/Logic/Model/CriterionSchemas/CriteriaFactory.cs new file mode 100644 index 0000000..a74f863 --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/CriteriaFactory.cs @@ -0,0 +1,53 @@ +using FileSearch.Logic.Model.Engine; +using FileSearch.Logic.Model.Entities; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal static class CriteriaFactory + { + public static IList Build(EngineOptions options) + { + var list = new List(); + + // Применить базовые параметры + if (!string.IsNullOrEmpty(options.SearchName)) + { + if (options.SearchInArchives) + list.Add(new NameAndZipCriterion(options.SearchName, options.SearchNameIgnoreCasing, options.SearchNameMatchFullPath)); + else if (!options.SearchNameAsRegularExpression) + list.Add(new NameCriterion(options.SearchName, options.SearchNameIgnoreCasing, options.SearchNameMatchFullPath)); + else + list.Add(new NameRegexCriterion(options.SearchName, options.SearchNameIgnoreCasing, options.SearchNameMatchFullPath)); + } + + // Добавить атрибуты файла + if (options.AttributesIncluded > 0 || options.AttributesExcluded > 0) + { + list.Add(new AttributeCriterion(options.AttributesIncluded, options.AttributesExcluded)); + } + + // Размеры + if (options.MinimumSize != null || options.MaximumSize != null) + { + list.Add(new SizeCriterion(options.MinimumSize, options.MaximumSize)); + } + + // Даты + if (options.DateOption != FileDateOption.None && (options.StartDateTime != null || options.EndDateTime != null)) + { + list.Add(new DateCriterion(options.DateOption, options.StartDateTime, options.EndDateTime)); + } + + // Содержание + if (!string.IsNullOrEmpty(options.ContentText)) + { + if (!options.ContentAsRegularExpression) + list.Add(new ContentCriterion(options.ContentText, options.ContentIgnoreCasing, options.ContentWholeWordsOnly, options.ContentEncodingFactory)); + else + list.Add(new ContentRegexCriterion(options.ContentText, options.ContentIgnoreCasing)); + } + + return list.ToArray(); + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/DateCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/DateCriterion.cs new file mode 100644 index 0000000..df22a04 --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/DateCriterion.cs @@ -0,0 +1,54 @@ +using FileSearch.Logic.Model.Engine; +using FileSearch.Logic.Model.Entities; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class DateCriterion : CriterionBase, ICriterion + { + private readonly FileDateOption _dateOption; + private readonly DateTime? _startDateTime; + private readonly DateTime? _endDateTime; + + public DateCriterion(FileDateOption dateOption, DateTime? startDateTime, DateTime? endDateTime) + { + if (dateOption == FileDateOption.None) + throw new ArgumentException("There is no date option specified.", "dateOption"); + if (startDateTime == null && endDateTime == null) + throw new ArgumentException("No start and end date time are specified."); + + _dateOption = dateOption; + _startDateTime = startDateTime != null ? (DateTime?)startDateTime.Value.ToUniversalTime() : null; + _endDateTime = endDateTime != null ? (DateTime?)endDateTime.Value.ToUniversalTime() : null; + } + + public string Name { get { return "Dates"; } } + + public CriterionWeight Weight { get { return CriterionWeight.Medium; } } + + public bool DirectorySupport { get { return true; } } + + public bool FileSupport { get { return true; } } + + public bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + DateTime dateTimeUtc; + switch (_dateOption) + { + case FileDateOption.Accessed: + dateTimeUtc = fileSystemInfo.LastAccessTimeUtc; + break; + case FileDateOption.Changed: + dateTimeUtc = fileSystemInfo.LastWriteTimeUtc; + break; + case FileDateOption.Created: + dateTimeUtc = fileSystemInfo.CreationTimeUtc; + break; + default: + throw new NotSupportedException(); + } + + return (_startDateTime == null || dateTimeUtc >= _startDateTime.Value) + && (_endDateTime == null || dateTimeUtc <= _endDateTime.Value); + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/IPostProcessingCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/IPostProcessingCriterion.cs new file mode 100644 index 0000000..373ce1f --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/IPostProcessingCriterion.cs @@ -0,0 +1,9 @@ +using FileSearch.Logic.Model.Engine; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal interface IPostProcessingCriterion + { + IEnumerable PostProcess(); + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/NameAndZipCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/NameAndZipCriterion.cs new file mode 100644 index 0000000..f0b2aba --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/NameAndZipCriterion.cs @@ -0,0 +1,51 @@ +using FileSearch.Logic.Model.Contexts; +using FileSearch.Logic.Model.Engine; +using Ionic.Zip; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class NameAndZipCriterion : NameCriterion + { + public NameAndZipCriterion(string value, bool ignoreCasing, bool matchFullPath) + : base(value, ignoreCasing, matchFullPath) + { + } + + public override CriterionWeight Weight + { + get { return CriterionWeight.Light; } + } + + public override bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + // базовое сравнение + if (base.IsMatch(fileSystemInfo, context)) + return true; + + // Должно заканчиваться на .ZIP. + if (string.IsNullOrEmpty(fileSystemInfo.Extension) || !fileSystemInfo.Extension.Equals(".zip", StringComparison.OrdinalIgnoreCase)) + return false; + + // Все найденные записи в ZIP-файле. + var myContext = (ZipCriterionContext)context; + + using (var zip = ZipFile.Read(fileSystemInfo.FullName)) + { + foreach (var entry in zip.EntryFileNames.Select(e => e.Replace('/', '\\'))) + { + if (IsMatch(this.MatchFullPath ? Path.Combine(fileSystemInfo.FullName, entry) : Path.GetFileName(entry))) + { + myContext.Childs.Add(entry); + } + } + } + + return myContext.Childs.Count > 0; + } + + public override ICriterionContext BuildContext() + { + return new ZipCriterionContext(); + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/NameCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/NameCriterion.cs new file mode 100644 index 0000000..9974d7e --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/NameCriterion.cs @@ -0,0 +1,93 @@ +using FileSearch.Logic.Model.Engine; +using System.Text.RegularExpressions; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class NameCriterion : CriterionBase, ICriterion + { + private static readonly string StarWildcard = Regex.Escape("*"); + private static readonly string QuestionWildcard = Regex.Escape("?"); + + private string[] _exactMatches; + private Regex[] _regexMatches; + + private readonly bool _ignoreCasing; + protected readonly bool MatchFullPath; + + public NameCriterion(string value, bool ignoreCasing, bool matchFullPath) + { + if (value == null) throw new ArgumentNullException("value"); + + _ignoreCasing = ignoreCasing; + this.MatchFullPath = matchFullPath; + BuildMatches(value); + } + + public string Name { get { return "File and directory names"; } } + + public virtual CriterionWeight Weight + { + get { return CriterionWeight.None; } + } + + public virtual bool DirectorySupport + { + get { return true; } + } + + public virtual bool FileSupport + { + get { return true; } + } + + public virtual bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + var name = this.MatchFullPath ? fileSystemInfo.FullName : fileSystemInfo.Name; + return IsMatch(name); + } + + protected bool IsMatch(string fileName) + { + if (_exactMatches.Length > 0 && _exactMatches.Any(m => SimpleMatch(fileName, m))) + return true; + if (_regexMatches.Length > 0 && _regexMatches.Any(m => WildcardMatch(fileName, m))) + return true; + return false; + } + + private bool SimpleMatch(string filePath, string value) + { + return filePath.IndexOf(value, _ignoreCasing ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) >= 0; + } + + private static bool WildcardMatch(string filePath, Regex value) + { + return value.IsMatch(filePath); + } + + private void BuildMatches(string value) + { + var exactMatches = new List(); + var regexMatches = new List(); + + var split = value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var item in split) + { + // Build regex for a like statement + if (item.Contains("*") || item.Contains("?")) + { + var valueRegex = Regex.Escape(item) + .Replace(QuestionWildcard, ".{1}") + .Replace(StarWildcard, ".*"); + + regexMatches.Add(new Regex(string.Concat("^", valueRegex, "$"), RegexOptions.Compiled | (_ignoreCasing ? RegexOptions.IgnoreCase : RegexOptions.None))); + } + else + exactMatches.Add(item); + } + + _exactMatches = exactMatches.ToArray(); + _regexMatches = regexMatches.ToArray(); + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/NameRegexCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/NameRegexCriterion.cs new file mode 100644 index 0000000..0e7a8d5 --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/NameRegexCriterion.cs @@ -0,0 +1,50 @@ +using FileSearch.Logic.Model.Engine; +using System.Text.RegularExpressions; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class NameRegexCriterion : CriterionBase, ICriterion + { + private readonly string _regularExpression; + private readonly bool _ignoreCase; + private readonly bool _matchFullPath; + private Regex _cachedRegex; + + public NameRegexCriterion(string regularExpression, bool ignoreCase, bool matchFullPath) + { + if (regularExpression == null) throw new ArgumentNullException("regularExpression"); + _regularExpression = regularExpression; + _ignoreCase = ignoreCase; + _matchFullPath = matchFullPath; + } + + public string Name { get { return "File and directory names using regular expressions"; } } + + public CriterionWeight Weight + { + get { return CriterionWeight.None; } + } + + public bool DirectorySupport + { + get { return true; } + } + + public bool FileSupport + { + get { return true; } + } + + public bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + return IsMatch(_matchFullPath ? fileSystemInfo.FullName : fileSystemInfo.Name); + } + + protected virtual bool IsMatch(string fileName) + { + if (_cachedRegex == null) + _cachedRegex = new Regex(_regularExpression, RegexOptions.Compiled | (_ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None)); + return _cachedRegex.IsMatch(fileName); + } + } +} diff --git a/FileSearch/Logic/Model/CriterionSchemas/SizeCriterion.cs b/FileSearch/Logic/Model/CriterionSchemas/SizeCriterion.cs new file mode 100644 index 0000000..a828507 --- /dev/null +++ b/FileSearch/Logic/Model/CriterionSchemas/SizeCriterion.cs @@ -0,0 +1,40 @@ +using FileSearch.Logic.Model.Engine; + +namespace FileSearch.Logic.Model.CriterionSchemas +{ + internal class SizeCriterion : CriterionBase, ICriterion + { + private readonly long? _minLengthInBytes; + private readonly long? _maxLengthInBytes; + + public SizeCriterion(long? minLengthInBytes, long? maxLengthInBytes) + { + if (minLengthInBytes != null && minLengthInBytes < 0) + throw new ArgumentOutOfRangeException("minLengthInBytes", "Указанная минимальная длина не может быть меньше 0 байт."); + if (maxLengthInBytes != null && maxLengthInBytes < 0) + throw new ArgumentOutOfRangeException("maxLengthInBytes", "Указанная максимальная длина не может быть меньше 0 байт."); + if (minLengthInBytes == null && maxLengthInBytes == null) + throw new ArgumentException("Невозможно установить как минимальное, так и максимальное значения равными нулю."); + if (minLengthInBytes != null && maxLengthInBytes != null && minLengthInBytes > maxLengthInBytes) + throw new ArgumentException("Максимальное значение должно быть больше или равно минимальному."); + + _minLengthInBytes = minLengthInBytes; + _maxLengthInBytes = maxLengthInBytes; + } + + public string Name { get { return "File sizes"; } } + + public CriterionWeight Weight { get { return CriterionWeight.Medium; } } + + public bool DirectorySupport { get { return false; } } + + public bool FileSupport { get { return true; } } + + public bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context) + { + var file = (FileInfo)fileSystemInfo; + return (_minLengthInBytes == null || file.Length >= _minLengthInBytes.Value) + && (_maxLengthInBytes == null || file.Length <= _maxLengthInBytes.Value); + } + } +} diff --git a/FileSearch/Logic/Model/EncodingDetection/IEncodingFactory.cs b/FileSearch/Logic/Model/EncodingDetection/IEncodingFactory.cs new file mode 100644 index 0000000..0bd5a35 --- /dev/null +++ b/FileSearch/Logic/Model/EncodingDetection/IEncodingFactory.cs @@ -0,0 +1,9 @@ +using System.Text; + +namespace FileSearch.Logic.Model.EncodingDetection +{ + internal interface IEncodingFactory + { + Encoding[] DetectEncoding(byte[] firstBytes); + } +} diff --git a/FileSearch/Logic/Model/Engine/CriterionBase.cs b/FileSearch/Logic/Model/Engine/CriterionBase.cs new file mode 100644 index 0000000..97b0603 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/CriterionBase.cs @@ -0,0 +1,18 @@ +namespace FileSearch.Logic.Model.Engine +{ + public class CriterionBase + { + public virtual ICriterionContext BuildContext() + { + return null; + } + } + + public class CriterionBase : CriterionBase where TContext : ICriterionContext, new() + { + public override ICriterionContext BuildContext() + { + return new TContext(); + } + } +} diff --git a/FileSearch/Logic/Model/Engine/CriterionWeight.cs b/FileSearch/Logic/Model/Engine/CriterionWeight.cs new file mode 100644 index 0000000..a855dd4 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/CriterionWeight.cs @@ -0,0 +1,26 @@ +namespace FileSearch.Logic.Model.Engine +{ + public enum CriterionWeight + { + /// + /// Простая проверка переменной + /// + None, + /// + /// Простая проверка переменной с помощью некоторых продвинутых алгоритмов. + /// + Light, + /// + /// Проверка переменной, которая еще не разрешена. + /// + Medium, + /// + /// Проверка очень большой переменной, которая еще не разрешена. + /// + Heavy, + /// + /// Проверка очень большой переменной со сложными вычислениями, которые лучше не использовать. + /// + Extreme + } +} diff --git a/FileSearch/Logic/Model/Engine/EngineOptions.cs b/FileSearch/Logic/Model/Engine/EngineOptions.cs new file mode 100644 index 0000000..5c2879d --- /dev/null +++ b/FileSearch/Logic/Model/Engine/EngineOptions.cs @@ -0,0 +1,78 @@ +using FileSearch.Logic.Model.EncodingDetection; +using FileSearch.Logic.Model.Entities; + +namespace FileSearch.Logic.Model.Engine +{ + internal class EngineOptions + { + public EngineOptions(DirectoryInfo[] rootDirectories) + { + if (rootDirectories == null) throw new ArgumentNullException("rootDirectories"); + if (rootDirectories.Length <= 0) throw new ArgumentException(@"Должен быть указан минимум 1 корневой каталог.", "rootDirectories"); + + RootDirectories = rootDirectories; + } + + public DirectoryInfo[] RootDirectories { get; private set; } + + #region Basic + + public string SearchName { get; set; } + + public bool SearchNameIgnoreCasing { get; set; } + + public bool SearchNameMatchFullPath { get; set; } + + public bool SearchNameAsRegularExpression { get; set; } + + public bool SearchRecursive { get; set; } + + public bool SearchIncludesFolders { get; set; } + + public bool SearchInArchives { get; set; } + + #endregion + + #region Attributes + + public FileAttributes AttributesIncluded { get; set; } + + public FileAttributes AttributesExcluded { get; set; } + + #endregion + + #region Size + + public long? MinimumSize { get; set; } + + public long? MaximumSize { get; set; } + + #endregion + + #region Dates + + public FileDateOption DateOption { get; set; } + + public DateTime? StartDateTime { get; set; } + + public DateTime? EndDateTime { get; set; } + + #endregion + + #region File content + + public string ContentText { get; set; } + + public IEncodingFactory ContentEncodingFactory { get; set; } + + public bool ContentIgnoreCasing { get; set; } + + public bool ContentWholeWordsOnly { get; set; } + + public bool ContentAsRegularExpression { get; set; } + + public bool ContentForOfficeXml { get; set; } + + #endregion + } +} diff --git a/FileSearch/Logic/Model/Engine/FileCounter.cs b/FileSearch/Logic/Model/Engine/FileCounter.cs new file mode 100644 index 0000000..af25607 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/FileCounter.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace FileSearch.Logic.Model.Engine +{ + internal class FileCounter + { + + } +} diff --git a/FileSearch/Logic/Model/Engine/ICriterion.cs b/FileSearch/Logic/Model/Engine/ICriterion.cs new file mode 100644 index 0000000..7cf1825 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/ICriterion.cs @@ -0,0 +1,40 @@ +namespace FileSearch.Logic.Model.Engine +{ + public interface ICriterion + { + /// + /// Имя критерия фильтра. + /// + string Name { get; } + + /// + /// Значение, указывающее усилия, необходимые системе для сопоставления файла. + /// Чем выше число, тем позже проверяется критерий. + /// + CriterionWeight Weight { get; } + + /// + /// Указывает, поддерживает ли этот экземпляр критерия каталоги. + /// + bool DirectorySupport { get; } + + /// + /// Указывает, поддерживает ли этот экземпляр критерия файлы. + /// + bool FileSupport { get; } + + /// + /// Проверяет, соответствует ли файл или каталог этому критерию. + /// + /// Entry файловой системы. + /// Контекст текущей работы всех критериев. + /// + bool IsMatch(FileSystemInfo fileSystemInfo, ICriterionContext context); + + /// + /// Создает новый контекст для этого критерия. + /// + /// Новый контекст для сопоставления файлов. + ICriterionContext BuildContext(); + } +} diff --git a/FileSearch/Logic/Model/Engine/ICriterionContext.cs b/FileSearch/Logic/Model/Engine/ICriterionContext.cs new file mode 100644 index 0000000..e9eec0c --- /dev/null +++ b/FileSearch/Logic/Model/Engine/ICriterionContext.cs @@ -0,0 +1,6 @@ +namespace FileSearch.Logic.Model.Engine +{ + public interface ICriterionContext + { + } +} diff --git a/FileSearch/Logic/Model/Engine/SearchException.cs b/FileSearch/Logic/Model/Engine/SearchException.cs new file mode 100644 index 0000000..7c1a4a3 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/SearchException.cs @@ -0,0 +1,35 @@ +namespace FileSearch.Logic.Model.Engine +{ + public class SearchException + { + private readonly FileSystemInfo _fileSystemInfo; + private readonly Exception _originalException; + private readonly string _friendlyDescription; + + public SearchException(FileSystemInfo fileSystemInfo, Exception originalException, string friendlyDescription) + { + if (fileSystemInfo == null) throw new ArgumentNullException("fileSystemInfo"); + if (originalException == null) throw new ArgumentNullException("originalException"); + if (friendlyDescription == null) throw new ArgumentNullException("friendlyDescription"); + + _fileSystemInfo = fileSystemInfo; + _originalException = originalException; + _friendlyDescription = friendlyDescription; + } + + public Exception OriginalException + { + get { return _originalException; } + } + + public string FriendlyDescription + { + get { return _friendlyDescription; } + } + + public FileSystemInfo FileSystemInfo + { + get { return _fileSystemInfo; } + } + } +} diff --git a/FileSearch/Logic/Model/Engine/SearchExceptionFactory.cs b/FileSearch/Logic/Model/Engine/SearchExceptionFactory.cs new file mode 100644 index 0000000..10f43a6 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/SearchExceptionFactory.cs @@ -0,0 +1,28 @@ +using System.Xml; +using Ionic.Zip; + +namespace FileSearch.Logic.Model.Engine +{ + internal static class SearchExceptionFactory + { + public static SearchException Build(FileSystemInfo fileSystemInfo, Exception originalException) + { + if (originalException is PathTooLongException) + return new SearchException(fileSystemInfo, originalException, "Путь слишком длинный."); + + if (originalException is IOException) + return new SearchException(fileSystemInfo, originalException, "Файл заблокирован для чтения."); + + if (originalException is UnauthorizedAccessException) + return new SearchException(fileSystemInfo, originalException, "Недостаточно прав на чтение файла или папки."); + + if (originalException is XmlException) + return new SearchException(fileSystemInfo, originalException, "XML содержит недопустимые символы."); + + if (originalException is ZipException) + return new SearchException(fileSystemInfo, originalException, "Невозможно обработать ZIP-файл."); + + return new SearchException(fileSystemInfo, originalException, "Неизвестное необработанное исключение."); + } + } +} diff --git a/FileSearch/Logic/Model/Engine/SearchResult.cs b/FileSearch/Logic/Model/Engine/SearchResult.cs new file mode 100644 index 0000000..d14d581 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/SearchResult.cs @@ -0,0 +1,21 @@ +namespace FileSearch.Logic.Model.Engine +{ + public class SearchResult + { + public SearchResult(FileSystemInfo fileSystemInfo) + { + this.FileSystemInfo = fileSystemInfo; + } + + /// + /// Получает файл или каталог для данного результата поиска. + /// + public FileSystemInfo FileSystemInfo { get; private set; } + + /// + /// Получает или устанавливает коллекцию со всеми метаданными для этого результата поиска. + /// Тип критерия, задавшего контекст. + /// + public IDictionary Metadata { get; set; } + } +} diff --git a/FileSearch/Logic/Model/Engine/TimedCallback.cs b/FileSearch/Logic/Model/Engine/TimedCallback.cs new file mode 100644 index 0000000..bbcbb11 --- /dev/null +++ b/FileSearch/Logic/Model/Engine/TimedCallback.cs @@ -0,0 +1,46 @@ +namespace FileSearch.Logic.Model.Engine +{ + internal class TimedCallback + { + private readonly TimeSpan _timeout; + private readonly Action> _callback; + private DateTime _lastTrigger; + private bool _isRunning = false; + + public TimedCallback(TimeSpan timeout, Action> callback) + { + if (callback == null) throw new ArgumentNullException("callback"); + if (timeout.TotalSeconds < 0.1) throw new ArgumentException(@"The timeout should be minimal 0.1 second.", "timeout"); + + _timeout = timeout; + _callback = callback; + _lastTrigger = DateTime.UtcNow; + } + + /// + /// Значение, указывающее, превышен ли период тайм-аута и можно ли получить новые данные. + /// + public bool DataNeeded + { + get { return !_isRunning && DateTime.UtcNow - _lastTrigger >= _timeout; } + } + + /// + /// Устанавливает данные для отправки делегату обратного вызова. + /// + /// Сбор с данными. + public void SetData(IEnumerable collection) + { + try + { + _isRunning = true; + _callback(collection); + _lastTrigger = DateTime.UtcNow; + } + finally + { + _isRunning = false; + } + } + } +} diff --git a/FileSearch/Logic/Model/Entities/FileDateOption.cs b/FileSearch/Logic/Model/Entities/FileDateOption.cs new file mode 100644 index 0000000..5263a7c --- /dev/null +++ b/FileSearch/Logic/Model/Entities/FileDateOption.cs @@ -0,0 +1,10 @@ +namespace FileSearch.Logic.Model.Entities +{ + internal enum FileDateOption + { + None, + Accessed, + Changed, + Created + } +} diff --git a/FileSearch/Logic/Model/Tree/TreeBuilder.cs b/FileSearch/Logic/Model/Tree/TreeBuilder.cs new file mode 100644 index 0000000..f5c8404 --- /dev/null +++ b/FileSearch/Logic/Model/Tree/TreeBuilder.cs @@ -0,0 +1,67 @@ +namespace FileSearch.Logic.Model.Tree +{ + public class TreeViewBuilder + { + public static void BuildTreeView(TreeView treeView, string[] paths) + { + if(paths.Length == 0) return; + + // Проверяем, существует ли уже корневой узел + TreeNode rootNode = treeView.Nodes.Cast().FirstOrDefault(); + + // Если корневой узел еще не создан, создаем его + if (rootNode == null) + { + // Извлекаем первое слово из первого пути + string rootName = GetRootName(paths[0]); + rootNode = new TreeNode(rootName); + treeView.Nodes.Add(rootNode); + } + treeView.BeginUpdate(); + // Добавляем каждый путь как узел в TreeView + foreach (string path in paths) + { + AddPathToTree(rootNode, path); + } + treeView.EndUpdate(); + } + + private static string GetRootName(string path) + { + // Извлекаем первое слово из пути + string[] parts = path.Split('\\'); + return parts[0]; + } + + private static void AddPathToTree(TreeNode parentNode, string path) + { + // Разбиваем путь на части + string[] parts = path.Split('\\'); + + // Проходим по каждой части пути + TreeNode currentNode = parentNode; + + // Пропускаем первое слово, так как оно является корневым узлом + for (int i = 1; i < parts.Length; i++) + { + // Проверяем, есть ли узел с текущим именем среди дочерних узлов текущего узла + TreeNode existingNode = currentNode.Nodes.Cast() + .FirstOrDefault(node => node.Text == parts[i]); + + // Если узел с таким именем уже существует, переходим к следующему уровню + if (existingNode != null) + { + currentNode = existingNode; + } + + // Если узел не существует, создаем новый + else + { + TreeNode newNode = new TreeNode(parts[i]); + currentNode.Nodes.Add(newNode); + currentNode = newNode; + } + } + } + } +} diff --git a/FileSearch/Logic/Plugin/ErrorPlugin.cs b/FileSearch/Logic/Plugin/ErrorPlugin.cs new file mode 100644 index 0000000..39b3bcc --- /dev/null +++ b/FileSearch/Logic/Plugin/ErrorPlugin.cs @@ -0,0 +1,46 @@ +namespace FileSearch.Logic.Plugin +{ + internal class ErrorPlugin : IPluginFacade + { + private readonly string _errorMessage; + + public ErrorPlugin(string tabTitle, string errorMessage = null) + { + _errorMessage = errorMessage; + this.TabTitle = string.Concat(tabTitle, " [ERROR]"); + } + + public Guid PluginId { get { return Guid.Empty; } } + + public string TabTitle { get; private set; } + + public UserControl BuildTabPage() + { + var uc = new UserControl(); + if (!string.IsNullOrEmpty(_errorMessage)) + { + var label = new Label + { + Text = string.Concat("PLUGIN ERROR: ", _errorMessage), + Location = new Point(3, 3), + ForeColor = Color.Red, + Font = new Font(SystemFonts.DefaultFont, FontStyle.Italic), + AutoSize = true + }; + uc.Controls.Add(label); + } + + return uc; + } + + public ICriterionPlugin[] GetCriterion() + { + return new ICriterionPlugin[0]; + } + + public IViewBuilderFactory GetViewBuilderFactory() + { + return null; + } + } +} diff --git a/FileSearch/Logic/Plugin/ICriterionPlugin.cs b/FileSearch/Logic/Plugin/ICriterionPlugin.cs new file mode 100644 index 0000000..7498aea --- /dev/null +++ b/FileSearch/Logic/Plugin/ICriterionPlugin.cs @@ -0,0 +1,9 @@ +using FileSearch.Logic.Model.Engine; + +namespace FileSearch.Logic.Plugin +{ + public interface ICriterionPlugin : ICriterion + { + + } +} diff --git a/FileSearch/Logic/Plugin/IPluginFacade.cs b/FileSearch/Logic/Plugin/IPluginFacade.cs new file mode 100644 index 0000000..5c6496a --- /dev/null +++ b/FileSearch/Logic/Plugin/IPluginFacade.cs @@ -0,0 +1,17 @@ +using FileSearch.Logic.UI.ViewBuilders; + +namespace FileSearch.Logic.Plugin +{ + public interface IPluginFacade + { + Guid PluginId { get; } + + string TabTitle { get; } + + UserControl BuildTabPage(); + + ICriterionPlugin[] GetCriterion(); + + IViewBuilderFactory GetViewBuilderFactory(); + } +} diff --git a/FileSearch/Logic/Plugin/IViewBuilderFactory.cs b/FileSearch/Logic/Plugin/IViewBuilderFactory.cs new file mode 100644 index 0000000..d3350f7 --- /dev/null +++ b/FileSearch/Logic/Plugin/IViewBuilderFactory.cs @@ -0,0 +1,9 @@ +using FileSearch.Logic.UI.ViewBuilders; + +namespace FileSearch.Logic.Plugin +{ + public interface IViewBuilderFactory + { + IViewBuilder CreateViewBuilder(ICriterionPlugin criteria); + } +} diff --git a/FileSearch/Logic/Plugin/Plugins.cs b/FileSearch/Logic/Plugin/Plugins.cs new file mode 100644 index 0000000..35aab4c --- /dev/null +++ b/FileSearch/Logic/Plugin/Plugins.cs @@ -0,0 +1,55 @@ +using System.Reflection; + +namespace FileSearch.Logic.Plugin +{ + internal static class Plugins + { + private static IPluginFacade[] _plugins; + + public static IPluginFacade[] All() + { + return _plugins ?? (_plugins = DetectAll()); + } + + public static IEnumerable Loaded() + { + return All().Where(p => p.PluginId != Guid.Empty); + } + + private static IPluginFacade[] DetectAll() + { + var collection = new List(); + var path = Path.GetDirectoryName(Application.ExecutablePath); + if (path == null) return new IPluginFacade[0]; + + foreach (var file in new DirectoryInfo(path).GetFiles("FileSearch.Plugin.*.dll", SearchOption.TopDirectoryOnly)) + { + try + { + var assembly = Assembly.LoadFile(file.FullName); + var facades = assembly.GetTypes().Where(t => typeof(IPluginFacade).IsAssignableFrom(t)); + foreach (var facade in facades) + { + var instance = (IPluginFacade)Activator.CreateInstance(facade); + + var duplicatePluginId = collection.FirstOrDefault(p => p.PluginId == instance.PluginId); + if (duplicatePluginId != null) + collection.Add(new ErrorPlugin(instance.TabTitle, "The plugin ID is already registered by another plugin named '" + duplicatePluginId.TabTitle + "'.")); + else + collection.Add(instance); + } + } + catch (ReflectionTypeLoadException ex) + { + collection.Add(new ErrorPlugin(Path.GetFileNameWithoutExtension(file.FullName), ex.Message)); + } + catch (Exception ex) + { + collection.Add(new ErrorPlugin(Path.GetFileNameWithoutExtension(file.FullName), ex.Message)); + } + } + + return collection.ToArray(); + } + } +} diff --git a/FileSearch/Logic/UI/Entries/IPathEntry.cs b/FileSearch/Logic/UI/Entries/IPathEntry.cs new file mode 100644 index 0000000..b5c4726 --- /dev/null +++ b/FileSearch/Logic/UI/Entries/IPathEntry.cs @@ -0,0 +1,11 @@ +namespace FileSearch.Logic.UI.Entries +{ + public interface IPathEntry + { + FileSystemInfo FileSystemInfo { get; } + + bool IsDirectory { get; } + + ListViewItem BuildListViewItem(); + } +} diff --git a/FileSearch/Logic/UI/Entries/PathEntry.cs b/FileSearch/Logic/UI/Entries/PathEntry.cs new file mode 100644 index 0000000..5b938d1 --- /dev/null +++ b/FileSearch/Logic/UI/Entries/PathEntry.cs @@ -0,0 +1,32 @@ +namespace FileSearch.Logic.UI.Entries +{ + internal class PathEntry : IPathEntry + { + private readonly FileSystemInfo _fileSystemInfo; + + public PathEntry(FileSystemInfo fileSystemInfo) + { + if (fileSystemInfo == null) throw new ArgumentNullException("fileSystemInfo"); + + _fileSystemInfo = fileSystemInfo; + } + + public FileSystemInfo FileSystemInfo + { + get { return _fileSystemInfo; } + } + + public bool IsDirectory + { + get { return _fileSystemInfo is DirectoryInfo; } + } + + public virtual ListViewItem BuildListViewItem() + { + var name = _fileSystemInfo.FullName; + if (this.IsDirectory) + name = string.Concat("[", name, "]"); + return new ListViewItem(name); + } + } +} diff --git a/FileSearch/Logic/UI/Entries/PathEventArgs.cs b/FileSearch/Logic/UI/Entries/PathEventArgs.cs new file mode 100644 index 0000000..b1920ed --- /dev/null +++ b/FileSearch/Logic/UI/Entries/PathEventArgs.cs @@ -0,0 +1,19 @@ +namespace FileSearch.Logic.UI.Entries +{ + internal class PathEventArgs : EventArgs + { + private readonly PathEntry _entry; + + public PathEventArgs(PathEntry entry) + { + if (entry == null) throw new ArgumentNullException("entry"); + + _entry = entry; + } + + public PathEntry Entry + { + get { return _entry; } + } + } +} diff --git a/FileSearch/Logic/UI/Entries/SearchExceptionEntry.cs b/FileSearch/Logic/UI/Entries/SearchExceptionEntry.cs new file mode 100644 index 0000000..0b674b7 --- /dev/null +++ b/FileSearch/Logic/UI/Entries/SearchExceptionEntry.cs @@ -0,0 +1,22 @@ +using FileSearch.Logic.Model.Engine; + +namespace FileSearch.Logic.UI.Entries +{ + internal sealed class SearchExceptionEntry : ListViewItem + { + private readonly SearchException _exception; + + public SearchExceptionEntry(SearchException exception) + { + _exception = exception; + + this.Text = exception.FileSystemInfo.FullName; + this.SubItems.Add(exception.FriendlyDescription); + } + + public SearchException Exception + { + get { return _exception; } + } + } +} diff --git a/FileSearch/Logic/UI/ViewBuilders/DefaultViewBuilder.cs b/FileSearch/Logic/UI/ViewBuilders/DefaultViewBuilder.cs new file mode 100644 index 0000000..d43b632 --- /dev/null +++ b/FileSearch/Logic/UI/ViewBuilders/DefaultViewBuilder.cs @@ -0,0 +1,20 @@ +using FileSearch.Logic.UI.Entries; + +namespace FileSearch.Logic.UI.ViewBuilders +{ + internal class DefaultViewBuilder : IViewBuilder + { + public IEnumerable Build(Model.Engine.SearchResult entry, int entryIndex) + { + return new[] { new PathEntry(entry.FileSystemInfo) }; + } + + public Tuple[] ColumnSizes + { + get + { + return new[] { new Tuple("Сканируемые директории", -1) }; + } + } + } +} diff --git a/FileSearch/Logic/UI/ViewBuilders/IViewBuilder.cs b/FileSearch/Logic/UI/ViewBuilders/IViewBuilder.cs new file mode 100644 index 0000000..c049cf5 --- /dev/null +++ b/FileSearch/Logic/UI/ViewBuilders/IViewBuilder.cs @@ -0,0 +1,12 @@ +using FileSearch.Logic.Model.Engine; +using FileSearch.Logic.UI.Entries; + +namespace FileSearch.Logic.UI.ViewBuilders +{ + public interface IViewBuilder + { + IEnumerable Build(SearchResult entry, int entryIndex); + + Tuple[] ColumnSizes { get; } + } +} diff --git a/FileSearch/Logic/UI/ViewBuilders/ViewBuilderFactory.cs b/FileSearch/Logic/UI/ViewBuilders/ViewBuilderFactory.cs new file mode 100644 index 0000000..67c2352 --- /dev/null +++ b/FileSearch/Logic/UI/ViewBuilders/ViewBuilderFactory.cs @@ -0,0 +1,12 @@ +namespace FileSearch.Logic.UI.ViewBuilders +{ + internal static class ViewBuilderFactory + { + public static IViewBuilder Create() + { + IViewBuilder builder = null; + + return builder ?? new DefaultViewBuilder(); + } + } +} diff --git a/FileSearch/MainForm.Designer.cs b/FileSearch/MainForm.Designer.cs new file mode 100644 index 0000000..c509659 --- /dev/null +++ b/FileSearch/MainForm.Designer.cs @@ -0,0 +1,340 @@ +namespace FileSearch +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + FileRegexPathTxt = new TextBox(); + SearchBtn = new Button(); + labelNameDir = new Label(); + labelNameFileRegex = new Label(); + DirsTreeView = new TreeView(); + FindFilesTxt = new TextBox(); + AllFilesTxt = new TextBox(); + timerTxt = new TextBox(); + PauseBtn = new Button(); + lstResults = new LargeListViewUserControl(); + statusProgress = new StatusStrip(); + toolStripProgressBar1 = new ToolStripProgressBar(); + statusLabel = new ToolStripStatusLabel(); + statusLabelExceptions = new ToolStripStatusLabel(); + btnBrowse = new Button(); + folderBrowserDialog = new FolderBrowserDialog(); + DirPathTxt = new ComboBox(); + labelInfo1 = new Label(); + labelInfo2 = new Label(); + labelInfo3 = new Label(); + pictureBoxLoading = new PictureBox(); + statusProgress.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)pictureBoxLoading).BeginInit(); + SuspendLayout(); + // + // FileRegexPathTxt + // + FileRegexPathTxt.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + FileRegexPathTxt.Location = new Point(2, 87); + FileRegexPathTxt.Name = "FileRegexPathTxt"; + FileRegexPathTxt.Size = new Size(329, 27); + FileRegexPathTxt.TabIndex = 1; + FileRegexPathTxt.Text = "\\.png"; + // + // SearchBtn + // + SearchBtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + SearchBtn.BackColor = Color.Red; + SearchBtn.Cursor = Cursors.Hand; + SearchBtn.Font = new Font("Segoe UI Symbol", 12F, FontStyle.Bold, GraphicsUnit.Point); + SearchBtn.ForeColor = SystemColors.ControlLightLight; + SearchBtn.Location = new Point(545, 19); + SearchBtn.Margin = new Padding(4); + SearchBtn.Name = "SearchBtn"; + SearchBtn.Size = new Size(139, 40); + SearchBtn.TabIndex = 0; + SearchBtn.Text = "Поиск"; + SearchBtn.UseVisualStyleBackColor = false; + SearchBtn.Click += SearchBtn_Click; + // + // labelNameDir + // + labelNameDir.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + labelNameDir.AutoSize = true; + labelNameDir.Location = new Point(97, 2); + labelNameDir.Name = "labelNameDir"; + labelNameDir.Size = new Size(149, 20); + labelNameDir.TabIndex = 0; + labelNameDir.Text = "Директория поиска"; + // + // labelNameFileRegex + // + labelNameFileRegex.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + labelNameFileRegex.AutoSize = true; + labelNameFileRegex.Location = new Point(59, 64); + labelNameFileRegex.Name = "labelNameFileRegex"; + labelNameFileRegex.Size = new Size(221, 20); + labelNameFileRegex.TabIndex = 4; + labelNameFileRegex.Text = "Шаблон имени файла (Regex)"; + // + // DirsTreeView + // + DirsTreeView.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + DirsTreeView.Location = new Point(0, 124); + DirsTreeView.Name = "DirsTreeView"; + DirsTreeView.Size = new Size(684, 430); + DirsTreeView.TabIndex = 9; + DirsTreeView.NodeMouseDoubleClick += DirsTreeView_NodeMouseDoubleClick; + // + // FindFilesTxt + // + FindFilesTxt.Anchor = AnchorStyles.Top | AnchorStyles.Right; + FindFilesTxt.Enabled = false; + FindFilesTxt.Location = new Point(374, 26); + FindFilesTxt.Name = "FindFilesTxt"; + FindFilesTxt.Size = new Size(72, 27); + FindFilesTxt.TabIndex = 10; + FindFilesTxt.Text = "0"; + FindFilesTxt.TextAlign = HorizontalAlignment.Center; + // + // AllFilesTxt + // + AllFilesTxt.Anchor = AnchorStyles.Top | AnchorStyles.Right; + AllFilesTxt.BackColor = Color.White; + AllFilesTxt.BorderStyle = BorderStyle.FixedSingle; + AllFilesTxt.Enabled = false; + AllFilesTxt.Location = new Point(452, 26); + AllFilesTxt.Name = "AllFilesTxt"; + AllFilesTxt.Size = new Size(86, 27); + AllFilesTxt.TabIndex = 11; + AllFilesTxt.Text = "0"; + AllFilesTxt.TextAlign = HorizontalAlignment.Center; + // + // timerTxt + // + timerTxt.Anchor = AnchorStyles.Top | AnchorStyles.Right; + timerTxt.Enabled = false; + timerTxt.Font = new Font("Arial", 14.25F, FontStyle.Bold, GraphicsUnit.Point); + timerTxt.Location = new Point(384, 85); + timerTxt.Name = "timerTxt"; + timerTxt.Size = new Size(129, 29); + timerTxt.TabIndex = 12; + timerTxt.Text = "0"; + timerTxt.TextAlign = HorizontalAlignment.Center; + // + // PauseBtn + // + PauseBtn.Anchor = AnchorStyles.Top | AnchorStyles.Right; + PauseBtn.BackColor = Color.Red; + PauseBtn.Font = new Font("Segoe UI Symbol", 12F, FontStyle.Bold, GraphicsUnit.Point); + PauseBtn.ForeColor = SystemColors.ControlLightLight; + PauseBtn.Location = new Point(545, 74); + PauseBtn.Margin = new Padding(4); + PauseBtn.Name = "PauseBtn"; + PauseBtn.Size = new Size(139, 40); + PauseBtn.TabIndex = 13; + PauseBtn.Text = "Пауза"; + PauseBtn.UseVisualStyleBackColor = false; + PauseBtn.Click += PauseBtn_Click; + // + // lstResults + // + lstResults.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + lstResults.Location = new Point(2, 560); + lstResults.Name = "lstResults"; + lstResults.Size = new Size(684, 376); + lstResults.TabIndex = 4; + lstResults.FileOpened += MainForm_FileOpened; + lstResults.DirectoryOpened += MainForm_DirectoryOpened; + // + // statusProgress + // + statusProgress.Items.AddRange(new ToolStripItem[] { toolStripProgressBar1, statusLabel, statusLabelExceptions }); + statusProgress.Location = new Point(0, 939); + statusProgress.MinimumSize = new Size(682, 0); + statusProgress.Name = "statusProgress"; + statusProgress.Size = new Size(684, 22); + statusProgress.TabIndex = 14; + statusProgress.Text = "statusStrip1"; + // + // toolStripProgressBar1 + // + toolStripProgressBar1.Name = "toolStripProgressBar1"; + toolStripProgressBar1.Size = new Size(200, 16); + // + // statusLabel + // + statusLabel.Margin = new Padding(50, 3, 50, 2); + statusLabel.Name = "statusLabel"; + statusLabel.Size = new Size(31, 17); + statusLabel.Text = "2024"; + // + // statusLabelExceptions + // + statusLabelExceptions.AutoToolTip = true; + statusLabelExceptions.BackColor = Color.DodgerBlue; + statusLabelExceptions.ForeColor = Color.White; + statusLabelExceptions.Margin = new Padding(20, 3, 0, 2); + statusLabelExceptions.Name = "statusLabelExceptions"; + statusLabelExceptions.Size = new Size(54, 17); + statusLabelExceptions.Text = "Ошибки"; + statusLabelExceptions.ToolTipText = "Открыть панель ошибок"; + statusLabelExceptions.Click += statusLabelExceptions_Click; + // + // btnBrowse + // + btnBrowse.Anchor = AnchorStyles.Top | AnchorStyles.Right; + btnBrowse.BackColor = Color.DodgerBlue; + btnBrowse.Cursor = Cursors.Hand; + btnBrowse.FlatStyle = FlatStyle.Popup; + btnBrowse.ForeColor = Color.White; + btnBrowse.Location = new Point(337, 24); + btnBrowse.Name = "btnBrowse"; + btnBrowse.Size = new Size(31, 29); + btnBrowse.TabIndex = 16; + btnBrowse.Text = "..."; + btnBrowse.TextAlign = ContentAlignment.TopCenter; + btnBrowse.UseVisualStyleBackColor = false; + btnBrowse.Click += btnBrowse_Click; + // + // DirPathTxt + // + DirPathTxt.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + DirPathTxt.FormattingEnabled = true; + DirPathTxt.Location = new Point(0, 25); + DirPathTxt.Name = "DirPathTxt"; + DirPathTxt.Size = new Size(331, 28); + DirPathTxt.TabIndex = 17; + DirPathTxt.TextChanged += DirPathTxt_TextChanged; + // + // labelInfo1 + // + labelInfo1.Anchor = AnchorStyles.Top | AnchorStyles.Right; + labelInfo1.AutoSize = true; + labelInfo1.Location = new Point(374, 2); + labelInfo1.Name = "labelInfo1"; + labelInfo1.Size = new Size(72, 20); + labelInfo1.TabIndex = 18; + labelInfo1.Text = "Найдено"; + // + // labelInfo2 + // + labelInfo2.Anchor = AnchorStyles.Top | AnchorStyles.Right; + labelInfo2.AutoSize = true; + labelInfo2.Location = new Point(472, 2); + labelInfo2.Name = "labelInfo2"; + labelInfo2.Size = new Size(48, 20); + labelInfo2.TabIndex = 19; + labelInfo2.Text = "Всего"; + // + // labelInfo3 + // + labelInfo3.Anchor = AnchorStyles.Top | AnchorStyles.Right; + labelInfo3.AutoSize = true; + labelInfo3.Location = new Point(421, 62); + labelInfo3.Name = "labelInfo3"; + labelInfo3.Size = new Size(54, 20); + labelInfo3.TabIndex = 20; + labelInfo3.Text = "Время"; + // + // pictureBoxLoading + // + pictureBoxLoading.Anchor = AnchorStyles.Top | AnchorStyles.Right; + pictureBoxLoading.Image = Resource.Loading_icon; + pictureBoxLoading.Location = new Point(461, 27); + pictureBoxLoading.Name = "pictureBoxLoading"; + pictureBoxLoading.Size = new Size(68, 25); + pictureBoxLoading.SizeMode = PictureBoxSizeMode.StretchImage; + pictureBoxLoading.TabIndex = 21; + pictureBoxLoading.TabStop = false; + pictureBoxLoading.Visible = false; + // + // MainForm + // + AutoScaleMode = AutoScaleMode.None; + BackColor = Color.FromArgb(255, 224, 192); + ClientSize = new Size(684, 961); + Controls.Add(pictureBoxLoading); + Controls.Add(labelInfo3); + Controls.Add(labelInfo2); + Controls.Add(labelInfo1); + Controls.Add(DirPathTxt); + Controls.Add(btnBrowse); + Controls.Add(statusProgress); + Controls.Add(PauseBtn); + Controls.Add(timerTxt); + Controls.Add(AllFilesTxt); + Controls.Add(FindFilesTxt); + Controls.Add(DirsTreeView); + Controls.Add(labelNameFileRegex); + Controls.Add(labelNameDir); + Controls.Add(FileRegexPathTxt); + Controls.Add(SearchBtn); + Controls.Add(lstResults); + Font = new Font("Segoe UI Semibold", 11.25F, FontStyle.Bold, GraphicsUnit.Point); + Margin = new Padding(4); + MaximizeBox = false; + MaximumSize = new Size(1200, 1200); + MdiChildrenMinimizedAnchorBottom = false; + MinimizeBox = false; + MinimumSize = new Size(700, 1000); + Name = "MainForm"; + ShowIcon = false; + StartPosition = FormStartPosition.CenterScreen; + Text = "Поиск файлов на диске"; + FormClosed += MainForm_FormClosed; + Load += MainForm_Load; + FileOpened += MainForm_FileOpened; + DirectoryOpened += MainForm_DirectoryOpened; + statusProgress.ResumeLayout(false); + statusProgress.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)pictureBoxLoading).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + private TextBox FileRegexPathTxt; + private Button SearchBtn; + private Label labelNameDir; + private Label labelNameFileRegex; + private TreeView DirsTreeView; + private TextBox FindFilesTxt; + private TextBox AllFilesTxt; + private TextBox timerTxt; + private Button PauseBtn; + private LargeListViewUserControl lstResults; + private StatusStrip statusProgress; + private ToolStripProgressBar toolStripProgressBar1; + private Button btnBrowse; + private FolderBrowserDialog folderBrowserDialog; + private ComboBox DirPathTxt; + private ToolStripStatusLabel statusLabel; + private Label labelInfo1; + private Label labelInfo2; + private Label labelInfo3; + private ToolStripStatusLabel statusLabelExceptions; + private PictureBox pictureBoxLoading; + } +} \ No newline at end of file diff --git a/FileSearch/MainForm.cs b/FileSearch/MainForm.cs new file mode 100644 index 0000000..9e759a1 --- /dev/null +++ b/FileSearch/MainForm.cs @@ -0,0 +1,450 @@ +using FileSearch.Logic; +using FileSearch.Logic.Model.Engine; +using FileSearch.Logic.Model.Tree; +using FileSearch.Logic.Plugin; +using FileSearch.Logic.UI.Entries; +using FileSearch.Logic.UI.ViewBuilders; +using System.Diagnostics; +using System.Globalization; + +namespace FileSearch +{ + internal partial class MainForm : Form + { + private ExceptionsForm _exceptionsForm; + + private event EventHandler FileOpened; + private event EventHandler DirectoryOpened; + + private static FileSearcher _fileSearcher; + private bool _resultsViewIsUpdated = false; + + private static CancellationTokenSource _tokenGetFilesSourceSearch = new CancellationTokenSource(); + + private string _searchPath = string.Empty; + + public MainForm() => InitializeComponent(); + + #region Logic + + /// + /// + /// + private DirectoryInfo SearchInit() + { + toolStripProgressBar1.Style = ProgressBarStyle.Marquee; + lstResults.ClearContent(); + _resultsViewIsUpdated = false; + DirsTreeView.Controls.Clear(); + DirsTreeView.Nodes.Clear(); + _tokenGetFilesSourceSearch.Cancel(); + + SaveInfo(comboBox: true); + + SearchBtn.Text = @""; + statusLabel.Text = @"... ."; + statusLabelExceptions.Text = null; + return new DirectoryInfo(DirPathTxt.Text.Trim('\\') + "\\"); + } + + /// + /// + /// + /// searchResults + private void LoadList(IEnumerable searchResults) + { + if (lstResults.InvokeRequired) + { + lstResults.BeginInvoke((Action>)LoadList, searchResults); + return; + } + + if (!_resultsViewIsUpdated) + { + lstResults.SetViewBuilder(ViewBuilderFactory.Create()); + _resultsViewIsUpdated = true; + } + + lstResults.AddSearchResults(searchResults); + AddTimerTextInfo(_fileSearcher.CurrentTime.GetFriendlyNotation()); // + AddFindFilesCount(lstResults.Count); // + AddThreeViewData(searchResults); // + } + + /// + /// + /// + private void LoadListFinished() + { + if (lstResults.InvokeRequired) + { + lstResults.Invoke((Action)LoadListFinished); + return; + } + + var exceptions = _fileSearcher.Exceptions; + if (exceptions.Count > 0) + statusLabelExceptions.Text = string.Format(CultureInfo.InvariantCulture, "{0} ", exceptions.Count); + + SearchBtn.Text = @""; + PauseBtn.Text = @""; + SearchBtn.Enabled = true; + + statusLabel.Text = @" " + _fileSearcher.OperatingTime.GetFriendlyNotation() + @". : " + lstResults.Count; + toolStripProgressBar1.Style = ProgressBarStyle.Blocks; + if (DirsTreeView.Nodes.Count > 0) + DirsTreeView.Focus(); + } + + /// + /// + /// + /// TODO: + /// UI Control + /// + /// directoryInfo + /// text + /// + private EngineOptions BuildOptions(DirectoryInfo directoryInfo, string text) + { + var options = new EngineOptions(new[] { directoryInfo }) + { + SearchName = text, + SearchIncludesFolders = true, + SearchNameIgnoreCasing = false, + SearchNameMatchFullPath = false, + SearchRecursive = true, + SearchNameAsRegularExpression = true, + SearchInArchives = false, + ContentAsRegularExpression = true, + ContentText = string.Empty, + ContentIgnoreCasing = true, + ContentWholeWordsOnly = false, + ContentEncodingFactory = null, + ContentForOfficeXml = false + }; + + return options; + } + + /// + /// + /// + /// IEnumerable + private void AddThreeViewData(IEnumerable searchResults) + { + TreeViewBuilder.BuildTreeView(DirsTreeView, searchResults.Select(res => res.FileSystemInfo.FullName).ToArray()); + } + + /// + /// + /// + private void LoadingMaxFiles() + { + if (_searchPath == DirPathTxt.Text) return; + _searchPath = DirPathTxt.Text; + + var task = Task.Run(async () => + { + AddProgressBarLoadMax(true); + await GetAllFilesExecute(_searchPath); + }); + + } + + /// + /// async + /// + /// path + public async Task GetAllFilesExecute(string searchPath) + { + AddMaxFilesTxtInfo("0"); + await Task.Factory.StartNew(() => + { + GetAllFiles(searchPath); + }); + } + + /// + /// ( ) + /// + /// path + private void GetAllFiles(string searchPath) + { + long count = 0; + + _tokenGetFilesSourceSearch = new CancellationTokenSource(); + + var searchTask = Task.Run(async () => + { + try + { + count = await GetFileCountAsync(searchPath, _tokenGetFilesSourceSearch.Token); + } + catch (OperationCanceledException) + { + // + AddMaxFilesTxtInfo(count.ToString()); + AddProgressBarLoadMax(false); + } + }, _tokenGetFilesSourceSearch.Token); + + searchTask.ContinueWith(t => + { + searchTask = null; + AddMaxFilesTxtInfo(count.ToString()); + AddProgressBarLoadMax(false); + }); + } + + public async Task GetFileCountAsync(string directory, CancellationToken cancellationToken) + { + try + { + long count = 0; + + foreach (var file in Directory.GetFiles(directory)) + { + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + count++; + } + + foreach (var subDir in Directory.GetDirectories(directory)) + { + if (cancellationToken.IsCancellationRequested) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + count += await GetFileCountAsync(subDir, cancellationToken); + } + + return count; + } + catch (OperationCanceledException) + { + throw; // + } + catch (UnauthorizedAccessException) + { + return 0; // , + } + catch (DirectoryNotFoundException) + { + return 0; // + } + } + + /// + /// () + /// + /// comboBox + private void SaveInfo(bool comboBox = false) + { + File.WriteAllText(@"DirInfo.dat", DirPathTxt.Text); + File.WriteAllText(@"FileRegex.dat", FileRegexPathTxt.Text); + + // + if (comboBox) + { + var content = DirPathTxt.SelectedItem != null ? (string)DirPathTxt.SelectedItem : DirPathTxt.Text; + if (!string.IsNullOrEmpty(content)) + { + if (DirPathTxt.Items.Contains(content)) + DirPathTxt.Items.Remove(content); + DirPathTxt.Items.Insert(0, content); + DirPathTxt.SelectedItem = 0; + DirPathTxt.Text = content; + } + } + } + + #endregion + + #region Events UI + + private void MainForm_Load(object sender, EventArgs e) + { + if (File.Exists(@"DirInfo.dat")) + DirPathTxt.Text = File.ReadAllText(@"DirInfo.dat"); + if (File.Exists(@"FileRegex.dat")) + FileRegexPathTxt.Text = File.ReadAllText(@"FileRegex.dat"); + } + + + private void MainForm_FormClosed(object sender, FormClosedEventArgs e) + { + SaveInfo(); + _tokenGetFilesSourceSearch.Cancel(); + } + + private void MainForm_DirectoryOpened(object? sender, PathEventArgs e) + { + Process.Start(new ProcessStartInfo("explorer.exe", "\"" + e.Entry.FileSystemInfo.FullName + "\"")); + } + + private void MainForm_FileOpened(object? sender, PathEventArgs e) + { + var file = e.Entry.FileSystemInfo.FullName; + + Process.Start(new ProcessStartInfo("explorer.exe", file)); + } + + private void SearchBtn_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(DirPathTxt.Text)) return; + + if (_fileSearcher != null && _fileSearcher.IsRunning) + { + _fileSearcher.Stop(); + SearchBtn.Enabled = false; + return; + } + + var dirInfo = SearchInit(); + if (!dirInfo.Exists) + { + MessageBox.Show(@" .", @"Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + LoadingMaxFiles(); + _fileSearcher = new FileSearcher(BuildOptions(dirInfo, FileRegexPathTxt.Text), Plugins.All().Select(f => f.GetCriterion()).Where(f => f != null).SelectMany(f => f)); + _fileSearcher.Start(LoadList, LoadListFinished); + } + + private void PauseBtn_Click(object sender, EventArgs e) + { + _fileSearcher?.Pause((state) => + { + PauseBtn.Text = (state) ? "" : ""; + }, (PauseBtn.Text == "") ? false : true); + } + + private void DirPathTxt_TextChanged(object sender, EventArgs e) + { + DirPathTxt.ForeColor = Directory.Exists(DirPathTxt.Text) ? SystemColors.WindowText : Color.Red; + } + + + private void DirsTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) + { + TreeNode node = e.Node; + var paths = e.Node.FullPath; + DirectoryInfo dir = new DirectoryInfo(paths); + FileSystemInfo fsi = dir as FileSystemInfo; + + var path = new PathEntry(fsi); + + if (path.IsDirectory) + { + if (DirectoryOpened != null) + DirectoryOpened(this, new PathEventArgs(path)); + } + else if (FileOpened != null) + FileOpened(this, new PathEventArgs(path)); + + } + + private void btnBrowse_Click(object sender, EventArgs e) + { + if (_fileSearcher != null && _fileSearcher.IsRunning) + { + MessageBox.Show(" !"); + return; + } + + folderBrowserDialog.SelectedPath = DirPathTxt.Text; + if (folderBrowserDialog.ShowDialog() == DialogResult.OK) + { + // Set the selected index to -1 so when the text is set the index is forgotten. + DirPathTxt.SelectedIndex = -1; + DirPathTxt.Text = folderBrowserDialog.SelectedPath; + } + } + + private void statusLabelExceptions_Click(object sender, EventArgs e) + { + if (_fileSearcher == null || _fileSearcher.Exceptions.Count <= 0) + return; + + var form = _exceptionsForm ?? (_exceptionsForm = new ExceptionsForm()); + form.SetContent(_fileSearcher.Exceptions); + form.ShowDialog(); + } + + #endregion + + #region Async UI + + /// + /// AllFilesTxt + /// + /// count + private void AddMaxFilesTxtInfo(string text) + { + if (string.IsNullOrEmpty(text)) { return; } + if (InvokeRequired) + { + Invoke((Action)AddMaxFilesTxtInfo, text); + return; + } + AllFilesTxt.Text = text; + AllFilesTxt.Update(); + } + + /// + /// pictureBoxLoading + /// + /// bool + private void AddProgressBarLoadMax(bool visible) + { + if (InvokeRequired) + { + Invoke((Action)AddProgressBarLoadMax, visible); + return; + } + pictureBoxLoading.Visible = visible; + pictureBoxLoading.Update(); + pictureBoxLoading.Refresh(); + } + + /// + /// timerTxt + /// + /// count + private void AddTimerTextInfo(string text) + { + if (string.IsNullOrEmpty(text)) { return; } + if (InvokeRequired) + { + Invoke((Action)AddTimerTextInfo, text); + return; + } + timerTxt.Text = text; + timerTxt.Update(); + } + + /// + /// FindFilesTxt + /// + /// count + private void AddFindFilesCount(int count) + { + if (InvokeRequired) + { + Invoke((Action)AddFindFilesCount, count); + return; + } + + FindFilesTxt.Text = count.ToString(); + FindFilesTxt.Update(); + } + + #endregion + } +} \ No newline at end of file diff --git a/FileSearch/MainForm.resx b/FileSearch/MainForm.resx new file mode 100644 index 0000000..d3728a9 --- /dev/null +++ b/FileSearch/MainForm.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 147, 17 + + \ No newline at end of file diff --git a/FileSearch/Program.cs b/FileSearch/Program.cs new file mode 100644 index 0000000..1f188fc --- /dev/null +++ b/FileSearch/Program.cs @@ -0,0 +1,17 @@ +namespace FileSearch +{ + internal static class Program + { + /// + /// . + /// + [STAThread] + static void Main() + { + // , , + // https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } + } +} \ No newline at end of file diff --git a/FileSearch/Resource.Designer.cs b/FileSearch/Resource.Designer.cs new file mode 100644 index 0000000..057ceb5 --- /dev/null +++ b/FileSearch/Resource.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// Этот код создан программой. +// Исполняемая версия:4.0.30319.42000 +// +// Изменения в этом файле могут привести к неправильной работе и будут потеряны в случае +// повторной генерации кода. +// +//------------------------------------------------------------------------------ + +namespace FileSearch { + using System; + + + /// + /// Класс ресурса со строгой типизацией для поиска локализованных строк и т.д. + /// + // Этот класс создан автоматически классом StronglyTypedResourceBuilder + // с помощью такого средства, как ResGen или Visual Studio. + // Чтобы добавить или удалить член, измените файл .ResX и снова запустите ResGen + // с параметром /str или перестройте свой проект VS. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// Возвращает кэшированный экземпляр ResourceManager, использованный этим классом. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FileSearch.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Перезаписывает свойство CurrentUICulture текущего потока для всех + /// обращений к ресурсу с помощью этого класса ресурса со строгой типизацией. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Поиск локализованного ресурса типа System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Loading_icon { + get { + object obj = ResourceManager.GetObject("Loading_icon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/FileSearch/Resource.resx b/FileSearch/Resource.resx new file mode 100644 index 0000000..b9767e4 --- /dev/null +++ b/FileSearch/Resource.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\Loading_icon.gif;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/FileSearch/Resources/Loading_icon.gif b/FileSearch/Resources/Loading_icon.gif new file mode 100644 index 0000000..3f90951 Binary files /dev/null and b/FileSearch/Resources/Loading_icon.gif differ diff --git a/GIT_Media/Wp69ZvG87t.png b/GIT_Media/Wp69ZvG87t.png new file mode 100644 index 0000000..fef4ee3 Binary files /dev/null and b/GIT_Media/Wp69ZvG87t.png differ diff --git a/GIT_Media/view.gif b/GIT_Media/view.gif new file mode 100644 index 0000000..833818f Binary files /dev/null and b/GIT_Media/view.gif differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1172ddd --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +

+ BetterCap +

+ Static Badge +

+

+ +# Проводник +> +Проводник для Windows +> + +## 🌐 **Что внутри?** 🌐 + +> Критерии: +1. Стартовая директория (с которой начинается поиск) +2. Шаблон имени файла в виде regex выражения + +

+ +

+ +> Что отображает в режиме реального времени: + +➡️1. Все найденные по критериям файлы +в виде дерева (как в левой части проводника). +Дерево не должно подвисать, моргать, тормозить и т.д. +Во время поиска пользователь может ходить по дереву, открывать/закрывать. + + +➡️2. Название директории, в которой идет текущий поиск + + +➡️3. Количество найденных и общее количество файлов. (Примечательно что поиск всех файлов в той или иной директории может затянуться в связи с чем число файлов появляется позднее чем заканчивается основной поиск файлов) + +➡️4. Прошедшее от начала запуска поиска время + +

+ +

+ +> Особенности: + +◀️1. Пользователь имеет возможность остановить поиск в любой момент и затем +либо продолжить его либо начать новый поиск. + +--- +В дополнение к пунктам выше ◀️ При нажатии на синюю строку с числом ошибок которые были обнаружены - откроется форма которая выведет их список. + +### 📜 Установка +--- + +📏 Visual Studio 2022 Professional +- +📏 .NET 6.0 +- \ No newline at end of file