From e56433a0efe5bb69e9dbab796c12f9ca56346580 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 18 Jun 2013 05:43:07 -0400 Subject: [PATCH] sqlite --- ...MediaBrowser.Server.Implementations.csproj | 17 +- .../JsonDisplayPreferencesRepository.cs | 164 ---------- .../Persistence/JsonItemRepository.cs | 235 ------------- .../Persistence/JsonUserRepository.cs | 189 ----------- .../SqliteDisplayPreferencesRepository.cs | 209 ++++++++++++ .../Persistence/SqliteExtensions.cs | 61 ++++ .../Persistence/SqliteItemRepository.cs | 309 ++++++++++++++++++ .../Persistence/SqliteRepository.cs | 182 +++++++++++ ...ository.cs => SqliteUserDataRepository.cs} | 168 ++++++---- .../Persistence/SqliteUserRepository.cs | 271 +++++++++++++++ .../packages.config | 1 + .../ApplicationHost.cs | 8 +- 12 files changed, 1154 insertions(+), 660 deletions(-) delete mode 100644 MediaBrowser.Server.Implementations/Persistence/JsonDisplayPreferencesRepository.cs delete mode 100644 MediaBrowser.Server.Implementations/Persistence/JsonItemRepository.cs delete mode 100644 MediaBrowser.Server.Implementations/Persistence/JsonUserRepository.cs create mode 100644 MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs create mode 100644 MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs create mode 100644 MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs create mode 100644 MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs rename MediaBrowser.Server.Implementations/Persistence/{JsonUserDataRepository.cs => SqliteUserDataRepository.cs} (58%) create mode 100644 MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index a3a5220a05..d3f21fefb1 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -82,6 +82,12 @@ + + ..\packages\System.Data.SQLite.x86.1.0.86.0\lib\net45\System.Data.SQLite.dll + + + ..\packages\System.Data.SQLite.x86.1.0.86.0\lib\net45\System.Data.SQLite.Linq.dll + ..\packages\Rx-Core.2.1.30214.0\lib\Net45\System.Reactive.Core.dll @@ -91,6 +97,7 @@ ..\packages\Rx-Linq.2.1.30214.0\lib\Net45\System.Reactive.Linq.dll + @@ -132,6 +139,8 @@ + + @@ -162,10 +171,10 @@ - - - - + + + + diff --git a/MediaBrowser.Server.Implementations/Persistence/JsonDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Persistence/JsonDisplayPreferencesRepository.cs deleted file mode 100644 index 6ac2ff07a6..0000000000 --- a/MediaBrowser.Server.Implementations/Persistence/JsonDisplayPreferencesRepository.cs +++ /dev/null @@ -1,164 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - public class JsonDisplayPreferencesRepository : IDisplayPreferencesRepository - { - private readonly ConcurrentDictionary _fileLocks = new ConcurrentDictionary(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// - /// Gets the name of the repository - /// - /// The name. - public string Name - { - get - { - return "Json"; - } - } - - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - private readonly string _dataPath; - - /// - /// Initializes a new instance of the class. - /// - /// The app paths. - /// The json serializer. - /// The log manager. - /// - /// jsonSerializer - /// or - /// appPaths - /// - public JsonDisplayPreferencesRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - { - if (jsonSerializer == null) - { - throw new ArgumentNullException("jsonSerializer"); - } - if (appPaths == null) - { - throw new ArgumentNullException("appPaths"); - } - - _jsonSerializer = jsonSerializer; - _dataPath = Path.Combine(appPaths.DataPath, "display-preferences"); - } - - /// - /// Opens the connection to the database - /// - /// Task. - public Task Initialize() - { - return Task.FromResult(true); - } - - /// - /// Save the display preferences associated with an item in the repo - /// - /// The display preferences. - /// The cancellation token. - /// Task. - /// item - public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, CancellationToken cancellationToken) - { - if (displayPreferences == null) - { - throw new ArgumentNullException("displayPreferences"); - } - if (displayPreferences.Id == Guid.Empty) - { - throw new ArgumentNullException("displayPreferences.Id"); - } - if (cancellationToken == null) - { - throw new ArgumentNullException("cancellationToken"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (!Directory.Exists(_dataPath)) - { - Directory.CreateDirectory(_dataPath); - } - - var path = Path.Combine(_dataPath, displayPreferences.Id + ".json"); - - var semaphore = GetLock(path); - - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - _jsonSerializer.SerializeToFile(displayPreferences, path); - } - finally - { - semaphore.Release(); - } - } - - /// - /// Gets the display preferences. - /// - /// The display preferences id. - /// Task{DisplayPreferences}. - /// item - public Task GetDisplayPreferences(Guid displayPreferencesId) - { - if (displayPreferencesId == Guid.Empty) - { - throw new ArgumentNullException("displayPreferencesId"); - } - - return Task.Run(() => - { - var path = Path.Combine(_dataPath, displayPreferencesId + ".json"); - - try - { - return _jsonSerializer.DeserializeFromFile(path); - } - catch (IOException) - { - // File doesn't exist or is currently bring written to - return null; - } - }); - } - - public void Dispose() - { - // Wait up to two seconds for any existing writes to finish - var locks = _fileLocks.Values.ToList() - .Where(i => i.CurrentCount == 1) - .Select(i => i.WaitAsync(2000)); - - var task = Task.WhenAll(locks); - - Task.WaitAll(task); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Persistence/JsonItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/JsonItemRepository.cs deleted file mode 100644 index d0333e334e..0000000000 --- a/MediaBrowser.Server.Implementations/Persistence/JsonItemRepository.cs +++ /dev/null @@ -1,235 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - public class JsonItemRepository : IItemRepository - { - private readonly ConcurrentDictionary _fileLocks = new ConcurrentDictionary(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// - /// Gets the name of the repository - /// - /// The name. - public string Name - { - get - { - return "Json"; - } - } - - /// - /// Gets the json serializer. - /// - /// The json serializer. - private readonly IJsonSerializer _jsonSerializer; - - private readonly string _criticReviewsPath; - - private readonly FileSystemRepository _itemRepo; - - /// - /// Initializes a new instance of the class. - /// - /// The app paths. - /// The json serializer. - /// The log manager. - /// appPaths - public JsonItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - { - if (appPaths == null) - { - throw new ArgumentNullException("appPaths"); - } - if (jsonSerializer == null) - { - throw new ArgumentNullException("jsonSerializer"); - } - - _jsonSerializer = jsonSerializer; - - _criticReviewsPath = Path.Combine(appPaths.DataPath, "critic-reviews"); - - _itemRepo = new FileSystemRepository(Path.Combine(appPaths.DataPath, "library")); - } - - /// - /// Opens the connection to the database - /// - /// Task. - public Task Initialize() - { - return Task.FromResult(true); - } - - /// - /// Save a standard item in the repo - /// - /// The item. - /// The cancellation token. - /// Task. - /// item - public async Task SaveItem(BaseItem item, CancellationToken cancellationToken) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - if (!Directory.Exists(_criticReviewsPath)) - { - Directory.CreateDirectory(_criticReviewsPath); - } - - var path = _itemRepo.GetResourcePath(item.Id + ".json"); - - var parentPath = Path.GetDirectoryName(path); - if (!Directory.Exists(parentPath)) - { - Directory.CreateDirectory(parentPath); - } - - var semaphore = GetLock(path); - - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - _jsonSerializer.SerializeToFile(item, path); - } - finally - { - semaphore.Release(); - } - } - - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// Task. - /// - /// items - /// or - /// cancellationToken - /// - public Task SaveItems(IEnumerable items, CancellationToken cancellationToken) - { - if (items == null) - { - throw new ArgumentNullException("items"); - } - - if (cancellationToken == null) - { - throw new ArgumentNullException("cancellationToken"); - } - - var tasks = items.Select(i => SaveItem(i, cancellationToken)); - - return Task.WhenAll(tasks); - } - - /// - /// Retrieves the item. - /// - /// The id. - /// The type. - /// BaseItem. - /// id - public BaseItem RetrieveItem(Guid id, Type type) - { - if (id == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - var path = _itemRepo.GetResourcePath(id + ".json"); - - try - { - return (BaseItem)_jsonSerializer.DeserializeFromFile(type, path); - } - catch (IOException) - { - // File doesn't exist or is currently bring written to - return null; - } - } - - /// - /// Gets the critic reviews. - /// - /// The item id. - /// Task{IEnumerable{ItemReview}}. - public Task> GetCriticReviews(Guid itemId) - { - return Task.Run>(() => - { - var path = Path.Combine(_criticReviewsPath, itemId + ".json"); - - try - { - return _jsonSerializer.DeserializeFromFile>(path); - } - catch (IOException) - { - // File doesn't exist or is currently bring written to - return new List(); - } - }); - } - - /// - /// Saves the critic reviews. - /// - /// The item id. - /// The critic reviews. - /// Task. - public Task SaveCriticReviews(Guid itemId, IEnumerable criticReviews) - { - return Task.Run(() => - { - if (!Directory.Exists(_criticReviewsPath)) - { - Directory.CreateDirectory(_criticReviewsPath); - } - - var path = Path.Combine(_criticReviewsPath, itemId + ".json"); - - _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); - }); - } - - public void Dispose() - { - // Wait up to two seconds for any existing writes to finish - var locks = _fileLocks.Values.ToList() - .Where(i => i.CurrentCount == 1) - .Select(i => i.WaitAsync(2000)); - - var task = Task.WhenAll(locks); - - Task.WaitAll(task); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Persistence/JsonUserRepository.cs b/MediaBrowser.Server.Implementations/Persistence/JsonUserRepository.cs deleted file mode 100644 index 0573c6e2ef..0000000000 --- a/MediaBrowser.Server.Implementations/Persistence/JsonUserRepository.cs +++ /dev/null @@ -1,189 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - public class JsonUserRepository : IUserRepository - { - private readonly ConcurrentDictionary _fileLocks = new ConcurrentDictionary(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// - /// Gets the name of the repository - /// - /// The name. - public string Name - { - get - { - return "Json"; - } - } - - /// - /// Gets the json serializer. - /// - /// The json serializer. - private readonly IJsonSerializer _jsonSerializer; - - private readonly string _dataPath; - - /// - /// Initializes a new instance of the class. - /// - /// The app paths. - /// The json serializer. - /// The log manager. - /// - /// appPaths - /// or - /// jsonSerializer - /// - public JsonUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - { - if (appPaths == null) - { - throw new ArgumentNullException("appPaths"); - } - if (jsonSerializer == null) - { - throw new ArgumentNullException("jsonSerializer"); - } - - _jsonSerializer = jsonSerializer; - - _dataPath = Path.Combine(appPaths.DataPath, "users"); - } - - /// - /// Opens the connection to the database - /// - /// Task. - public Task Initialize() - { - return Task.FromResult(true); - } - - /// - /// Save a user in the repo - /// - /// The user. - /// The cancellation token. - /// Task. - /// user - public async Task SaveUser(User user, CancellationToken cancellationToken) - { - if (user == null) - { - throw new ArgumentNullException("user"); - } - - if (cancellationToken == null) - { - throw new ArgumentNullException("cancellationToken"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (!Directory.Exists(_dataPath)) - { - Directory.CreateDirectory(_dataPath); - } - - var path = Path.Combine(_dataPath, user.Id + ".json"); - - var semaphore = GetLock(path); - - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - _jsonSerializer.SerializeToFile(user, path); - } - finally - { - semaphore.Release(); - } - } - - /// - /// Retrieve all users from the database - /// - /// IEnumerable{User}. - public IEnumerable RetrieveAllUsers() - { - try - { - return Directory.EnumerateFiles(_dataPath, "*.json", SearchOption.TopDirectoryOnly) - .Select(i => _jsonSerializer.DeserializeFromFile(i)); - } - catch (IOException) - { - return new List(); - } - } - - /// - /// Deletes the user. - /// - /// The user. - /// The cancellation token. - /// Task. - /// user - public async Task DeleteUser(User user, CancellationToken cancellationToken) - { - if (user == null) - { - throw new ArgumentNullException("user"); - } - - if (cancellationToken == null) - { - throw new ArgumentNullException("cancellationToken"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - var path = Path.Combine(_dataPath, user.Id + ".json"); - - var semaphore = GetLock(path); - - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - File.Delete(path); - } - finally - { - semaphore.Release(); - } - } - - public void Dispose() - { - // Wait up to two seconds for any existing writes to finish - var locks = _fileLocks.Values.ToList() - .Where(i => i.CurrentCount == 1) - .Select(i => i.WaitAsync(2000)); - - var task = Task.WhenAll(locks); - - Task.WaitAll(task); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs new file mode 100644 index 0000000000..f4d341c347 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs @@ -0,0 +1,209 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Data; +using System.Data.SQLite; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + /// + /// Class SQLiteDisplayPreferencesRepository + /// + public class SqliteDisplayPreferencesRepository : SqliteRepository, IDisplayPreferencesRepository + { + /// + /// The repository name + /// + public const string RepositoryName = "SQLite"; + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return RepositoryName; + } + } + + /// + /// The _json serializer + /// + private readonly IJsonSerializer _jsonSerializer; + + /// + /// The _app paths + /// + private readonly IApplicationPaths _appPaths; + + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The json serializer. + /// The log manager. + /// + /// jsonSerializer + /// or + /// appPaths + /// + public SqliteDisplayPreferencesRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) + : base(logManager) + { + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + if (appPaths == null) + { + throw new ArgumentNullException("appPaths"); + } + + _jsonSerializer = jsonSerializer; + _appPaths = appPaths; + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + var dbFile = Path.Combine(_appPaths.DataPath, "displaypreferences.db"); + + await ConnectToDb(dbFile).ConfigureAwait(false); + + string[] queries = { + + "create table if not exists displaypreferences (id GUID, data BLOB)", + "create unique index if not exists displaypreferencesindex on displaypreferences (id)", + "create table if not exists schema_version (table_name primary key, version)", + //pragmas + "pragma temp_store = memory" + }; + + RunQueries(queries); + } + + /// + /// Save the display preferences associated with an item in the repo + /// + /// The display preferences. + /// The cancellation token. + /// Task. + /// item + public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException("displayPreferences"); + } + if (displayPreferences.Id == Guid.Empty) + { + throw new ArgumentNullException("displayPreferences.Id"); + } + if (cancellationToken == null) + { + throw new ArgumentNullException("cancellationToken"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var serialized = _jsonSerializer.SerializeToBytes(displayPreferences); + + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + SQLiteTransaction transaction = null; + + try + { + transaction = Connection.BeginTransaction(); + + using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "replace into displaypreferences (id, data) values (@1, @2)"; + cmd.AddParam("@1", displayPreferences.Id); + cmd.AddParam("@2", serialized); + + cmd.Transaction = transaction; + + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + + 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(); + } + + _writeLock.Release(); + } + } + + /// + /// Gets the display preferences. + /// + /// The display preferences id. + /// Task{DisplayPreferences}. + /// item + public async Task GetDisplayPreferences(Guid displayPreferencesId) + { + if (displayPreferencesId == Guid.Empty) + { + throw new ArgumentNullException("displayPreferencesId"); + } + + var cmd = Connection.CreateCommand(); + cmd.CommandText = "select data from displaypreferences where id = @id"; + + var idParam = cmd.Parameters.Add("@id", DbType.Guid); + idParam.Value = displayPreferencesId; + + using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow).ConfigureAwait(false)) + { + if (reader.Read()) + { + using (var stream = GetStream(reader, 0)) + { + return _jsonSerializer.DeserializeFromStream(stream); + } + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs new file mode 100644 index 0000000000..00dbbe513f --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs @@ -0,0 +1,61 @@ +using System; +using System.Data; +using System.Data.SQLite; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + /// + /// Class SQLiteExtensions + /// + static class SqliteExtensions + { + /// + /// Adds the param. + /// + /// The CMD. + /// The param. + /// SQLiteParameter. + /// + public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param) + { + if (string.IsNullOrEmpty(param)) + { + throw new ArgumentNullException(); + } + + var sqliteParam = new SQLiteParameter(param); + cmd.Parameters.Add(sqliteParam); + return sqliteParam; + } + + /// + /// Adds the param. + /// + /// The CMD. + /// The param. + /// The data. + /// SQLiteParameter. + /// + public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param, object data) + { + if (string.IsNullOrEmpty(param)) + { + throw new ArgumentNullException(); + } + + var sqliteParam = AddParam(cmd, param); + sqliteParam.Value = data; + return sqliteParam; + } + + /// + /// Determines whether the specified conn is open. + /// + /// The conn. + /// true if the specified conn is open; otherwise, false. + public static bool IsOpen(this SQLiteConnection conn) + { + return conn.State == ConnectionState.Open; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs new file mode 100644 index 0000000000..a9cd3d1eb4 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -0,0 +1,309 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SQLite; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + /// + /// Class SQLiteItemRepository + /// + public class SqliteItemRepository : SqliteRepository, IItemRepository + { + /// + /// The repository name + /// + public const string RepositoryName = "SQLite"; + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return RepositoryName; + } + } + + /// + /// Gets the json serializer. + /// + /// The json serializer. + private readonly IJsonSerializer _jsonSerializer; + + /// + /// The _app paths + /// + private readonly IApplicationPaths _appPaths; + + /// + /// The _save item command + /// + private SQLiteCommand _saveItemCommand; + + private readonly string _criticReviewsPath; + + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The json serializer. + /// The log manager. + /// + /// appPaths + /// or + /// jsonSerializer + /// + public SqliteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) + : base(logManager) + { + if (appPaths == null) + { + throw new ArgumentNullException("appPaths"); + } + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + + _appPaths = appPaths; + _jsonSerializer = jsonSerializer; + + _criticReviewsPath = Path.Combine(_appPaths.DataPath, "critic-reviews"); + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + var dbFile = Path.Combine(_appPaths.DataPath, "library.db"); + + await ConnectToDb(dbFile).ConfigureAwait(false); + + string[] queries = { + + "create table if not exists baseitems (guid GUID primary key, data BLOB)", + "create index if not exists idx_baseitems on baseitems(guid)", + "create table if not exists schema_version (table_name primary key, version)", + //pragmas + "pragma temp_store = memory" + }; + + RunQueries(queries); + + PrepareStatements(); + } + + /// + /// The _write lock + /// + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// + /// Prepares the statements. + /// + private void PrepareStatements() + { + _saveItemCommand = new SQLiteCommand + { + CommandText = "replace into baseitems (guid, data) values (@1, @2)" + }; + + _saveItemCommand.Parameters.Add(new SQLiteParameter("@1")); + _saveItemCommand.Parameters.Add(new SQLiteParameter("@2")); + } + + /// + /// Save a standard item in the repo + /// + /// The item. + /// The cancellation token. + /// Task. + /// item + public Task SaveItem(BaseItem item, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + return SaveItems(new[] { item }, cancellationToken); + } + + /// + /// Saves the items. + /// + /// The items. + /// The cancellation token. + /// Task. + /// + /// items + /// or + /// cancellationToken + /// + public async Task SaveItems(IEnumerable items, CancellationToken cancellationToken) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + if (cancellationToken == null) + { + throw new ArgumentNullException("cancellationToken"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + SQLiteTransaction transaction = null; + + try + { + transaction = Connection.BeginTransaction(); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + _saveItemCommand.Parameters[0].Value = item.Id; + _saveItemCommand.Parameters[1].Value = _jsonSerializer.SerializeToBytes(item); + + _saveItemCommand.Transaction = transaction; + + await _saveItemCommand.ExecuteNonQueryAsync(cancellationToken); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save items:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } + } + + /// + /// Internal retrieve from items or users table + /// + /// The id. + /// The type. + /// BaseItem. + /// id + /// + public BaseItem RetrieveItem(Guid id, Type type) + { + if (id == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "select data from baseitems where guid = @guid"; + var guidParam = cmd.Parameters.Add("@guid", DbType.Guid); + guidParam.Value = id; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + using (var stream = GetStream(reader, 0)) + { + return _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; + } + } + } + return null; + } + } + + /// + /// Gets the critic reviews. + /// + /// The item id. + /// Task{IEnumerable{ItemReview}}. + public Task> GetCriticReviews(Guid itemId) + { + return Task.Run>(() => + { + + try + { + var path = Path.Combine(_criticReviewsPath, itemId + ".json"); + + return _jsonSerializer.DeserializeFromFile>(path); + } + catch (DirectoryNotFoundException) + { + return new List(); + } + catch (FileNotFoundException) + { + return new List(); + } + + }); + } + + /// + /// Saves the critic reviews. + /// + /// The item id. + /// The critic reviews. + /// Task. + public Task SaveCriticReviews(Guid itemId, IEnumerable criticReviews) + { + return Task.Run(() => + { + if (!Directory.Exists(_criticReviewsPath)) + { + Directory.CreateDirectory(_criticReviewsPath); + } + + var path = Path.Combine(_criticReviewsPath, itemId + ".json"); + + _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); + }); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs new file mode 100644 index 0000000000..cfdc9b5fb6 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs @@ -0,0 +1,182 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Data; +using System.Data.SQLite; +using System.IO; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + /// + /// Class SqliteRepository + /// + public abstract class SqliteRepository : IDisposable + { + /// + /// The db file name + /// + protected string DbFileName; + /// + /// The connection + /// + protected SQLiteConnection Connection; + + /// + /// Gets the logger. + /// + /// The logger. + protected ILogger Logger { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The log manager. + /// logger + protected SqliteRepository(ILogManager logManager) + { + if (logManager == null) + { + throw new ArgumentNullException("logManager"); + } + + Logger = logManager.GetLogger(GetType().Name); + } + + /// + /// Connects to DB. + /// + /// The db path. + /// Task{System.Boolean}. + /// dbPath + protected Task ConnectToDb(string dbPath) + { + if (string.IsNullOrEmpty(dbPath)) + { + throw new ArgumentNullException("dbPath"); + } + + DbFileName = dbPath; + var connectionstr = new SQLiteConnectionStringBuilder + { + PageSize = 4096, + CacheSize = 40960, + SyncMode = SynchronizationModes.Off, + DataSource = dbPath, + JournalMode = SQLiteJournalModeEnum.Wal + }; + + Connection = new SQLiteConnection(connectionstr.ConnectionString); + + return Connection.OpenAsync(); + } + + /// + /// Runs the queries. + /// + /// The queries. + /// true if XXXX, false otherwise + /// queries + protected void RunQueries(string[] queries) + { + if (queries == null) + { + throw new ArgumentNullException("queries"); + } + + using (var tran = Connection.BeginTransaction()) + { + try + { + using (var cmd = Connection.CreateCommand()) + { + foreach (var query in queries) + { + cmd.Transaction = tran; + cmd.CommandText = query; + cmd.ExecuteNonQuery(); + } + } + + tran.Commit(); + } + catch (Exception e) + { + Logger.ErrorException("Error running queries", e); + tran.Rollback(); + throw; + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + 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) + { + if (Connection != null) + { + if (Connection.IsOpen()) + { + Connection.Close(); + } + + Connection.Dispose(); + Connection = null; + } + } + } + catch (Exception ex) + { + Logger.ErrorException("Error disposing database", ex); + } + } + } + + /// + /// Gets a stream from a DataReader at a given ordinal + /// + /// The reader. + /// The ordinal. + /// Stream. + /// reader + protected static Stream GetStream(IDataReader reader, int ordinal) + { + if (reader == null) + { + throw new ArgumentNullException("reader"); + } + + var memoryStream = new MemoryStream(); + var num = 0L; + var array = new byte[4096]; + long bytes; + do + { + bytes = reader.GetBytes(ordinal, num, array, 0, array.Length); + memoryStream.Write(array, 0, (int)bytes); + num += bytes; + } + while (bytes > 0L); + memoryStream.Position = 0; + return memoryStream; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/JsonUserDataRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs similarity index 58% rename from MediaBrowser.Server.Implementations/Persistence/JsonUserDataRepository.cs rename to MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs index 2f1129bebc..05829e007b 100644 --- a/MediaBrowser.Server.Implementations/Persistence/JsonUserDataRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs @@ -1,29 +1,29 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Concurrent; +using System.Data; +using System.Data.SQLite; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Persistence { - public class JsonUserDataRepository : IUserDataRepository + public class SqliteUserDataRepository : SqliteRepository, IUserDataRepository { - private readonly ConcurrentDictionary _fileLocks = new ConcurrentDictionary(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - private readonly ConcurrentDictionary _userData = new ConcurrentDictionary(); + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// + /// The repository name + /// + public const string RepositoryName = "SQLite"; + /// /// Gets the name of the repository /// @@ -32,18 +32,19 @@ namespace MediaBrowser.Server.Implementations.Persistence { get { - return "Json"; + return RepositoryName; } } private readonly IJsonSerializer _jsonSerializer; - private readonly string _dataPath; - - private readonly ILogger _logger; + /// + /// The _app paths + /// + private readonly IApplicationPaths _appPaths; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The app paths. /// The json serializer. @@ -53,7 +54,8 @@ namespace MediaBrowser.Server.Implementations.Persistence /// or /// appPaths /// - public JsonUserDataRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) + public SqliteUserDataRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) + : base(logManager) { if (jsonSerializer == null) { @@ -64,18 +66,30 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("appPaths"); } - _logger = logManager.GetLogger(GetType().Name); _jsonSerializer = jsonSerializer; - _dataPath = Path.Combine(appPaths.DataPath, "userdata"); + _appPaths = appPaths; } /// /// Opens the connection to the database /// /// Task. - public Task Initialize() + public async Task Initialize() { - return Task.FromResult(true); + var dbFile = Path.Combine(_appPaths.DataPath, "userdata.db"); + + await ConnectToDb(dbFile).ConfigureAwait(false); + + string[] queries = { + + "create table if not exists userdata (key nvarchar, userId GUID, data BLOB)", + "create unique index if not exists userdataindex on userdata (key, userId)", + "create table if not exists schema_version (table_name primary key, version)", + //pragmas + "pragma temp_store = memory" + }; + + RunQueries(queries); } /// @@ -118,12 +132,14 @@ namespace MediaBrowser.Server.Implementations.Persistence { await PersistUserData(userId, key, userData, cancellationToken).ConfigureAwait(false); + var newValue = userData; + // Once it succeeds, put it into the dictionary to make it available to everyone else - _userData.AddOrUpdate(GetInternalKey(userId, key), userData, delegate { return userData; }); + _userData.AddOrUpdate(GetInternalKey(userId, key), newValue, delegate { return newValue; }); } catch (Exception ex) { - _logger.ErrorException("Error saving user data", ex); + Logger.ErrorException("Error saving user data", ex); throw; } @@ -152,25 +168,60 @@ namespace MediaBrowser.Server.Implementations.Persistence { cancellationToken.ThrowIfCancellationRequested(); - var path = GetUserDataPath(userId, key); + var serialized = _jsonSerializer.SerializeToBytes(userData); - var parentPath = Path.GetDirectoryName(path); - if (!Directory.Exists(parentPath)) - { - Directory.CreateDirectory(parentPath); - } + cancellationToken.ThrowIfCancellationRequested(); - var semaphore = GetLock(path); + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + SQLiteTransaction transaction = null; try { - _jsonSerializer.SerializeToFile(userData, path); + transaction = Connection.BeginTransaction(); + + using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "replace into userdata (key, userId, data) values (@1, @2, @3)"; + cmd.AddParam("@1", key); + cmd.AddParam("@2", userId); + cmd.AddParam("@3", serialized); + + cmd.Transaction = transaction; + + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save user data:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; } finally { - semaphore.Release(); + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); } } @@ -207,40 +258,29 @@ namespace MediaBrowser.Server.Implementations.Persistence /// Task{UserItemData}. private UserItemData RetrieveUserData(Guid userId, string key) { - var path = GetUserDataPath(userId, key); - - try + using (var cmd = Connection.CreateCommand()) { - return _jsonSerializer.DeserializeFromFile(path); + cmd.CommandText = "select data from userdata where key = @key and userId=@userId"; + + var idParam = cmd.Parameters.Add("@key", DbType.String); + idParam.Value = key; + + var userIdParam = cmd.Parameters.Add("@userId", DbType.Guid); + userIdParam.Value = userId; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + { + if (reader.Read()) + { + using (var stream = GetStream(reader, 0)) + { + return _jsonSerializer.DeserializeFromStream(stream); + } + } + } + + return new UserItemData(); } - catch (IOException) - { - // File doesn't exist or is currently bring written to - return new UserItemData { UserId = userId }; - } - } - - private string GetUserDataPath(Guid userId, string key) - { - var userFolder = Path.Combine(_dataPath, userId.ToString()); - - var keyHash = key.GetMD5().ToString(); - - var prefix = keyHash.Substring(0, 1); - - return Path.Combine(userFolder, prefix, keyHash + ".json"); - } - - public void Dispose() - { - // Wait up to two seconds for any existing writes to finish - var locks = _fileLocks.Values.ToList() - .Where(i => i.CurrentCount == 1) - .Select(i => i.WaitAsync(2000)); - - var task = Task.WhenAll(locks); - - Task.WaitAll(task); } } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs new file mode 100644 index 0000000000..efd39529a9 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs @@ -0,0 +1,271 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SQLite; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + /// + /// Class SQLiteUserRepository + /// + public class SqliteUserRepository : SqliteRepository, IUserRepository + { + /// + /// The repository name + /// + public const string RepositoryName = "SQLite"; + + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return RepositoryName; + } + } + + /// + /// Gets the json serializer. + /// + /// The json serializer. + private readonly IJsonSerializer _jsonSerializer; + + /// + /// The _app paths + /// + private readonly IApplicationPaths _appPaths; + + /// + /// Initializes a new instance of the class. + /// + /// The app paths. + /// The json serializer. + /// The log manager. + /// appPaths + public SqliteUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) + : base(logManager) + { + if (appPaths == null) + { + throw new ArgumentNullException("appPaths"); + } + if (jsonSerializer == null) + { + throw new ArgumentNullException("jsonSerializer"); + } + + _appPaths = appPaths; + _jsonSerializer = jsonSerializer; + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + var dbFile = Path.Combine(_appPaths.DataPath, "users.db"); + + await ConnectToDb(dbFile).ConfigureAwait(false); + + string[] queries = { + + "create table if not exists users (guid GUID primary key, data BLOB)", + "create index if not exists idx_users on users(guid)", + "create table if not exists schema_version (table_name primary key, version)", + //pragmas + "pragma temp_store = memory" + }; + + RunQueries(queries); + } + + /// + /// Save a user in the repo + /// + /// The user. + /// The cancellation token. + /// Task. + /// user + public async Task SaveUser(User user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (cancellationToken == null) + { + throw new ArgumentNullException("cancellationToken"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var serialized = _jsonSerializer.SerializeToBytes(user); + + cancellationToken.ThrowIfCancellationRequested(); + + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + SQLiteTransaction transaction = null; + + try + { + transaction = Connection.BeginTransaction(); + + using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "replace into users (guid, data) values (@1, @2)"; + cmd.AddParam("@1", user.Id); + cmd.AddParam("@2", serialized); + + cmd.Transaction = transaction; + + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save user:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } + } + + /// + /// Retrieve all users from the database + /// + /// IEnumerable{User}. + public IEnumerable RetrieveAllUsers() + { + using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "select data from users"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + using (var stream = GetStream(reader, 0)) + { + var user = _jsonSerializer.DeserializeFromStream(stream); + yield return user; + } + } + } + } + } + + /// + /// Deletes the user. + /// + /// The user. + /// The cancellation token. + /// Task. + /// user + public async Task DeleteUser(User user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (cancellationToken == null) + { + throw new ArgumentNullException("cancellationToken"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + SQLiteTransaction transaction = null; + + try + { + transaction = Connection.BeginTransaction(); + + using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "delete from users where guid=@guid"; + + var guidParam = cmd.Parameters.Add("@guid", DbType.Guid); + guidParam.Value = user.Id; + + cmd.Transaction = transaction; + + await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to delete user:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index f13933eb6e..12b6ef6500 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -14,4 +14,5 @@ + \ No newline at end of file diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index da871cc120..24093a181c 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -244,16 +244,16 @@ namespace MediaBrowser.ServerApplication ZipClient = new DotNetZipClient(); RegisterSingleInstance(ZipClient); - UserDataRepository = new JsonUserDataRepository(ApplicationPaths, JsonSerializer, LogManager); + UserDataRepository = new SqliteUserDataRepository(ApplicationPaths, JsonSerializer, LogManager); RegisterSingleInstance(UserDataRepository); - UserRepository = new JsonUserRepository(ApplicationPaths, JsonSerializer, LogManager); + UserRepository = new SqliteUserRepository(ApplicationPaths, JsonSerializer, LogManager); RegisterSingleInstance(UserRepository); - DisplayPreferencesRepository = new JsonDisplayPreferencesRepository(ApplicationPaths, JsonSerializer, LogManager); + DisplayPreferencesRepository = new SqliteDisplayPreferencesRepository(ApplicationPaths, JsonSerializer, LogManager); RegisterSingleInstance(DisplayPreferencesRepository); - ItemRepository = new JsonItemRepository(ApplicationPaths, JsonSerializer, LogManager); + ItemRepository = new SqliteItemRepository(ApplicationPaths, JsonSerializer, LogManager); RegisterSingleInstance(ItemRepository); UserManager = new UserManager(Logger, ServerConfigurationManager);