diff --git a/ZeroLevel.SQL/DBReader.cs b/ZeroLevel.SQL/DBReader.cs index 91b3bfa..832b373 100644 --- a/ZeroLevel.SQL/DBReader.cs +++ b/ZeroLevel.SQL/DBReader.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.Common; +using System.Linq; namespace ZeroLevel.SqlServer { @@ -33,8 +34,7 @@ namespace ZeroLevel.SqlServer { public static T Read(this DbDataReader reader, int index) { - if (reader == null) return default; - if (reader[index] == DBNull.Value) return default; + if (reader == null || reader.FieldCount <= index || reader[index] == DBNull.Value) return default; Type t; if ((t = Nullable.GetUnderlyingType(typeof(T))) != null) { @@ -44,8 +44,7 @@ namespace ZeroLevel.SqlServer } public static T Read(this DbDataReader reader, string name) { - if (reader == null) return default; - if (reader[name] == DBNull.Value) return default; + if (reader == null || false == reader.GetColumnSchema().Any(c => c.ColumnName.Equals(name, StringComparison.OrdinalIgnoreCase)) || reader[name] == DBNull.Value) return default; Type t; if ((t = Nullable.GetUnderlyingType(typeof(T))) != null) { @@ -55,8 +54,7 @@ namespace ZeroLevel.SqlServer } public static T Read(this IDataReader reader, int index) { - if (reader == null) return default; - if (reader[index] == DBNull.Value) return default; + if (reader == null || reader.FieldCount <= index || reader[index] == DBNull.Value) return default; Type t; if ((t = Nullable.GetUnderlyingType(typeof(T))) != null) { @@ -66,8 +64,7 @@ namespace ZeroLevel.SqlServer } public static T Read(this IDataReader reader, string name) { - if (reader == null) return default; - if (reader[name] == DBNull.Value) return default; + if (reader == null || false == reader.HasColumn(name) || reader[name] == DBNull.Value) return default; Type t; if ((t = Nullable.GetUnderlyingType(typeof(T))) != null) { @@ -75,11 +72,9 @@ namespace ZeroLevel.SqlServer } return (T)Convert.ChangeType(reader[name], typeof(T)); } - public static T Read(this DataRow reader, int index) { - if (reader == null) return default; - if (reader.ItemArray[index] == DBNull.Value) return default; + if (reader == null || reader.ItemArray.Length <= index || reader.ItemArray[index] == DBNull.Value) return default; Type t; if ((t = Nullable.GetUnderlyingType(typeof(T))) != null) { @@ -87,11 +82,9 @@ namespace ZeroLevel.SqlServer } return (T)Convert.ChangeType(reader.ItemArray[index], typeof(T)); } - public static T Read(this DataRow reader, string name) { - if (reader == null) return default; - if (reader[name] == DBNull.Value) return default; + if (reader == null || false == reader.Table.Columns.Contains(name) || reader[name] == DBNull.Value) return default; Type t; if ((t = Nullable.GetUnderlyingType(typeof(T))) != null) { @@ -99,5 +92,14 @@ namespace ZeroLevel.SqlServer } return (T)Convert.ChangeType(reader[name], typeof(T)); } + public static bool HasColumn(this IDataRecord dr, string columnName) + { + for (int i = 0; i < dr.FieldCount; i++) + { + if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + return false; + } } } diff --git a/ZeroLevel.SqLite/AuthRepository.cs b/ZeroLevel.SqLite/AuthRepository.cs new file mode 100644 index 0000000..69534b5 --- /dev/null +++ b/ZeroLevel.SqLite/AuthRepository.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using ZeroLevel.Models; + +namespace ZeroLevel.SqLite +{ + public class AuthRepository + { + private static byte[] DEFAULT_ADMIN_PWD_HASH = null; + + private readonly SqLiteUserRepository _userRepository = new SqLiteUserRepository(); + + public UserInfo GetUserInfo(string username, string password) + { + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + { + return UserInfo.GetAnonimus(); + } + // Check built-in admin + if (DEFAULT_ADMIN_PWD_HASH != null && DEFAULT_ADMIN_PWD_HASH.Length > 0 && (username.Equals("root", System.StringComparison.Ordinal) || username.Equals("admin", System.StringComparison.Ordinal)) + && DEFAULT_ADMIN_PWD_HASH.SequenceEqual(ComputeHash(password))) + { + return new UserInfo + { + Role = UserRole.SysAdmin, + UserId = -1, + UserName = "sysadmin", + DisplayName = "System Administrator", + Created = DateTime.Now + }; + } + else + { + var user = _userRepository.Get(username, ComputeHash(password)); + if (user != null) + { + return new UserInfo + { + Created = new DateTime(user.Timestamp, DateTimeKind.Utc), + DisplayName = user.DisplayName, + Role = user.Role, + UserId = user.Id, + UserName = user.UserName + }; + } + } + return null; + } + + public InvokeResult CreateUser(string username, string pwd, string displayName, UserRole role, long currentUserId) + { + return _userRepository.SaveUser(new User + { + Creator = currentUserId, + DisplayName = displayName, + PasswordHash = ComputeHash(pwd), + Role = role, + Timestamp = DateTime.UtcNow.Ticks, + UserName = username + }); + } + + public InvokeResult> GetUsers() + { + try + { + return InvokeResult>.Succeeding(_userRepository.GetAll()); + } + catch (Exception ex) + { + return InvokeResult>.Fault>(ex.Message); + } + } + + public InvokeResult RemoveUser(string login) + { + return _userRepository.RemoveUser(login); + } + + public void SetAdminPassword(string rootPwd) => DEFAULT_ADMIN_PWD_HASH = ComputeHash(rootPwd); + + private byte[] ComputeHash(string pwd) + { + using (SHA256 shaM = new SHA256Managed()) + { + return shaM.ComputeHash(Encoding.UTF8.GetBytes(pwd)); + } + } + } +} diff --git a/ZeroLevel.SqLite/BaseSqLiteDB.cs b/ZeroLevel.SqLite/BaseSqLiteDB.cs new file mode 100644 index 0000000..bd8fb26 --- /dev/null +++ b/ZeroLevel.SqLite/BaseSqLiteDB.cs @@ -0,0 +1,92 @@ +using System; +using System.Data.SQLite; +using System.IO; +using ZeroLevel.Services.FileSystem; + +namespace ZeroLevel.SqLite +{ + public abstract class BaseSqLiteDB + { + #region Helpers + protected static bool HasColumn(SQLiteDataReader dr, string columnName) + { + for (int i = 0; i < dr.FieldCount; i++) + { + if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + return false; + } + protected static Tr Read(SQLiteDataReader reader, int index) + { + if (reader == null || reader.FieldCount <= index || reader[index] == DBNull.Value) return default; + Type t; + if ((t = Nullable.GetUnderlyingType(typeof(Tr))) != null) + { + return (Tr)Convert.ChangeType(reader[index], t); + } + return (Tr)Convert.ChangeType(reader[index], typeof(Tr)); + } + protected static Tr Read(SQLiteDataReader reader, string name) + { + if (reader == null || HasColumn(reader, name) || reader[name] == DBNull.Value) return default; + Type t; + if ((t = Nullable.GetUnderlyingType(typeof(Tr))) != null) + { + return (Tr)Convert.ChangeType(reader[name], t); + } + return (Tr)Convert.ChangeType(reader[name], typeof(Tr)); + } + + protected static void Execute(string query, SQLiteConnection connection, SQLiteParameter[] parameters = null) + { + using (var cmd = new SQLiteCommand(query, connection)) + { + if (parameters != null && parameters.Length > 0) + { + cmd.Parameters.AddRange(parameters); + } + cmd.ExecuteNonQuery(); + } + } + + protected static object ExecuteScalar(string query, SQLiteConnection connection, SQLiteParameter[] parameters = null) + { + using (var cmd = new SQLiteCommand(query, connection)) + { + if (parameters != null && parameters.Length > 0) + { + cmd.Parameters.AddRange(parameters); + } + return cmd.ExecuteScalar(); + } + } + + protected static SQLiteDataReader Read(string query, SQLiteConnection connection, SQLiteParameter[] parameters = null) + { + using (var cmd = new SQLiteCommand(query, connection)) + { + if (parameters != null && parameters.Length > 0) + { + cmd.Parameters.AddRange(parameters); + } + return cmd.ExecuteReader(); + } + } + + protected static string PrepareDb(string path) + { + if (Path.IsPathRooted(path) == false) + { + path = Path.Combine(FSUtils.GetAppLocalDbDirectory(), path); + } + if (!File.Exists(path)) + { + SQLiteConnection.CreateFile(path); + } + return Path.GetFullPath(path); + } + + #endregion Helpers + } +} diff --git a/ZeroLevel.SqLite/Model/SqLiteDupStorage.cs b/ZeroLevel.SqLite/Model/SqLiteDupStorage.cs new file mode 100644 index 0000000..fae8287 --- /dev/null +++ b/ZeroLevel.SqLite/Model/SqLiteDupStorage.cs @@ -0,0 +1,183 @@ +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 + } +} diff --git a/ZeroLevel.SqLite/Model/User.cs b/ZeroLevel.SqLite/Model/User.cs new file mode 100644 index 0000000..f944657 --- /dev/null +++ b/ZeroLevel.SqLite/Model/User.cs @@ -0,0 +1,13 @@ +namespace ZeroLevel.SqLite +{ + public class User + { + public long Id { get; set; } + public string UserName { get; set; } + public string DisplayName { get; set; } + public byte[] PasswordHash { get; set; } + public long Timestamp { get; set; } + public long Creator { get; set; } + public UserRole Role { get; set; } + } +} diff --git a/ZeroLevel.SqLite/Model/UserInfo.cs b/ZeroLevel.SqLite/Model/UserInfo.cs new file mode 100644 index 0000000..9299445 --- /dev/null +++ b/ZeroLevel.SqLite/Model/UserInfo.cs @@ -0,0 +1,24 @@ +using System; + +namespace ZeroLevel.SqLite +{ + public class UserInfo + { + private readonly static UserInfo _anon = new UserInfo + { + Created = DateTime.MinValue, + DisplayName = "Anonimus", + Role = UserRole.Anonimus, + UserId = -2, + UserName = "anonimus" + }; + + public static UserInfo GetAnonimus() => _anon; + + public long UserId { get; set; } + public string UserName { get; set; } + public string DisplayName { get; set; } + public DateTime Created { get; set; } + public UserRole Role { get; set; } + } +} diff --git a/ZeroLevel.SqLite/Model/UserRole.cs b/ZeroLevel.SqLite/Model/UserRole.cs new file mode 100644 index 0000000..a4de25f --- /dev/null +++ b/ZeroLevel.SqLite/Model/UserRole.cs @@ -0,0 +1,14 @@ +using System; + +namespace ZeroLevel.SqLite +{ + public enum UserRole + : Int32 + { + Anonimus = 0, + Operator = 1, + Editor = 512, + Administrator = 1024, + SysAdmin = 4096 + } +} diff --git a/ZeroLevel.SqLite/SqLiteDelayDataStorage.cs b/ZeroLevel.SqLite/SqLiteDelayDataStorage.cs new file mode 100644 index 0000000..943dc6c --- /dev/null +++ b/ZeroLevel.SqLite/SqLiteDelayDataStorage.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.IO; +using System.Text; +using System.Threading; +using ZeroLevel.Services.Serialization; +using ZeroLevel.Services.Shedulling; + +namespace ZeroLevel.SqLite +{ + public sealed class SqLiteDelayDataStorage + : BaseSqLiteDB, IDisposable + where T : IBinarySerializable + { + #region Fields + + private readonly IExpirationSheduller _sheduller; + private readonly Func _expire_date_calc_func; + private readonly SQLiteConnection _db; + private readonly string _table_name; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + + #endregion Fields + + #region Ctor + + public SqLiteDelayDataStorage(string database_file_path, + Func expire_callback, + Func expire_date_calc_func) + { + this._expire_date_calc_func = expire_date_calc_func; + var path = PrepareDb(database_file_path); + _table_name = "expiration"; + _db = new SQLiteConnection($"Data Source={path};Version=3;"); + _db.Open(); + Execute($"CREATE TABLE IF NOT EXISTS {_table_name} (id INTEGER PRIMARY KEY AUTOINCREMENT, body BLOB, expirationtime INTEGER)", _db); + Execute($"CREATE INDEX IF NOT EXISTS expirationtime_index ON {_table_name} (expirationtime)", _db); + _sheduller = Sheduller.CreateExpirationSheduller(); + OnExpire += expire_callback; + Preload(); + } + + #endregion Ctor + + #region API + + public event Func OnExpire; + + public bool Push(T packet) + { + DateTime expirationDate; + try + { + expirationDate = _expire_date_calc_func(packet); + } + catch (Exception ex) + { + Log.Error(ex, "[SqLiteDelayDataStorage] Fault append data to storage"); + return false; + } + var expirationTime = expirationDate.Ticks; + _rwLock.EnterWriteLock(); + long id = -1; + try + { + Execute($"INSERT INTO {_table_name} ('body', 'expirationtime') values (@body, @expirationtime)", _db, + new SQLiteParameter[] + { + new SQLiteParameter("body", MessageSerializer.Serialize(packet)), + new SQLiteParameter("expirationtime", expirationTime) + }); + id = (long)ExecuteScalar("select last_insert_rowid();", _db); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteDelayDataStorage] Fault insert record in delay storage. Expiration time: '{expirationTime}'."); + return false; + } + finally + { + _rwLock.ExitWriteLock(); + } + _sheduller.Push(expirationDate, (k) => Pop(id)); + return true; + } + + #endregion API + + #region Private members + + private void Preload() + { + SQLiteDataReader reader; + _rwLock.EnterReadLock(); + try + { + reader = Read($"SELECT id, expirationtime FROM {_table_name}", _db); + while (reader.Read()) + { + var id = reader.GetInt64(0); + _sheduller.Push(new DateTime(reader.GetInt64(1), DateTimeKind.Local), (k) => Pop(id)); + } + reader.Close(); + } + catch (Exception ex) + { + Log.Error(ex, "[SqLiteDelayDataStorage] Fault preload datafrom db"); + } + finally + { + _rwLock.ExitReadLock(); + } + reader = null; + } + + private void Pop(long id) + { + try + { + byte[] body; + _rwLock.EnterReadLock(); + try + { + body = (byte[])ExecuteScalar($"SELECT body FROM {_table_name} WHERE id=@id", _db, new SQLiteParameter[] { new SQLiteParameter("id", id) }); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteDelayDataStorage] Fault get body by id '{id}'"); + RemoveRecordById(id); + return; + } + finally + { + _rwLock.ExitReadLock(); + } + T packet; + try + { + packet = MessageSerializer.Deserialize(body); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteDelayDataStorage] Fault deserialize body. Id '{id}'"); + RemoveRecordById(id); + return; + } + if (OnExpire?.Invoke(packet) ?? false) + { + RemoveRecordById(id); + } + } + catch (Exception ex) + { + Log.Error(ex, "[SqLiteDelayDataStorage] Сбой обработки отложенной записи из DB"); + } + } + + private void RemoveRecordById(long id) + { + _rwLock.EnterWriteLock(); + try + { + Execute($"DELETE FROM {_table_name} WHERE id = @id", _db, + new SQLiteParameter[] { new SQLiteParameter("id", id) }); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteDelayDataStorage] Fault remove record by id '{id}'"); + } + finally + { + _rwLock.ExitWriteLock(); + } + } + + #endregion Private members + + #region IDisposable + + public void Dispose() + { + try + { + _db?.Close(); + _db?.Dispose(); + } + catch (Exception ex) + { + Log.Error(ex, "[SqLiteDelayDataStorage] Fault close db connection"); + } + _sheduller.Dispose(); + } + + #endregion IDisposable + } +} diff --git a/ZeroLevel.SqLite/SqLitePacketBuffer.cs b/ZeroLevel.SqLite/SqLitePacketBuffer.cs new file mode 100644 index 0000000..cfdeeba --- /dev/null +++ b/ZeroLevel.SqLite/SqLitePacketBuffer.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.IO; +using System.Text; +using System.Threading; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.SqLite +{ + /// + /// Промежуточное/временное хранилище пакетов данных, для случаев сбоя доставок через шину данных + /// + public sealed class SqLitePacketBuffer + : BaseSqLiteDB, IDisposable + where T : IBinarySerializable + { + private sealed class PacketBufferRecord + { + public int Id { get; set; } + public byte[] Body { get; set; } + } + + #region Fields + + private readonly SQLiteConnection _db; + private readonly string _table_name; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + + #endregion Fields + + public SqLitePacketBuffer(string database_file_path) + { + var path = PrepareDb(database_file_path); + _table_name = "packets"; + _db = new SQLiteConnection($"Data Source={path};Version=3;"); + _db.Open(); + Execute($"CREATE TABLE IF NOT EXISTS {_table_name} (id INTEGER PRIMARY KEY AUTOINCREMENT, body BLOB, created INTEGER)", _db); + Execute($"CREATE INDEX IF NOT EXISTS created_index ON {_table_name} (created)", _db); + } + + public void Push(T frame) + { + long id = -1; + _rwLock.EnterWriteLock(); + var creationTime = DateTime.Now.Ticks; + try + { + Execute($"INSERT INTO {_table_name} ('body', 'created') values (@body, @created)", _db, + new SQLiteParameter[] + { + new SQLiteParameter("body", MessageSerializer.Serialize(frame)), + new SQLiteParameter("created", creationTime) + }); + id = (long)ExecuteScalar("select last_insert_rowid();", _db); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLitePacketBuffer] Fault insert record in buffer storage."); + } + finally + { + _rwLock.ExitWriteLock(); + } + } + + public bool Pop(Func pop_callback) + { + bool success = false; + long id = -1; + SQLiteDataReader reader; + _rwLock.EnterReadLock(); + try + { + reader = Read($"SELECT id, body FROM {_table_name} ORDER BY created ASC LIMIT 1", _db); + if (reader.Read()) + { + id = reader.GetInt64(0); + var body = (byte[])reader.GetValue(1); + try + { + success = pop_callback(MessageSerializer.Deserialize(body)); + } + catch (Exception ex) + { + Log.Error(ex, "Fault handle buffered data"); + } + } + reader.Close(); + } + catch (Exception ex) + { + Log.Error(ex, "[SqLitePacketBuffer] Fault preload datafrom db"); + } + finally + { + _rwLock.ExitReadLock(); + } + if (success) + { + RemoveRecordById(id); + } + reader = null; + return success; + } + + private void RemoveRecordById(long id) + { + _rwLock.EnterWriteLock(); + try + { + Execute($"DELETE FROM {_table_name} WHERE id = @id", _db, + new SQLiteParameter[] { new SQLiteParameter("id", id) }); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLitePacketBuffer] Fault remove record by id '{id}'"); + } + finally + { + _rwLock.ExitWriteLock(); + } + } + + public void Dispose() + { + try + { + _db?.Close(); + _db?.Dispose(); + } + catch (Exception ex) + { + Log.Error(ex, "[SqLitePacketBuffer] Fault close db connection"); + } + } + } +} diff --git a/ZeroLevel.SqLite/SqLiteUserRepository.cs b/ZeroLevel.SqLite/SqLiteUserRepository.cs new file mode 100644 index 0000000..68fac3d --- /dev/null +++ b/ZeroLevel.SqLite/SqLiteUserRepository.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.IO; +using System.Threading; +using ZeroLevel.Models; +using ZeroLevel.Services.FileSystem; + +namespace ZeroLevel.SqLite +{ + public class SqLiteUserRepository + : BaseSqLiteDB + { + #region Fields + + private readonly SQLiteConnection _db; + private readonly string _table_name = "users"; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + + #endregion Fields + + #region Ctor + + public SqLiteUserRepository() + { + var path =PrepareDb("users.db"); + _db = new SQLiteConnection($"Data Source={path};Version=3;"); + _db.Open(); + Execute($"CREATE TABLE IF NOT EXISTS {_table_name} (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, displayname TEXT, hash BLOB, timestamp INTEGER, creator INTEGER, role INTEGER)", _db); + Execute($"CREATE INDEX IF NOT EXISTS username_index ON {_table_name} (username)", _db); + Execute($"CREATE INDEX IF NOT EXISTS hash_index ON {_table_name} (hash)", _db); + } + + #endregion Ctor + + public IEnumerable GetAll() + { + var list = new List(); + SQLiteDataReader reader; + _rwLock.EnterReadLock(); + try + { + reader = Read($"SELECT id, username, displayname, hash, timestamp, creator, role FROM {_table_name}", _db); + while (reader.Read()) + { + list.Add(new User + { + Id = reader.GetInt64(0), + UserName = reader.GetString(1), + DisplayName = Read(reader, 2), + PasswordHash = (byte[])reader.GetValue(3), + Timestamp = reader.GetInt64(4), + Creator = reader.GetInt64(5), + Role = (UserRole)reader.GetInt32(6) + }); + } + reader.Close(); + } + catch (Exception ex) + { + Log.Error(ex, "[SqLiteUserRepository] Fault get all users"); + } + finally + { + _rwLock.ExitReadLock(); + } + reader = null; + return list; + } + + public User Get(long id) + { + User user = null; + SQLiteDataReader reader; + _rwLock.EnterReadLock(); + try + { + reader = Read($"SELECT id, username, displayname, hash, timestamp, creator, role FROM {_table_name} WHERE id = @id", _db, + new SQLiteParameter[] { new SQLiteParameter("id", id) }); + if (reader.Read()) + { + var body = (byte[])reader.GetValue(1); + user = new User + { + Id = reader.GetInt64(0), + UserName = reader.GetString(1), + DisplayName = reader.GetString(2), + PasswordHash = (byte[])reader.GetValue(3), + Timestamp = reader.GetInt64(4), + Creator = reader.GetInt64(5), + Role = (UserRole)reader.GetInt32(6) + }; + } + reader.Close(); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteUserRepository] Fault get user by id '{id}'"); + } + finally + { + _rwLock.ExitReadLock(); + } + reader = null; + return user; + } + + public User Get(string username, byte[] hash) + { + User user = null; + SQLiteDataReader reader; + _rwLock.EnterReadLock(); + try + { + reader = Read($"SELECT id, username, displayname, hash, timestamp, creator, role FROM {_table_name} WHERE username = @username AND hash = @hash", _db, + new SQLiteParameter[] + { + new SQLiteParameter("username", username), + new SQLiteParameter("hash", hash) + }); + if (reader.Read()) + { + user = new User + { + Id = reader.GetInt64(0), + UserName = reader.GetString(1), + DisplayName = reader.GetString(2), + PasswordHash = (byte[])reader.GetValue(3), + Timestamp = reader.GetInt64(4), + Creator = reader.GetInt64(5), + Role = (UserRole)reader.GetInt32(6) + }; + } + reader.Close(); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteUserRepository] Fault get user by username '{username}' and pwdhash"); + } + finally + { + _rwLock.ExitReadLock(); + } + reader = null; + return user; + } + + public InvokeResult SaveUser(User user) + { + long id = -1; + _rwLock.EnterWriteLock(); + var creationTime = DateTime.UtcNow.Ticks; + try + { + var count_obj = ExecuteScalar($"SELECT COUNT(*) FROM {_table_name} WHERE username=@username", _db, new SQLiteParameter[] { new SQLiteParameter("username", user.UserName) }); + if (count_obj != null && (long)count_obj > 0) + { + return InvokeResult.Fault("Пользователь уже существует"); + } + Execute($"INSERT INTO {_table_name} ('username', 'displayname', 'hash', 'timestamp', 'creator', 'role') values (@username, @displayname, @hash, @timestamp, @creator, @role)", _db, + new SQLiteParameter[] + { + new SQLiteParameter("username", user.UserName), + new SQLiteParameter("displayname", user.DisplayName), + new SQLiteParameter("hash", user.PasswordHash), + new SQLiteParameter("timestamp", creationTime), + new SQLiteParameter("creator", user.Creator), + new SQLiteParameter("role", user.Role) + }); + id = (long)ExecuteScalar("select last_insert_rowid();", _db); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteUserRepository] Fault insert user in storage."); + InvokeResult.Fault(ex.Message); + } + finally + { + _rwLock.ExitWriteLock(); + } + return InvokeResult.Succeeding(id); + } + + public InvokeResult RemoveUser(string login) + { + _rwLock.EnterWriteLock(); + try + { + Execute($"DELETE FROM {_table_name} WHERE username = @username", _db, + new SQLiteParameter[] { new SQLiteParameter("username", login) }); + return InvokeResult.Succeeding(); + } + catch (Exception ex) + { + Log.Error(ex, $"[SqLiteUserRepository] Fault remove user '{login}'"); + return InvokeResult.Fault(ex.Message); + } + finally + { + _rwLock.ExitWriteLock(); + } + } + } +} diff --git a/ZeroLevel.SqLite/UserCacheRepository.cs b/ZeroLevel.SqLite/UserCacheRepository.cs new file mode 100644 index 0000000..55716b8 --- /dev/null +++ b/ZeroLevel.SqLite/UserCacheRepository.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using ZeroLevel.Services.FileSystem; +using ZeroLevel.Services.Serialization; + +namespace ZeroLevel.SqLite +{ + public sealed class UserCacheRepository + : BaseSqLiteDB, IDisposable + where T : IBinarySerializable + { + #region Fields + + private readonly SQLiteConnection _db; + private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + private readonly string _tableName; + + #endregion Fields + + #region Ctor + + public UserCacheRepository() + { + _tableName = typeof(T).Name; + + var path = PrepareDb($"{_tableName}_user_cachee.db"); + _db = new SQLiteConnection($"Data Source={path};Version=3;"); + _db.Open(); + + Execute($"CREATE TABLE IF NOT EXISTS {_tableName} (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT, body BLOB)", _db); + Execute($"CREATE INDEX IF NOT EXISTS key_index ON {_tableName} (key)", _db); + } + + #endregion Ctor + + #region API + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string KEY(long userId, string name) => $"{userId}.{name}"; + + public bool AddOrUpdate(long userid, string name, T data) + { + var key = KEY(userid, name); + bool update = false; + _rwLock.EnterReadLock(); + try + { + var count_obj = ExecuteScalar($"SELECT COUNT(*) FROM {_tableName} WHERE key=@key", _db, new SQLiteParameter[] { new SQLiteParameter("key", key) }); + if (count_obj != null && (long)count_obj > 0) + { + update = true; + } + } + catch (Exception ex) + { + Log.Error(ex, $"[UserCacheRepository] Fault search existing records by name ({name})"); + // no return! + } + finally + { + _rwLock.ExitReadLock(); + } + _rwLock.EnterWriteLock(); + try + { + var body = MessageSerializer.Serialize(data); + if (update) + { + Execute($"UPDATE {_tableName} SET key=@key, body=@body", _db, + new SQLiteParameter[] + { + new SQLiteParameter("key", key), + new SQLiteParameter("body", body) + }); + } + else + { + Execute($"INSERT INTO {_tableName} ('key', 'body') values (@key, @body)", _db, + new SQLiteParameter[] + { + new SQLiteParameter("key", key), + new SQLiteParameter("body", body) + }); + } + return true; + } + catch (Exception ex) + { + Log.Error(ex, $"[UserCacheRepository] Fault insert record in storage. UserId: {userid}. Name '{name}'. Data: {typeof(T).Name}."); + } + finally + { + _rwLock.ExitWriteLock(); + } + return false; + } + + public void Remove(long userid, string name) + { + var key = KEY(userid, name); + _rwLock.EnterWriteLock(); + try + { + Execute($"DELETE FROM {_tableName} WHERE key=@key", _db, new SQLiteParameter[] + { + new SQLiteParameter("key", key) + }); + } + catch (Exception ex) + { + Log.Error(ex, $"[UserCacheRepository] Fault remove record from db by name '{name}'. UserId: {userid}. Data: {typeof(T).Name}."); + } + finally + { + _rwLock.ExitWriteLock(); + } + } + + public long Count(long userid, string name) + { + var key = KEY(userid, name); + _rwLock.EnterWriteLock(); + try + { + return Convert.ToInt64(ExecuteScalar($"SELECT COUNT(*) FROM {_tableName} WHERE key=@key", _db, new SQLiteParameter[] + { + new SQLiteParameter("key", key) + })); + } + catch (Exception ex) + { + Log.Error(ex, $"[UserCacheRepository] Fault get count record from db by name '{name}'. UserId: {userid}. Data: {typeof(T).Name}."); + } + finally + { + _rwLock.ExitWriteLock(); + } + return 0; + } + + public IEnumerable GetAll(long userid, string name) + { + var key = KEY(userid, name); + var result = new List(); + SQLiteDataReader reader; + _rwLock.EnterReadLock(); + try + { + reader = Read($"SELECT [body] FROM {_tableName} WHERE key=@key", _db, new SQLiteParameter[] + { + new SQLiteParameter("key", key) + }); + while (reader.Read()) + { + var data = Read(reader, 0); + if (data != null) + { + result.Add(MessageSerializer.Deserialize(data)); + } + } + reader.Close(); + } + catch (Exception ex) + { + Log.Error(ex, $"[UserCacheRepository] Fault read all records by name: {name}. UserId: {userid}. Data: {typeof(T).Name}."); + } + finally + { + _rwLock.ExitReadLock(); + reader = null; + } + return result; + } + + #endregion API + + #region IDisposable + + public void Dispose() + { + try + { + _db?.Dispose(); + } + catch (Exception ex) + { + Log.Error(ex, "[UserCacheRepository] Fault close db connection"); + } + } + + #endregion IDisposable + } +} diff --git a/ZeroLevel.SqLite/ZeroLevel.SqLite.csproj b/ZeroLevel.SqLite/ZeroLevel.SqLite.csproj new file mode 100644 index 0000000..606fa43 --- /dev/null +++ b/ZeroLevel.SqLite/ZeroLevel.SqLite.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/ZeroLevel.sln b/ZeroLevel.sln index a6b9837..094539c 100644 --- a/ZeroLevel.sln +++ b/ZeroLevel.sln @@ -37,7 +37,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjectionTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZeroLevel.Logger", "ZeroLevel.Logger\ZeroLevel.Logger.csproj", "{D1C061DB-3565-43C3-B8F3-628DE4908750}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.WPF", "ZeroLevel.WPF\ZeroLevel.WPF.csproj", "{0D70D688-1E21-4E9D-AA49-4D255DF27D8D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZeroLevel.WPF", "ZeroLevel.WPF\ZeroLevel.WPF.csproj", "{0D70D688-1E21-4E9D-AA49-4D255DF27D8D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZeroLevel.SqLite", "ZeroLevel.SqLite\ZeroLevel.SqLite.csproj", "{5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -241,6 +243,18 @@ Global {0D70D688-1E21-4E9D-AA49-4D255DF27D8D}.Release|x64.Build.0 = Release|Any CPU {0D70D688-1E21-4E9D-AA49-4D255DF27D8D}.Release|x86.ActiveCfg = Release|Any CPU {0D70D688-1E21-4E9D-AA49-4D255DF27D8D}.Release|x86.Build.0 = Release|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Debug|x64.Build.0 = Debug|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Debug|x86.Build.0 = Debug|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Release|Any CPU.Build.0 = Release|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Release|x64.ActiveCfg = Release|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Release|x64.Build.0 = Release|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Release|x86.ActiveCfg = Release|Any CPU + {5B545DD6-8573-4CDD-B32D-9B0AA2AC2F9A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE