diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2013-06-18 05:43:07 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2013-06-18 05:43:07 -0400 |
| commit | e56433a0efe5bb69e9dbab796c12f9ca56346580 (patch) | |
| tree | 88b261ff8f92ea8877b7fa5087bb3c5bdc678f58 | |
| parent | e677a57bf1cedc55214b0e457778311b8f1ea5ac (diff) | |
sqlite
12 files changed, 1154 insertions, 660 deletions
diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index a3a5220a0..d3f21fefb 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -82,6 +82,12 @@ </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> + <Reference Include="System.Data.SQLite"> + <HintPath>..\packages\System.Data.SQLite.x86.1.0.86.0\lib\net45\System.Data.SQLite.dll</HintPath> + </Reference> + <Reference Include="System.Data.SQLite.Linq"> + <HintPath>..\packages\System.Data.SQLite.x86.1.0.86.0\lib\net45\System.Data.SQLite.Linq.dll</HintPath> + </Reference> <Reference Include="System.Reactive.Core"> <HintPath>..\packages\Rx-Core.2.1.30214.0\lib\Net45\System.Reactive.Core.dll</HintPath> </Reference> @@ -91,6 +97,7 @@ <Reference Include="System.Reactive.Linq"> <HintPath>..\packages\Rx-Linq.2.1.30214.0\lib\Net45\System.Reactive.Linq.dll</HintPath> </Reference> + <Reference Include="System.Runtime.Serialization" /> <Reference Include="System.Web" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> @@ -132,6 +139,8 @@ <Compile Include="Library\UserManager.cs" /> <Compile Include="Localization\LocalizationManager.cs" /> <Compile Include="MediaEncoder\MediaEncoder.cs" /> + <Compile Include="Persistence\SqliteExtensions.cs" /> + <Compile Include="Persistence\SqliteRepository.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Providers\ProviderManager.cs" /> <Compile Include="ScheduledTasks\ArtistValidationTask.cs" /> @@ -162,10 +171,10 @@ <Compile Include="Sorting\RevenueComparer.cs" /> <Compile Include="Sorting\RuntimeComparer.cs" /> <Compile Include="Sorting\SortNameComparer.cs" /> - <Compile Include="Persistence\JsonDisplayPreferencesRepository.cs" /> - <Compile Include="Persistence\JsonItemRepository.cs" /> - <Compile Include="Persistence\JsonUserDataRepository.cs" /> - <Compile Include="Persistence\JsonUserRepository.cs" /> + <Compile Include="Persistence\SqliteDisplayPreferencesRepository.cs" /> + <Compile Include="Persistence\SqliteItemRepository.cs" /> + <Compile Include="Persistence\SqliteUserDataRepository.cs" /> + <Compile Include="Persistence\SqliteUserRepository.cs" /> <Compile Include="Udp\UdpMessageReceivedEventArgs.cs" /> <Compile Include="Udp\UdpServer.cs" /> <Compile Include="Updates\InstallationManager.cs" /> diff --git a/MediaBrowser.Server.Implementations/Persistence/JsonDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Persistence/JsonDisplayPreferencesRepository.cs deleted file mode 100644 index 6ac2ff07a..000000000 --- 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<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// <summary> - /// Gets the name of the repository - /// </summary> - /// <value>The name.</value> - public string Name - { - get - { - return "Json"; - } - } - - /// <summary> - /// The _json serializer - /// </summary> - private readonly IJsonSerializer _jsonSerializer; - - private readonly string _dataPath; - - /// <summary> - /// Initializes a new instance of the <see cref="JsonUserDataRepository" /> class. - /// </summary> - /// <param name="appPaths">The app paths.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="logManager">The log manager.</param> - /// <exception cref="System.ArgumentNullException"> - /// jsonSerializer - /// or - /// appPaths - /// </exception> - 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"); - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public Task Initialize() - { - return Task.FromResult(true); - } - - /// <summary> - /// Save the display preferences associated with an item in the repo - /// </summary> - /// <param name="displayPreferences">The display preferences.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">item</exception> - 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(); - } - } - - /// <summary> - /// Gets the display preferences. - /// </summary> - /// <param name="displayPreferencesId">The display preferences id.</param> - /// <returns>Task{DisplayPreferences}.</returns> - /// <exception cref="System.ArgumentNullException">item</exception> - public Task<DisplayPreferences> 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<DisplayPreferences>(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 d0333e334..000000000 --- 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<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// <summary> - /// Gets the name of the repository - /// </summary> - /// <value>The name.</value> - public string Name - { - get - { - return "Json"; - } - } - - /// <summary> - /// Gets the json serializer. - /// </summary> - /// <value>The json serializer.</value> - private readonly IJsonSerializer _jsonSerializer; - - private readonly string _criticReviewsPath; - - private readonly FileSystemRepository _itemRepo; - - /// <summary> - /// Initializes a new instance of the <see cref="JsonUserDataRepository" /> class. - /// </summary> - /// <param name="appPaths">The app paths.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="logManager">The log manager.</param> - /// <exception cref="System.ArgumentNullException">appPaths</exception> - 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")); - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public Task Initialize() - { - return Task.FromResult(true); - } - - /// <summary> - /// Save a standard item in the repo - /// </summary> - /// <param name="item">The item.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">item</exception> - 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(); - } - } - - /// <summary> - /// Saves the items. - /// </summary> - /// <param name="items">The items.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException"> - /// items - /// or - /// cancellationToken - /// </exception> - public Task SaveItems(IEnumerable<BaseItem> 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); - } - - /// <summary> - /// Retrieves the item. - /// </summary> - /// <param name="id">The id.</param> - /// <param name="type">The type.</param> - /// <returns>BaseItem.</returns> - /// <exception cref="System.ArgumentNullException">id</exception> - 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; - } - } - - /// <summary> - /// Gets the critic reviews. - /// </summary> - /// <param name="itemId">The item id.</param> - /// <returns>Task{IEnumerable{ItemReview}}.</returns> - public Task<IEnumerable<ItemReview>> GetCriticReviews(Guid itemId) - { - return Task.Run<IEnumerable<ItemReview>>(() => - { - var path = Path.Combine(_criticReviewsPath, itemId + ".json"); - - try - { - return _jsonSerializer.DeserializeFromFile<List<ItemReview>>(path); - } - catch (IOException) - { - // File doesn't exist or is currently bring written to - return new List<ItemReview>(); - } - }); - } - - /// <summary> - /// Saves the critic reviews. - /// </summary> - /// <param name="itemId">The item id.</param> - /// <param name="criticReviews">The critic reviews.</param> - /// <returns>Task.</returns> - public Task SaveCriticReviews(Guid itemId, IEnumerable<ItemReview> 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 0573c6e2e..000000000 --- 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<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// <summary> - /// Gets the name of the repository - /// </summary> - /// <value>The name.</value> - public string Name - { - get - { - return "Json"; - } - } - - /// <summary> - /// Gets the json serializer. - /// </summary> - /// <value>The json serializer.</value> - private readonly IJsonSerializer _jsonSerializer; - - private readonly string _dataPath; - - /// <summary> - /// Initializes a new instance of the <see cref="JsonUserRepository"/> class. - /// </summary> - /// <param name="appPaths">The app paths.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="logManager">The log manager.</param> - /// <exception cref="System.ArgumentNullException"> - /// appPaths - /// or - /// jsonSerializer - /// </exception> - 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"); - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public Task Initialize() - { - return Task.FromResult(true); - } - - /// <summary> - /// Save a user in the repo - /// </summary> - /// <param name="user">The user.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">user</exception> - 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(); - } - } - - /// <summary> - /// Retrieve all users from the database - /// </summary> - /// <returns>IEnumerable{User}.</returns> - public IEnumerable<User> RetrieveAllUsers() - { - try - { - return Directory.EnumerateFiles(_dataPath, "*.json", SearchOption.TopDirectoryOnly) - .Select(i => _jsonSerializer.DeserializeFromFile<User>(i)); - } - catch (IOException) - { - return new List<User>(); - } - } - - /// <summary> - /// Deletes the user. - /// </summary> - /// <param name="user">The user.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="System.ArgumentNullException">user</exception> - 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 000000000..f4d341c34 --- /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 +{ + /// <summary> + /// Class SQLiteDisplayPreferencesRepository + /// </summary> + public class SqliteDisplayPreferencesRepository : SqliteRepository, IDisplayPreferencesRepository + { + /// <summary> + /// The repository name + /// </summary> + public const string RepositoryName = "SQLite"; + + /// <summary> + /// Gets the name of the repository + /// </summary> + /// <value>The name.</value> + public string Name + { + get + { + return RepositoryName; + } + } + + /// <summary> + /// The _json serializer + /// </summary> + private readonly IJsonSerializer _jsonSerializer; + + /// <summary> + /// The _app paths + /// </summary> + private readonly IApplicationPaths _appPaths; + + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// <summary> + /// Initializes a new instance of the <see cref="SqliteDisplayPreferencesRepository" /> class. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <param name="jsonSerializer">The json serializer.</param> + /// <param name="logManager">The log manager.</param> + /// <exception cref="System.ArgumentNullException"> + /// jsonSerializer + /// or + /// appPaths + /// </exception> + 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; + } + + /// <summary> + /// Opens the connection to the database + /// </summary> + /// <returns>Task.</returns> + 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); + } + + /// <summary> + /// Save the display preferences associated with an item in the repo + /// </summary> + /// <param name="displayPreferences">The display preferences.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">item</exception> + 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(); + } + } + + /// <summary> + /// Gets the display preferences. + /// </summary> + /// <param name="displayPreferencesId">The display preferences id.</param> + /// <returns>Task{DisplayPreferences}.</returns> + /// <exception cref="System.ArgumentNullException">item</exception> + public async Task<DisplayPreferences> 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<DisplayPreferences>(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 000000000..00dbbe513 --- /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 +{ + /// <summary> + /// Class SQLiteExtensions + /// </summary> + static class SqliteExtensions + { + /// <summary> + /// Adds the param. + /// </summary> + /// <param name="cmd">The CMD.</param> + /// <param name="param">The param.</param> + /// <returns>SQLiteParameter.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + 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; + } + + /// <summary> + /// Adds the param. + /// </summary> + /// <param name="cmd">The CMD.</param> + /// <param name="param">The param.</param> + /// <param name="data">The data.</param> + /// <returns>SQLiteParameter.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + 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; + } + + /// <summary> + /// Determines whether the specified conn is open. + /// </summary> + /// <param name="conn">The conn.</param> + /// <returns><c>true</c> if the specified conn is open; otherwise, <c>false</c>.</returns> + 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 000000000..a9cd3d1eb --- /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 +{ + /// <summary> + /// Class SQLiteItemRepository + /// </summary> + public class SqliteItemRepository : SqliteRepository, IItemRepository + { + /// <summary> + /// The repository name + /// </summary> + public const string RepositoryName = "SQLite"; + + /// <summary> + /// Gets the name of the repository + /// </summary> + /// <value>The name.</value> + public string Name + { + get + { + return RepositoryName; + } + } + + /// <summary> + /// Gets the json serializer. + /// </summary> + /// <value>The json serializer.</value> + private readonly IJsonSerializer _jsonSerializer; + + /// <summary> + /// The _app paths + /// </summary> + private readonly IApplicationPaths _appPaths; + + /// <summary> + /// The _save item command + /// </summary> + private SQLiteCommand _saveItemCommand; + + private readonly string _criticReviewsPath; + + /// <summary> + /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <param name="jsonSerializer">The json serializer.</param> + /// <param name="logManager">The log manager.</param> + /// <exception cref="System.ArgumentNullException"> + /// appPaths + /// or + /// jsonSerializer + /// </exception> + 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"); + } + + /// <summary> + /// Opens the connection to the database + /// </summary> + /// <returns>Task.</returns> + 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(); + } + + /// <summary> + /// The _write lock + /// </summary> + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// <summary> + /// Prepares the statements. + /// </summary> + 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")); + } + + /// <summary> + /// Save a standard item in the repo + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">item</exception> + public Task SaveItem(BaseItem item, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + return SaveItems(new[] { item }, cancellationToken); + } + + /// <summary> + /// Saves the items. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"> + /// items + /// or + /// cancellationToken + /// </exception> + public async Task SaveItems(IEnumerable<BaseItem> 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(); + } + } + + /// <summary> + /// Internal retrieve from items or users table + /// </summary> + /// <param name="id">The id.</param> + /// <param name="type">The type.</param> + /// <returns>BaseItem.</returns> + /// <exception cref="System.ArgumentNullException">id</exception> + /// <exception cref="System.ArgumentException"></exception> + 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; + } + } + + /// <summary> + /// Gets the critic reviews. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <returns>Task{IEnumerable{ItemReview}}.</returns> + public Task<IEnumerable<ItemReview>> GetCriticReviews(Guid itemId) + { + return Task.Run<IEnumerable<ItemReview>>(() => + { + + try + { + var path = Path.Combine(_criticReviewsPath, itemId + ".json"); + + return _jsonSerializer.DeserializeFromFile<List<ItemReview>>(path); + } + catch (DirectoryNotFoundException) + { + return new List<ItemReview>(); + } + catch (FileNotFoundException) + { + return new List<ItemReview>(); + } + + }); + } + + /// <summary> + /// Saves the critic reviews. + /// </summary> + /// <param name="itemId">The item id.</param> + /// <param name="criticReviews">The critic reviews.</param> + /// <returns>Task.</returns> + public Task SaveCriticReviews(Guid itemId, IEnumerable<ItemReview> 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 000000000..cfdc9b5fb --- /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 +{ + /// <summary> + /// Class SqliteRepository + /// </summary> + public abstract class SqliteRepository : IDisposable + { + /// <summary> + /// The db file name + /// </summary> + protected string DbFileName; + /// <summary> + /// The connection + /// </summary> + protected SQLiteConnection Connection; + + /// <summary> + /// Gets the logger. + /// </summary> + /// <value>The logger.</value> + protected ILogger Logger { get; private set; } + + /// <summary> + /// Initializes a new instance of the <see cref="SqliteRepository" /> class. + /// </summary> + /// <param name="logManager">The log manager.</param> + /// <exception cref="System.ArgumentNullException">logger</exception> + protected SqliteRepository(ILogManager logManager) + { + if (logManager == null) + { + throw new ArgumentNullException("logManager"); + } + + Logger = logManager.GetLogger(GetType().Name); + } + + /// <summary> + /// Connects to DB. + /// </summary> + /// <param name="dbPath">The db path.</param> + /// <returns>Task{System.Boolean}.</returns> + /// <exception cref="System.ArgumentNullException">dbPath</exception> + 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(); + } + + /// <summary> + /// Runs the queries. + /// </summary> + /// <param name="queries">The queries.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <exception cref="System.ArgumentNullException">queries</exception> + 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; + } + } + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + 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); + } + } + } + + /// <summary> + /// Gets a stream from a DataReader at a given ordinal + /// </summary> + /// <param name="reader">The reader.</param> + /// <param name="ordinal">The ordinal.</param> + /// <returns>Stream.</returns> + /// <exception cref="System.ArgumentNullException">reader</exception> + 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 index 2f1129beb..05829e007 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<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>(); - - private SemaphoreSlim GetLock(string filename) - { - return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>(); + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// <summary> + /// The repository name + /// </summary> + public const string RepositoryName = "SQLite"; + /// <summary> /// Gets the name of the repository /// </summary> @@ -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; + /// <summary> + /// The _app paths + /// </summary> + private readonly IApplicationPaths _appPaths; /// <summary> - /// Initializes a new instance of the <see cref="JsonUserDataRepository" /> class. + /// Initializes a new instance of the <see cref="SqliteUserDataRepository"/> class. /// </summary> /// <param name="appPaths">The app paths.</param> /// <param name="jsonSerializer">The json serializer.</param> @@ -53,7 +54,8 @@ namespace MediaBrowser.Server.Implementations.Persistence /// or /// appPaths /// </exception> - 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; } /// <summary> /// Opens the connection to the database /// </summary> /// <returns>Task.</returns> - 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); } /// <summary> @@ -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 /// <returns>Task{UserItemData}.</returns> private UserItemData RetrieveUserData(Guid userId, string key) { - var path = GetUserDataPath(userId, key); - - try - { - return _jsonSerializer.DeserializeFromFile<UserItemData>(path); - } - catch (IOException) + using (var cmd = Connection.CreateCommand()) { - // File doesn't exist or is currently bring written to - return new UserItemData { UserId = userId }; + 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<UserItemData>(stream); + } + } + } + + return new UserItemData(); } } - - 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 000000000..efd39529a --- /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 +{ + /// <summary> + /// Class SQLiteUserRepository + /// </summary> + public class SqliteUserRepository : SqliteRepository, IUserRepository + { + /// <summary> + /// The repository name + /// </summary> + public const string RepositoryName = "SQLite"; + + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + + /// <summary> + /// Gets the name of the repository + /// </summary> + /// <value>The name.</value> + public string Name + { + get + { + return RepositoryName; + } + } + + /// <summary> + /// Gets the json serializer. + /// </summary> + /// <value>The json serializer.</value> + private readonly IJsonSerializer _jsonSerializer; + + /// <summary> + /// The _app paths + /// </summary> + private readonly IApplicationPaths _appPaths; + + /// <summary> + /// Initializes a new instance of the <see cref="SqliteUserRepository" /> class. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <param name="jsonSerializer">The json serializer.</param> + /// <param name="logManager">The log manager.</param> + /// <exception cref="System.ArgumentNullException">appPaths</exception> + 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; + } + + /// <summary> + /// Opens the connection to the database + /// </summary> + /// <returns>Task.</returns> + 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); + } + + /// <summary> + /// Save a user in the repo + /// </summary> + /// <param name="user">The user.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + 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(); + } + } + + /// <summary> + /// Retrieve all users from the database + /// </summary> + /// <returns>IEnumerable{User}.</returns> + public IEnumerable<User> 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<User>(stream); + yield return user; + } + } + } + } + } + + /// <summary> + /// Deletes the user. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">user</exception> + 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 f13933eb6..12b6ef650 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -14,4 +14,5 @@ <package id="ServiceStack.Redis" version="3.9.43" targetFramework="net45" /> <package id="ServiceStack.Text" version="3.9.45" targetFramework="net45" /> <package id="SharpZipLib" version="0.86.0" targetFramework="net45" /> + <package id="System.Data.SQLite.x86" version="1.0.86.0" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index da871cc12..24093a181 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); |
