using System; using System.Collections.Generic; using System.Data.SQLite; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; namespace ZeroLevel.SqLite.Model { /// /// Хранит данные указанное число дней, и позволяет выполнить проверку наличия данных, для отбрасывания дубликатов /// public sealed class SqLiteDupStorage : BaseSqLiteDB, IDisposable { #region Fields private const string DEFAUL_TABLE_NAME = "History"; private readonly SQLiteConnection _db; private readonly long _removeOldRecordsTaskKey; private readonly int _countDays; private readonly string _table_name; private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); #endregion Fields #region Private members private sealed class DuplicationStorageRecord { public string Hash { get; set; } public byte[] Body { get; set; } public long Timestamp { get; set; } } private void RemoveOldRecordsTask(long key) { _rwLock.EnterWriteLock(); try { Execute($"DELETE FROM {_table_name} WHERE timestamp < @limit", _db, new SQLiteParameter[] { new SQLiteParameter("limit", DateTime.Now.AddDays(-_countDays).Ticks) }); } catch (Exception ex) { Log.Error(ex, "[SQLiteDupStorage] Fault remove old records from db"); } finally { _rwLock.ExitWriteLock(); } } #endregion Private members #region Ctor public SqLiteDupStorage(string database_file_path, string tableName, int period) { var path = PrepareDb(database_file_path); if (string.IsNullOrWhiteSpace(tableName)) { _table_name = DEFAUL_TABLE_NAME; } else { _table_name = tableName; } _db = new SQLiteConnection($"Data Source={path};Version=3;"); _db.Open(); Execute($"CREATE TABLE IF NOT EXISTS {_table_name} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT, body BLOB, timestamp INTEGER)", _db); Execute($"CREATE INDEX IF NOT EXISTS hash_index ON {_table_name} (hash)", _db); _countDays = period > 0 ? period : 1; _removeOldRecordsTaskKey = Sheduller.RemindEvery(TimeSpan.FromMinutes(1), RemoveOldRecordsTask); } #endregion Ctor #region API /// /// true в случае обнаружения дубликата /// public bool TestAndInsert(byte[] body) { var hash = GenerateSHA256String(body); var timestamp = DateTime.Now.Ticks; SQLiteDataReader reader; _rwLock.EnterReadLock(); var exists = new List(); try { reader = Read($"SELECT body FROM {_table_name} WHERE hash=@hash", _db, new SQLiteParameter[] { new SQLiteParameter("hash", hash) }); while (reader.Read()) { exists.Add((byte[])reader.GetValue(0)); } reader.Close(); } catch (Exception ex) { Log.Error(ex, $"[SQLiteDupStorage] Fault search existing records by hash ({hash})"); // no return! } finally { _rwLock.ExitReadLock(); } reader = null; if (exists.Any()) { foreach (var candidate in exists) { if (ArrayExtensions.UnsafeEquals(candidate, body)) return true; } } _rwLock.EnterWriteLock(); try { Execute($"INSERT INTO {_table_name} ('hash', 'body', 'timestamp') values (@hash, @body, @timestamp)", _db, new SQLiteParameter[] { new SQLiteParameter("hash", hash), new SQLiteParameter("body", body), new SQLiteParameter("timestamp", timestamp) }); } catch (Exception ex) { Log.Error(ex, $"[SQLiteDupStorage] Fault insert record in duplications storage. Hash '{hash}'. Timestamp '{timestamp}'."); } finally { _rwLock.ExitWriteLock(); } return false; } #endregion API #region IDisposable public void Dispose() { Sheduller.Remove(_removeOldRecordsTaskKey); try { _db?.Dispose(); } catch (Exception ex) { Log.Error(ex, "[SQLiteDupStorage] Fault close db connection"); } } #endregion IDisposable #region Helpers private static string GenerateSHA256String(byte[] bytes) { using (SHA256 sha256 = SHA256Managed.Create()) { byte[] hash = sha256.ComputeHash(bytes); return ByteArrayToString(hash); } } private static string ByteArrayToString(byte[] ba) { StringBuilder hex = new StringBuilder(ba.Length * 2); foreach (byte b in ba) hex.AppendFormat("{0:x2}", b); return hex.ToString(); } #endregion Helpers } }