From fa714425dd91fed2ca691cd45f73f7ea5a579dff Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 18 Nov 2016 03:39:20 -0500 Subject: begin to rework repositories --- .../Data/BaseSqliteRepository.cs | 123 +++++++ .../Data/CleanDatabaseScheduledTask.cs | 361 +++++++++++++++++++++ .../Data/SqliteExtensions.cs | 105 ++++++ 3 files changed, 589 insertions(+) create mode 100644 Emby.Server.Implementations/Data/BaseSqliteRepository.cs create mode 100644 Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs create mode 100644 Emby.Server.Implementations/Data/SqliteExtensions.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs new file mode 100644 index 000000000..c7ac630a0 --- /dev/null +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + public abstract class BaseSqliteRepository : IDisposable + { + protected string DbFilePath { get; set; } + protected SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1); + protected ILogger Logger { get; private set; } + + protected BaseSqliteRepository(ILogger logger) + { + Logger = logger; + } + + protected virtual bool EnableConnectionPooling + { + get { return true; } + } + + protected virtual SQLiteDatabaseConnection CreateConnection(bool isReadOnly = false) + { + SQLite3.EnableSharedCache = false; + + ConnectionFlags connectionFlags; + + if (isReadOnly) + { + connectionFlags = ConnectionFlags.ReadOnly; + //connectionFlags = ConnectionFlags.Create; + //connectionFlags |= ConnectionFlags.ReadWrite; + } + else + { + connectionFlags = ConnectionFlags.Create; + connectionFlags |= ConnectionFlags.ReadWrite; + } + + if (EnableConnectionPooling) + { + connectionFlags |= ConnectionFlags.SharedCached; + } + else + { + connectionFlags |= ConnectionFlags.PrivateCache; + } + + connectionFlags |= ConnectionFlags.NoMutex; + + var db = SQLite3.Open(DbFilePath, connectionFlags, null); + + var queries = new[] + { + "PRAGMA page_size=4096", + "PRAGMA journal_mode=WAL", + "PRAGMA temp_store=memory", + "PRAGMA synchronous=Normal", + //"PRAGMA cache size=-10000" + }; + + //foreach (var query in queries) + //{ + // db.Execute(query); + //} + + db.ExecuteAll(string.Join(";", queries)); + + return db; + } + + private bool _disposed; + protected void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name + " has been disposed and cannot be accessed."); + } + } + + public void Dispose() + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + try + { + lock (_disposeLock) + { + WriteLock.Wait(); + + CloseConnection(); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error disposing database", ex); + } + } + } + + protected virtual void CloseConnection() + { + + } + } +} diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs new file mode 100644 index 000000000..dd32e2cbd --- /dev/null +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -0,0 +1,361 @@ +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; +using Emby.Server.Implementations.ScheduledTasks; + +namespace Emby.Server.Implementations.Data +{ + public class CleanDatabaseScheduledTask : IScheduledTask + { + private readonly ILibraryManager _libraryManager; + private readonly IItemRepository _itemRepo; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly IHttpServer _httpServer; + private readonly ILocalizationManager _localization; + private readonly ITaskManager _taskManager; + + public const int MigrationVersion = 23; + public static bool EnableUnavailableMessage = false; + const int LatestSchemaVersion = 109; + + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IHttpServer httpServer, ILocalizationManager localization, ITaskManager taskManager) + { + _libraryManager = libraryManager; + _itemRepo = itemRepo; + _logger = logger; + _config = config; + _fileSystem = fileSystem; + _httpServer = httpServer; + _localization = localization; + _taskManager = taskManager; + } + + public string Name + { + get { return "Clean Database"; } + } + + public string Description + { + get { return "Deletes obsolete content from the database."; } + } + + public string Category + { + get { return "Library"; } + } + + public async Task Execute(CancellationToken cancellationToken, IProgress progress) + { + OnProgress(0); + + // Ensure these objects are lazy loaded. + // Without this there is a deadlock that will need to be investigated + var rootChildren = _libraryManager.RootFolder.Children.ToList(); + rootChildren = _libraryManager.GetUserRootFolder().Children.ToList(); + + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => + { + double newPercentCommplete = .4 * p; + OnProgress(newPercentCommplete); + + progress.Report(newPercentCommplete); + }); + + await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false); + + innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => + { + double newPercentCommplete = 40 + .05 * p; + OnProgress(newPercentCommplete); + progress.Report(newPercentCommplete); + }); + await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); + progress.Report(45); + + innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => + { + double newPercentCommplete = 45 + .55 * p; + OnProgress(newPercentCommplete); + progress.Report(newPercentCommplete); + }); + await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false); + progress.Report(100); + + await _itemRepo.UpdateInheritedValues(cancellationToken).ConfigureAwait(false); + + if (_config.Configuration.MigrationVersion < MigrationVersion) + { + _config.Configuration.MigrationVersion = MigrationVersion; + _config.SaveConfiguration(); + } + + if (_config.Configuration.SchemaVersion < LatestSchemaVersion) + { + _config.Configuration.SchemaVersion = LatestSchemaVersion; + _config.SaveConfiguration(); + } + + if (EnableUnavailableMessage) + { + EnableUnavailableMessage = false; + _httpServer.GlobalResponse = null; + _taskManager.QueueScheduledTask(); + } + + _taskManager.SuspendTriggers = false; + } + + private void OnProgress(double newPercentCommplete) + { + if (EnableUnavailableMessage) + { + var html = "Emby"; + var text = _localization.GetLocalizedString("DbUpgradeMessage"); + html += string.Format(text, newPercentCommplete.ToString("N2", CultureInfo.InvariantCulture)); + + html += ""; + html += ""; + + _httpServer.GlobalResponse = html; + } + } + + private async Task UpdateToLatestSchema(CancellationToken cancellationToken, IProgress progress) + { + var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + IsCurrentSchema = false, + ExcludeItemTypes = new[] { typeof(LiveTvProgram).Name } + }); + + var numComplete = 0; + var numItems = itemIds.Count; + + _logger.Debug("Upgrading schema for {0} items", numItems); + + var list = new List(); + + foreach (var itemId in itemIds) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (itemId != Guid.Empty) + { + // Somehow some invalid data got into the db. It probably predates the boundary checking + var item = _libraryManager.GetItemById(itemId); + + if (item != null) + { + list.Add(item); + } + } + + if (list.Count >= 1000) + { + try + { + await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error saving item", ex); + } + + list.Clear(); + } + + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); + } + + if (list.Count > 0) + { + try + { + await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error saving item", ex); + } + } + + progress.Report(100); + } + + private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + { + var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + HasDeadParentId = true + }); + + var numComplete = 0; + var numItems = itemIds.Count; + + _logger.Debug("Cleaning {0} items with dead parent links", numItems); + + foreach (var itemId in itemIds) + { + cancellationToken.ThrowIfCancellationRequested(); + + var item = _libraryManager.GetItemById(itemId); + + if (item != null) + { + _logger.Info("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); + + await item.Delete(new DeleteOptions + { + DeleteFileLocation = false + + }).ConfigureAwait(false); + } + + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); + } + + progress.Report(100); + } + + private async Task CleanDeletedItems(CancellationToken cancellationToken, IProgress progress) + { + var result = _itemRepo.GetItemIdsWithPath(new InternalItemsQuery + { + LocationTypes = new[] { LocationType.FileSystem }, + //Limit = limit, + + // These have their own cleanup routines + ExcludeItemTypes = new[] + { + typeof(Person).Name, + typeof(Genre).Name, + typeof(MusicGenre).Name, + typeof(GameGenre).Name, + typeof(Studio).Name, + typeof(Year).Name, + typeof(Channel).Name, + typeof(AggregateFolder).Name, + typeof(CollectionFolder).Name + } + }); + + var numComplete = 0; + var numItems = result.Items.Length; + + foreach (var item in result.Items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var path = item.Item2; + + try + { + if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path)) + { + continue; + } + + var libraryItem = _libraryManager.GetItemById(item.Item1); + + if (libraryItem.IsTopParent) + { + continue; + } + + var hasDualAccess = libraryItem as IHasDualAccess; + if (hasDualAccess != null && hasDualAccess.IsAccessedByName) + { + continue; + } + + var libraryItemPath = libraryItem.Path; + if (!string.Equals(libraryItemPath, path, StringComparison.OrdinalIgnoreCase)) + { + _logger.Error("CleanDeletedItems aborting delete for item {0}-{1} because paths don't match. {2}---{3}", libraryItem.Id, libraryItem.Name, libraryItem.Path ?? string.Empty, path ?? string.Empty); + continue; + } + + if (Folder.IsPathOffline(path)) + { + await libraryItem.UpdateIsOffline(true).ConfigureAwait(false); + continue; + } + + _logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty); + + await libraryItem.OnFileDeleted().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error in CleanDeletedItems. File {0}", ex, path); + } + + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); + } + } + + /// + /// Creates the triggers that define when the task will run + /// + /// IEnumerable{BaseTaskTrigger}. + public IEnumerable GetDefaultTriggers() + { + return new[] { + + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + }; + } + + public string Key + { + get { return "CleanDatabase"; } + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs new file mode 100644 index 000000000..62615c669 --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -0,0 +1,105 @@ +using System; +using System.Globalization; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + public static class SqliteExtensions + { + public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) + { + if (queries == null) + { + throw new ArgumentNullException("queries"); + } + + connection.RunInTransaction(conn => + { + //foreach (var query in queries) + //{ + // conn.Execute(query); + //} + conn.ExecuteAll(string.Join(";", queries)); + }); + } + + public static byte[] ToGuidParamValue(this string str) + { + return new Guid(str).ToByteArray(); + } + + public static Guid ReadGuid(this IResultSetValue result) + { + return new Guid(result.ToBlob()); + } + + public static string ToDateTimeParamValue(this DateTime dateValue) + { + var kind = DateTimeKind.Utc; + + return (dateValue.Kind == DateTimeKind.Unspecified) + ? DateTime.SpecifyKind(dateValue, kind).ToString( + GetDateTimeKindFormat(kind), + CultureInfo.InvariantCulture) + : dateValue.ToString( + GetDateTimeKindFormat(dateValue.Kind), + CultureInfo.InvariantCulture); + } + + private static string GetDateTimeKindFormat( + DateTimeKind kind) + { + return (kind == DateTimeKind.Utc) ? _datetimeFormatUtc : _datetimeFormatLocal; + } + + /// + /// An array of ISO-8601 DateTime formats that we support parsing. + /// + private static string[] _datetimeFormats = new string[] { + "THHmmssK", + "THHmmK", + "HH:mm:ss.FFFFFFFK", + "HH:mm:ssK", + "HH:mmK", + "yyyy-MM-dd HH:mm:ss.FFFFFFFK", /* NOTE: UTC default (5). */ + "yyyy-MM-dd HH:mm:ssK", + "yyyy-MM-dd HH:mmK", + "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", + "yyyy-MM-ddTHH:mmK", + "yyyy-MM-ddTHH:mm:ssK", + "yyyyMMddHHmmssK", + "yyyyMMddHHmmK", + "yyyyMMddTHHmmssFFFFFFFK", + "THHmmss", + "THHmm", + "HH:mm:ss.FFFFFFF", + "HH:mm:ss", + "HH:mm", + "yyyy-MM-dd HH:mm:ss.FFFFFFF", /* NOTE: Non-UTC default (19). */ + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy-MM-ddTHH:mm:ss.FFFFFFF", + "yyyy-MM-ddTHH:mm", + "yyyy-MM-ddTHH:mm:ss", + "yyyyMMddHHmmss", + "yyyyMMddHHmm", + "yyyyMMddTHHmmssFFFFFFF", + "yyyy-MM-dd", + "yyyyMMdd", + "yy-MM-dd" + }; + + private static string _datetimeFormatUtc = _datetimeFormats[5]; + private static string _datetimeFormatLocal = _datetimeFormats[19]; + + public static DateTime ReadDateTime(this IResultSetValue result) + { + var dateText = result.ToString(); + + return DateTime.ParseExact( + dateText, _datetimeFormats, + DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.None).ToUniversalTime(); + } + } +} -- cgit v1.2.3 From 9f40c1982bf2b63d2779a8971361364772e33298 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 18 Nov 2016 04:28:39 -0500 Subject: rework additional repositories --- Emby.Server.Core/ApplicationHost.cs | 10 +- .../Data/SqliteDisplayPreferencesRepository.cs | 312 -------------------- .../Security/AuthenticationRepository.cs | 315 --------------------- .../Data/BaseSqliteRepository.cs | 24 +- .../Data/SqliteDisplayPreferencesRepository.cs | 228 +++++++++++++++ .../Data/SqliteExtensions.cs | 28 +- .../Emby.Server.Implementations.csproj | 2 + .../Security/AuthenticationRepository.cs | 258 +++++++++++++++++ 8 files changed, 543 insertions(+), 634 deletions(-) delete mode 100644 Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs delete mode 100644 Emby.Server.Core/Security/AuthenticationRepository.cs create mode 100644 Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs create mode 100644 Emby.Server.Implementations/Security/AuthenticationRepository.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 7e67c8a08..8a62a9b27 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -91,7 +91,7 @@ using Emby.Server.Core.FFMpeg; using Emby.Server.Core.IO; using Emby.Server.Core.Localization; using Emby.Server.Core.Migrations; -using Emby.Server.Core.Security; +using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Social; using Emby.Server.Core.Sync; using Emby.Server.Implementations.Channels; @@ -554,7 +554,7 @@ namespace Emby.Server.Core UserRepository = await GetUserRepository().ConfigureAwait(false); - var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LogManager, JsonSerializer, ApplicationPaths, GetDbConnector(), MemoryStreamFactory); + var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LogManager.GetLogger("SqliteDisplayPreferencesRepository"), JsonSerializer, ApplicationPaths, MemoryStreamFactory); DisplayPreferencesRepository = displayPreferencesRepo; RegisterSingleInstance(DisplayPreferencesRepository); @@ -699,7 +699,7 @@ namespace Emby.Server.Core SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, MemoryStreamFactory, ProcessFactory, textEncoding); RegisterSingleInstance(SubtitleEncoder); - await displayPreferencesRepo.Initialize().ConfigureAwait(false); + displayPreferencesRepo.Initialize(); var userDataRepo = new SqliteUserDataRepository(LogManager, ApplicationPaths, GetDbConnector()); @@ -828,9 +828,9 @@ namespace Emby.Server.Core private async Task GetAuthenticationRepository() { - var repo = new AuthenticationRepository(LogManager, ServerConfigurationManager.ApplicationPaths, GetDbConnector()); + var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths); - await repo.Initialize().ConfigureAwait(false); + repo.Initialize(); return repo; } diff --git a/Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs deleted file mode 100644 index a9e63a11d..000000000 --- a/Emby.Server.Core/Data/SqliteDisplayPreferencesRepository.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; - -namespace Emby.Server.Core.Data -{ - /// - /// Class SQLiteDisplayPreferencesRepository - /// - public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository - { - private readonly IMemoryStreamFactory _memoryStreamProvider; - - public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IDbConnector dbConnector, IMemoryStreamFactory memoryStreamProvider) - : base(logManager, dbConnector) - { - _jsonSerializer = jsonSerializer; - _memoryStreamProvider = memoryStreamProvider; - DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); - } - - /// - /// Gets the name of the repository - /// - /// The name. - public string Name - { - get - { - return "SQLite"; - } - } - - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// Opens the connection to the database - /// - /// Task. - public async Task Initialize() - { - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - string[] queries = { - - "create table if not exists userdisplaypreferences (id GUID, userId GUID, client text, data BLOB)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" - }; - - connection.RunQueries(queries, Logger); - } - } - - /// - /// Save the display preferences associated with an item in the repo - /// - /// The display preferences. - /// The user id. - /// The client. - /// The cancellation token. - /// Task. - /// item - public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException("displayPreferences"); - } - if (string.IsNullOrWhiteSpace(displayPreferences.Id)) - { - throw new ArgumentNullException("displayPreferences.Id"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; - - cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreferences.Id); - cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; - cmd.Parameters.Add(cmd, "@3", DbType.String).Value = client; - cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; - - cmd.Transaction = transaction; - - cmd.ExecuteNonQuery(); - } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save display preferences:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - - /// - /// Save all display preferences associated with a user in the repo - /// - /// The display preferences. - /// The user id. - /// The cancellation token. - /// Task. - /// item - public async Task SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException("displayPreferences"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - foreach (var displayPreference in displayPreferences) - { - - var serialized = _jsonSerializer.SerializeToBytes(displayPreference, _memoryStreamProvider); - - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; - - cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreference.Id); - cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; - cmd.Parameters.Add(cmd, "@3", DbType.String).Value = displayPreference.Client; - cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; - - cmd.Transaction = transaction; - - cmd.ExecuteNonQuery(); - } - } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save display preferences:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - - /// - /// Gets the display preferences. - /// - /// The display preferences id. - /// The user id. - /// The client. - /// Task{DisplayPreferences}. - /// item - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) - { - if (string.IsNullOrWhiteSpace(displayPreferencesId)) - { - throw new ArgumentNullException("displayPreferencesId"); - } - - var guidId = displayPreferencesId.GetMD5(); - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"; - - cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = guidId; - cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; - cmd.Parameters.Add(cmd, "@client", DbType.String).Value = client; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) - { - using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider)) - { - return _jsonSerializer.DeserializeFromStream(stream); - } - } - } - - return new DisplayPreferences - { - Id = guidId.ToString("N") - }; - } - } - } - - /// - /// Gets all display preferences for the given user. - /// - /// The user id. - /// Task{DisplayPreferences}. - /// item - public IEnumerable GetAllDisplayPreferences(Guid userId) - { - var list = new List(); - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "select data from userdisplaypreferences where userId=@userId"; - - cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider)) - { - list.Add(_jsonSerializer.DeserializeFromStream(stream)); - } - } - } - } - } - - return list; - } - - public Task SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) - { - return SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); - } - - public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) - { - return GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); - } - } -} \ No newline at end of file diff --git a/Emby.Server.Core/Security/AuthenticationRepository.cs b/Emby.Server.Core/Security/AuthenticationRepository.cs deleted file mode 100644 index 548585375..000000000 --- a/Emby.Server.Core/Security/AuthenticationRepository.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Core.Data; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; - -namespace Emby.Server.Core.Security -{ - public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository - { - private readonly IServerApplicationPaths _appPaths; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public AuthenticationRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector connector) - : base(logManager, connector) - { - _appPaths = appPaths; - DbFilePath = Path.Combine(appPaths.DataPath, "authentication.db"); - } - - public async Task Initialize() - { - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - string[] queries = { - - "create table if not exists AccessTokens (Id GUID PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT, AppName TEXT, AppVersion TEXT, DeviceName TEXT, UserId TEXT, IsActive BIT, DateCreated DATETIME NOT NULL, DateRevoked DATETIME)", - "create index if not exists idx_AccessTokens on AccessTokens(Id)" - }; - - connection.RunQueries(queries, Logger); - - connection.AddColumn(Logger, "AccessTokens", "AppVersion", "TEXT"); - } - } - - public Task Create(AuthenticationInfo info, CancellationToken cancellationToken) - { - info.Id = Guid.NewGuid().ToString("N"); - - return Update(info, cancellationToken); - } - - public async Task Update(AuthenticationInfo info, CancellationToken cancellationToken) - { - if (info == null) - { - throw new ArgumentNullException("info"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var saveInfoCommand = connection.CreateCommand()) - { - saveInfoCommand.CommandText = "replace into AccessTokens (Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked) values (@Id, @AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @IsActive, @DateCreated, @DateRevoked)"; - - saveInfoCommand.Parameters.Add(saveInfoCommand, "@Id"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@AccessToken"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@DeviceId"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@AppName"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@AppVersion"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@DeviceName"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@UserId"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@IsActive"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@DateCreated"); - saveInfoCommand.Parameters.Add(saveInfoCommand, "@DateRevoked"); - - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - saveInfoCommand.GetParameter("@Id").Value = new Guid(info.Id); - saveInfoCommand.GetParameter("@AccessToken").Value = info.AccessToken; - saveInfoCommand.GetParameter("@DeviceId").Value = info.DeviceId; - saveInfoCommand.GetParameter("@AppName").Value = info.AppName; - saveInfoCommand.GetParameter("@AppVersion").Value = info.AppVersion; - saveInfoCommand.GetParameter("@DeviceName").Value = info.DeviceName; - saveInfoCommand.GetParameter("@UserId").Value = info.UserId; - saveInfoCommand.GetParameter("@IsActive").Value = info.IsActive; - saveInfoCommand.GetParameter("@DateCreated").Value = info.DateCreated; - saveInfoCommand.GetParameter("@DateRevoked").Value = info.DateRevoked; - - saveInfoCommand.Transaction = transaction; - - saveInfoCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - - private const string BaseSelectText = "select Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked from AccessTokens"; - - public QueryResult Get(AuthenticationInfoQuery query) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseSelectText; - - var whereClauses = new List(); - - var startIndex = query.StartIndex ?? 0; - - if (!string.IsNullOrWhiteSpace(query.AccessToken)) - { - whereClauses.Add("AccessToken=@AccessToken"); - cmd.Parameters.Add(cmd, "@AccessToken", DbType.String).Value = query.AccessToken; - } - - if (!string.IsNullOrWhiteSpace(query.UserId)) - { - whereClauses.Add("UserId=@UserId"); - cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; - } - - if (!string.IsNullOrWhiteSpace(query.DeviceId)) - { - whereClauses.Add("DeviceId=@DeviceId"); - cmd.Parameters.Add(cmd, "@DeviceId", DbType.String).Value = query.DeviceId; - } - - if (query.IsActive.HasValue) - { - whereClauses.Add("IsActive=@IsActive"); - cmd.Parameters.Add(cmd, "@IsActive", DbType.Boolean).Value = query.IsActive.Value; - } - - if (query.HasUser.HasValue) - { - if (query.HasUser.Value) - { - whereClauses.Add("UserId not null"); - } - else - { - whereClauses.Add("UserId is null"); - } - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens {0} ORDER BY DateCreated LIMIT {1})", - pagingWhereText, - startIndex.ToString(_usCulture))); - } - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; - - cmd.CommandText += " ORDER BY DateCreated"; - - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } - - cmd.CommandText += "; select count (Id) from AccessTokens" + whereTextWithoutPaging; - - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) - { - list.Add(Get(reader)); - } - - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); - } - } - - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } - } - } - - public AuthenticationInfo Get(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - using (var connection = CreateConnection(true).Result) - { - var guid = new Guid(id); - - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseSelectText + " where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) - { - return Get(reader); - } - } - } - - return null; - } - } - - private AuthenticationInfo Get(IDataReader reader) - { - var info = new AuthenticationInfo - { - Id = reader.GetGuid(0).ToString("N"), - AccessToken = reader.GetString(1) - }; - - if (!reader.IsDBNull(2)) - { - info.DeviceId = reader.GetString(2); - } - - if (!reader.IsDBNull(3)) - { - info.AppName = reader.GetString(3); - } - - if (!reader.IsDBNull(4)) - { - info.AppVersion = reader.GetString(4); - } - - if (!reader.IsDBNull(5)) - { - info.DeviceName = reader.GetString(5); - } - - if (!reader.IsDBNull(6)) - { - info.UserId = reader.GetString(6); - } - - info.IsActive = reader.GetBoolean(7); - info.DateCreated = reader.GetDateTime(8).ToUniversalTime(); - - if (!reader.IsDBNull(9)) - { - info.DateRevoked = reader.GetDateTime(9).ToUniversalTime(); - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index c7ac630a0..8febe83b2 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.Data //} db.ExecuteAll(string.Join(";", queries)); - + return db; } @@ -119,5 +119,27 @@ namespace Emby.Server.Implementations.Data { } + + protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type) + { + foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) + { + if (row[1].SQLiteType != SQLiteType.Null) + { + var name = row[1].ToString(); + + if (string.Equals(name, columnName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + + connection.ExecuteAll(string.Join(";", new string[] + { + "alter table " + table, + "add column " + columnName + " " + type + " NULL" + })); + } } } diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs new file mode 100644 index 000000000..79fc893f4 --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + /// + /// Class SQLiteDisplayPreferencesRepository + /// + public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository + { + private readonly IMemoryStreamFactory _memoryStreamProvider; + + public SqliteDisplayPreferencesRepository(ILogger logger, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IMemoryStreamFactory memoryStreamProvider) + : base(logger) + { + _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; + DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + /// + /// The _json serializer + /// + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Opens the connection to the database + /// + /// Task. + public void Initialize() + { + using (var connection = CreateConnection()) + { + string[] queries = { + + "create table if not exists userdisplaypreferences (id GUID, userId GUID, client text, data BLOB)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + }; + + connection.RunQueries(queries); + } + } + + /// + /// Save the display preferences associated with an item in the repo + /// + /// The display preferences. + /// The user id. + /// The client. + /// The cancellation token. + /// Task. + /// item + public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException("displayPreferences"); + } + if (string.IsNullOrWhiteSpace(displayPreferences.Id)) + { + throw new ArgumentNullException("displayPreferences.Id"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + SaveDisplayPreferences(displayPreferences, userId, client, db); + }); + } + } + } + + private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) + { + var commandText = "replace into userdisplaypreferences (id, userid, client, data) values (?, ?, ?, ?)"; + var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); + + connection.Execute(commandText, + displayPreferences.Id.ToGuidParamValue(), + userId.ToGuidParamValue(), + client, + serialized); + } + + /// + /// Save all display preferences associated with a user in the repo + /// + /// The display preferences. + /// The user id. + /// The cancellation token. + /// Task. + /// item + public async Task SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException("displayPreferences"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + foreach (var displayPreference in displayPreferences) + { + SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); + } + }); + } + } + } + + /// + /// Gets the display preferences. + /// + /// The display preferences id. + /// The user id. + /// The client. + /// Task{DisplayPreferences}. + /// item + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) + { + if (string.IsNullOrWhiteSpace(displayPreferencesId)) + { + throw new ArgumentNullException("displayPreferencesId"); + } + + var guidId = displayPreferencesId.GetMD5(); + + using (var connection = CreateConnection(true)) + { + var commandText = "select data from userdisplaypreferences where id = ? and userId=? and client=?"; + + var paramList = new List(); + paramList.Add(guidId.ToGuidParamValue()); + paramList.Add(userId.ToGuidParamValue()); + paramList.Add(client); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + return Get(row); + } + + return new DisplayPreferences + { + Id = guidId.ToString("N") + }; + } + } + + /// + /// Gets all display preferences for the given user. + /// + /// The user id. + /// Task{DisplayPreferences}. + /// item + public IEnumerable GetAllDisplayPreferences(Guid userId) + { + var list = new List(); + + using (var connection = CreateConnection(true)) + { + var commandText = "select data from userdisplaypreferences where userId=?"; + + var paramList = new List(); + paramList.Add(userId.ToGuidParamValue()); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + list.Add(Get(row)); + } + } + + return list; + } + + private DisplayPreferences Get(IReadOnlyList row) + { + using (var stream = _memoryStreamProvider.CreateNew(row[0].ToBlob())) + { + stream.Position = 0; + return _jsonSerializer.DeserializeFromStream(stream); + } + } + + public Task SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) + { + return SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); + } + + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) + { + return GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 62615c669..d9536ae9c 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data @@ -25,7 +27,12 @@ namespace Emby.Server.Implementations.Data public static byte[] ToGuidParamValue(this string str) { - return new Guid(str).ToByteArray(); + return ToGuidParamValue(new Guid(str)); + } + + public static byte[] ToGuidParamValue(this Guid guid) + { + return guid.ToByteArray(); } public static Guid ReadGuid(this IResultSetValue result) @@ -101,5 +108,24 @@ namespace Emby.Server.Implementations.Data DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None).ToUniversalTime(); } + + /// + /// Serializes to bytes. + /// + /// System.Byte[][]. + /// obj + public static byte[] SerializeToBytes(this IJsonSerializer json, object obj, IMemoryStreamFactory streamProvider) + { + if (obj == null) + { + throw new ArgumentNullException("obj"); + } + + using (var stream = streamProvider.CreateNew()) + { + json.SerializeToStream(obj, stream); + return stream.ToArray(); + } + } } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 6843ad9d7..a4f26bc60 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -50,6 +50,7 @@ + @@ -192,6 +193,7 @@ + diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs new file mode 100644 index 000000000..5179bd258 --- /dev/null +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Security +{ + public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository + { + private readonly IServerApplicationPaths _appPaths; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public AuthenticationRepository(ILogger logger, IServerApplicationPaths appPaths) + : base(logger) + { + _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "authentication.db"); + } + + public void Initialize() + { + using (var connection = CreateConnection()) + { + string[] queries = { + + "create table if not exists AccessTokens (Id GUID PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT, AppName TEXT, AppVersion TEXT, DeviceName TEXT, UserId TEXT, IsActive BIT, DateCreated DATETIME NOT NULL, DateRevoked DATETIME)", + "create index if not exists idx_AccessTokens on AccessTokens(Id)" + }; + + connection.RunQueries(queries); + + connection.RunInTransaction(db => + { + AddColumn(db, "AccessTokens", "AppVersion", "TEXT"); + }); + } + } + + public Task Create(AuthenticationInfo info, CancellationToken cancellationToken) + { + info.Id = Guid.NewGuid().ToString("N"); + + return Update(info, cancellationToken); + } + + public async Task Update(AuthenticationInfo info, CancellationToken cancellationToken) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + var commandText = "replace into AccessTokens (Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + db.Execute(commandText, + info.Id.ToGuidParamValue(), + info.AccessToken, + info.DeviceId, + info.AppName, + info.AppVersion, + info.DeviceName, + info.UserId, + info.IsActive, + info.DateCreated.ToDateTimeParamValue(), + info.DateRevoked.HasValue ? info.DateRevoked.Value.ToDateTimeParamValue() : null); + }); + } + } + } + + private const string BaseSelectText = "select Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked from AccessTokens"; + + public QueryResult Get(AuthenticationInfoQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + using (var connection = CreateConnection(true)) + { + var commandText = BaseSelectText; + var paramList = new List(); + + var whereClauses = new List(); + + var startIndex = query.StartIndex ?? 0; + + if (!string.IsNullOrWhiteSpace(query.AccessToken)) + { + whereClauses.Add("AccessToken=?"); + paramList.Add(query.AccessToken); + } + + if (!string.IsNullOrWhiteSpace(query.UserId)) + { + whereClauses.Add("UserId=?"); + paramList.Add(query.UserId); + } + + if (!string.IsNullOrWhiteSpace(query.DeviceId)) + { + whereClauses.Add("DeviceId=?"); + paramList.Add(query.DeviceId); + } + + if (query.IsActive.HasValue) + { + whereClauses.Add("IsActive=?"); + paramList.Add(query.IsActive.Value); + } + + if (query.HasUser.HasValue) + { + if (query.HasUser.Value) + { + whereClauses.Add("UserId not null"); + } + else + { + whereClauses.Add("UserId is null"); + } + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + if (startIndex > 0) + { + var pagingWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens {0} ORDER BY DateCreated LIMIT {1})", + pagingWhereText, + startIndex.ToString(_usCulture))); + } + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + commandText += whereText; + + commandText += " ORDER BY DateCreated"; + + if (query.Limit.HasValue) + { + commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + var list = new List(); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + list.Add(Get(row)); + } + + var count = connection.Query("select count (Id) from AccessTokens" + whereTextWithoutPaging, paramList.ToArray()) + .SelectScalarInt() + .First(); + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + + public AuthenticationInfo Get(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = BaseSelectText + " where Id=?"; + var paramList = new List(); + + paramList.Add(id.ToGuidParamValue()); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + return Get(row); + } + return null; + } + } + } + + private AuthenticationInfo Get(IReadOnlyList reader) + { + var info = new AuthenticationInfo + { + Id = reader[0].ReadGuid().ToString("N"), + AccessToken = reader[1].ToString() + }; + + if (reader[2].SQLiteType != SQLiteType.Null) + { + info.DeviceId = reader[2].ToString(); + } + + if (reader[3].SQLiteType != SQLiteType.Null) + { + info.AppName = reader[3].ToString(); + } + + if (reader[4].SQLiteType != SQLiteType.Null) + { + info.AppVersion = reader[4].ToString(); + } + + if (reader[5].SQLiteType != SQLiteType.Null) + { + info.DeviceName = reader[5].ToString(); + } + + if (reader[6].SQLiteType != SQLiteType.Null) + { + info.UserId = reader[6].ToString(); + } + + info.IsActive = reader[7].ToBool(); + info.DateCreated = reader[8].ReadDateTime(); + + if (reader[9].SQLiteType != SQLiteType.Null) + { + info.DateRevoked = reader[9].ReadDateTime(); + } + + return info; + } + } +} -- cgit v1.2.3