|
@@ -2383,9 +2383,21 @@ namespace InABox.Database.SQLite
|
|
|
//LogReset();
|
|
|
|
|
|
//LogStart();
|
|
|
- var cols = CoreUtils.GetColumns(T, columns);
|
|
|
//LogStop("GetColumns");
|
|
|
|
|
|
+ var cols = CoreUtils.GetColumns(T, columns);
|
|
|
+
|
|
|
+ var blobColumns = Columns.Create(T);
|
|
|
+ foreach(var column in cols.GetColumns())
|
|
|
+ {
|
|
|
+ var prop = DatabaseSchema.Property(T, column.Property);
|
|
|
+ if (prop is not null && prop.HasAttribute<ExternalStorageAttribute>())
|
|
|
+ {
|
|
|
+ blobColumns.Add(column);
|
|
|
+ cols.Add("ID");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
var result = new CoreTable(T.EntityName());
|
|
|
foreach (var col in cols.GetColumns())
|
|
|
result.Columns.Add(new CoreColumn { ColumnName = col.Property, DataType = col.Type });
|
|
@@ -2508,6 +2520,19 @@ namespace InABox.Database.SQLite
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ foreach(var column in blobColumns.ColumnNames())
|
|
|
+ {
|
|
|
+ foreach(var row in result.Rows)
|
|
|
+ {
|
|
|
+ var id = row.Get<Guid>("ID");
|
|
|
+ var data = GetExternalData(T, column, id);
|
|
|
+ if(data is not null)
|
|
|
+ {
|
|
|
+ row.Set<byte[]>(column, data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (log)
|
|
|
{
|
|
|
var duration = DateTime.Now - start;
|
|
@@ -2609,38 +2634,110 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
#region Save
|
|
|
|
|
|
+ private static readonly Dictionary<Type, IProperty[]> _externalProperties = new Dictionary<Type, IProperty[]>();
|
|
|
+ private static IProperty[] GetExternalProperties(Type T)
|
|
|
+ {
|
|
|
+ if(!_externalProperties.TryGetValue(T, out var properties))
|
|
|
+ {
|
|
|
+ properties = DatabaseSchema.Properties(T).Where(x => x.HasAttribute<ExternalStorageAttribute>()).ToArray();
|
|
|
+ _externalProperties.Add(T, properties);
|
|
|
+ }
|
|
|
+ return properties;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnSaveNonGeneric(Type T, Entity entity)
|
|
|
+ {
|
|
|
+ var props = GetExternalProperties(T);
|
|
|
+ List<(IProperty, byte[])>? data = null;
|
|
|
+ if (props.Any())
|
|
|
+ {
|
|
|
+ data = new List<(IProperty, byte[])>();
|
|
|
+ foreach(var prop in props)
|
|
|
+ {
|
|
|
+ var value = prop.Getter()(entity) as byte[];
|
|
|
+ if(value is not null && value.Length > 0)
|
|
|
+ {
|
|
|
+ data.Add(new(prop, value));
|
|
|
+ prop.Setter()(entity, Array.Empty<byte>());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ using var access = GetWriteAccess();
|
|
|
+ using var command = access.CreateCommand();
|
|
|
+
|
|
|
+ PrepareUpsertNonGeneric(T, command, entity);
|
|
|
+ command.ExecuteNonQuery();
|
|
|
+
|
|
|
+ if(data is not null)
|
|
|
+ {
|
|
|
+ foreach(var (prop, value) in data)
|
|
|
+ {
|
|
|
+ SaveExternalData(T, prop.Name, entity.ID, value);
|
|
|
+ prop.Setter()(entity, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private void OnSaveNonGeneric(Type T, IEnumerable<Entity> entities)
|
|
|
{
|
|
|
+ // Casting to IList so that we can use it multiple times.
|
|
|
+ entities = entities.AsIList();
|
|
|
+
|
|
|
if (!entities.Any())
|
|
|
return;
|
|
|
|
|
|
- using (var access = GetWriteAccess())
|
|
|
+ var props = GetExternalProperties(T);
|
|
|
+ List<(IProperty, List<(Entity, byte[])>)>? data = null;
|
|
|
+ if (props.Any())
|
|
|
{
|
|
|
- Exception? error = null;
|
|
|
- using (var transaction = access.BeginTransaction())
|
|
|
+ data = new List<(IProperty, List<(Entity, byte[])>)>();
|
|
|
+ foreach(var prop in props)
|
|
|
{
|
|
|
- try
|
|
|
+ var lst = new List<(Entity, byte[])>();
|
|
|
+ foreach(var entity in entities)
|
|
|
{
|
|
|
- using (var command = access.CreateCommand())
|
|
|
+ var value = prop.Getter()(entity) as byte[];
|
|
|
+ if(value is not null && value.Length > 0)
|
|
|
{
|
|
|
- foreach (var entity in entities)
|
|
|
- {
|
|
|
- PrepareUpsertNonGeneric(T, command, entity);
|
|
|
- command.ExecuteNonQuery();
|
|
|
- }
|
|
|
-
|
|
|
- transaction.Commit();
|
|
|
+ lst.Add((entity, value));
|
|
|
+ prop.Setter()(entity, Array.Empty<byte>());
|
|
|
}
|
|
|
}
|
|
|
- catch (Exception e)
|
|
|
+ data.Add(new(prop, lst));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ using var access = GetWriteAccess();
|
|
|
+ using var transaction = access.BeginTransaction();
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var command = access.CreateCommand();
|
|
|
+ foreach (var entity in entities)
|
|
|
+ {
|
|
|
+ PrepareUpsertNonGeneric(T, command, entity);
|
|
|
+ command.ExecuteNonQuery();
|
|
|
+ }
|
|
|
+
|
|
|
+ transaction.Commit();
|
|
|
+
|
|
|
+ if(data is not null)
|
|
|
+ {
|
|
|
+ foreach(var (property, list) in data)
|
|
|
{
|
|
|
- error = e;
|
|
|
- transaction.Rollback();
|
|
|
+ foreach(var (entity, value) in list)
|
|
|
+ {
|
|
|
+ SaveExternalData(T, property.Name, entity.ID, value);
|
|
|
+ property.Setter()(entity, value);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- if (error != null)
|
|
|
- throw error;
|
|
|
+ }
|
|
|
+ catch (Exception)
|
|
|
+ {
|
|
|
+ transaction.Rollback();
|
|
|
+ throw;
|
|
|
}
|
|
|
}
|
|
|
private void OnSave<T>(IEnumerable<T> entities) where T : Entity
|
|
@@ -2673,28 +2770,6 @@ namespace InABox.Database.SQLite
|
|
|
}
|
|
|
OnSave(entities);
|
|
|
}
|
|
|
- private void OnSaveNonGeneric(Type T, Entity entity)
|
|
|
- {
|
|
|
- Exception? error = null;
|
|
|
- using (var access = GetWriteAccess())
|
|
|
- {
|
|
|
- using (var command = access.CreateCommand())
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- PrepareUpsertNonGeneric(T, command, entity);
|
|
|
- command.ExecuteNonQuery();
|
|
|
- }
|
|
|
- catch (Exception e)
|
|
|
- {
|
|
|
- error = e;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (error != null)
|
|
|
- throw error;
|
|
|
- }
|
|
|
|
|
|
private void OnSave<T>(T entity) where T : Entity
|
|
|
=> OnSaveNonGeneric(typeof(T), entity);
|
|
@@ -2714,49 +2789,40 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
public void Purge<T>(T entity) where T : Entity
|
|
|
{
|
|
|
- using (var access = GetWriteAccess())
|
|
|
- {
|
|
|
- using (var command = access.CreateCommand())
|
|
|
- {
|
|
|
- PrepareDelete(command, entity);
|
|
|
- var rows = command.ExecuteNonQuery();
|
|
|
- }
|
|
|
- }
|
|
|
+ using var access = GetWriteAccess();
|
|
|
+ using var command = access.CreateCommand();
|
|
|
+ PrepareDelete(command, entity);
|
|
|
+ var rows = command.ExecuteNonQuery();
|
|
|
}
|
|
|
|
|
|
public void Purge<T>(IEnumerable<T> entities) where T : Entity
|
|
|
{
|
|
|
+ // Casting to IList so that we can use it multiple times.
|
|
|
+ entities = entities.AsIList();
|
|
|
+
|
|
|
if (!entities.Any())
|
|
|
return;
|
|
|
|
|
|
- Exception? error = null;
|
|
|
- using (var access = GetWriteAccess())
|
|
|
+ using var access = GetWriteAccess();
|
|
|
+ using var transaction = access.BeginTransaction();
|
|
|
+ try
|
|
|
{
|
|
|
- using (var transaction = access.BeginTransaction())
|
|
|
+ using (var command = access.CreateCommand())
|
|
|
{
|
|
|
- try
|
|
|
+ foreach (var entity in entities)
|
|
|
{
|
|
|
- using (var command = access.CreateCommand())
|
|
|
- {
|
|
|
- foreach (var entity in entities)
|
|
|
- {
|
|
|
- PrepareDelete(command, entity);
|
|
|
- var rows = command.ExecuteNonQuery();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- transaction.Commit();
|
|
|
- }
|
|
|
- catch (Exception e)
|
|
|
- {
|
|
|
- transaction.Rollback();
|
|
|
- error = e;
|
|
|
+ PrepareDelete(command, entity);
|
|
|
+ var rows = command.ExecuteNonQuery();
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if (error != null)
|
|
|
- throw error;
|
|
|
+ transaction.Commit();
|
|
|
+ }
|
|
|
+ catch (Exception)
|
|
|
+ {
|
|
|
+ transaction.Rollback();
|
|
|
+ throw;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private Dictionary<Type, List<Tuple<Type, string>>> _cascades = new();
|
|
@@ -2898,30 +2964,43 @@ namespace InABox.Database.SQLite
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
- entity = DoQuery(
|
|
|
- new Filter<T>(x => x.ID).IsEqualTo(entity.ID),
|
|
|
- DeletionData.DeletionColumns<T>(),
|
|
|
- null,
|
|
|
- int.MaxValue,
|
|
|
- false,
|
|
|
- false
|
|
|
- ).Rows.First().ToObject<T>();
|
|
|
-
|
|
|
- var deletionData = new DeletionData();
|
|
|
- deletionData.DeleteEntity(entity);
|
|
|
- CascadeDelete(typeof(T), new Guid[] { entity.ID }, deletionData);
|
|
|
-
|
|
|
- var tableName = typeof(T).Name;
|
|
|
- var deletion = new Deletion()
|
|
|
- {
|
|
|
- DeletionDate = DateTime.Now,
|
|
|
- HeadTable = tableName,
|
|
|
- Description = entity.ToString() ?? "",
|
|
|
- DeletedBy = userID,
|
|
|
- Data = Serialization.Serialize(deletionData)
|
|
|
- };
|
|
|
- OnSave(deletion);
|
|
|
- Purge(entity);
|
|
|
+ if (typeof(T).HasAttribute<UnrecoverableAttribute>())
|
|
|
+ {
|
|
|
+ Purge(entity);
|
|
|
+
|
|
|
+ var props = GetExternalProperties(typeof(T));
|
|
|
+ foreach(var prop in props)
|
|
|
+ {
|
|
|
+ DeleteExternalData(typeof(T), prop.Name, entity.ID);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ entity = DoQuery(
|
|
|
+ new Filter<T>(x => x.ID).IsEqualTo(entity.ID),
|
|
|
+ DeletionData.DeletionColumns<T>(),
|
|
|
+ null,
|
|
|
+ int.MaxValue,
|
|
|
+ false,
|
|
|
+ false
|
|
|
+ ).Rows.First().ToObject<T>();
|
|
|
+
|
|
|
+ var deletionData = new DeletionData();
|
|
|
+ deletionData.DeleteEntity(entity);
|
|
|
+ CascadeDelete(typeof(T), new Guid[] { entity.ID }, deletionData);
|
|
|
+
|
|
|
+ var tableName = typeof(T).Name;
|
|
|
+ var deletion = new Deletion()
|
|
|
+ {
|
|
|
+ DeletionDate = DateTime.Now,
|
|
|
+ HeadTable = tableName,
|
|
|
+ Description = entity.ToString() ?? "",
|
|
|
+ DeletedBy = userID,
|
|
|
+ Data = Serialization.Serialize(deletionData)
|
|
|
+ };
|
|
|
+ OnSave(deletion);
|
|
|
+ Purge(entity);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
public void Delete<T>(IEnumerable<T> entities, string userID) where T : Entity, new()
|
|
@@ -2930,33 +3009,51 @@ namespace InABox.Database.SQLite
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
+ entities = entities.AsIList();
|
|
|
if (!entities.Any())
|
|
|
return;
|
|
|
- var ids = entities.Select(x => x.ID).ToArray();
|
|
|
- var entityList = Query(
|
|
|
- new Filter<T>(x => x.ID).InList(ids),
|
|
|
- DeletionData.DeletionColumns<T>()).Rows.Select(x => x.ToObject<T>()).ToList();
|
|
|
- if (!entityList.Any())
|
|
|
- return;
|
|
|
|
|
|
- var deletionData = new DeletionData();
|
|
|
- foreach (var entity in entityList)
|
|
|
+ if (typeof(T).HasAttribute<UnrecoverableAttribute>())
|
|
|
{
|
|
|
- deletionData.DeleteEntity(entity);
|
|
|
- }
|
|
|
- CascadeDelete(typeof(T), ids, deletionData);
|
|
|
+ Purge(entities);
|
|
|
|
|
|
- var tableName = typeof(T).Name;
|
|
|
- var deletion = new Deletion()
|
|
|
+ var props = GetExternalProperties(typeof(T));
|
|
|
+ foreach(var prop in props)
|
|
|
+ {
|
|
|
+ foreach(var entity in entities)
|
|
|
+ {
|
|
|
+ DeleteExternalData(typeof(T), prop.Name, entity.ID);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
{
|
|
|
- DeletionDate = DateTime.Now,
|
|
|
- HeadTable = tableName,
|
|
|
- Description = $"Deleted {entityList.Count} entries",
|
|
|
- DeletedBy = userID,
|
|
|
- Data = Serialization.Serialize(deletionData)
|
|
|
- };
|
|
|
- OnSave(deletion);
|
|
|
- Purge(entities);
|
|
|
+ var ids = entities.Select(x => x.ID).ToArray();
|
|
|
+ var entityList = Query(
|
|
|
+ new Filter<T>(x => x.ID).InList(ids),
|
|
|
+ DeletionData.DeletionColumns<T>()).Rows.Select(x => x.ToObject<T>()).ToList();
|
|
|
+ if (!entityList.Any())
|
|
|
+ return;
|
|
|
+
|
|
|
+ var deletionData = new DeletionData();
|
|
|
+ foreach (var entity in entityList)
|
|
|
+ {
|
|
|
+ deletionData.DeleteEntity(entity);
|
|
|
+ }
|
|
|
+ CascadeDelete(typeof(T), ids, deletionData);
|
|
|
+
|
|
|
+ var tableName = typeof(T).Name;
|
|
|
+ var deletion = new Deletion()
|
|
|
+ {
|
|
|
+ DeletionDate = DateTime.Now,
|
|
|
+ HeadTable = tableName,
|
|
|
+ Description = $"Deleted {entityList.Count} entries",
|
|
|
+ DeletedBy = userID,
|
|
|
+ Data = Serialization.Serialize(deletionData)
|
|
|
+ };
|
|
|
+ OnSave(deletion);
|
|
|
+ Purge(entities);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private void AddDeletionType(Type type, List<Type> deletions)
|
|
@@ -2985,6 +3082,24 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
public void Purge(Deletion deletion)
|
|
|
{
|
|
|
+ var data = Serialization.Deserialize<DeletionData>(deletion.Data);
|
|
|
+ if(data is not null)
|
|
|
+ {
|
|
|
+ foreach(var (entityName, cascade) in data.Cascades)
|
|
|
+ {
|
|
|
+ if (!CoreUtils.TryGetEntity(entityName, out var entityType)) continue;
|
|
|
+
|
|
|
+ var props = GetExternalProperties(entityType);
|
|
|
+ foreach(var prop in props)
|
|
|
+ {
|
|
|
+ foreach(var entity in cascade.ToObjects(entityType).Cast<Entity>())
|
|
|
+ {
|
|
|
+ DeleteExternalData(entityType, prop.Name, entity.ID);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
Purge<Deletion>(deletion);
|
|
|
}
|
|
|
|
|
@@ -3050,5 +3165,52 @@ namespace InABox.Database.SQLite
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
+ #region External Data Storage
|
|
|
+
|
|
|
+ private string ExternalDataFolder(Type T, string columnName, string idString)
|
|
|
+ {
|
|
|
+ return Path.Combine(
|
|
|
+ Path.GetDirectoryName(URL) ?? "",
|
|
|
+ $"{Path.GetFileName(URL)}.data",
|
|
|
+ T.Name,
|
|
|
+ columnName,
|
|
|
+ idString.Substring(0, 2));
|
|
|
+ }
|
|
|
+
|
|
|
+ private byte[]? GetExternalData(Type T, string columnName, Guid id)
|
|
|
+ {
|
|
|
+ var idString = id.ToString();
|
|
|
+ var filename = Path.Combine(ExternalDataFolder(T, columnName, idString), idString);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return File.ReadAllBytes(filename);
|
|
|
+ }
|
|
|
+ catch(Exception e)
|
|
|
+ {
|
|
|
+ Logger.Send(LogType.Error, "", $"Could not load external {T.Name}.{columnName}: {e.Message}");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private void SaveExternalData(Type T, string columnName, Guid id, byte[] data)
|
|
|
+ {
|
|
|
+ var idString = id.ToString();
|
|
|
+ var directory = ExternalDataFolder(T, columnName, idString);
|
|
|
+ Directory.CreateDirectory(directory);
|
|
|
+ var filename = Path.Combine(directory, idString);
|
|
|
+ File.WriteAllBytes(filename, data);
|
|
|
+ }
|
|
|
+ private void DeleteExternalData(Type T, string columnName, Guid id)
|
|
|
+ {
|
|
|
+ var idString = id.ToString();
|
|
|
+ var directory = ExternalDataFolder(T, columnName, idString);
|
|
|
+ Directory.CreateDirectory(directory);
|
|
|
+ var filename = Path.Combine(directory, idString);
|
|
|
+ if (File.Exists(filename))
|
|
|
+ {
|
|
|
+ File.Delete(filename);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
}
|
|
|
}
|