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 @@
+
+
+
+
+
+
+
+# Проводник
+>
+Проводник для Windows
+>
+
+## 🌐 **Что внутри?** 🌐
+
+> Критерии:
+1. Стартовая директория (с которой начинается поиск)
+2. Шаблон имени файла в виде regex выражения
+
+
+
+
+
+> Что отображает в режиме реального времени:
+
+➡️1. Все найденные по критериям файлы
+в виде дерева (как в левой части проводника).
+Дерево не должно подвисать, моргать, тормозить и т.д.
+Во время поиска пользователь может ходить по дереву, открывать/закрывать.
+
+
+➡️2. Название директории, в которой идет текущий поиск
+
+
+➡️3. Количество найденных и общее количество файлов. (Примечательно что поиск всех файлов в той или иной директории может затянуться в связи с чем число файлов появляется позднее чем заканчивается основной поиск файлов)
+
+➡️4. Прошедшее от начала запуска поиска время
+
+
+
+
+
+> Особенности:
+
+◀️1. Пользователь имеет возможность остановить поиск в любой момент и затем
+либо продолжить его либо начать новый поиск.
+
+---
+В дополнение к пунктам выше ◀️ При нажатии на синюю строку с числом ошибок которые были обнаружены - откроется форма которая выведет их список.
+
+### 📜 Установка
+---
+
+📏 Visual Studio 2022 Professional
+-
+📏 .NET 6.0
+-
\ No newline at end of file