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 MediaBrowser.Server.Implementations.Reflection; 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.Sqlite { /// /// Class SQLiteItemRepository /// public class SQLiteItemRepository : SqliteRepository, IItemRepository { /// /// The _type mapper /// private readonly TypeMapper _typeMapper = new TypeMapper(); /// /// 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; /// /// The _delete children command /// private SQLiteCommand _deleteChildrenCommand; /// /// The _save children command /// private SQLiteCommand _saveChildrenCommand; /// /// Initializes a new instance of the class. /// /// The app paths. /// The json serializer. /// The log manager. /// appPaths 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; } /// /// 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 items (guid GUID primary key, obj_type, data BLOB)", "create index if not exists idx_items on items(guid)", "create table if not exists children (guid GUID, child GUID)", "create unique index if not exists idx_children on children(guid, child)", "create table if not exists schema_version (table_name primary key, version)", //triggers TriggerSql, //pragmas "pragma temp_store = memory" }; RunQueries(queries); PrepareStatements(); } //cascade delete triggers /// /// The trigger SQL /// protected string TriggerSql = @"CREATE TRIGGER if not exists delete_item AFTER DELETE ON items FOR EACH ROW BEGIN DELETE FROM children WHERE children.guid = old.child; DELETE FROM children WHERE children.child = old.child; END"; /// /// The _write lock /// private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); /// /// Prepares the statements. /// private void PrepareStatements() { _saveItemCommand = new SQLiteCommand { CommandText = "replace into items (guid, obj_type, data) values (@1, @2, @3)" }; _saveItemCommand.Parameters.Add(new SQLiteParameter("@1")); _saveItemCommand.Parameters.Add(new SQLiteParameter("@2")); _saveItemCommand.Parameters.Add(new SQLiteParameter("@3")); _deleteChildrenCommand = new SQLiteCommand { CommandText = "delete from children where guid = @guid" }; _deleteChildrenCommand.Parameters.Add(new SQLiteParameter("@guid")); _saveChildrenCommand = new SQLiteCommand { CommandText = "replace into children (guid, child) values (@guid, @child)" }; _saveChildrenCommand.Parameters.Add(new SQLiteParameter("@guid")); _saveChildrenCommand.Parameters.Add(new SQLiteParameter("@child")); } /// /// 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 = item.GetType().FullName; _saveItemCommand.Parameters[2].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(); } } /// /// Retrieve a standard item from the repo /// /// The id. /// BaseItem. /// id /// public BaseItem GetItem(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } return RetrieveItemInternal(id); } /// /// Retrieves the items. /// /// The ids. /// IEnumerable{BaseItem}. /// ids public IEnumerable GetItems(IEnumerable ids) { if (ids == null) { throw new ArgumentNullException("ids"); } return ids.Select(RetrieveItemInternal); } /// /// Internal retrieve from items or users table /// /// The id. /// BaseItem. /// id /// protected BaseItem RetrieveItemInternal(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } using (var cmd = Connection.CreateCommand()) { cmd.CommandText = "select obj_type,data from items 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()) { var type = reader.GetString(0); using (var stream = GetStream(reader, 1)) { var itemType = _typeMapper.GetType(type); if (itemType == null) { Logger.Error("Cannot find type {0}. Probably belongs to plug-in that is no longer loaded.", type); return null; } var item = _jsonSerializer.DeserializeFromStream(stream, itemType); return item as BaseItem; } } } return null; } } /// /// Retrieve all the children of the given folder /// /// The parent. /// IEnumerable{BaseItem}. /// public IEnumerable RetrieveChildren(Folder parent) { if (parent == null) { throw new ArgumentNullException(); } using (var cmd = Connection.CreateCommand()) { cmd.CommandText = "select obj_type,data from items where guid in (select child from children where guid = @guid)"; var guidParam = cmd.Parameters.Add("@guid", DbType.Guid); guidParam.Value = parent.Id; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { var type = reader.GetString(0); using (var stream = GetStream(reader, 1)) { var itemType = _typeMapper.GetType(type); if (itemType == null) { Logger.Error("Cannot find type {0}. Probably belongs to plug-in that is no longer loaded.", type); continue; } var item = _jsonSerializer.DeserializeFromStream(stream, itemType) as BaseItem; if (item != null) { item.Parent = parent; yield return item; } } } } } } /// /// Save references to all the children for the given folder /// (Doesn't actually save the child entities) /// /// The id. /// The children. /// The cancellation token. /// Task. /// id public async Task SaveChildren(Guid id, IEnumerable children, CancellationToken cancellationToken) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } if (children == null) { throw new ArgumentNullException("children"); } if (cancellationToken == null) { throw new ArgumentNullException("cancellationToken"); } cancellationToken.ThrowIfCancellationRequested(); await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); SQLiteTransaction transaction = null; try { transaction = Connection.BeginTransaction(); // Delete exising children _deleteChildrenCommand.Parameters[0].Value = id; _deleteChildrenCommand.Transaction = transaction; await _deleteChildrenCommand.ExecuteNonQueryAsync(cancellationToken); // Save new children foreach (var child in children) { _saveChildrenCommand.Transaction = transaction; _saveChildrenCommand.Parameters[0].Value = id; _saveChildrenCommand.Parameters[1].Value = child.Id; await _saveChildrenCommand.ExecuteNonQueryAsync(cancellationToken); } transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { Logger.ErrorException("Failed to save children:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } _writeLock.Release(); } } /// /// Gets the critic reviews path. /// /// The critic reviews path. private string CriticReviewsPath { get { var path = Path.Combine(_appPaths.DataPath, "critic-reviews"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } return path; } } /// /// 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 (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(() => { var path = Path.Combine(CriticReviewsPath, itemId + ".json"); _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); }); } } }