Horizon – Blame information for rev 23

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Specialized;
4 using System.ComponentModel;
5 using System.Data.SQLite;
12 office 6 using System.Diagnostics;
1 office 7 using System.Drawing;
8 using System.IO;
9 using System.Linq;
10 using System.Net;
11 using System.Reflection;
12 office 12 using System.Text;
1 office 13 using System.Threading;
14 using System.Threading.Tasks;
5 office 15 using System.Threading.Tasks.Dataflow;
1 office 16 using System.Windows.Forms;
17 using Horizon.Database;
18 using Horizon.Snapshots;
19 using Horizon.Utilities;
20 using Horizon.Utilities.Serialization;
12 office 21 using Mono.Zeroconf;
1 office 22 using NetSparkleUpdater;
23 using NetSparkleUpdater.Enums;
24 using NetSparkleUpdater.SignatureVerifiers;
25 using NetSparkleUpdater.UI.WinForms;
12 office 26 using Newtonsoft.Json;
1 office 27 using Serilog;
28 using TrackedFolders;
12 office 29 using WatsonTcp;
30 using static Horizon.Utilities.Networking.Miscellaneous;
1 office 31 using CaptureMode = Configuration.CaptureMode;
5 office 32 using Path = System.IO.Path;
1 office 33  
34 namespace Horizon
35 {
36 public partial class MainForm : Form
37 {
38 #region Public Enums, Properties and Fields
39  
40 public List<FileSystemWatcher> FileSystemWatchers { get; }
41  
42 public TrackedFolders.TrackedFolders TrackedFolders { get; set; }
43  
44 public Configuration.Configuration Configuration { get; set; }
45  
46 public ScheduledContinuation ChangedConfigurationContinuation { get; set; }
47  
48 #endregion
49  
50 #region Static Fields and Constants
51  
52 private static SemaphoreSlim _changedFilesLock;
53  
54 private static HashSet<string> _changedFiles;
55  
56 private static ScheduledContinuation _changedFilesContinuation;
57  
58 #endregion
59  
60 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
61  
62 private readonly CancellationToken _cancellationToken;
63  
64 private readonly CancellationTokenSource _cancellationTokenSource;
65  
66 private AboutForm _aboutForm;
67  
68 private ManageFoldersForm _manageFoldersForm;
69  
70 private readonly SnapshotDatabase _snapshotDatabase;
71  
72 private SnapshotManagerForm _snapshotManagerForm;
73  
74 private readonly SparkleUpdater _sparkle;
75  
76 private LogViewForm _logViewForm;
77  
78 private readonly LogMemorySink _memorySink;
79  
12 office 80 private RegisterService _horizonDiscoveryService;
81  
82 private WatsonTcpServer _horizonNetworkShare;
83  
1 office 84 public bool MemorySinkEnabled { get; set; }
85  
86 #endregion
87  
88 #region Constructors, Destructors and Finalizers
89  
90 public MainForm(Mutex mutex) : this()
91 {
92 _memorySink = new LogMemorySink();
93  
94 Log.Logger = new LoggerConfiguration()
95 .MinimumLevel.Debug()
96 .WriteTo.Conditional(condition => MemorySinkEnabled, configureSink => configureSink.Sink(_memorySink))
97 .WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"),
98 rollingInterval: RollingInterval.Day)
99 .CreateLogger();
100  
5 office 101 _snapshotDatabase = new SnapshotDatabase(_cancellationToken);
1 office 102 _snapshotDatabase.SnapshotRevert += SnapshotDatabase_SnapshotRevert;
103 _snapshotDatabase.SnapshotCreate += SnapshotDatabase_SnapshotCreate;
23 office 104 _snapshotDatabase.SnapshotTransferReceived += SnapshotDatabase_SnapshotTransferReceived;
1 office 105  
106 TrackedFolders = new TrackedFolders.TrackedFolders();
107 TrackedFolders.Folder.CollectionChanged += Folder_CollectionChanged;
108  
109 // Start application update.
110 var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
111 var icon = Icon.ExtractAssociatedIcon(manifestModuleName);
112  
113 _sparkle = new SparkleUpdater("https://horizon.grimore.org/update/appcast.xml",
114 new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY="))
115 {
116 UIFactory = new UIFactory(icon),
117 RelaunchAfterUpdate = true,
118 SecurityProtocolType = SecurityProtocolType.Tls12
119 };
120 _sparkle.StartLoop(true, true);
121 }
122  
123 public MainForm()
124 {
9 office 125 InitializeComponent();
126  
1 office 127 _cancellationTokenSource = new CancellationTokenSource();
128 _cancellationToken = _cancellationTokenSource.Token;
129  
130 _changedFilesLock = new SemaphoreSlim(1, 1);
131 FileSystemWatchers = new List<FileSystemWatcher>();
132  
133 _changedFiles = new HashSet<string>();
134 _changedFilesContinuation = new ScheduledContinuation();
135  
136 ChangedConfigurationContinuation = new ScheduledContinuation();
137 }
138  
139 /// <summary>
140 /// Clean up any resources being used.
141 /// </summary>
142 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
143 protected override void Dispose(bool disposing)
144 {
145 if (disposing && components != null)
146 {
147 components.Dispose();
148 }
149  
150 _cancellationTokenSource.Cancel();
151  
152 _snapshotDatabase.SnapshotRevert -= SnapshotDatabase_SnapshotRevert;
153 _snapshotDatabase.SnapshotCreate -= SnapshotDatabase_SnapshotCreate;
23 office 154 _snapshotDatabase.SnapshotTransferReceived -= SnapshotDatabase_SnapshotTransferReceived;
1 office 155  
156 _snapshotDatabase.Dispose();
157  
158 base.Dispose(disposing);
159 }
160  
161 #endregion
162  
163 #region Event Handlers
164  
12 office 165 private void networkSharingToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
166 {
167 var toolStripMenuItem = (ToolStripMenuItem)sender;
168  
169 switch (toolStripMenuItem.CheckState)
170 {
171 case CheckState.Checked:
172 var freePort = GetAvailableTcpPort();
173  
174 _horizonNetworkShare = new WatsonTcpServer("0.0.0.0", freePort);
175 _horizonNetworkShare.Events.ClientConnected += Events_ClientConnected;
176 _horizonNetworkShare.Events.ClientDisconnected += Events_ClientDisconnected;
177 _horizonNetworkShare.Events.MessageReceived += Events_MessageReceived;
21 office 178 _horizonNetworkShare.Events.ExceptionEncountered += Events_ExceptionEncountered;
12 office 179 #pragma warning disable CS4014
180 _horizonNetworkShare.Start();
181 #pragma warning restore CS4014
182  
183 try
184 {
185 _horizonDiscoveryService = new RegisterService();
186 _horizonDiscoveryService.Name = $"Horizon ({Environment.MachineName})";
187 _horizonDiscoveryService.RegType = "_horizon._tcp";
188 _horizonDiscoveryService.ReplyDomain = "local.";
189 _horizonDiscoveryService.UPort = freePort;
190 _horizonDiscoveryService.Register();
191 }
192 catch (Exception exception)
193 {
194 Log.Error(exception, "Service discovery protocol could not be stared.");
195 }
196  
197 Configuration.NetworkSharing = true;
198 break;
199 case CheckState.Unchecked:
200 if (_horizonNetworkShare != null)
201 {
21 office 202 _horizonNetworkShare.Events.ClientConnected -= Events_ClientConnected;
203 _horizonNetworkShare.Events.ClientDisconnected -= Events_ClientDisconnected;
204 _horizonNetworkShare.Events.MessageReceived -= Events_MessageReceived;
205 _horizonNetworkShare.Events.ExceptionEncountered -= Events_ExceptionEncountered;
206  
12 office 207 _horizonNetworkShare.Dispose();
208 _horizonNetworkShare = null;
209 }
210  
211 if (_horizonDiscoveryService != null)
212 {
213 _horizonDiscoveryService.Dispose();
214 _horizonDiscoveryService = null;
215  
216 }
217  
218 Configuration.NetworkSharing = false;
219 break;
220 }
221  
222 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
223 async () => { await SaveConfiguration(); }, _cancellationToken);
224 }
225  
21 office 226 private void Events_ExceptionEncountered(object sender, ExceptionEventArgs e)
227 {
228 Log.Error(e.Exception,$"Client threw exception.");
229 }
230  
12 office 231 private async void Events_MessageReceived(object sender, MessageReceivedEventArgs e)
232 {
21 office 233 Log.Information($"Client {e.Client?.IpPort} sent {e.Data?.Length} bytes via network sharing.");
12 office 234  
21 office 235 if (e.Data?.Length == 0)
12 office 236 {
237 return;
238 }
239  
240 try
241 {
242 var payload = Encoding.UTF8.GetString(e.Data);
243  
244 var completeSnapshot = JsonConvert.DeserializeObject<TransferSnapshot>(payload);
245  
20 office 246 await _snapshotDatabase.ApplyTransferSnapshotAsync(completeSnapshot, _cancellationToken);
12 office 247  
21 office 248 Log.Information($"Stored {completeSnapshot.Name} from {e.Client?.IpPort}");
12 office 249 }
250 catch (Exception exception)
251 {
21 office 252 Log.Error(exception, $"Failed to process network share from {e.Client?.IpPort}.");
12 office 253 }
254 }
255  
256 private void Events_ClientDisconnected(object sender, WatsonTcp.DisconnectionEventArgs e)
257 {
21 office 258 Log.Information($"Client {e.Client?.IpPort} disconnected from network sharing.");
12 office 259 }
260  
261 private void Events_ClientConnected(object sender, WatsonTcp.ConnectionEventArgs e)
262 {
21 office 263 Log.Information($"Client {e.Client?.IpPort} connected to network sharing.");
12 office 264 }
265  
1 office 266 private void WindowToolStripMenuItem_Click(object sender, EventArgs e)
267 {
268 windowToolStripMenuItem.Checked = true;
269 screenToolStripMenuItem.Checked = false;
270  
271 Configuration.CaptureMode = CaptureMode.Window;
272  
273 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
274 async () => { await SaveConfiguration(); }, _cancellationToken);
275 }
276  
277 private void ScreenToolStripMenuItem_Click(object sender, EventArgs e)
278 {
279 screenToolStripMenuItem.Checked = true;
280 windowToolStripMenuItem.Checked = false;
281  
282 Configuration.CaptureMode = CaptureMode.Screen;
283  
284 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
285 async () => { await SaveConfiguration(); }, _cancellationToken);
286 }
287  
288 private void LogViewToolStripMenuItem_Click(object sender, EventArgs e)
289 {
290 if (_logViewForm != null)
291 {
292 return;
293 }
294  
295 _logViewForm = new LogViewForm(this, _memorySink, _cancellationToken);
296 _logViewForm.Closing += LogViewFormClosing;
297 _logViewForm.Show();
298 }
299  
300 private void LogViewFormClosing(object sender, CancelEventArgs e)
301 {
302 if (_logViewForm == null)
303 {
304 return;
305 }
306  
307 _logViewForm.Closing -= LogViewFormClosing;
308 _logViewForm.Close();
309 _logViewForm = null;
310 }
311  
312 private void SnapshotDatabase_SnapshotCreate(object sender, SnapshotCreateEventArgs e)
313 {
314 switch (e)
315 {
316 case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
317 if (Configuration.ShowBalloonTooltips)
318 {
319 ShowBalloon("Snapshot Succeeded", $"Took a snapshot of {snapshotCreateSuccessEventArgs.Path}.",
320 5000);
321 }
322  
323 Log.Information($"Took a snapshot of {snapshotCreateSuccessEventArgs.Path}.");
324  
325 break;
326 case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
327 if (Configuration.ShowBalloonTooltips)
328 {
329 ShowBalloon("Snapshot Failed",
330 $"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.", 5000);
331 }
332  
333 Log.Information($"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.");
334  
335 break;
336 }
337 }
338  
23 office 339 private void SnapshotDatabase_SnapshotTransferReceived(object sender, SnapshotCreateEventArgs e)
340 {
341 switch (e)
342 {
343 case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
344 if (Configuration.ShowBalloonTooltips)
345 {
346 ShowBalloon("Snapshot Transfer Success", $"A snapshot has been transferred {snapshotCreateSuccessEventArgs.Path}.",
347 5000);
348 }
349  
350 Log.Information($"A snapshot transfer succeeded {snapshotCreateSuccessEventArgs.Path}.");
351  
352 break;
353 case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
354 if (Configuration.ShowBalloonTooltips)
355 {
356 ShowBalloon("Snapshot Transfer Failure",
357 $"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.", 5000);
358 }
359  
360 Log.Information($"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.");
361  
362 break;
363 }
364 }
365  
1 office 366 private void SnapshotDatabase_SnapshotRevert(object sender, SnapshotRevertEventArgs e)
367 {
368 switch (e)
369 {
370 case SnapshotRevertSuccessEventArgs snapshotRevertSuccessEventArgs:
371 if (Configuration.ShowBalloonTooltips)
372 {
373 ShowBalloon("Revert Succeeded", $"File {snapshotRevertSuccessEventArgs.Name} reverted.", 5000);
374 }
375  
376 Log.Information($"File {snapshotRevertSuccessEventArgs.Name} reverted.");
377  
378 break;
379 case SnapshotRevertFailureEventArgs snapshotRevertFailureEventArgs:
380 if (Configuration.ShowBalloonTooltips)
381 {
382 ShowBalloon("Revert Failed", $"Reverting file {snapshotRevertFailureEventArgs.Name} failed.",
383 5000);
384 }
385  
386 Log.Information($"Reverting file {snapshotRevertFailureEventArgs.Name} failed.");
387  
388 break;
389 }
390 }
391  
392 private async void Folder_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
393 {
394 if (e.OldItems != null)
395 {
396 foreach (var item in e.OldItems.OfType<Folder>())
397 {
398 RemoveWatcher(item.Path);
399 }
400 }
401  
402 if (e.NewItems != null)
403 {
404 foreach (var item in e.NewItems.OfType<Folder>())
405 {
406 // If the folder is not enabled then do not add watchers for the path.
407 if (!item.Enable)
408 {
409 continue;
410 }
411  
412 if (Directory.Exists(item.Path))
413 {
414 AddWatcher(item.Path, item.Recursive);
415 }
416 }
417 }
418  
419 await SaveFolders();
420 }
421  
422 private async void MainForm_Load(object sender, EventArgs e)
423 {
424 // Load configuration.
425 Configuration = await LoadConfiguration();
426 launchOnBootToolStripMenuItem.Checked = Configuration.LaunchOnBoot;
427 atomicOperationsToolStripMenuItem.Checked = Configuration.AtomicOperations;
428 enableToolStripMenuItem.Checked = Configuration.Enabled;
429 showBalloonTooltipsToolStripMenuItem.Checked = Configuration.Enabled;
430 windowToolStripMenuItem.Checked = Configuration.CaptureMode == CaptureMode.Window;
431 screenToolStripMenuItem.Checked = Configuration.CaptureMode == CaptureMode.Screen;
12 office 432 networkSharingToolStripMenuItem.Checked = Configuration.NetworkSharing;
1 office 433  
434 // Load all tracked folders.
435 var folders = await LoadFolders();
436  
437 foreach (var folder in folders.Folder)
438 {
439 TrackedFolders.Folder.Add(folder);
440 }
441  
442 ToggleWatchers();
443 }
444  
445 private void ShowBalloonTooltipsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
446 {
447 Configuration.ShowBalloonTooltips = ((ToolStripMenuItem)sender).Checked;
448  
449 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
450 async () => { await SaveConfiguration(); }, _cancellationToken);
451 }
452  
453 private void LaunchOnBootToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
454 {
455 Configuration.LaunchOnBoot = ((ToolStripMenuItem)sender).Checked;
456  
457 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
458 async () => { await SaveConfiguration(); }, _cancellationToken);
459  
460 Miscellaneous.LaunchOnBootSet(Configuration.LaunchOnBoot);
461 }
462  
463 private void AtomicOperationsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
464 {
465 Configuration.AtomicOperations = ((ToolStripMenuItem)sender).Checked;
466 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
467 async () => { await SaveConfiguration(); }, _cancellationToken);
468 }
469  
470 private void TrashDatabaseToolStripMenuItem_Click(object sender, EventArgs e)
471 {
472 try
473 {
474 File.Delete(Constants.DatabaseFilePath);
475  
476 if (Configuration.ShowBalloonTooltips)
477 {
478 ShowBalloon("Database Deleted", $"Database file {Constants.DatabaseFilePath} has been deleted.",
479 5000);
480 }
481  
482 Log.Information($"Database file {Constants.DatabaseFilePath} has been deleted.");
483 }
484 catch (Exception exception)
485 {
486 if (Configuration.ShowBalloonTooltips)
487 {
488 ShowBalloon("Could not Delete Database",
489 $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}",
490 5000);
491 }
492  
493 Log.Information(
494 $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}");
495 }
496 }
497  
498 private void EnableToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
499 {
500 Configuration.Enabled = enableToolStripMenuItem.Checked;
501  
502 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
503 async () => { await SaveConfiguration(); }, _cancellationToken);
504  
505 ToggleWatchers();
506 }
507  
508 private void SnapshotsToolStripMenuItem_Click(object sender, EventArgs e)
509 {
510 if (_snapshotManagerForm != null)
511 {
512 return;
513 }
514  
515 _snapshotManagerForm = new SnapshotManagerForm(this, _snapshotDatabase, _cancellationToken);
516 _snapshotManagerForm.Closing += SnapshotManagerFormClosing;
517 _snapshotManagerForm.Show();
518 }
519  
520 private void SnapshotManagerFormClosing(object sender, CancelEventArgs e)
521 {
522 if (_snapshotManagerForm == null)
523 {
524 return;
525 }
526  
527 _snapshotManagerForm.Closing -= SnapshotManagerFormClosing;
528 _snapshotManagerForm.Close();
529 _snapshotManagerForm = null;
530 }
531  
532 private async void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
533 {
534 // Ignore directories.
535 if (Directory.Exists(e.FullPath))
536 {
537 return;
538 }
539  
540 await _changedFilesLock.WaitAsync(_cancellationToken);
541 try
542 {
543 var delay = global::TrackedFolders.Constants.Delay;
544 var color = Color.Empty;
545  
546 if (TrackedFolders.TryGet(e.FullPath, out var folder))
547 {
548 delay = folder.Delay;
549 color = folder.Color;
550 }
551  
552 if (_changedFiles.Contains(e.FullPath))
553 {
5 office 554 _changedFilesContinuation.Schedule(delay, async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
1 office 555 return;
556 }
557  
558 _changedFiles.Add(e.FullPath);
559  
5 office 560 _changedFilesContinuation.Schedule(delay, async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
1 office 561 }
562 catch (Exception exception)
563 {
564 Log.Error(exception, "Could not process changed files.");
565 }
566 finally
567 {
568 _changedFilesLock.Release();
569 }
570 }
571  
572 private void AboutToolStripMenuItem_Click(object sender, EventArgs e)
573 {
574 if (_aboutForm != null)
575 {
576 return;
577 }
578  
579 _aboutForm = new AboutForm(_cancellationToken);
580 _aboutForm.Closing += AboutForm_Closing;
581 _aboutForm.Show();
582 }
583  
584 private void AboutForm_Closing(object sender, CancelEventArgs e)
585 {
586 if (_aboutForm == null)
587 {
588 return;
589 }
590  
591 _aboutForm.Dispose();
592 _aboutForm = null;
593 }
594  
595 private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
596 {
597 Close();
598 }
599  
600 private async void UpdateToolStripMenuItem_Click(object sender, EventArgs e)
601 {
602 // Manually check for updates, this will not show a ui
603 var result = await _sparkle.CheckForUpdatesQuietly();
604 if (result.Status == UpdateStatus.UpdateAvailable)
605 {
606 // if update(s) are found, then we have to trigger the UI to show it gracefully
607 _sparkle.ShowUpdateNeededUI();
608 return;
609 }
610  
611 MessageBox.Show("No updates available at this time.", "Horizon", MessageBoxButtons.OK,
612 MessageBoxIcon.Asterisk,
613 MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false);
614 }
615  
616 private void NotifyIcon1_Click(object sender, EventArgs e)
617 {
618 if (e is MouseEventArgs mouseEventArgs && mouseEventArgs.Button == MouseButtons.Left)
619 {
620 }
621 }
622  
623 private void ManageFoldersToolStripMenuItem_Click(object sender, EventArgs e)
624 {
625 if (_manageFoldersForm != null)
626 {
627 return;
628 }
629  
630 _manageFoldersForm = new ManageFoldersForm(this, _cancellationToken);
631 _manageFoldersForm.Closing += ManageFoldersForm_Closing;
632 _manageFoldersForm.Show();
633 }
634  
635 private void ManageFoldersForm_Closing(object sender, CancelEventArgs e)
636 {
637 if (_manageFoldersForm == null)
638 {
639 return;
640 }
641  
642 _manageFoldersForm.Closing -= ManageFoldersForm_Closing;
643 _manageFoldersForm.Close();
644 _manageFoldersForm = null;
645 }
646  
647 #endregion
648  
649 #region Public Methods
650  
651 public void ShowBalloon(string title, string text, int time)
652 {
653 notifyIcon1.BalloonTipTitle = title;
654 notifyIcon1.BalloonTipText = text;
655 notifyIcon1.ShowBalloonTip(time);
656 }
657  
658 public async Task TakeSnapshot(string path)
659 {
660 foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.TopDirectoryOnly))
661 {
662 try
663 {
664 var fileName = Path.GetFileName(file);
665 var directory = Path.GetDirectoryName(fileName);
666 var color = Color.Empty;
667 if (TrackedFolders.TryGet(directory, out var folder))
668 {
669 color = folder.Color;
670 }
671  
11 office 672 await _snapshotDatabase.CreateSnapshotAsync(fileName, file, color, _cancellationToken);
1 office 673 }
674 catch (SQLiteException exception)
675 {
676 if (exception.ResultCode == SQLiteErrorCode.Constraint)
677 {
678 Log.Information(exception, "Snapshot already exists.");
679 }
680 }
681 catch (Exception exception)
682 {
683 Log.Error(exception, "Could not take snapshot.", file);
684 }
685 }
686 }
687  
688 public async Task TakeSnapshotRecursive(string path)
689 {
690 foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
691 {
692 try
693 {
694 var fileName = Path.GetFileName(file);
695 var directory = Path.GetDirectoryName(fileName);
696 var color = Color.Empty;
697 if (TrackedFolders.TryGet(directory, out var folder))
698 {
699 color = folder.Color;
700 }
701  
11 office 702 await _snapshotDatabase.CreateSnapshotAsync(fileName, file, color, _cancellationToken);
1 office 703 }
704 catch (SQLiteException exception)
705 {
706 if (exception.ResultCode == SQLiteErrorCode.Constraint)
707 {
708 Log.Information(exception, "Snapshot already exists.");
709 }
710 }
711 catch (Exception exception)
712 {
713 Log.Error(exception, "Could not take snapshot.", file);
714 }
715 }
716 }
717  
718 public async Task SaveConfiguration()
719 {
720 if (!Directory.Exists(Constants.UserApplicationDirectory))
721 {
722 Directory.CreateDirectory(Constants.UserApplicationDirectory);
723 }
724  
725 switch (await Serialization.Serialize(Configuration, Constants.ConfigurationFile, "Configuration",
726 "<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
727 CancellationToken.None))
728 {
729 case SerializationSuccess<Configuration.Configuration> _:
730 Log.Information("Serialized configuration.");
731 break;
732 case SerializationFailure serializationFailure:
733 Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration.");
734 break;
735 }
736 }
737  
738 public static async Task<Configuration.Configuration> LoadConfiguration()
739 {
740 if (!Directory.Exists(Constants.UserApplicationDirectory))
741 {
742 Directory.CreateDirectory(Constants.UserApplicationDirectory);
743 }
744  
745 var deserializationResult =
746 await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
747 Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);
748  
749 switch (deserializationResult)
750 {
751 case SerializationSuccess<Configuration.Configuration> serializationSuccess:
752 return serializationSuccess.Result;
753 case SerializationFailure serializationFailure:
754 Log.Warning(serializationFailure.Exception, "Failed to load configuration.");
755 return new Configuration.Configuration();
756 default:
757 return new Configuration.Configuration();
758 }
759 }
760  
761 public static async Task<TrackedFolders.TrackedFolders> LoadFolders()
762 {
763 if (!Directory.Exists(Constants.UserApplicationDirectory))
764 {
765 Directory.CreateDirectory(Constants.UserApplicationDirectory);
766 }
767  
768 var deserializationResult =
769 await Serialization.Deserialize<TrackedFolders.TrackedFolders>(Constants.FoldersFile,
770 Constants.TrackedFoldersNamespace, Constants.TrackedFoldersXsd, CancellationToken.None);
771  
772 switch (deserializationResult)
773 {
774 case SerializationSuccess<TrackedFolders.TrackedFolders> serializationSuccess:
775 return serializationSuccess.Result;
776 case SerializationFailure serializationFailure:
777 Log.Warning(serializationFailure.Exception, "Failed to load tracked folders");
778 return new TrackedFolders.TrackedFolders();
779 default:
780 return new TrackedFolders.TrackedFolders();
781 }
782 }
783  
784 public async Task SaveFolders()
785 {
786 if (!Directory.Exists(Constants.UserApplicationDirectory))
787 {
788 Directory.CreateDirectory(Constants.UserApplicationDirectory);
789 }
790  
791 switch (await Serialization.Serialize(TrackedFolders, Constants.FoldersFile, "TrackedFolders",
792 "<!ATTLIST TrackedFolders xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
793 CancellationToken.None))
794 {
795 case SerializationSuccess<TrackedFolders.TrackedFolders> _:
796 Log.Information("Serialized tracked folders.");
797 break;
798 case SerializationFailure serializationFailure:
799 Log.Warning(serializationFailure.Exception.Message, "Failed to serialize tracked folders.");
800 break;
801 }
802 }
803  
804 #endregion
805  
806 #region Private Methods
807  
808 private void RemoveWatcher(string folder)
809 {
810 var removeList = new List<FileSystemWatcher>();
811 foreach (var fileSystemWatcher in FileSystemWatchers)
812 {
813 if (fileSystemWatcher.Path.IsPathEqual(folder) ||
814 fileSystemWatcher.Path.IsSubPathOf(folder))
815 {
816 removeList.Add(fileSystemWatcher);
817 }
818 }
819  
820 foreach (var fileSystemWatcher in removeList)
821 {
822 FileSystemWatchers.Remove(fileSystemWatcher);
823 fileSystemWatcher.Changed -= FileSystemWatcher_Changed;
824 fileSystemWatcher.Dispose();
825 }
826 }
827  
828 private void AddWatcher(string folder, bool recursive)
829 {
830 var fileSystemWatcher = new FileSystemWatcher
831 {
832 IncludeSubdirectories = recursive,
833 NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Attributes,
834 Path = folder,
835 EnableRaisingEvents = true
836 };
837  
838 fileSystemWatcher.Changed += FileSystemWatcher_Changed;
839  
840 FileSystemWatchers.Add(fileSystemWatcher);
841 }
842  
843 private void ToggleWatchers()
844 {
845 switch (Configuration.Enabled)
846 {
847 case true:
848 foreach (var watcher in FileSystemWatchers)
849 {
850 watcher.EnableRaisingEvents = true;
851 }
852  
853 if (Configuration.ShowBalloonTooltips)
854 {
855 ShowBalloon("Watching", "Watching folders...", 5000);
856 }
857  
858 Log.Information("Watching folders.");
859  
860 break;
861 default:
862 foreach (var watcher in FileSystemWatchers)
863 {
864 watcher.EnableRaisingEvents = false;
865 }
866  
867 if (Configuration.ShowBalloonTooltips)
868 {
869 ShowBalloon("Not Watching", "Folders are not being watched.", 5000);
870 }
871  
872 Log.Information("Folders are not being watched.");
873  
874 break;
875 }
876 }
877  
5 office 878 private async Task TakeSnapshots(Color color, CancellationToken cancellationToken)
1 office 879 {
5 office 880 var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions() {CancellationToken = cancellationToken});
881 var actionBlock = new ActionBlock<string>(async path =>
1 office 882 {
5 office 883 // In case files have vanished strictly due to the time specified by the tracked folders delay.
884 if (!File.Exists(path))
1 office 885 {
5 office 886 Log.Warning("File vanished after tracked folder delay.", path);
1 office 887  
5 office 888 return;
889 }
1 office 890  
5 office 891 try
892 {
893 var fileName = System.IO.Path.GetFileName(path);
894 var screenCapture = ScreenCapture.Capture((Utilities.CaptureMode)Configuration.CaptureMode);
1 office 895  
11 office 896 await _snapshotDatabase.CreateSnapshotAsync(fileName, path, screenCapture, color,
5 office 897 _cancellationToken);
898 }
899 catch (SQLiteException exception)
900 {
901 if (exception.ResultCode == SQLiteErrorCode.Constraint)
1 office 902 {
5 office 903 Log.Information(exception, "Snapshot already exists.");
1 office 904 }
5 office 905 }
906 catch (Exception exception)
907 {
908 Log.Error(exception, "Could not take snapshot.", path);
909 }
910 });
911  
912 using (var snapshotLink =
913 bufferBlock.LinkTo(actionBlock, new DataflowLinkOptions() { PropagateCompletion = true }))
914 {
915 await _changedFilesLock.WaitAsync(_cancellationToken);
916 try
917 {
918 foreach (var path in _changedFiles)
1 office 919 {
5 office 920 await bufferBlock.SendAsync(path, cancellationToken);
1 office 921 }
5 office 922 bufferBlock.Complete();
923 await bufferBlock.Completion;
1 office 924 }
5 office 925 catch (Exception exception)
926 {
927 Log.Error(exception, "Could not take snapshots.");
928 }
929 finally
930 {
931 _changedFiles.Clear();
932 _changedFilesLock.Release();
933 }
1 office 934 }
935 }
936  
937 #endregion
938 }
939 }