Horizon – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 using System;
2 using System.Collections.Generic;
3 using System.Data.SQLite;
4 using System.Drawing;
5 using System.Drawing.Imaging;
6 using System.Globalization;
7 using System.IO;
8 using System.IO.Compression;
9 using System.Security.Cryptography;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Horizon.Snapshots;
13 using Horizon.Utilities;
14 using Serilog;
15  
16 namespace Horizon.Database
17 {
18 public class SnapshotDatabase : IDisposable
19 {
20 #region Static Fields and Constants
21  
22 private const string CreateTableSql =
23 "CREATE TABLE IF NOT EXISTS \"Snapshots\" ( \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"Name\" TEXT NOT NULL, \"Path\" TEXT NOT NULL, \"Time\" TEXT NOT NULL, \"Hash\" TEXT NOT NULL, \"Data\" BLOB, \"Color\" INTEGER, \"Shot\" BLOB, \"Note\" TEXT, UNIQUE (\"Hash\") ON CONFLICT FAIL)";
24  
25 private const string SetAutoVacuumSql = "PRAGMA auto_vacuum = FULL";
26  
27 private const string SnapshotFileSql =
28 "INSERT INTO \"Snapshots\" ( \"Name\", \"Path\", \"Time\", \"Data\", \"Shot\", \"Color\", \"Hash\" ) VALUES ( @name, @path, @time, zeroblob(@dataLength), zeroblob(@shotLength), @color, @hash )";
29  
30 private const string SnapshotFileNoScreenshotSql =
31 "INSERT INTO \"Snapshots\" ( \"Name\", \"Path\", \"Time\", \"Data\", \"Shot\", \"Color\", \"Hash\" ) VALUES ( @name, @path, @time, zeroblob(@dataLength), null, @color, @hash )";
32  
33 private const string RetrieveSnapshotsSql =
34 "SELECT \"Name\", \"Path\", \"Time\", \"Color\", \"Hash\" FROM \"Snapshots\" ORDER BY datetime(\"Time\") DESC";
35  
36 private const string RetrieveDataPathFromHashSql =
37 "SELECT \"id\", \"Path\", \"Data\" FROM \"Snapshots\" WHERE Hash = @hash";
38  
39 private const string RetrieveDataFromHashSql =
40 "SELECT \"id\", \"Data\" FROM \"Snapshots\" WHERE Hash = @hash";
41  
42 private const string UpdateFileSql =
43 "UPDATE \"Snapshots\" SET Data = zeroblob(@dataLength), Hash = @recomputedHash WHERE Hash = @hash";
44  
45 private const string RemoveSnapshotFromHashSql =
46 "DELETE FROM \"Snapshots\" WHERE Hash = @hash";
47  
48 private const string RemoveScreenshotFromHashSql =
49 "UPDATE \"Snapshots\" SET Shot = null WHERE Hash = @hash";
50  
51 private const string UpdateColorFromHashSql =
52 "UPDATE \"Snapshots\" SET Color = @color WHERE Hash = @hash";
53  
54 private const string UpdateNoteFromHashSql =
55 "UPDATE \"Snapshots\" SET Note = @note WHERE Hash = @hash";
56  
57 private const string UpdateHashFromHashSql = "UPDATE \"Snapshots\" SET Hash = @to WHERE Hash = @from";
58  
59 private const string RelocateFileFromHashSql =
60 "UPDATE \"Snapshots\" SET Path = @path WHERE Hash = @hash";
61  
62 private const string RemoveColorFromHashSql =
63 "UPDATE \"Snapshots\" SET Color = null WHERE Hash = @hash";
64  
65 private const string RetrievePreviewFromHashSql =
66 "SELECT \"id\", \"Note\", \"Shot\" FROM \"Snapshots\" WHERE Hash = @hash";
67  
68 private const string CountSnapshotsSql = "SELECT COUNT(*) FROM \"Snapshots\"";
69  
70 private const string GetLastRowInsertSql = "SELECT last_insert_rowid()";
71  
72 private const string GetRowFromHashSql = "SELECT \"id\" FROM \"Snapshots\" WHERE Hash = @hash";
73  
74 private const string RetrieveTimeFromHash = "SELECT \"Time\" FROM \"Snapshots\" WHERE Hash = @hash";
75  
76 private const string UpdateTimeFromHash = "UPDATE \"Snapshots\" SET Time = @time WHERE Hash = @hash";
77  
78 private static readonly string DatabaseConnectionString = $"Data Source={Constants.DatabaseFilePath};";
79  
80 private static CancellationToken _cancellationToken;
81  
82 #endregion
83  
84 #region Public Events & Delegates
85  
86 public event EventHandler<SnapshotDataUpdateEventArgs> SnapshotDataUpdate;
87  
88 public event EventHandler<SnapshotNoteUpdateEventArgs> SnapshotNoteUpdate;
89  
90 public event EventHandler<SnapshotCreateEventArgs> SnapshotCreate;
91  
92 public event EventHandler<SnapshotRevertEventArgs> SnapshotRevert;
93  
94 #endregion
95  
96 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
97  
98 private readonly CancellationTokenSource _cancellationTokenSource;
99  
100 private SemaphoreSlim _snapshotSemaphore;
101  
102 #endregion
103  
104 #region Constructors, Destructors and Finalizers
105  
106 public SnapshotDatabase()
107 {
108 _cancellationTokenSource = new CancellationTokenSource();
109 _cancellationToken = _cancellationTokenSource.Token;
110  
111 _snapshotSemaphore = new SemaphoreSlim(1, 1);
112  
113 Directory.CreateDirectory(Constants.DatabaseDirectory);
114  
115 CreateDatabase(_cancellationToken).ContinueWith(async createDatabaseTask =>
116 {
117 try
118 {
119 await createDatabaseTask;
120  
121 try
122 {
123 await SetAutoVacuum(_cancellationToken);
124 }
125 catch
126 {
127 Log.Error("Unable to set auto vacuum for database.");
128 }
129 }
130 catch
131 {
132 Log.Error("Unable to create database;");
133 }
134 }).Wait(_cancellationToken);
135 }
136  
137 public void Dispose()
138 {
139 _cancellationTokenSource.Cancel();
140  
141 _snapshotSemaphore?.Dispose();
142 _snapshotSemaphore = null;
143 }
144  
145 #endregion
146  
147 #region Public Methods
148  
149 public async Task DeleteScreenshot(string hash, CancellationToken cancellationToken)
150 {
151 var connectionString = new SQLiteConnectionStringBuilder
152 {
153 ConnectionString = DatabaseConnectionString
154 };
155  
156 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
157 {
158 await sqliteConnection.OpenAsync(cancellationToken);
159  
160 using (var dbTransaction = sqliteConnection.BeginTransaction())
161 {
162 // Insert the file change.
163 using (var sqliteCommand =
164 new SQLiteCommand(RemoveScreenshotFromHashSql, sqliteConnection, dbTransaction))
165 {
166 try
167 {
168 sqliteCommand.Parameters.AddRange(new[]
169 {
170 new SQLiteParameter("@hash", hash)
171 });
172  
173 sqliteCommand.Prepare();
174  
175 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
176  
177 dbTransaction.Commit();
178 }
179 catch
180 {
181 dbTransaction.Rollback();
182  
183 throw;
184 }
185 }
186 }
187 }
188 }
189  
190 public async Task NormalizeTime(string hash, CancellationToken cancellationToken)
191 {
192 var connectionString = new SQLiteConnectionStringBuilder
193 {
194 ConnectionString = DatabaseConnectionString
195 };
196  
197 using (var sqliteConnection =
198 new SQLiteConnection(connectionString.ConnectionString))
199 {
200 await sqliteConnection.OpenAsync(cancellationToken);
201  
202 using (var readSQLiteCommand = new SQLiteCommand(RetrieveTimeFromHash, sqliteConnection))
203 {
204 readSQLiteCommand.Parameters.AddRange(new[]
205 {
206 new SQLiteParameter("@hash", hash)
207 });
208  
209 readSQLiteCommand.Prepare();
210  
211 using (var sqlDataReader = await readSQLiteCommand.ExecuteReaderAsync(cancellationToken))
212 {
213 using (var dbTransaction = sqliteConnection.BeginTransaction())
214 {
215 try
216 {
217 while (await sqlDataReader.ReadAsync(cancellationToken))
218 {
219 var time = (string)sqlDataReader["Time"];
220  
221 // Skip if already ISO 8601
222 if (DateTime.TryParseExact(time,
223 "yyyy-MM-ddTHH:mm:ss.fff",
224 CultureInfo.InvariantCulture,
225 DateTimeStyles.None, out _))
226 {
227 continue;
228 }
229  
230 if (!DateTime.TryParse(time, out var dateTime))
231 {
232 dateTime = DateTime.Now;
233 }
234  
235 using (var writeSQLiteCommand =
236 new SQLiteCommand(UpdateTimeFromHash, sqliteConnection, dbTransaction))
237 {
238 writeSQLiteCommand.Parameters.AddRange(new[]
239 {
240 new SQLiteParameter("@time", dateTime.ToString("yyyy-MM-ddTHH:mm:ss.fff")),
241 new SQLiteParameter("@hash", hash)
242 });
243  
244 writeSQLiteCommand.Prepare();
245  
246 await writeSQLiteCommand.ExecuteNonQueryAsync(cancellationToken);
247 }
248 }
249  
250 dbTransaction.Commit();
251 }
252 catch
253 {
254 dbTransaction.Rollback();
255  
256 throw;
257 }
258 }
259 }
260 }
261 }
262 }
263  
264 public async Task<long> CountSnapshots(CancellationToken cancellationToken)
265 {
266 var connectionString = new SQLiteConnectionStringBuilder
267 {
268 ConnectionString = DatabaseConnectionString
269 };
270  
271 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
272 {
273 await sqliteConnection.OpenAsync(cancellationToken);
274  
275 // Insert the file change.
276 using (var sqliteCommand = new SQLiteCommand(CountSnapshotsSql, sqliteConnection))
277 {
278 long count = 0;
279  
280 sqliteCommand.Prepare();
281  
282 using (var sqlDataReader = await sqliteCommand.ExecuteReaderAsync(cancellationToken))
283 {
284 while (await sqlDataReader.ReadAsync(cancellationToken))
285 {
286 if (!(sqlDataReader[0] is long dbCount))
287 {
288 count = -1;
289 break;
290 }
291  
292 count = dbCount;
293 }
294  
295 return count;
296 }
297 }
298 }
299 }
300  
301 public async Task<IEnumerable<Snapshot>> LoadSnapshots(CancellationToken cancellationToken)
302 {
303 var connectionString = new SQLiteConnectionStringBuilder
304 {
305 ConnectionString = DatabaseConnectionString
306 };
307  
308 using (var sqliteConnection =
309 new SQLiteConnection(connectionString.ConnectionString))
310 {
311 await sqliteConnection.OpenAsync(cancellationToken);
312  
313 // Insert the file change.
314 using (var sqliteCommand = new SQLiteCommand(RetrieveSnapshotsSql, sqliteConnection))
315 {
316 sqliteCommand.Prepare();
317  
318 using (var sqlDataReader = await sqliteCommand.ExecuteReaderAsync(cancellationToken))
319 {
320 var snapshots = new List<Snapshot>();
321 while (await sqlDataReader.ReadAsync(cancellationToken))
322 {
323 var name = (string)sqlDataReader["Name"];
324 var path = (string)sqlDataReader["Path"];
325 var time = (string)sqlDataReader["Time"];
326 var hash = (string)sqlDataReader["Hash"];
327  
328 var color = Color.Empty;
329  
330 if (!(sqlDataReader["Color"] is DBNull))
331 {
332 var dbColor = Convert.ToInt32(sqlDataReader["Color"]);
333  
334 switch (dbColor)
335 {
336 case 0:
337 color = Color.Empty;
338 break;
339 default:
340 color = Color.FromArgb(dbColor);
341 break;
342 }
343 }
344  
345 snapshots.Add(new Snapshot(name, path, time, hash, color));
346 }
347  
348 return snapshots;
349 }
350 }
351 }
352 }
353  
354 public async Task CreateSnapshot(string name, string path, Color color, CancellationToken cancellationToken)
355 {
356 await _snapshotSemaphore.WaitAsync(cancellationToken);
357  
358 var connectionString = new SQLiteConnectionStringBuilder
359 {
360 ConnectionString = DatabaseConnectionString
361 };
362  
363 using (var sqliteConnection =
364 new SQLiteConnection(connectionString.ConnectionString))
365 {
366 await sqliteConnection.OpenAsync(cancellationToken);
367  
368 using (var dbTransaction = sqliteConnection.BeginTransaction())
369 {
370 try
371 {
372 using (var md5 = MD5.Create())
373 {
374 using (var hashMemoryStream = new MemoryStream())
375 {
376 using (var fileStream =
377 await Miscellaneous.GetFileStream(path, FileMode.Open, FileAccess.Read,
378 FileShare.Read,
379 cancellationToken))
380 {
381 fileStream.Position = 0L;
382 await fileStream.CopyToAsync(hashMemoryStream);
383  
384 hashMemoryStream.Position = 0L;
385 var hash = md5.ComputeHash(hashMemoryStream);
386 var hashHex = BitConverter.ToString(hash).Replace("-", "")
387 .ToLowerInvariant();
388  
389 using (var fileMemoryStream = new MemoryStream())
390 {
391 using (var fileZipStream =
392 new GZipStream(fileMemoryStream, CompressionMode.Compress, true))
393 {
394 fileStream.Position = 0L;
395 await fileStream.CopyToAsync(fileZipStream);
396 fileZipStream.Close();
397  
398 var time = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fff");
399  
400 fileMemoryStream.Position = 0L;
401  
402 // Insert the file change.
403 using (var sqliteCommand =
404 new SQLiteCommand(SnapshotFileNoScreenshotSql, sqliteConnection,
405 dbTransaction))
406 {
407 sqliteCommand.Parameters.AddRange(new[]
408 {
409 new SQLiteParameter("@name", name),
410 new SQLiteParameter("@time", time),
411 new SQLiteParameter("@path", path),
412 new SQLiteParameter("@dataLength",
413 fileMemoryStream.Length),
414 new SQLiteParameter("@hash", hashHex)
415 });
416  
417 var numeric = color.ToArgb();
418 switch (numeric)
419 {
420 case 0:
421 sqliteCommand.Parameters.Add(
422 new SQLiteParameter("@color", null));
423 break;
424 default:
425 sqliteCommand.Parameters.Add(
426 new SQLiteParameter("@color", numeric));
427 break;
428 }
429  
430 sqliteCommand.Prepare();
431  
432 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
433 }
434  
435 // Insert the data blobs.
436 using (var sqliteCommand =
437 new SQLiteCommand(GetLastRowInsertSql, sqliteConnection,
438 dbTransaction))
439 {
440 sqliteCommand.Prepare();
441  
442 var rowId =
443 (long)await sqliteCommand.ExecuteScalarAsync(cancellationToken);
444  
445 using (var sqliteBlob =
446 SQLiteBlob.Create(sqliteConnection, "main", "Snapshots", "Data",
447 rowId,
448 false))
449 {
450 var fileMemoryStreamData = fileMemoryStream.ToArray();
451  
452 sqliteBlob.Write(fileMemoryStreamData, fileMemoryStreamData.Length,
453 0);
454 }
455 }
456  
457 dbTransaction.Commit();
458  
459 SnapshotCreate?.Invoke(this,
460 new SnapshotCreateSuccessEventArgs(name, time, path, color,
461 hashHex));
462 }
463 }
464 }
465 }
466 }
467 }
468 catch (SQLiteException exception)
469 {
470 dbTransaction.Rollback();
471  
472 if (exception.ResultCode != SQLiteErrorCode.Constraint)
473 {
474 SnapshotCreate?.Invoke(this,
475 new SnapshotCreateFailureEventArgs(name, path, color, exception));
476 }
477  
478 throw;
479 }
480 catch (Exception exception)
481 {
482 dbTransaction.Rollback();
483  
484 SnapshotCreate?.Invoke(this, new SnapshotCreateFailureEventArgs(name, path, color, exception));
485  
486 throw;
487 }
488 finally
489 {
490 _snapshotSemaphore.Release();
491 }
492 }
493 }
494 }
495  
496 public async Task CreateSnapshot(string name, string path,
497 Bitmap shot, Color color, CancellationToken cancellationToken)
498 {
499 await _snapshotSemaphore.WaitAsync(cancellationToken);
500  
501 var connectionString = new SQLiteConnectionStringBuilder
502 {
503 ConnectionString = DatabaseConnectionString
504 };
505  
506 using (var sqliteConnection =
507 new SQLiteConnection(connectionString.ConnectionString))
508 {
509 await sqliteConnection.OpenAsync(cancellationToken);
510  
511 using (var dbTransaction = sqliteConnection.BeginTransaction())
512 {
513 try
514 {
515 using (var md5 = MD5.Create())
516 {
517 using (var hashMemoryStream = new MemoryStream())
518 {
519 using (var fileStream =
520 await Miscellaneous.GetFileStream(path, FileMode.Open, FileAccess.Read,
521 FileShare.Read,
522 cancellationToken))
523 {
524 fileStream.Position = 0L;
525 await fileStream.CopyToAsync(hashMemoryStream);
526  
527 hashMemoryStream.Position = 0L;
528 var hash = md5.ComputeHash(hashMemoryStream);
529 var hashHex = BitConverter.ToString(hash).Replace("-", "")
530 .ToLowerInvariant();
531  
532 using (var fileMemoryStream = new MemoryStream())
533 {
534 using (var fileZipStream =
535 new GZipStream(fileMemoryStream, CompressionMode.Compress, true))
536 {
537 fileStream.Position = 0L;
538 await fileStream.CopyToAsync(fileZipStream);
539 fileZipStream.Close();
540  
541 using (var bitmapMemoryStream = new MemoryStream())
542 {
543 using (var bitmapZipStream =
544 new GZipStream(bitmapMemoryStream, CompressionMode.Compress,
545 true))
546 {
547 shot.Save(bitmapZipStream, ImageFormat.Bmp);
548 bitmapZipStream.Close();
549  
550 var time = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fff");
551  
552 fileMemoryStream.Position = 0L;
553 bitmapMemoryStream.Position = 0L;
554  
555 // Insert the file change.
556 using (var sqliteCommand =
557 new SQLiteCommand(SnapshotFileSql, sqliteConnection,
558 dbTransaction))
559 {
560 sqliteCommand.Parameters.AddRange(new[]
561 {
562 new SQLiteParameter("@name", name),
563 new SQLiteParameter("@time", time),
564 new SQLiteParameter("@path", path),
565 new SQLiteParameter("@shotLength",
566 bitmapMemoryStream.Length),
567 new SQLiteParameter("@dataLength",
568 fileMemoryStream.Length),
569 new SQLiteParameter("@hash", hashHex)
570 });
571  
572 var numeric = color.ToArgb();
573 switch (numeric)
574 {
575 case 0:
576 sqliteCommand.Parameters.Add(
577 new SQLiteParameter("@color", null));
578 break;
579 default:
580 sqliteCommand.Parameters.Add(
581 new SQLiteParameter("@color", numeric));
582 break;
583 }
584  
585 sqliteCommand.Prepare();
586  
587 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
588 }
589  
590 // Insert the data blobs.
591 using (var sqliteCommand =
592 new SQLiteCommand(GetLastRowInsertSql, sqliteConnection,
593 dbTransaction))
594 {
595 sqliteCommand.Prepare();
596  
597 var rowId =
598 (long)await sqliteCommand.ExecuteScalarAsync(
599 cancellationToken);
600  
601 using (var sqliteBlob =
602 SQLiteBlob.Create(sqliteConnection, "main", "Snapshots",
603 "Data",
604 rowId,
605 false))
606 {
607 var fileMemoryStreamData = fileMemoryStream.ToArray();
608  
609 sqliteBlob.Write(fileMemoryStreamData,
610 fileMemoryStreamData.Length,
611 0);
612 }
613  
614 using (var sqliteBlob =
615 SQLiteBlob.Create(sqliteConnection, "main", "Snapshots",
616 "Shot",
617 rowId,
618 false))
619 {
620 var bitmapMemoryStreamData = bitmapMemoryStream.ToArray();
621  
622 sqliteBlob.Write(bitmapMemoryStreamData,
623 bitmapMemoryStreamData.Length,
624 0);
625 }
626 }
627  
628 dbTransaction.Commit();
629  
630 SnapshotCreate?.Invoke(this,
631 new SnapshotCreateSuccessEventArgs(name, time, path, color,
632 hashHex));
633 }
634 }
635 }
636 }
637 }
638 }
639 }
640 }
641 catch (SQLiteException exception)
642 {
643 dbTransaction.Rollback();
644  
645 if (exception.ResultCode != SQLiteErrorCode.Constraint)
646 {
647 SnapshotCreate?.Invoke(this,
648 new SnapshotCreateFailureEventArgs(name, path, color, exception));
649 }
650  
651 throw;
652 }
653 catch (Exception exception)
654 {
655 dbTransaction.Rollback();
656  
657 SnapshotCreate?.Invoke(this, new SnapshotCreateFailureEventArgs(name, path, color, exception));
658  
659 throw;
660 }
661 finally
662 {
663 _snapshotSemaphore.Release();
664 }
665 }
666 }
667 }
668  
669 public async Task SaveFile(string path, string hash, CancellationToken cancellationToken)
670 {
671 var connectionString = new SQLiteConnectionStringBuilder
672 {
673 ConnectionString = DatabaseConnectionString
674 };
675  
676 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
677 {
678 await sqliteConnection.OpenAsync(cancellationToken);
679  
680 // Insert the file change.
681 using (var sqliteCommand =
682 new SQLiteCommand(RetrieveDataPathFromHashSql, sqliteConnection))
683 {
684 sqliteCommand.Parameters.AddRange(new[]
685 {
686 new SQLiteParameter("@hash", hash)
687 });
688  
689 sqliteCommand.Prepare();
690  
691 using (var sqlDataReader = await sqliteCommand.ExecuteReaderAsync(cancellationToken))
692 {
693 while (await sqlDataReader.ReadAsync(cancellationToken))
694 {
695 // Create directories if they do not exist.
696 var dir = Path.GetDirectoryName(path);
697  
698 if (dir != null && !Directory.Exists(dir))
699 {
700 Directory.CreateDirectory(dir);
701 }
702  
703 using (var readStream = sqlDataReader.GetStream(2))
704 {
705 using (var fileStream =
706 await Miscellaneous.GetFileStream(path, FileMode.Create, FileAccess.Write,
707 FileShare.Write,
708 cancellationToken))
709 {
710 readStream.Position = 0L;
711  
712 using (var zipStream = new GZipStream(readStream, CompressionMode.Decompress))
713 {
714 await zipStream.CopyToAsync(fileStream);
715 }
716 }
717 }
718 }
719 }
720 }
721 }
722 }
723  
724 public async Task RevertFile(string name, string hash, CancellationToken cancellationToken, bool atomic = true)
725 {
726 await _snapshotSemaphore.WaitAsync(cancellationToken);
727  
728 var connectionString = new SQLiteConnectionStringBuilder
729 {
730 ConnectionString = DatabaseConnectionString
731 };
732  
733 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
734 {
735 await sqliteConnection.OpenAsync(cancellationToken);
736  
737 // Insert the file change.
738 using (var sqliteCommand =
739 new SQLiteCommand(RetrieveDataPathFromHashSql, sqliteConnection))
740 {
741 try
742 {
743 sqliteCommand.Parameters.AddRange(new[]
744 {
745 new SQLiteParameter("@hash", hash)
746 });
747  
748 sqliteCommand.Prepare();
749  
750 using (var sqlDataReader = await sqliteCommand.ExecuteReaderAsync(cancellationToken))
751 {
752 while (await sqlDataReader.ReadAsync(cancellationToken))
753 {
754 var path = (string)sqlDataReader["Path"];
755  
756 // Create directories if they do not exist.
757 var dir = Path.GetDirectoryName(path);
758  
759 if (dir != null && !Directory.Exists(dir))
760 {
761 Directory.CreateDirectory(dir);
762 }
763  
764 switch (atomic)
765 {
766 case true:
767 // Atomic
768 var temp = Path.Combine(Path.GetDirectoryName(path),
769 $"{Path.GetFileName(path)}.temp");
770  
771 using (var readStream = sqlDataReader.GetStream(2))
772 {
773 using (var fileStream = new FileStream(temp, FileMode.Create,
774 FileAccess.Write,
775 FileShare.None))
776 {
777 using (var zipStream =
778 new GZipStream(readStream, CompressionMode.Decompress))
779 {
780 zipStream.CopyTo(fileStream);
781 }
782 }
783 }
784  
785 try
786 {
787 File.Replace(temp, path, null, true);
788 }
789 catch
790 {
791 try
792 {
793 File.Delete(temp);
794 }
795 catch (Exception exception)
796 {
797 // Suppress deletion errors of temporary file.
798 Log.Warning(exception, "Could not delete temporary file.", temp);
799 }
800  
801 throw;
802 }
803  
804 break;
805 default:
806 // Asynchronous
807 using (var readStream = sqlDataReader.GetStream(2))
808 {
809 using (var fileStream =
810 await Miscellaneous.GetFileStream(path, FileMode.Create,
811 FileAccess.Write,
812 FileShare.Write,
813 cancellationToken))
814 {
815 readStream.Position = 0L;
816  
817 using (var zipStream =
818 new GZipStream(readStream, CompressionMode.Decompress))
819 {
820 await zipStream.CopyToAsync(fileStream);
821 }
822 }
823 }
824  
825  
826 break;
827 }
828  
829 SnapshotRevert?.Invoke(this, new SnapshotRevertSuccessEventArgs(name));
830 }
831 }
832 }
833 catch
834 {
835 SnapshotRevert?.Invoke(this, new SnapshotRevertFailureEventArgs(name));
836  
837 throw;
838 }
839 finally
840 {
841 _snapshotSemaphore.Release();
842 }
843 }
844 }
845 }
846  
847 public async Task RemoveFileFast(IEnumerable<string> hashes, CancellationToken cancellationToken)
848 {
849 var connectionString = new SQLiteConnectionStringBuilder
850 {
851 ConnectionString = DatabaseConnectionString
852 };
853  
854 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
855 {
856 await sqliteConnection.OpenAsync(cancellationToken);
857  
858 using (var dbTransaction = sqliteConnection.BeginTransaction())
859 {
860 try
861 {
862 var transactionCommands = new List<Task>();
863  
864 foreach (var hash in hashes)
865 {
866 // Insert the file change.
867 using (var sqliteCommand =
868 new SQLiteCommand(RemoveSnapshotFromHashSql, sqliteConnection, dbTransaction))
869 {
870 sqliteCommand.Parameters.AddRange(new[]
871 {
872 new SQLiteParameter("@hash", hash)
873 });
874  
875 sqliteCommand.Prepare();
876  
877 var command = sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
878  
879 transactionCommands.Add(command);
880 }
881 }
882  
883 await Task.WhenAll(transactionCommands);
884  
885 dbTransaction.Commit();
886 }
887 catch
888 {
889 dbTransaction.Rollback();
890  
891 throw;
892 }
893 }
894 }
895 }
896  
897 public async Task RemoveFile(string hash, CancellationToken cancellationToken)
898 {
899 var connectionString = new SQLiteConnectionStringBuilder
900 {
901 ConnectionString = DatabaseConnectionString
902 };
903  
904 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
905 {
906 await sqliteConnection.OpenAsync(cancellationToken);
907  
908 using (var dbTransaction = sqliteConnection.BeginTransaction())
909 {
910 // Insert the file change.
911 using (var sqliteCommand =
912 new SQLiteCommand(RemoveSnapshotFromHashSql, sqliteConnection, dbTransaction))
913 {
914 try
915 {
916 sqliteCommand.Parameters.AddRange(new[]
917 {
918 new SQLiteParameter("@hash", hash)
919 });
920  
921 sqliteCommand.Prepare();
922  
923 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
924  
925 dbTransaction.Commit();
926 }
927 catch
928 {
929 dbTransaction.Rollback();
930  
931 throw;
932 }
933 }
934 }
935 }
936 }
937  
938 public async Task UpdateColor(string hash, Color color, CancellationToken cancellationToken)
939 {
940 var connectionString = new SQLiteConnectionStringBuilder
941 {
942 ConnectionString = DatabaseConnectionString
943 };
944  
945 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
946 {
947 await sqliteConnection.OpenAsync(cancellationToken);
948  
949 using (var dbTransaction = sqliteConnection.BeginTransaction())
950 {
951 // Insert the file change.
952 using (var sqliteCommand =
953 new SQLiteCommand(UpdateColorFromHashSql, sqliteConnection, dbTransaction))
954 {
955 try
956 {
957 sqliteCommand.Parameters.AddRange(new[]
958 {
959 new SQLiteParameter("@hash", hash),
960 new SQLiteParameter("@color", color.ToArgb())
961 });
962  
963 sqliteCommand.Prepare();
964  
965 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
966  
967 dbTransaction.Commit();
968 }
969 catch
970 {
971 dbTransaction.Rollback();
972  
973 throw;
974 }
975 }
976 }
977 }
978 }
979  
980 public async Task RemoveColor(string hash, CancellationToken cancellationToken)
981 {
982 var connectionString = new SQLiteConnectionStringBuilder
983 {
984 ConnectionString = DatabaseConnectionString
985 };
986  
987 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
988 {
989 await sqliteConnection.OpenAsync(cancellationToken);
990  
991 using (var dbTransaction = sqliteConnection.BeginTransaction())
992 {
993 // Insert the file change.
994 using (var sqliteCommand =
995 new SQLiteCommand(RemoveColorFromHashSql, sqliteConnection, dbTransaction))
996 {
997 try
998 {
999 sqliteCommand.Parameters.AddRange(new[]
1000 {
1001 new SQLiteParameter("@hash", hash)
1002 });
1003  
1004 sqliteCommand.Prepare();
1005  
1006 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
1007  
1008 dbTransaction.Commit();
1009 }
1010 catch
1011 {
1012 dbTransaction.Rollback();
1013  
1014 throw;
1015 }
1016 }
1017 }
1018 }
1019 }
1020  
1021 public async Task<SnapshotPreview> RetrievePreview(string hash, CancellationToken cancellationToken)
1022 {
1023 var connectionString = new SQLiteConnectionStringBuilder
1024 {
1025 ConnectionString = DatabaseConnectionString
1026 };
1027  
1028 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1029 {
1030 await sqliteConnection.OpenAsync(cancellationToken);
1031  
1032 // Insert the file change.
1033 using (var sqliteCommand = new SQLiteCommand(RetrievePreviewFromHashSql, sqliteConnection))
1034 {
1035 sqliteCommand.Parameters.AddRange(new[]
1036 {
1037 new SQLiteParameter("@hash", hash)
1038 });
1039  
1040 var note = string.Empty;
1041  
1042 sqliteCommand.Prepare();
1043  
1044 using (var sqlDataReader = await sqliteCommand.ExecuteReaderAsync(cancellationToken))
1045 {
1046 while (await sqlDataReader.ReadAsync(cancellationToken))
1047 {
1048 if (!(sqlDataReader["Note"] is DBNull))
1049 {
1050 note = (string)sqlDataReader["Note"];
1051 }
1052  
1053 Bitmap shot = null;
1054  
1055 if (!(sqlDataReader["Shot"] is DBNull))
1056 {
1057 var readStream = sqlDataReader.GetStream(2);
1058  
1059 readStream.Position = 0L;
1060  
1061 using (var zipStream = new GZipStream(readStream, CompressionMode.Decompress))
1062 {
1063 using (var image = Image.FromStream(zipStream))
1064 {
1065 shot = new Bitmap(image);
1066 }
1067 }
1068 }
1069  
1070 return new SnapshotPreview(hash, shot, note);
1071 }
1072  
1073 return null;
1074 }
1075 }
1076 }
1077 }
1078  
1079 /*
1080 public MemoryStream RetrieveFileStream(string hash, CancellationToken cancellationToken)
1081 {
1082 var connectionString = new SQLiteConnectionStringBuilder
1083 {
1084 ConnectionString = DatabaseConnectionString
1085 };
1086  
1087 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1088 {
1089  
1090 sqliteConnection.Open();
1091  
1092 // Insert the file change.
1093 using (var sqliteCommand = new SQLiteCommand(RetrieveDataFromHashSql, sqliteConnection))
1094 {
1095 sqliteCommand.Parameters.AddRange(new[]
1096 {
1097 new SQLiteParameter("@hash", hash)
1098 });
1099  
1100 sqliteCommand.Prepare();
1101  
1102 using (var sqlDataReader = sqliteCommand.ExecuteReader())
1103 {
1104  
1105 while (sqlDataReader.Read())
1106 {
1107 using (var readStream = sqlDataReader.GetStream(1))
1108 {
1109  
1110 using (var memoryStream = new MemoryStream())
1111 {
1112  
1113 readStream.Position = 0L;
1114  
1115 readStream.CopyTo(memoryStream);
1116  
1117 memoryStream.Position = 0L;
1118  
1119 using (var zipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
1120 {
1121  
1122 var outputStream = new MemoryStream();
1123  
1124 zipStream.CopyTo(outputStream);
1125  
1126 outputStream.Position = 0L;
1127  
1128 return outputStream;
1129 }
1130 }
1131 }
1132 }
1133  
1134 return null;
1135 }
1136 }
1137 }
1138 }
1139 */
1140  
1141 public async Task<MemoryStream> RetrieveFileStream(string hash, CancellationToken cancellationToken)
1142 {
1143 var connectionString = new SQLiteConnectionStringBuilder
1144 {
1145 ConnectionString = DatabaseConnectionString
1146 };
1147  
1148 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1149 {
1150 await sqliteConnection.OpenAsync(cancellationToken);
1151  
1152 // Insert the file change.
1153 using (var sqliteCommand = new SQLiteCommand(RetrieveDataFromHashSql, sqliteConnection))
1154 {
1155 sqliteCommand.Parameters.AddRange(new[]
1156 {
1157 new SQLiteParameter("@hash", hash)
1158 });
1159  
1160 sqliteCommand.Prepare();
1161  
1162 using (var sqlDataReader = await sqliteCommand.ExecuteReaderAsync(cancellationToken))
1163 {
1164 while (await sqlDataReader.ReadAsync(cancellationToken))
1165 {
1166 using (var readStream = sqlDataReader.GetStream(1))
1167 {
1168 using (var memoryStream = new MemoryStream())
1169 {
1170 readStream.Position = 0L;
1171  
1172 await readStream.CopyToAsync(memoryStream);
1173  
1174 memoryStream.Position = 0L;
1175  
1176 using (var zipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
1177 {
1178 // Do not dispose the returned stream and leave it up to callers to dispose.
1179 var outputStream = new MemoryStream();
1180  
1181 await zipStream.CopyToAsync(outputStream);
1182  
1183 outputStream.Position = 0L;
1184  
1185 return outputStream;
1186 }
1187 }
1188 }
1189 }
1190  
1191 return null;
1192 }
1193 }
1194 }
1195 }
1196  
1197 public async Task RelocateFile(string hash, string path, CancellationToken cancellationToken)
1198 {
1199 var connectionString = new SQLiteConnectionStringBuilder
1200 {
1201 ConnectionString = DatabaseConnectionString
1202 };
1203  
1204 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1205 {
1206 await sqliteConnection.OpenAsync(cancellationToken);
1207  
1208 using (var dbTransaction = sqliteConnection.BeginTransaction())
1209 {
1210 // Insert the file change.
1211 using (var sqliteCommand =
1212 new SQLiteCommand(RelocateFileFromHashSql, sqliteConnection, dbTransaction))
1213 {
1214 try
1215 {
1216 sqliteCommand.Parameters.AddRange(new[]
1217 {
1218 new SQLiteParameter("@hash", hash),
1219 new SQLiteParameter("@path", path)
1220 });
1221  
1222 sqliteCommand.Prepare();
1223  
1224 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
1225  
1226 dbTransaction.Commit();
1227 }
1228 catch
1229 {
1230 dbTransaction.Rollback();
1231  
1232 throw;
1233 }
1234 }
1235 }
1236 }
1237 }
1238  
1239 public async Task UpdateNote(string hash, string note, CancellationToken cancellationToken)
1240 {
1241 var connectionString = new SQLiteConnectionStringBuilder
1242 {
1243 ConnectionString = DatabaseConnectionString
1244 };
1245  
1246 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1247 {
1248 await sqliteConnection.OpenAsync(cancellationToken);
1249  
1250 using (var dbTransaction = sqliteConnection.BeginTransaction())
1251 {
1252 // Insert the file change.
1253 using (var sqliteCommand =
1254 new SQLiteCommand(UpdateNoteFromHashSql, sqliteConnection, dbTransaction))
1255 {
1256 try
1257 {
1258 sqliteCommand.Parameters.AddRange(new[]
1259 {
1260 new SQLiteParameter("@hash", hash),
1261 new SQLiteParameter("@note", note)
1262 });
1263  
1264 sqliteCommand.Prepare();
1265  
1266 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
1267  
1268 dbTransaction.Commit();
1269  
1270 SnapshotNoteUpdate?.Invoke(this, new SnapshotNoteUpdateSuccessEventArgs(note));
1271 }
1272 catch
1273 {
1274 dbTransaction.Rollback();
1275  
1276 SnapshotNoteUpdate?.Invoke(this, new SnapshotNoteUpdateFailureEventArgs());
1277  
1278 throw;
1279 }
1280 }
1281 }
1282 }
1283 }
1284  
1285 public async Task<string> UpdateFile(string hash, byte[] data, CancellationToken cancellationToken)
1286 {
1287 using (var dataMemoryStream = new MemoryStream(data))
1288 {
1289 var connectionString = new SQLiteConnectionStringBuilder
1290 {
1291 ConnectionString = DatabaseConnectionString
1292 };
1293  
1294 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1295 {
1296 await sqliteConnection.OpenAsync(cancellationToken);
1297  
1298 using (var dbTransaction = sqliteConnection.BeginTransaction())
1299 {
1300 try
1301 {
1302 using (var md5 = MD5.Create())
1303 {
1304 using (var hashMemoryStream = new MemoryStream())
1305 {
1306 dataMemoryStream.Position = 0L;
1307 await dataMemoryStream.CopyToAsync(hashMemoryStream);
1308  
1309 hashMemoryStream.Position = 0L;
1310 var recomputedHash = md5.ComputeHash(hashMemoryStream);
1311 var hashHex = BitConverter.ToString(recomputedHash).Replace("-", "")
1312 .ToLowerInvariant();
1313  
1314 using (var fileMemoryStream = new MemoryStream())
1315 {
1316 using (var fileZipStream =
1317 new GZipStream(fileMemoryStream, CompressionMode.Compress, true))
1318 {
1319 dataMemoryStream.Position = 0L;
1320 await dataMemoryStream.CopyToAsync(fileZipStream);
1321 fileZipStream.Close();
1322  
1323 fileMemoryStream.Position = 0L;
1324  
1325 // Insert the file change.
1326 using (var sqliteCommand =
1327 new SQLiteCommand(UpdateFileSql, sqliteConnection, dbTransaction))
1328 {
1329 sqliteCommand.Parameters.AddRange(new[]
1330 {
1331 new SQLiteParameter("@dataLength", fileMemoryStream.Length),
1332 new SQLiteParameter("@recomputedHash", hashHex),
1333 new SQLiteParameter("@hash", hash)
1334 });
1335  
1336 sqliteCommand.Prepare();
1337 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
1338 }
1339  
1340 using (var sqliteCommand =
1341 new SQLiteCommand(GetRowFromHashSql, sqliteConnection,
1342 dbTransaction))
1343 {
1344 sqliteCommand.Parameters.AddRange(new[]
1345 {
1346 new SQLiteParameter("@hash", hashHex)
1347 });
1348  
1349 sqliteCommand.Prepare();
1350  
1351 using (var sqlDataReader =
1352 await sqliteCommand.ExecuteReaderAsync(cancellationToken))
1353 {
1354 while (await sqlDataReader.ReadAsync(cancellationToken))
1355 {
1356 if (sqlDataReader["id"] is long rowId)
1357 {
1358 using (var sqliteBlob = SQLiteBlob.Create(sqliteConnection,
1359 "main",
1360 "Snapshots",
1361 "Data",
1362 rowId, false))
1363 {
1364 var fileMemoryStreamData = fileMemoryStream.ToArray();
1365  
1366 sqliteBlob.Write(fileMemoryStreamData,
1367 fileMemoryStreamData.Length,
1368 0);
1369 }
1370 }
1371 }
1372 }
1373 }
1374  
1375 dbTransaction.Commit();
1376  
1377 SnapshotDataUpdate?.Invoke(this,
1378 new SnapshotDataUpdateSuccessEventArgs(hash, hashHex));
1379  
1380 return hashHex;
1381 }
1382 }
1383 }
1384 }
1385 }
1386 catch
1387 {
1388 dbTransaction.Rollback();
1389  
1390 SnapshotDataUpdate?.Invoke(this, new SnapshotDataUpdateFailureEventArgs(hash));
1391  
1392 throw;
1393 }
1394 }
1395 }
1396 }
1397 }
1398  
1399 public async Task UpdateHash(string from, string to, CancellationToken cancellationToken)
1400 {
1401 var connectionString = new SQLiteConnectionStringBuilder
1402 {
1403 ConnectionString = DatabaseConnectionString
1404 };
1405  
1406 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1407 {
1408 await sqliteConnection.OpenAsync(cancellationToken);
1409  
1410 using (var dbTransaction = sqliteConnection.BeginTransaction())
1411 {
1412 // Insert the file change.
1413 using (var sqliteCommand =
1414 new SQLiteCommand(UpdateHashFromHashSql, sqliteConnection, dbTransaction))
1415 {
1416 try
1417 {
1418 sqliteCommand.Parameters.AddRange(new[]
1419 {
1420 new SQLiteParameter("@from", from),
1421 new SQLiteParameter("@to", to)
1422 });
1423  
1424 sqliteCommand.Prepare();
1425  
1426 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
1427  
1428 dbTransaction.Commit();
1429 }
1430 catch
1431 {
1432 dbTransaction.Rollback();
1433  
1434 throw;
1435 }
1436 }
1437 }
1438 }
1439 }
1440  
1441 #endregion
1442  
1443 #region Private Methods
1444  
1445 private static async Task SetAutoVacuum(CancellationToken cancellationToken)
1446 {
1447 var connectionString = new SQLiteConnectionStringBuilder
1448 {
1449 ConnectionString = DatabaseConnectionString
1450 };
1451  
1452 using (var sqliteConnection =
1453 new SQLiteConnection(connectionString.ConnectionString))
1454 {
1455 await sqliteConnection.OpenAsync(cancellationToken);
1456  
1457 // Set auto vacuum.
1458 using (var sqliteCommand = new SQLiteCommand(SetAutoVacuumSql, sqliteConnection))
1459 {
1460 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
1461 }
1462 }
1463 }
1464  
1465 private static async Task CreateDatabase(CancellationToken cancellationToken)
1466 {
1467 var connectionString = new SQLiteConnectionStringBuilder
1468 {
1469 ConnectionString = DatabaseConnectionString
1470 };
1471  
1472 using (var sqliteConnection = new SQLiteConnection(connectionString.ConnectionString))
1473 {
1474 await sqliteConnection.OpenAsync(cancellationToken);
1475  
1476 using (var dbTransaction = sqliteConnection.BeginTransaction())
1477 {
1478 // Create the table if it does not exist.
1479 using (var sqliteCommand = new SQLiteCommand(CreateTableSql, sqliteConnection, dbTransaction))
1480 {
1481 try
1482 {
1483 await sqliteCommand.ExecuteNonQueryAsync(cancellationToken);
1484  
1485 dbTransaction.Commit();
1486 }
1487 catch
1488 {
1489 dbTransaction.Rollback();
1490  
1491 throw;
1492 }
1493 }
1494 }
1495 }
1496 }
1497  
1498 #endregion
1499 }
1500 }