Horizon – Rev 17
?pathlinks?
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data.SQLite;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows;
using System.Windows.Forms;
using Configuration;
using Horizon.Database;
using Horizon.Notifications.Gotify;
using Horizon.Snapshots;
using Horizon.Utilities;
using Horizon.Utilities.Serialization;
using Mono.Zeroconf;
using NetSparkleUpdater;
using NetSparkleUpdater.Enums;
using NetSparkleUpdater.SignatureVerifiers;
using NetSparkleUpdater.UI.WinForms;
using Newtonsoft.Json;
using Serilog;
using TrackedFolders;
using WatsonTcp;
using Newtonsoft;
using static Horizon.Utilities.Networking.Miscellaneous;
using CaptureMode = Configuration.CaptureMode;
using Path = System.IO.Path;
using System.Text;
using Tesseract;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Horizon.Searching;
namespace Horizon
{
public partial class MainForm : Form
{
#region Static Fields and Constants
private static SemaphoreSlim _changedFilesLock;
private static HashSet<string> _changedFiles;
private static ScheduledContinuation _changedFilesContinuation;
private static readonly LogMemorySink _memorySink = new LogMemorySink();
#endregion
#region Private Delegates, Events, Enums, Properties, Indexers and Fields
private ScheduledContinuation _changedConfigurationContinuation;
private ScheduledContinuation _trackedFoldersChangedContinuation;
private Configuration.Configuration _configuration;
private TrackedFolders.TrackedFolders _trackedFolders;
private readonly ConcurrentQueue<FileSystemWatcher> _fileSystemWatchers;
private readonly CancellationToken _cancellationToken;
private static HttpClient _httpClient;
private readonly CancellationTokenSource _cancellationTokenSource;
private AboutForm _aboutForm;
private ManageFoldersForm _manageFoldersForm;
private readonly SnapshotDatabase _snapshotDatabase;
private SnapshotManagerForm _snapshotManagerForm;
private SparkleUpdater _sparkle;
private LogViewForm _logViewForm;
private static JsonSerializer _jsonSerializer;
private RegisterService _horizonDiscoveryService;
private WatsonTcpServer _horizonNetworkShare;
private NotifyFilters _fileSystemWatchersNotifyFilters = NotifyFilters.LastWrite | NotifyFilters.Attributes;
private readonly bool _memorySinkEnabled = true;
private SearchEngine _searchEngine;
#endregion
#region Constructors, Destructors and Finalizers
public MainForm()
{
InitializeComponent();
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
_jsonSerializer = new JsonSerializer();
_httpClient = new HttpClient();
_trackedFoldersChangedContinuation = new ScheduledContinuation();
_changedFilesLock = new SemaphoreSlim(1, 1);
_fileSystemWatchers = new ConcurrentQueue<FileSystemWatcher>();
_changedFiles = new HashSet<string>();
_changedFilesContinuation = new ScheduledContinuation();
_changedConfigurationContinuation = new ScheduledContinuation();
}
public MainForm(Mutex mutex) : this()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Conditional(condition => _memorySinkEnabled, configureSink => configureSink.Sink(_memorySink))
.WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"),
rollingInterval: RollingInterval.Day)
.CreateLogger();
_snapshotDatabase = new SnapshotDatabase(_cancellationToken);
_snapshotDatabase.SnapshotRevert += SnapshotDatabase_SnapshotRevert;
_snapshotDatabase.SnapshotCreate += SnapshotDatabase_SnapshotCreateAsync;
_snapshotDatabase.SnapshotTransferReceived += SnapshotDatabase_SnapshotTransferReceived;
_snapshotDatabase.SnapshotNoteUpdate += _snapshotDatabase_SnapshotNoteUpdate;
_snapshotDatabase.SnapshotDataUpdate += _snapshotDatabase_SnapshotDataUpdate;
_trackedFolders = new TrackedFolders.TrackedFolders();
_trackedFolders.Folder.CollectionChanged += Folder_CollectionChanged;
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
toolStripMenuItem3.DropDown.Closing -= toolStropMenuItem3DropDown_Closing;
eventsToolStripMenuItem.DropDown.Closing -= eventsToolStripMenuItem_Closing;
components.Dispose();
}
_cancellationTokenSource.Cancel();
_configuration.PropertyChanged -= _configuration_PropertyChanged;
_snapshotDatabase.SnapshotNoteUpdate -= _snapshotDatabase_SnapshotNoteUpdate;
_snapshotDatabase.SnapshotDataUpdate -= _snapshotDatabase_SnapshotDataUpdate;
_snapshotDatabase.SnapshotRevert -= SnapshotDatabase_SnapshotRevert;
_snapshotDatabase.SnapshotCreate -= SnapshotDatabase_SnapshotCreateAsync;
_snapshotDatabase.SnapshotTransferReceived -= SnapshotDatabase_SnapshotTransferReceived;
_snapshotDatabase.Dispose();
base.Dispose(disposing);
}
private void eventsToolStripMenuItem_Closing(object sender, ToolStripDropDownClosingEventArgs e)
{
if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked)
{
e.Cancel = true;
}
}
private void toolStropMenuItem3DropDown_Closing(object sender, ToolStripDropDownClosingEventArgs e)
{
if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked)
{
e.Cancel = true;
}
}
#endregion
#region Event Handlers
private void autoNotesToolStripMenuItem_Click(object sender, EventArgs e)
{
_configuration.AutoNotes = ((ToolStripMenuItem)sender).Checked;
}
private void gotfyToolStripTextBox_TextChanged(object sender, EventArgs e)
{
var toolStripTextBox = ((ToolStripTextBox)sender);
_configuration.GotifyURL = toolStripTextBox.Text;
}
private void eventsToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
{
var toolStripMenuItem = (ToolStripMenuItem)sender;
var text = toolStripMenuItem.Text;
var state = toolStripMenuItem.CheckState;
foreach (var flag in Enum.GetNames(typeof(NotifyEvent)))
{
if (string.Equals(flag, text, StringComparison.OrdinalIgnoreCase))
{
if (Enum.TryParse<NotifyEvent>(flag, true, out var setting))
{
switch (state)
{
case CheckState.Checked:
_configuration.NotifyEvents = _configuration.NotifyEvents | setting;
break;
case CheckState.Unchecked:
_configuration.NotifyEvents = _configuration.NotifyEvents & ~setting;
break;
}
}
}
}
}
private void gotifyToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
_configuration.EnableGotify = ((ToolStripMenuItem)sender).Checked;
}
private void attributesToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
{
var toolStripMenuItem = (ToolStripMenuItem)sender;
var text = toolStripMenuItem.Text;
var state = toolStripMenuItem.CheckState;
foreach (var flag in Enum.GetNames(typeof(NotifyFilters)))
{
if (string.Equals(flag, text, StringComparison.OrdinalIgnoreCase))
{
if (Enum.TryParse<NotifyFilters>(flag, true, out var setting))
{
switch (state)
{
case CheckState.Checked:
_fileSystemWatchersNotifyFilters = _fileSystemWatchersNotifyFilters | setting;
break;
case CheckState.Unchecked:
_fileSystemWatchersNotifyFilters = _fileSystemWatchersNotifyFilters & ~setting;
break;
}
}
}
}
_configuration.NotifyFilters = _fileSystemWatchersNotifyFilters;
}
private void networkSharingToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
{
var toolStripMenuItem = (ToolStripMenuItem)sender;
switch (toolStripMenuItem.CheckState)
{
case CheckState.Checked:
var freePort = GetAvailableTcpPort();
_horizonNetworkShare = new WatsonTcpServer("0.0.0.0", freePort);
_horizonNetworkShare.Events.ClientConnected += Events_ClientConnected;
_horizonNetworkShare.Events.ClientDisconnected += Events_ClientDisconnected;
_horizonNetworkShare.Events.MessageReceived += Events_MessageReceived;
_horizonNetworkShare.Events.ExceptionEncountered += Events_ExceptionEncountered;
#pragma warning disable CS4014
_horizonNetworkShare.Start();
#pragma warning restore CS4014
try
{
_horizonDiscoveryService = new RegisterService();
_horizonDiscoveryService.Name = $"Horizon ({Environment.MachineName})";
_horizonDiscoveryService.RegType = "_horizon._tcp";
_horizonDiscoveryService.ReplyDomain = "local.";
_horizonDiscoveryService.UPort = freePort;
_horizonDiscoveryService.Register();
}
catch (Exception exception)
{
Log.Error(exception, "Service discovery protocol could not be stared.");
}
_configuration.NetworkSharing = true;
break;
case CheckState.Unchecked:
if (_horizonNetworkShare != null)
{
_horizonNetworkShare.Events.ClientConnected -= Events_ClientConnected;
_horizonNetworkShare.Events.ClientDisconnected -= Events_ClientDisconnected;
_horizonNetworkShare.Events.MessageReceived -= Events_MessageReceived;
_horizonNetworkShare.Events.ExceptionEncountered -= Events_ExceptionEncountered;
_horizonNetworkShare.Dispose();
_horizonNetworkShare = null;
}
if (_horizonDiscoveryService != null)
{
_horizonDiscoveryService.Dispose();
_horizonDiscoveryService = null;
}
_configuration.NetworkSharing = false;
break;
}
}
private void Events_ExceptionEncountered(object sender, ExceptionEventArgs e)
{
Log.Error(e.Exception,$"Client threw exception.");
}
private async void Events_MessageReceived(object sender, MessageReceivedEventArgs e)
{
Log.Information($"Client {e.Client?.IpPort} sent {e.Data?.Length} bytes via network sharing.");
if (e.Data?.Length == 0)
{
return;
}
try
{
//var payload = Encoding.UTF8.GetString();
using var memoryStream = new MemoryStream(e.Data);
using var streamReader = new StreamReader(memoryStream);
using var jsonTextReader = new JsonTextReader(streamReader);
var completeSnapshot = _jsonSerializer.Deserialize<Snapshot>(jsonTextReader);
await _snapshotDatabase.ApplySnapshotAsync(completeSnapshot, _cancellationToken);
Log.Information($"Stored {completeSnapshot.Name} from {e.Client?.IpPort}");
}
catch (Exception exception)
{
Log.Error(exception, $"Failed to process network share from {e.Client?.IpPort}.");
}
}
private void Events_ClientDisconnected(object sender, WatsonTcp.DisconnectionEventArgs e)
{
Log.Information($"Client {e.Client?.IpPort} disconnected from network sharing.");
}
private void Events_ClientConnected(object sender, WatsonTcp.ConnectionEventArgs e)
{
Log.Information($"Client {e.Client?.IpPort} connected to network sharing.");
}
private void WindowToolStripMenuItem_Click(object sender, EventArgs e)
{
windowToolStripMenuItem.Checked = true;
screenToolStripMenuItem.Checked = false;
_configuration.CaptureMode = CaptureMode.Window;
}
private void ScreenToolStripMenuItem_Click(object sender, EventArgs e)
{
screenToolStripMenuItem.Checked = true;
windowToolStripMenuItem.Checked = false;
_configuration.CaptureMode = CaptureMode.Screen;
}
private void LogViewToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_logViewForm != null)
{
return;
}
_logViewForm = new LogViewForm(this, _memorySink, _cancellationToken);
_logViewForm.Closing += LogViewFormClosing;
_logViewForm.Show();
}
private void LogViewFormClosing(object sender, CancelEventArgs e)
{
if (_logViewForm == null)
{
return;
}
_logViewForm.Closing -= LogViewFormClosing;
_logViewForm.Close();
_logViewForm = null;
}
private async void SnapshotDatabase_SnapshotCreateAsync(object sender, SnapshotCreateEventArgs e)
{
switch (e)
{
case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
var snapshot = snapshotCreateSuccessEventArgs.Snapshot;
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Snapshot Created", $"Took a snapshot of {snapshot.Path}.",
5000);
}
if(_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Create))
{
await SendGotifyNotification("Snapshot Created", $"Took a snapshot of {snapshot.Name}.");
}
Log.Information($"Took a snapshot of {snapshot.Path}.");
break;
case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Snapshot Failed",
$"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.", 5000);
}
Log.Information($"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.");
break;
}
}
private async void _snapshotDatabase_SnapshotDataUpdate(object sender, SnapshotDataUpdateEventArgs e)
{
switch(e)
{
case SnapshotDataUpdateSuccessEventArgs snapshotDataUpdateSuccessEventArgs:
if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Update))
{
await SendGotifyNotification("Snapshot Updated", $"Snapshot data updated {snapshotDataUpdateSuccessEventArgs.NewHash} from {snapshotDataUpdateSuccessEventArgs.OldHash}.");
}
break;
}
}
private async void _snapshotDatabase_SnapshotNoteUpdate(object sender, SnapshotNoteUpdateEventArgs e)
{
switch(e)
{
case SnapshotNoteUpdateSuccessEventArgs snapshotNoteUpdateSuccessEventArgs:
if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Update))
{
await SendGotifyNotification("Snapshot Updated", $"Snapshot note updated for {snapshotNoteUpdateSuccessEventArgs.Hash}.");
}
break;
}
}
private async void SnapshotDatabase_SnapshotTransferReceived(object sender, SnapshotCreateEventArgs e)
{
switch (e)
{
case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
var snapshot = snapshotCreateSuccessEventArgs.Snapshot;
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Snapshot Transfer Success", $"A snapshot has been transferred {snapshot.Path}.",
5000);
}
if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Transfer))
{
await SendGotifyNotification("Snapshot Transerred", $"A snapshot has been transferred {snapshot.Name}.");
}
Log.Information($"A snapshot transfer succeeded {snapshot.Path}.");
break;
case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Snapshot Transfer Failure",
$"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.", 5000);
}
Log.Information($"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.");
break;
}
}
private async void SnapshotDatabase_SnapshotRevert(object sender, SnapshotRevertEventArgs e)
{
switch (e)
{
case SnapshotRevertSuccessEventArgs snapshotRevertSuccessEventArgs:
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Revert Succeeded", $"File {snapshotRevertSuccessEventArgs.Name} reverted.", 5000);
}
if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Revert))
{
await SendGotifyNotification("Snapshot Reverted", $"Reverted a snapshot of {snapshotRevertSuccessEventArgs.Name}.");
}
Log.Information($"File {snapshotRevertSuccessEventArgs.Name} reverted.");
break;
case SnapshotRevertFailureEventArgs snapshotRevertFailureEventArgs:
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Revert Failed", $"Reverting file {snapshotRevertFailureEventArgs.Name} failed.",
5000);
}
Log.Information($"Reverting file {snapshotRevertFailureEventArgs.Name} failed.");
break;
}
}
private void Folder_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (var item in e.OldItems.OfType<Folder>())
{
RemoveWatcher(item.Path);
}
}
if (e.NewItems != null)
{
foreach (var item in e.NewItems.OfType<Folder>())
{
// If the folder is not enabled then do not add watchers for the path.
if (!item.Enable)
{
continue;
}
if (Directory.Exists(item.Path))
{
AddWatcher(item.Path, item.Recursive);
}
}
}
_trackedFoldersChangedContinuation.Schedule(TimeSpan.FromSeconds(1), SaveFolders, _cancellationToken);
}
private async void MainForm_Load(object sender, EventArgs e)
{
// Start application update.
var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
var icon = Icon.ExtractAssociatedIcon(manifestModuleName);
_sparkle = new SparkleUpdater("https://horizon.grimore.org/update/appcast.xml",
new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY="))
{
UIFactory = new UIFactory(icon),
RelaunchAfterUpdate = true
};
await _sparkle.StartLoop(true, true);
// attempt an upgrade
#pragma warning disable CS4014
//PerformUpgrade();
#pragma warning restore CS4014
// Set form properties.
toolStripMenuItem3.DropDown.Closing += toolStropMenuItem3DropDown_Closing;
eventsToolStripMenuItem.DropDown.Closing += eventsToolStripMenuItem_Closing;
// Load configuration.
_configuration = await LoadConfiguration();
_configuration.PropertyChanged += _configuration_PropertyChanged;
launchOnBootToolStripMenuItem.Checked = _configuration.LaunchOnBoot;
atomicOperationsToolStripMenuItem.Checked = _configuration.AtomicOperations;
autoNotesToolStripMenuItem.Checked = _configuration.AutoNotes;
gotifyToolStripMenuItem.Checked = _configuration.EnableGotify;
gotifyToolStripTextBox.Text = _configuration.GotifyURL;
enableToolStripMenuItem.Checked = _configuration.Enabled;
showBalloonTooltipsToolStripMenuItem.Checked = _configuration.Enabled;
windowToolStripMenuItem.Checked = _configuration.CaptureMode == CaptureMode.Window;
screenToolStripMenuItem.Checked = _configuration.CaptureMode == CaptureMode.Screen;
networkSharingToolStripMenuItem.Checked = _configuration.NetworkSharing;
_searchEngine = new SearchEngine(_configuration, _cancellationToken);
foreach (var item in attributesToolStripMenuItem.DropDownItems.OfType<ToolStripMenuItem>())
{
var text = item.Text;
if (Enum.TryParse<NotifyFilters>(text, out var notifyFilter))
{
item.Checked = _configuration.NotifyFilters.HasFlag(notifyFilter);
}
}
// Load all tracked folders.
try
{
var folders = await LoadFolders();
foreach (var folder in folders.Folder)
{
_trackedFolders.Folder.Add(folder);
}
ToggleWatchers();
return;
}
catch (FileNotFoundException)
{
ToggleWatchers();
return;
}
catch(Exception exception)
{
Log.Error(exception, "Error loading tracked folders.");
}
if (System.Windows.Forms.MessageBox.Show("Tracked folders could not be loaded, should they be deleted?", "Question", MessageBoxButtons.YesNo) == DialogResult.No)
{
return;
}
ToggleWatchers();
}
private void _configuration_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
_changedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1), SaveConfiguration, _cancellationToken);
}
private void ShowBalloonTooltipsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
_configuration.ShowBalloonTooltips = ((ToolStripMenuItem)sender).Checked;
}
private void LaunchOnBootToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
_configuration.LaunchOnBoot = ((ToolStripMenuItem)sender).Checked;
Miscellaneous.LaunchOnBootSet(_configuration.LaunchOnBoot);
}
private void AtomicOperationsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
_configuration.AtomicOperations = ((ToolStripMenuItem)sender).Checked;
}
private void TrashDatabaseToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
File.Delete(Constants.DatabaseFilePath);
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Database Deleted", $"Database file {Constants.DatabaseFilePath} has been deleted.",
5000);
}
Log.Information($"Database file {Constants.DatabaseFilePath} has been deleted.");
}
catch (Exception exception)
{
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Could not Delete Database",
$"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}",
5000);
}
Log.Information(
$"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}");
}
}
private void EnableToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
_configuration.Enabled = enableToolStripMenuItem.Checked;
ToggleWatchers();
}
private void SnapshotsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_snapshotManagerForm != null)
{
return;
}
_snapshotManagerForm = new SnapshotManagerForm(_configuration, _trackedFolders, _fileSystemWatchers, _snapshotDatabase, _searchEngine, _cancellationToken);
_snapshotManagerForm.Closing += SnapshotManagerFormClosing;
_snapshotManagerForm.Show();
}
private void SnapshotManagerFormClosing(object sender, CancelEventArgs e)
{
if (_snapshotManagerForm == null)
{
return;
}
_snapshotManagerForm.Closing -= SnapshotManagerFormClosing;
_snapshotManagerForm.Close();
_snapshotManagerForm = null;
}
private void FileSystemWatcher_Deleted(object sender, FileSystemEventArgs e)
{
Log.Information($"File deleted {e.Name}.");
//ProcessFilesystemWatcherEvent(e.FullPath);
}
private void FileSystemWatcher_Created(object sender, FileSystemEventArgs e)
{
Log.Information($"File created {e.Name}.");
ProcessFilesystemWatcherEvent(e.FullPath);
}
private void FileSystemWatcher_Renamed(object sender, RenamedEventArgs e)
{
Log.Information($"File renamed from {e.OldName} to {e.Name}.");
ProcessFilesystemWatcherEvent(e.FullPath);
}
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{
Log.Information($"File changed {e.Name}.");
ProcessFilesystemWatcherEvent(e.FullPath);
}
private void ProcessFilesystemWatcherEvent(string path)
{
// Ignore directories.
if (Directory.Exists(path))
{
return;
}
#pragma warning disable CS4014
Task.Run(async () =>
#pragma warning restore CS4014
{
await _changedFilesLock.WaitAsync(_cancellationToken);
try
{
var delay = global::TrackedFolders.Constants.Delay;
var color = Color.Empty;
if (_trackedFolders.TryGet(path, out var folder))
{
delay = folder.Delay;
color = folder.Color;
}
if (_changedFiles.Contains(path))
{
_changedFilesContinuation.Schedule(delay,
async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
return;
}
_changedFiles.Add(path);
_changedFilesContinuation.Schedule(delay,
async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
}
catch (Exception exception)
{
Log.Error(exception, "Could not process changed files.");
}
finally
{
_changedFilesLock.Release();
}
}, CancellationToken.None);
}
private void AboutToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_aboutForm != null)
{
return;
}
_aboutForm = new AboutForm(_cancellationToken);
_aboutForm.Closing += AboutForm_Closing;
_aboutForm.Show();
}
private void AboutForm_Closing(object sender, CancelEventArgs e)
{
if (_aboutForm == null)
{
return;
}
_aboutForm.Dispose();
_aboutForm = null;
}
private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private async void UpdateToolStripMenuItem_Click(object sender, EventArgs e)
{
await PerformUpgrade();
}
private void NotifyIcon1_Click(object sender, EventArgs e)
{
if (e is MouseEventArgs mouseEventArgs && mouseEventArgs.Button == MouseButtons.Left)
{
}
}
private void ManageFoldersToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_manageFoldersForm != null)
{
return;
}
_manageFoldersForm = new ManageFoldersForm(_configuration, _trackedFolders, _snapshotDatabase, _searchEngine, _cancellationToken);
_manageFoldersForm.Closing += ManageFoldersForm_Closing;
_manageFoldersForm.Show();
}
private void ManageFoldersForm_Closing(object sender, CancelEventArgs e)
{
if (_manageFoldersForm == null)
{
return;
}
_manageFoldersForm.Closing -= ManageFoldersForm_Closing;
_manageFoldersForm.Close();
_manageFoldersForm = null;
}
#endregion
#region Public Methods
public void ShowBalloon(string title, string text, int time)
{
notifyIcon1.BalloonTipTitle = title;
notifyIcon1.BalloonTipText = text;
notifyIcon1.ShowBalloonTip(time);
}
public async Task SaveConfiguration()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
switch (await Serialization.Serialize(_configuration, Constants.ConfigurationFile, "Configuration",
"<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
CancellationToken.None))
{
case SerializationSuccess<Configuration.Configuration> _:
Log.Information("Serialized configuration.");
break;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration.");
break;
}
}
public static async Task<Configuration.Configuration> LoadConfiguration()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
var deserializationResult =
await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);
switch (deserializationResult)
{
case SerializationSuccess<Configuration.Configuration> serializationSuccess:
return serializationSuccess.Result;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception, "Failed to load configuration.");
return new Configuration.Configuration();
default:
return new Configuration.Configuration();
}
}
public static async Task<TrackedFolders.TrackedFolders> LoadFolders()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
var deserializationResult =
await Serialization.Deserialize<TrackedFolders.TrackedFolders>(Constants.FoldersFile,
Constants.TrackedFoldersNamespace, Constants.TrackedFoldersXsd, CancellationToken.None);
switch (deserializationResult)
{
case SerializationSuccess<TrackedFolders.TrackedFolders> serializationSuccess:
return serializationSuccess.Result;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception, "Failed to load tracked folders");
throw serializationFailure.Exception;
}
return null;
}
public async Task SaveFolders()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
switch (await Serialization.Serialize(_trackedFolders, Constants.FoldersFile, "TrackedFolders",
"<!ATTLIST TrackedFolders xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
CancellationToken.None))
{
case SerializationSuccess<TrackedFolders.TrackedFolders> _:
Log.Information("Serialized tracked folders.");
break;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception.Message, "Failed to serialize tracked folders.");
break;
}
}
#endregion
#region Private Methods
private async Task PerformUpgrade()
{
// Manually check for updates, this will not show a ui
var updateCheck = await _sparkle.CheckForUpdatesQuietly();
switch (updateCheck.Status)
{
case UpdateStatus.UserSkipped:
var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;
updateCheck.Updates.Sort(UpdateComparer);
var latestVersion = updateCheck.Updates.FirstOrDefault();
if (latestVersion != null)
{
var availableVersion = new Version(latestVersion.Version);
if (availableVersion <= assemblyVersion)
{
return;
}
}
// Only offer an update nag screen if the version is one month old since skipping an update.
if (DateTime.Now.Subtract(latestVersion.PublicationDate).TotalDays < 30)
{
return;
}
var decision = System.Windows.Forms.MessageBox.Show(
"Update available but it has been previously skipped and a month has passed since. Should the update proceed now?",
Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.YesNo,
MessageBoxIcon.Asterisk,
MessageBoxDefaultButton.Button1, System.Windows.Forms.MessageBoxOptions.DefaultDesktopOnly, false);
if (decision.HasFlag(DialogResult.No))
{
return;
}
goto default;
case UpdateStatus.UpdateNotAvailable:
System.Windows.Forms.MessageBox.Show("No updates available at this time.",
Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.OK,
MessageBoxIcon.Asterisk,
MessageBoxDefaultButton.Button1, System.Windows.Forms.MessageBoxOptions.DefaultDesktopOnly, false);
break;
case UpdateStatus.CouldNotDetermine:
Log.Error("Could not determine the update availability status.");
break;
default:
_sparkle.ShowUpdateNeededUI();
break;
}
}
private static int UpdateComparer(AppCastItem x, AppCastItem y)
{
if (x == null)
{
return 1;
}
if (y == null)
{
return -1;
}
if (x == y)
{
return 0;
}
return new Version(y.Version).CompareTo(new Version(x.Version));
}
private void RemoveWatcher(string folder)
{
var count = _fileSystemWatchers.Count;
while (_fileSystemWatchers.TryDequeue(out var fileSystemWatcher))
{
if(--count == 0)
{
break;
}
if (fileSystemWatcher.Path.IsPathEqual(folder) ||
fileSystemWatcher.Path.IsSubPathOf(folder))
{
continue;
}
_fileSystemWatchers.Enqueue(fileSystemWatcher);
}
}
private void AddWatcher(string folder, bool recursive)
{
var fileSystemWatcher = new FileSystemWatcher
{
IncludeSubdirectories = recursive,
NotifyFilter = _fileSystemWatchersNotifyFilters,
Path = folder,
EnableRaisingEvents = true,
InternalBufferSize = 65536
};
fileSystemWatcher.Changed += FileSystemWatcher_Changed;
fileSystemWatcher.Renamed += FileSystemWatcher_Renamed;
fileSystemWatcher.Created += FileSystemWatcher_Created;
fileSystemWatcher.Deleted += FileSystemWatcher_Deleted;
_fileSystemWatchers.Enqueue(fileSystemWatcher);
}
private void ToggleWatchers()
{
switch (_configuration.Enabled)
{
case true:
foreach (var watcher in _fileSystemWatchers)
{
watcher.EnableRaisingEvents = true;
}
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Watching", "Watching folders...", 5000);
}
Log.Information("Watching folders.");
break;
default:
foreach (var watcher in _fileSystemWatchers)
{
watcher.EnableRaisingEvents = false;
}
if (_configuration.ShowBalloonTooltips)
{
ShowBalloon("Not Watching", "Folders are not being watched.", 5000);
}
Log.Information("Folders are not being watched.");
break;
}
}
private async Task TakeSnapshots(Color color, CancellationToken cancellationToken)
{
var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions() {CancellationToken = cancellationToken});
var actionBlock = new ActionBlock<string>(async path =>
{
// In case files have vanished strictly due to the time specified by the tracked folders delay.
if (!File.Exists(path))
{
Log.Warning($"File vanished after tracked folder delay: {path}");
return;
}
try
{
var fileName = Path.GetFileName(path);
var screenCapture = ScreenCapture.Capture((Utilities.CaptureMode)_configuration.CaptureMode);
var terms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (_configuration.AutoNotes)
{
await foreach (var term in Extensions.RecognizeStrings(screenCapture, cancellationToken))
{
terms.Add(term);
}
}
var note = string.Join(" ", terms);
// async, decompose, branch
if (await _snapshotDatabase.CreateSnapshotAsync(fileName, path, screenCapture, color, note, cancellationToken) is Snapshot snapshot)
{
await _searchEngine.Index(snapshot, cancellationToken);
}
}
catch (SQLiteException exception)
{
if (exception.ResultCode == SQLiteErrorCode.Constraint)
{
Log.Information(exception, "Snapshot already exists.");
}
}
catch (Exception exception)
{
Log.Error(exception, $"Could not take snapshot of file {path}");
}
});
using var snapshotLink = bufferBlock.LinkTo(actionBlock, new DataflowLinkOptions() { PropagateCompletion = true });
await _changedFilesLock.WaitAsync(_cancellationToken);
try
{
foreach (var path in _changedFiles)
{
await bufferBlock.SendAsync(path, cancellationToken);
}
bufferBlock.Complete();
await bufferBlock.Completion;
}
catch (Exception exception)
{
Log.Error(exception, "Could not take snapshots.");
}
finally
{
_changedFiles.Clear();
_changedFilesLock.Release();
}
}
private async Task SendGotifyNotification(string v1, string v2)
{
if (!Uri.TryCreate(_configuration.GotifyURL, UriKind.RelativeOrAbsolute, out var uri))
{
Log.Warning($"Invalid Gotify URL provided.");
return;
}
var gotifyMessageSending = new GotifyMessageOutgoing()
{
Title = v1,
Message = v2
};
var payload = JsonConvert.SerializeObject(gotifyMessageSending);
using var stringContent = new StringContent(payload, Encoding.UTF8, "application/json");
using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri);
httpRequestMessage.Content = stringContent;
var response = await _httpClient.SendAsync(httpRequestMessage, _cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
var gotifyReply = JsonConvert.DeserializeObject<GotifyMessageIncoming>(responseContent);
if (gotifyReply?.AppId == null)
{
Log.Error($"Failed Sending notification.");
}
}
#endregion
}
}
Generated by GNU Enscript 1.6.5.90.