Files
duplicati/Duplicati/Library/Main/Database/LocalDatabase.cs
T

1375 lines
63 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace Duplicati.Library.Main.Database
{
internal class LocalDatabase : IDisposable
{
protected readonly System.Data.IDbConnection m_connection;
protected readonly long m_operationid = -1;
private readonly System.Data.IDbCommand m_updateremotevolumeCommand;
private readonly System.Data.IDbCommand m_selectremotevolumesCommand;
private readonly System.Data.IDbCommand m_selectremotevolumeCommand;
private readonly System.Data.IDbCommand m_removeremotevolumeCommand;
private readonly System.Data.IDbCommand m_selectremotevolumeIdCommand;
private readonly System.Data.IDbCommand m_createremotevolumeCommand;
private readonly System.Data.IDbCommand m_selectduplicateRemoteVolumesCommand;
private readonly System.Data.IDbCommand m_insertlogCommand;
2013-03-08 22:24:54 +01:00
private readonly System.Data.IDbCommand m_insertremotelogCommand;
private readonly System.Data.IDbCommand m_insertIndexBlockLink;
protected BasicResults m_result;
public const long FOLDER_BLOCKSET_ID = -100;
public const long SYMLINK_BLOCKSET_ID = -200;
2013-03-08 22:24:54 +01:00
public DateTime OperationTimestamp { get; private set; }
internal System.Data.IDbConnection Connection { get { return m_connection; } }
public bool IsDisposed { get; private set; }
2013-03-08 22:24:54 +01:00
public bool ShouldCloseConnection { get; set; }
2016-01-28 22:24:51 +01:00
private System.Data.IDbTransaction m_transaction;
private LocalDatabase m_parent;
internal System.Data.IDbTransaction Transaction { get { return m_parent?.Transaction ?? m_transaction; } }
2013-03-08 22:24:54 +01:00
protected static System.Data.IDbConnection CreateConnection(string path)
{
path = System.IO.Path.GetFullPath(path);
2013-03-08 22:24:54 +01:00
if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(path)))
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path));
var c = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(path);
2013-03-08 22:24:54 +01:00
Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(c, path, typeof(LocalDatabase));
return c;
}
/// <summary>
/// Creates a new database instance and starts a new operation
/// </summary>
/// <param name="path">The path to the database</param>
2013-03-08 22:24:54 +01:00
/// <param name="operation">The name of the operation</param>
public LocalDatabase(string path, string operation, bool shouldclose)
2013-03-08 22:24:54 +01:00
: this(CreateConnection(path), operation)
{
ShouldCloseConnection = shouldclose;
this.m_transaction = m_connection.BeginTransaction();
}
/// <summary>
/// Creates a new database instance and starts a new operation
/// </summary>
public LocalDatabase(LocalDatabase db)
: this(db.m_connection)
{
this.OperationTimestamp = db.OperationTimestamp;
this.m_connection = db.m_connection;
this.m_operationid = db.m_operationid;
this.m_result = db.m_result;
this.m_parent = db;
}
/// <summary>
/// Creates a new database instance and starts a new operation
/// </summary>
2013-03-08 22:24:54 +01:00
/// <param name="operation">The name of the operation</param>
private LocalDatabase(System.Data.IDbConnection connection, string operation)
: this(connection)
{
2013-03-08 22:24:54 +01:00
this.OperationTimestamp = DateTime.UtcNow;
m_connection = connection;
if (m_connection.State != System.Data.ConnectionState.Open)
m_connection.Open();
using (var cmd = m_connection.CreateCommand())
m_operationid = cmd.ExecuteScalarInt64( @"INSERT INTO ""Operation"" (""Description"", ""Timestamp"") VALUES (?, ?); SELECT last_insert_rowid();", -1, operation, NormalizeDateTimeToEpochSeconds(OperationTimestamp));
}
private LocalDatabase(System.Data.IDbConnection connection)
{
m_updateremotevolumeCommand = connection.CreateCommand();
m_selectremotevolumesCommand = connection.CreateCommand();
m_selectduplicateRemoteVolumesCommand = connection.CreateCommand();
m_selectremotevolumeCommand = connection.CreateCommand();
m_insertlogCommand = connection.CreateCommand();
m_insertremotelogCommand = connection.CreateCommand();
m_removeremotevolumeCommand = connection.CreateCommand();
m_selectremotevolumeIdCommand = connection.CreateCommand();
m_createremotevolumeCommand = connection.CreateCommand();
m_insertIndexBlockLink = connection.CreateCommand();
m_insertlogCommand.CommandText = @"INSERT INTO ""LogData"" (""OperationID"", ""Timestamp"", ""Type"", ""Message"", ""Exception"") VALUES (?, ?, ?, ?, ?)";
m_insertlogCommand.AddParameters(5);
m_insertremotelogCommand.CommandText = @"INSERT INTO ""RemoteOperation"" (""OperationID"", ""Timestamp"", ""Operation"", ""Path"", ""Data"") VALUES (?, ?, ?, ?, ?)";
m_insertremotelogCommand.AddParameters(5);
m_updateremotevolumeCommand.CommandText = @"UPDATE ""Remotevolume"" SET ""OperationID"" = ?, ""State"" = ?, ""Hash"" = ?, ""Size"" = ? WHERE ""Name"" = ?";
m_updateremotevolumeCommand.AddParameters(5);
m_selectremotevolumesCommand.CommandText = @"SELECT ""ID"", ""Name"", ""Type"", ""Size"", ""Hash"", ""State"", ""DeleteGraceTime"" FROM ""Remotevolume""";
m_selectremotevolumeCommand.CommandText = m_selectremotevolumesCommand.CommandText + @" WHERE ""Name"" = ?";
m_selectduplicateRemoteVolumesCommand.CommandText = string.Format(@"SELECT DISTINCT ""Name"", ""State"" FROM ""Remotevolume"" WHERE ""Name"" IN (SELECT ""Name"" FROM ""Remotevolume"" WHERE ""State"" IN (""{0}"", ""{1}"")) AND NOT ""State"" IN (""{0}"", ""{1}"")", RemoteVolumeState.Deleted.ToString(), RemoteVolumeState.Deleting.ToString());
m_selectremotevolumeCommand.AddParameter();
m_removeremotevolumeCommand.CommandText = @"DELETE FROM ""Remotevolume"" WHERE ""Name"" = ? AND (""DeleteGraceTime"" < ? OR ""State"" != ?)";
m_removeremotevolumeCommand.AddParameters(3);
m_selectremotevolumeIdCommand.CommandText = @"SELECT ""ID"" FROM ""Remotevolume"" WHERE ""Name"" = ?";
m_createremotevolumeCommand.CommandText = @"INSERT INTO ""Remotevolume"" (""OperationID"", ""Name"", ""Type"", ""State"", ""Size"", ""VerificationCount"", ""DeleteGraceTime"") VALUES (?, ?, ?, ?, ?, ?, ?); SELECT last_insert_rowid();";
m_createremotevolumeCommand.AddParameters(7);
m_insertIndexBlockLink.CommandText = @"INSERT INTO ""IndexBlockLink"" (""IndexVolumeID"", ""BlockVolumeID"") VALUES (?, ?)";
m_insertIndexBlockLink.AddParameters(2);
}
internal void SetResult(BasicResults result)
{
m_result = result;
}
/// <summary>
/// Normalizes a DateTime instance floor'ed to seconds and in UTC
/// </summary>
/// <returns>The normalised date time</returns>
/// <param name="input">The input time</param>
public static DateTime NormalizeDateTime(DateTime input)
{
var ticks = input.ToUniversalTime().Ticks;
ticks -= ticks % TimeSpan.TicksPerSecond;
return new DateTime(ticks, DateTimeKind.Utc);
}
public static long NormalizeDateTimeToEpochSeconds(DateTime input)
{
return (long)Math.Floor((NormalizeDateTime(input) - Library.Utility.Utility.EPOCH).TotalSeconds);
}
/// <summary>
/// Creates a DateTime instance by adding the specified number of seconds to the EPOCH value
/// </summary>
public static DateTime ParseFromEpochSeconds(long seconds)
{
return Library.Utility.Utility.EPOCH.AddSeconds(seconds);
}
public void UpdateRemoteVolume(string name, RemoteVolumeState state, long size, string hash)
2016-03-24 16:30:19 +01:00
{
UpdateRemoteVolume(name, state, size, hash, false);
2016-03-24 16:30:19 +01:00
}
public void UpdateRemoteVolume(string name, RemoteVolumeState state, long size, string hash, bool suppressCleanup)
2016-03-24 16:30:19 +01:00
{
UpdateRemoteVolume(name, state, size, hash, suppressCleanup, new TimeSpan(0));
2016-03-24 16:30:19 +01:00
}
public void UpdateRemoteVolume(string name, RemoteVolumeState state, long size, string hash, bool suppressCleanup, TimeSpan deleteGraceTime)
{
m_updateremotevolumeCommand.Transaction = Transaction;
m_updateremotevolumeCommand.SetParameterValue(0, m_operationid);
m_updateremotevolumeCommand.SetParameterValue(1, state.ToString());
m_updateremotevolumeCommand.SetParameterValue(2, hash);
m_updateremotevolumeCommand.SetParameterValue(3, size);
m_updateremotevolumeCommand.SetParameterValue(4, name);
var c = m_updateremotevolumeCommand.ExecuteNonQuery();
if (c != 1)
2014-12-30 18:25:30 +01:00
throw new Exception(string.Format("Unexpected number of remote volumes detected: {0}!", c));
2016-03-24 16:30:19 +01:00
if (deleteGraceTime.Ticks > 0)
using(var cmd = m_connection.CreateCommand(Transaction))
2016-03-24 16:30:19 +01:00
if ((c = cmd.ExecuteNonQuery(@"UPDATE ""RemoteVolume"" SET ""DeleteGraceTime"" = ? WHERE ""Name"" = ? ", (DateTime.UtcNow + deleteGraceTime).Ticks, name)) != 1)
throw new Exception(string.Format("Unexpected number of updates when recording remote volume updates: {0}!", c));
2016-03-24 16:30:19 +01:00
if (!suppressCleanup && state == RemoteVolumeState.Deleted)
RemoveRemoteVolume(name);
}
public IEnumerable<KeyValuePair<long, DateTime>> FilesetTimes
{
get
{
using(var cmd = m_connection.CreateCommand(Transaction))
using(var rd = cmd.ExecuteReader(@"SELECT ""ID"", ""Timestamp"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC"))
while (rd.Read())
yield return new KeyValuePair<long, DateTime>(rd.GetInt64(0), ParseFromEpochSeconds(rd.GetInt64(1)).ToLocalTime());
}
}
public Tuple<string, object[]> GetFilelistWhereClause(DateTime time, long[] versions, IEnumerable<KeyValuePair<long, DateTime>> filesetslist = null, bool singleTimeMatch = false)
{
var filesets = (filesetslist ?? this.FilesetTimes).ToArray();
string query = "";
var args = new List<object>();
if (time.Ticks > 0 || (versions != null && versions.Length > 0))
{
2013-06-20 20:17:10 +02:00
var hasTime = false;
if (time.Ticks > 0)
{
if (time.Kind == DateTimeKind.Unspecified)
throw new Exception("Invalid DateTime given, must be either local or UTC");
query += singleTimeMatch ? @" ""Timestamp"" = ?" : @" ""Timestamp"" <= ?";
// Make sure the resolution is the same (i.e. no milliseconds)
args.Add(NormalizeDateTimeToEpochSeconds(time));
2013-06-20 20:17:10 +02:00
hasTime = true;
}
if (versions != null && versions.Length > 0)
{
2014-01-19 23:25:17 +01:00
var qs = "";
foreach(var v in versions)
if (v >= 0 && v < filesets.Length)
{
args.Add(filesets[v].Key);
qs += "?,";
}
else
m_result.AddWarning(string.Format("Skipping invalid version: {0}", v), null);
if (qs.Length > 0)
{
qs = qs.Substring(0, qs.Length - 1);
2013-06-20 20:17:10 +02:00
if (hasTime)
query += " OR ";
query += @" ""ID"" IN (" + qs + ")";
}
}
if (!string.IsNullOrEmpty(query))
query = " WHERE " + query;
}
return new Tuple<string, object[]>(query, args.ToArray());
}
public long GetRemoteVolumeID(string file)
{
m_selectremotevolumeIdCommand.Transaction = Transaction;
return m_selectremotevolumeIdCommand.ExecuteScalarInt64(null, -1, file);
}
public RemoteVolumeEntry GetRemoteVolume(string file)
{
m_selectremotevolumeCommand.Transaction = Transaction;
m_selectremotevolumeCommand.SetParameterValue(0, file);
using(var rd = m_selectremotevolumeCommand.ExecuteReader())
if (rd.Read())
return new RemoteVolumeEntry(
rd.ConvertValueToInt64(0),
rd.GetValue(1).ToString(),
(rd.GetValue(4) == null || rd.GetValue(4) == DBNull.Value) ? null : rd.GetValue(4).ToString(),
rd.ConvertValueToInt64(3, -1),
(RemoteVolumeType)Enum.Parse(typeof(RemoteVolumeType), rd.GetValue(2).ToString()),
(RemoteVolumeState)Enum.Parse(typeof(RemoteVolumeState), rd.GetValue(5).ToString()),
new DateTime(rd.ConvertValueToInt64(6, 0), DateTimeKind.Utc)
);
return RemoteVolumeEntry.Empty;
}
public IEnumerable<KeyValuePair<string, RemoteVolumeState>> DuplicateRemoteVolumes()
{
m_selectduplicateRemoteVolumesCommand.Transaction = Transaction;
foreach(var rd in m_selectduplicateRemoteVolumesCommand.ExecuteReaderEnumerable(null))
{
yield return new KeyValuePair<string, RemoteVolumeState>(
rd.GetValue(0).ToString(),
(RemoteVolumeState)Enum.Parse(typeof(RemoteVolumeState), rd.GetValue(1).ToString())
);
}
}
public IEnumerable<RemoteVolumeEntry> GetRemoteVolumes()
{
m_selectremotevolumesCommand.Transaction = Transaction;
using (var rd = m_selectremotevolumesCommand.ExecuteReader())
{
while (rd.Read())
{
yield return new RemoteVolumeEntry(
rd.ConvertValueToInt64(0),
rd.GetValue(1).ToString(),
(rd.GetValue(4) == null || rd.GetValue(4) == DBNull.Value) ? null : rd.GetValue(4).ToString(),
rd.ConvertValueToInt64(3, -1),
(RemoteVolumeType)Enum.Parse(typeof(RemoteVolumeType), rd.GetValue(2).ToString()),
(RemoteVolumeState)Enum.Parse(typeof(RemoteVolumeState), rd.GetValue(5).ToString()),
new DateTime(rd.ConvertValueToInt64(6, 0), DateTimeKind.Utc)
);
}
}
}
/// <summary>
/// Log an operation performed on the remote backend
/// </summary>
/// <param name="operation">The operation performed</param>
/// <param name="path">The path involved</param>
/// <param name="data">Any data relating to the operation</param>
public void LogRemoteOperation(string operation, string path, string data)
{
m_insertremotelogCommand.Transaction = Transaction;
m_insertremotelogCommand.SetParameterValue(0, m_operationid);
m_insertremotelogCommand.SetParameterValue(1, NormalizeDateTimeToEpochSeconds(DateTime.UtcNow));
m_insertremotelogCommand.SetParameterValue(2, operation);
m_insertremotelogCommand.SetParameterValue(3, path);
m_insertremotelogCommand.SetParameterValue(4, data);
m_insertremotelogCommand.ExecuteNonQuery();
}
/// <summary>
/// Log a debug message
/// </summary>
/// <param name="type">The message type</param>
/// <param name="message">The message</param>
/// <param name="exception">An optional exception</param>
public void LogMessage(string type, string message, Exception exception)
{
m_insertlogCommand.Transaction = Transaction;
m_insertlogCommand.SetParameterValue(0, m_operationid);
m_insertlogCommand.SetParameterValue(1, NormalizeDateTimeToEpochSeconds(DateTime.UtcNow));
m_insertlogCommand.SetParameterValue(2, type);
m_insertlogCommand.SetParameterValue(3, message);
m_insertlogCommand.SetParameterValue(4, exception == null ? null : exception.ToString());
m_insertlogCommand.ExecuteNonQuery();
}
public void UnlinkRemoteVolume(string name, RemoteVolumeState state)
{
using(var cmd = m_connection.CreateCommand())
{
cmd.Transaction = Transaction;
var c = cmd.ExecuteNonQuery(@"DELETE FROM ""RemoteVolume"" WHERE ""Name"" = ? AND ""State"" = ? ", name, state.ToString());
if (c != 1)
throw new Exception(string.Format("Unexpected number of remote volumes deleted: {0}, expected {1}", c, 1));
}
}
public void RemoveRemoteVolume(string name)
{
RemoveRemoteVolumes(new string[] { name });
}
public void RemoveRemoteVolumes(IEnumerable<string> names)
{
if (names == null || !names.Any()) return;
using (var deletecmd = m_connection.CreateCommand())
{
deletecmd.Transaction = Transaction;
string temptransguid = Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
var volidstable = "DelVolSetIds-" + temptransguid;
var blocksetidstable = "DelBlockSetIds-" + temptransguid;
// Create and fill a temp table with the volids to delete. We avoid using too many parameters that way.
deletecmd.ExecuteNonQuery(string.Format(@"CREATE TEMP TABLE ""{0}"" (""ID"" INTEGER PRIMARY KEY)", volidstable));
deletecmd.CommandText = string.Format(@"INSERT OR IGNORE INTO ""{0}"" (""ID"") VALUES (?)", volidstable);
deletecmd.Parameters.Clear();
deletecmd.AddParameters(1);
foreach (var name in names)
{
var volumeid = GetRemoteVolumeID(name);
deletecmd.SetParameterValue(0, volumeid);
deletecmd.ExecuteNonQuery();
}
var volIdsSubQuery = string.Format(@"SELECT ""ID"" FROM ""{0}"" ", volidstable);
deletecmd.Parameters.Clear();
// If the volume is a block or index volume, this will update the crosslink table, otherwise nothing will happen
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""IndexBlockLink"" WHERE ""BlockVolumeID"" IN ({0}) OR ""IndexVolumeID"" IN ({0})", volIdsSubQuery));
// If the volume is a fileset, this will remove the fileset, otherwise nothing will happen
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""FilesetEntry"" WHERE ""FilesetID"" IN (SELECT ""ID"" FROM ""Fileset"" WHERE ""VolumeID"" IN ({0}))", volIdsSubQuery));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""Fileset"" WHERE ""VolumeID"" IN ({0})", volIdsSubQuery));
var bsIdsSubQuery = string.Format(
@"SELECT ""BlocksetEntry"".""BlocksetID"" FROM ""BlocksetEntry"", ""Block"" "
+ @" WHERE ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""Block"".""VolumeID"" IN ({0}) "
+ @"UNION ALL "
+ @"SELECT ""BlocksetID"" FROM ""BlocklistHash"" "
+ @"WHERE ""Hash"" IN (SELECT ""Hash"" FROM ""Block"" WHERE ""VolumeID"" IN ({0}))"
, volIdsSubQuery);
// Create a temporary table to cache subquery result, as it might take long (SQLite does not cache at all).
deletecmd.ExecuteNonQuery(string.Format(@"CREATE TEMP TABLE ""{0}"" (""ID"" INTEGER PRIMARY KEY)", blocksetidstable));
deletecmd.ExecuteNonQuery(string.Format(@"INSERT OR IGNORE INTO ""{0}"" (""ID"") {1}", blocksetidstable, bsIdsSubQuery));
bsIdsSubQuery = string.Format(@"SELECT ""ID"" FROM ""{0}"" ", blocksetidstable);
deletecmd.Parameters.Clear();
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""File"" WHERE ""BlocksetID"" IN ({0}) OR ""MetadataID"" IN ({0})", bsIdsSubQuery));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""Metadataset"" WHERE ""BlocksetID"" IN ({0})", bsIdsSubQuery));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""Blockset"" WHERE ""ID"" IN ({0})", bsIdsSubQuery));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""BlocksetEntry"" WHERE ""BlocksetID"" IN ({0})", bsIdsSubQuery));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""BlocklistHash"" WHERE ""Hash"" IN (SELECT ""Hash"" FROM ""Block"" WHERE ""VolumeID"" IN ({0}))", volIdsSubQuery));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""Block"" WHERE ""VolumeID"" IN ({0})", volIdsSubQuery));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""DeletedBlock"" WHERE ""VolumeID"" IN ({0})", volIdsSubQuery));
// Clean up temp tables for subqueries. We truncate content and then try to delete.
// Drop in try-block, as it fails in nested transactions (SQLite problem)
// System.Data.SQLite.SQLiteException (0x80004005): database table is locked
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""{0}"" ", blocksetidstable));
deletecmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""{0}"" ", volidstable));
try
{
deletecmd.CommandTimeout = 2;
deletecmd.ExecuteNonQuery(string.Format(@"DROP TABLE IF EXISTS ""{0}"" ", blocksetidstable));
deletecmd.ExecuteNonQuery(string.Format(@"DROP TABLE IF EXISTS ""{0}"" ", volidstable));
}
catch { /* Ignore, will be deleted on close anyway. */ }
m_removeremotevolumeCommand.Transaction = Transaction;
m_removeremotevolumeCommand.SetParameterValue(1, DateTime.UtcNow.Ticks);
m_removeremotevolumeCommand.SetParameterValue(2, RemoteVolumeState.Deleted.ToString());
foreach (var name in names)
{
m_removeremotevolumeCommand.SetParameterValue(0, name);
m_removeremotevolumeCommand.ExecuteNonQuery();
}
}
}
2013-08-23 22:15:07 +02:00
public void Vacuum()
{
using(var cmd = m_connection.CreateCommand())
cmd.ExecuteNonQuery("VACUUM");
}
public long RegisterRemoteVolume(string name, RemoteVolumeType type, long size, RemoteVolumeState state)
{
return RegisterRemoteVolume(name, type, state, size, new TimeSpan(0));
}
public long RegisterRemoteVolume(string name, RemoteVolumeType type, RemoteVolumeState state)
{
return RegisterRemoteVolume(name, type, state, new TimeSpan(0));
}
public long RegisterRemoteVolume(string name, RemoteVolumeType type, RemoteVolumeState state, TimeSpan deleteGraceTime)
{
return RegisterRemoteVolume(name, type, state, -1, deleteGraceTime);
}
public long RegisterRemoteVolume(string name, RemoteVolumeType type, RemoteVolumeState state, long size, TimeSpan deleteGraceTime)
{
m_createremotevolumeCommand.SetParameterValue(0, m_operationid);
m_createremotevolumeCommand.SetParameterValue(1, name);
m_createremotevolumeCommand.SetParameterValue(2, type.ToString());
m_createremotevolumeCommand.SetParameterValue(3, state.ToString());
m_createremotevolumeCommand.SetParameterValue(4, size);
m_createremotevolumeCommand.SetParameterValue(5, 0);
if (deleteGraceTime.Ticks <= 0)
m_createremotevolumeCommand.SetParameterValue(6, 0);
else
m_createremotevolumeCommand.SetParameterValue(6, (DateTime.UtcNow + deleteGraceTime).Ticks);
m_createremotevolumeCommand.Transaction = Transaction;
return m_createremotevolumeCommand.ExecuteScalarInt64();
}
public long GetFilesetID(DateTime restoretime, long[] versions)
{
return GetFilesetIDs(restoretime, versions).First();
}
public IEnumerable<long> GetFilesetIDs(DateTime restoretime, long[] versions)
{
if (restoretime.Kind == DateTimeKind.Unspecified)
throw new Exception("Invalid DateTime given, must be either local or UTC");
var tmp = GetFilelistWhereClause(restoretime, versions);
string query = tmp.Item1;
var args = tmp.Item2;
var res = new List<long>();
using(var cmd = m_connection.CreateCommand())
{
using(var rd = cmd.ExecuteReader(@"SELECT ""ID"" FROM ""Fileset"" " + query + @" ORDER BY ""Timestamp"" DESC", args))
while (rd.Read())
res.Add(rd.GetInt64(0));
if (res.Count == 0)
{
cmd.Parameters.Clear();
using(var rd = cmd.ExecuteReader(@"SELECT ""ID"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC "))
while (rd.Read())
2017-01-05 09:57:06 +01:00
res.Add(rd.ConvertValueToInt64(0));
if (res.Count == 0)
2017-01-09 11:35:38 +01:00
throw new Duplicati.Library.Interface.UserInformationException("No backup at the specified date");
else
m_result.AddWarning(string.Format("Restore time or version did not match any existing backups, selecting newest backup"), null);
}
return res;
}
2013-03-08 22:24:54 +01:00
}
public IEnumerable<long> FindMatchingFilesets(DateTime restoretime, long[] versions)
{
if (restoretime.Kind == DateTimeKind.Unspecified)
throw new Exception("Invalid DateTime given, must be either local or UTC");
var tmp = GetFilelistWhereClause(restoretime, versions, singleTimeMatch: true);
string query = tmp.Item1;
var args = tmp.Item2;
var res = new List<long>();
using(var cmd = m_connection.CreateCommand())
using(var rd = cmd.ExecuteReader(@"SELECT ""ID"" FROM ""Fileset"" " + query + @" ORDER BY ""Timestamp"" DESC", args))
while (rd.Read())
res.Add(rd.GetInt64(0));
return res;
}
2013-03-08 22:24:54 +01:00
/// <summary>
/// Writes the current changes to the database
/// </summary>
/// <returns>An awaitable task.</returns>
/// <param name="message">The message to use for logging the time spent in this operation.</param>
/// <param name="restart">If set to <c>true</c>, a transaction will be started again after this call.</param>
public void CommitTransaction(string message, bool restart = true)
2013-03-08 22:24:54 +01:00
{
if (m_parent != null)
m_parent.CommitTransaction(message, restart);
else
2013-03-08 22:24:54 +01:00
{
using (new Logging.Timer(message))
2013-03-08 22:24:54 +01:00
{
m_transaction.Commit();
m_transaction = restart ? m_connection.BeginTransaction() : null;
2013-03-08 22:24:54 +01:00
}
}
}
2013-03-08 22:24:54 +01:00
public void RollbackTransaction(bool restart = true)
{
if (m_parent != null)
m_parent.RollbackTransaction();
else
2013-03-08 22:24:54 +01:00
{
m_transaction.Rollback();
if (restart)
m_transaction = m_connection.BeginTransaction();
2013-03-08 22:24:54 +01:00
}
}
2013-03-08 22:24:54 +01:00
private class LocalFileEntry : ILocalFileEntry
{
private System.Data.IDataReader m_reader;
public LocalFileEntry(System.Data.IDataReader reader)
{
m_reader = reader;
}
public string Path
{
get
{
var c = m_reader.GetValue(0);
if (c == null || c == DBNull.Value)
return null;
return c.ToString();
}
}
public long Length
{
get
{
return m_reader.ConvertValueToInt64(1);;
}
}
public string Hash
{
get
{
var c = m_reader.GetValue(2);
if (c == null || c == DBNull.Value)
return null;
return c.ToString();
}
}
public string Metahash
{
get
{
var c = m_reader.GetValue(3);
if (c == null || c == DBNull.Value)
return null;
return c.ToString();
}
}
}
public IEnumerable<ILocalFileEntry> GetFiles(long filesetId)
{
using(var cmd = m_connection.CreateCommand())
using(var rd = cmd.ExecuteReader(@"SELECT ""A"".""Path"", ""B"".""Length"", ""B"".""FullHash"", ""D"".""FullHash"" FROM ""File"" A, ""Blockset"" B, ""Metadataset"" C, ""Blockset"" D, ""FilesetEntry"" E WHERE ""A"".""BlocksetID"" = ""B"".""ID"" AND ""A"".""MetadataID"" = ""C"".""ID"" AND ""C"".""BlocksetID"" = ""D"".""ID"" AND ""A"".""ID"" = ""E"".""FileID"" AND ""E"".""FilesetID"" = ? ", filesetId))
while(rd.Read())
yield return new LocalFileEntry(rd);
}
private IEnumerable<KeyValuePair<string, string>> GetDbOptionList()
{
using(var cmd = m_connection.CreateCommand(Transaction))
using(var rd = cmd.ExecuteReader(@"SELECT ""Key"", ""Value"" FROM ""Configuration"" "))
while(rd.Read())
yield return new KeyValuePair<string, string>(rd.GetValue(0).ToString(), rd.GetValue(1).ToString());
}
public IDictionary<string, string> GetDbOptions()
{
return GetDbOptionList().ToDictionary(x => x.Key, x => x.Value);
}
public bool RepairInProgress
{
get
{
return GetDbOptions().ContainsKey("repair-in-progress");
}
set
{
var opts = GetDbOptions();
if (value)
opts["repair-in-progress"] = "true";
else
opts.Remove("repair-in-progress");
SetDbOptions(opts);
}
}
public bool PartiallyRecreated
{
get
{
return GetDbOptions().ContainsKey("partially-recreated");
}
set
{
var opts = GetDbOptions();
if (value)
opts["partially-recreated"] = "true";
else
opts.Remove("partially-recreated");
SetDbOptions(opts);
}
}
public void SetDbOptions(IDictionary<string, string> options)
{
using(var cmd = m_connection.CreateCommand())
{
cmd.Transaction = Transaction;
cmd.ExecuteNonQuery(@"DELETE FROM ""Configuration"" ");
foreach(var kp in options)
cmd.ExecuteNonQuery(@"INSERT INTO ""Configuration"" (""Key"", ""Value"") VALUES (?, ?) ", kp.Key, kp.Value);
}
}
public long GetBlocksLargerThan(long fhblocksize)
{
using(var cmd = m_connection.CreateCommand())
return cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""Block"" WHERE ""Size"" > ?", -1, fhblocksize);
}
public void VerifyConsistency(long blocksize, long hashsize, bool verifyfilelists)
{
using (var cmd = m_connection.CreateCommand(Transaction))
{
// Calculate the lengths for each blockset
var combinedLengths = @"
SELECT
""A"".""ID"" AS ""BlocksetID"",
IFNULL(""B"".""CalcLen"", 0) AS ""CalcLen"",
""A"".""Length""
FROM
""Blockset"" A
LEFT OUTER JOIN
(
SELECT
""BlocksetEntry"".""BlocksetID"",
SUM(""Block"".""Size"") AS ""CalcLen""
FROM
""BlocksetEntry""
LEFT OUTER JOIN
""Block""
ON
""Block"".""ID"" = ""BlocksetEntry"".""BlockID""
GROUP BY ""BlocksetEntry"".""BlocksetID""
) B
ON
""A"".""ID"" = ""B"".""BlocksetID""
";
// For each blockset with wrong lengths, fetch the file path
var reportDetails = @"SELECT ""CalcLen"", ""Length"", ""A"".""BlocksetID"", ""File"".""Path"" FROM (" + combinedLengths + @") A, ""File"" WHERE ""A"".""BlocksetID"" = ""File"".""BlocksetID"" AND ""A"".""CalcLen"" != ""A"".""Length"" ";
using(var rd = cmd.ExecuteReader(reportDetails))
if (rd.Read())
{
var sb = new StringBuilder();
sb.AppendLine("Found inconsistency in the following files while validating database: ");
var c = 0;
do
{
if (c < 5)
sb.AppendFormat("{0}, actual size {1}, dbsize {2}, blocksetid: {3}{4}", rd.GetValue(3), rd.GetValue(1), rd.GetValue(0), rd.GetValue(2), Environment.NewLine);
c++;
} while(rd.Read());
c -= 5;
if (c > 0)
sb.AppendFormat("... and {0} more", c);
sb.Append(". Run repair to fix it.");
throw new InvalidDataException(sb.ToString());
}
var real_count = cmd.ExecuteScalarInt64(@"SELECT Count(*) FROM ""BlocklistHash""", 0);
var unique_count = cmd.ExecuteScalarInt64(@"SELECT Count(*) FROM (SELECT DISTINCT ""BlocksetID"", ""Index"" FROM ""BlocklistHash"")", 0);
if (real_count != unique_count)
throw new InvalidDataException(string.Format("Found {0} blocklist hashes, but there should be {1}. Run repair to fix it.", real_count, unique_count));
var itemswithnoblocklisthash = cmd.ExecuteScalarInt64(string.Format(@"SELECT COUNT(*) FROM (SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {0} - 1) / {0}) AS ""BlocklistHashCountExpected"", CASE WHEN ""G"".""BlocklistHashCount"" IS NULL THEN 0 ELSE ""G"".""BlocklistHashCount"" END AS ""BlocklistHashCountActual"" FROM (SELECT ""BlocksetID"", COUNT(*) AS ""BlockCount"" FROM ""BlocksetEntry"" GROUP BY ""BlocksetID"") ""N"" LEFT OUTER JOIN (SELECT ""BlocksetID"", COUNT(*) AS ""BlocklistHashCount"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"") ""G"" ON ""N"".""BlocksetID"" = ""G"".""BlocksetID"" WHERE ""N"".""BlockCount"" > 1) WHERE ""BlocklistHashCountExpected"" != ""BlocklistHashCountActual"")", blocksize / hashsize), 0);
if (itemswithnoblocklisthash != 0)
throw new InvalidDataException(string.Format("Found {0} file(s) with missing blocklist hashes", itemswithnoblocklisthash));
2017-08-12 19:14:11 +01:00
if (cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""Blockset"" WHERE ""Length"" > 0 AND ""ID"" NOT IN (SELECT ""BlocksetId"" FROM ""BlocksetEntry"")") != 0)
2017-08-12 14:09:55 +01:00
{
throw new Exception("Detected non-empty blocksets with no associated blocks!");
}
2017-08-12 14:09:55 +01:00
if (cmd.ExecuteScalarInt64(@"SELECT COUNT(*) FROM ""File"" WHERE ""BlocksetID"" != ? AND ""BlocksetID"" != ? AND NOT ""BlocksetID"" IN (SELECT ""ID"" FROM ""Blockset"")", 0, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID) != 0)
throw new Exception("Detected files associated with non-existing blocksets!");
if (verifyfilelists)
{
using(var cmd2 = m_connection.CreateCommand(Transaction))
foreach(var filesetid in cmd.ExecuteReaderEnumerable(@"SELECT ""ID"" FROM ""Fileset"" ").Select(x => x.ConvertValueToInt64(0, -1)))
{
2017-08-12 14:09:55 +01:00
var expandedCmd = string.Format(@"SELECT COUNT(*) FROM (SELECT DISTINCT ""Path"" FROM ({0}) UNION SELECT DISTINCT ""Path"" FROM ({1}))", LocalDatabase.LIST_FILESETS, LocalDatabase.LIST_FOLDERS_AND_SYMLINKS);
var expandedlist = cmd2.ExecuteScalarInt64(expandedCmd, 0, filesetid, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID, filesetid);
//var storedfilelist = cmd2.ExecuteScalarInt64(string.Format(@"SELECT COUNT(*) FROM ""FilesetEntry"", ""File"" WHERE ""FilesetEntry"".""FilesetID"" = ? AND ""File"".""ID"" = ""FilesetEntry"".""FileID"" AND ""File"".""BlocksetID"" != ? AND ""File"".""BlocksetID"" != ?"), 0, filesetid, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID);
var storedlist = cmd2.ExecuteScalarInt64(string.Format(@"SELECT COUNT(*) FROM ""FilesetEntry"" WHERE ""FilesetEntry"".""FilesetID"" = ?"), 0, filesetid);
if (expandedlist != storedlist)
throw new Exception(string.Format("Unexpected difference in fileset {0}, found {1} entries, but expected {2}", filesetid, expandedlist, storedlist));
}
}
}
}
2013-04-27 15:13:14 +02:00
public interface IBlock
{
string Hash { get; }
long Size { get; }
}
internal class Block : IBlock
{
public string Hash { get; private set; }
public long Size { get; private set; }
public Block(string hash, long size)
{
this.Hash = hash;
this.Size = size;
}
}
public IEnumerable<IBlock> GetBlocks(long volumeid)
2013-04-27 15:13:14 +02:00
{
using(var cmd = m_connection.CreateCommand(Transaction))
2013-04-27 15:13:14 +02:00
using(var rd = cmd.ExecuteReader(@"SELECT DISTINCT ""Hash"", ""Size"" FROM ""Block"" WHERE ""VolumeID"" = ?", volumeid))
while (rd.Read())
yield return new Block(rd.GetValue(0).ToString(), rd.GetInt64(1));
}
2013-04-27 15:13:14 +02:00
private class BlocklistHashEnumerable : IEnumerable<string>
{
private class BlocklistHashEnumerator : IEnumerator<string>
{
private System.Data.IDataReader m_reader;
private BlocklistHashEnumerable m_parent;
private string m_path = null;
private bool m_first = true;
private string m_current = null;
public BlocklistHashEnumerator(BlocklistHashEnumerable parent, System.Data.IDataReader reader)
{
m_reader = reader;
m_parent = parent;
}
public string Current { get{ return m_current; } }
public void Dispose()
{
}
object System.Collections.IEnumerator.Current { get { return this.Current; } }
public bool MoveNext()
{
m_first = false;
if (m_path == null)
{
m_path = m_reader.GetValue(0).ToString();
m_current = m_reader.GetValue(6).ToString();
return true;
}
else
{
if (m_current == null)
return false;
if (!m_reader.Read())
{
m_current = null;
m_parent.MoreData = false;
return false;
}
var np = m_reader.GetValue(0).ToString();
if (m_path != np)
{
m_current = null;
return false;
}
m_current = m_reader.GetValue(6).ToString();
return true;
}
}
public void Reset()
{
if (!m_first)
throw new Exception("Iterator reset not supported");
m_first = false;
}
}
private System.Data.IDataReader m_reader;
public BlocklistHashEnumerable(System.Data.IDataReader reader)
{
m_reader = reader;
this.MoreData = true;
}
public bool MoreData { get; protected set; }
public IEnumerator<string> GetEnumerator()
{
return new BlocklistHashEnumerator(this, m_reader);
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
public const string LIST_FILESETS = @"
SELECT
""L"".""Path"",
""L"".""Lastmodified"",
""L"".""Filelength"",
""L"".""Filehash"",
""L"".""Metahash"",
""L"".""Metalength"",
""L"".""BlocklistHash"",
""L"".""FirstBlockHash"",
""L"".""FirstBlockSize"",
""L"".""FirstMetaBlockHash"",
""L"".""FirstMetaBlockSize"",
""M"".""Hash"" AS ""MetaBlocklistHash""
FROM
(
SELECT
""J"".""Path"",
""J"".""Lastmodified"",
""J"".""Filelength"",
""J"".""Filehash"",
""J"".""Metahash"",
""J"".""Metalength"",
""K"".""Hash"" AS ""BlocklistHash"",
""J"".""FirstBlockHash"",
""J"".""FirstBlockSize"",
""J"".""FirstMetaBlockHash"",
""J"".""FirstMetaBlockSize"",
""J"".""MetablocksetID""
FROM
(
SELECT
2017-08-12 14:09:55 +01:00
""A"".""Path"" AS ""Path"",
""D"".""Lastmodified"" AS ""Lastmodified"",
""B"".""Length"" AS ""Filelength"",
""B"".""FullHash"" AS ""Filehash"",
""E"".""FullHash"" AS ""Metahash"",
""E"".""Length"" AS ""Metalength"",
""A"".""BlocksetID"" AS ""BlocksetID"",
""F"".""Hash"" AS ""FirstBlockHash"",
""F"".""Size"" AS ""FirstBlockSize"",
""H"".""Hash"" AS ""FirstMetaBlockHash"",
""H"".""Size"" AS ""FirstMetaBlockSize"",
""C"".""BlocksetID"" AS ""MetablocksetID""
FROM
2017-08-12 14:09:55 +01:00
""File"" A
LEFT JOIN ""Blockset"" B
ON ""A"".""BlocksetID"" = ""B"".""ID""
LEFT JOIN ""Metadataset"" C
ON ""A"".""MetadataID"" = ""C"".""ID""
LEFT JOIN ""FilesetEntry"" D
ON ""A"".""ID"" = ""D"".""FileID""
LEFT JOIN ""Blockset"" E
ON ""E"".""ID"" = ""C"".""BlocksetID""
LEFT JOIN ""BlocksetEntry"" G
ON ""B"".""ID"" = ""G"".""BlocksetID""
LEFT JOIN ""Block"" F
ON ""G"".""BlockID"" = ""F"".""ID""
LEFT JOIN ""BlocksetEntry"" I
ON ""E"".""ID"" = ""I"".""BlocksetID""
LEFT JOIN ""Block"" H
ON ""I"".""BlockID"" = ""H"".""ID""
WHERE
2017-08-12 14:09:55 +01:00
""A"".""BlocksetId"" >= 0 AND
""D"".""FilesetID"" = ? AND
(""I"".""Index"" = 0 OR ""I"".""Index"" IS NULL) AND
(""G"".""Index"" = 0 OR ""G"".""Index"" IS NULL)
) J
LEFT OUTER JOIN
""BlocklistHash"" K
ON
""K"".""BlocksetID"" = ""J"".""BlocksetID""
ORDER BY ""J"".""Path"", ""K"".""Index""
) L
LEFT OUTER JOIN
""BlocklistHash"" M
ON
""M"".""BlocksetID"" = ""L"".""MetablocksetID""
";
public const string LIST_FOLDERS_AND_SYMLINKS = @"
SELECT
""G"".""BlocksetID"",
""G"".""ID"",
""G"".""Path"",
""G"".""Length"",
""G"".""FullHash"",
""G"".""Lastmodified"",
""G"".""FirstMetaBlockHash"",
""H"".""Hash"" AS ""MetablocklistHash""
FROM
(
SELECT
""B"".""BlocksetID"",
""B"".""ID"",
""B"".""Path"",
""D"".""Length"",
""D"".""FullHash"",
""A"".""Lastmodified"",
""F"".""Hash"" AS ""FirstMetaBlockHash"",
""C"".""BlocksetID"" AS ""MetaBlocksetID""
FROM
""FilesetEntry"" A,
""File"" B,
""Metadataset"" C,
""Blockset"" D,
""BlocksetEntry"" E,
""Block"" F
WHERE
""A"".""FileID"" = ""B"".""ID""
AND ""B"".""MetadataID"" = ""C"".""ID""
AND ""C"".""BlocksetID"" = ""D"".""ID""
AND ""E"".""BlocksetID"" = ""C"".""BlocksetID""
AND ""E"".""BlockID"" = ""F"".""ID""
AND ""E"".""Index"" = 0
AND (""B"".""BlocksetID"" = ? OR ""B"".""BlocksetID"" = ?)
AND ""A"".""FilesetID"" = ?
) G
LEFT OUTER JOIN
""BlocklistHash"" H
ON
""H"".""BlocksetID"" = ""G"".""MetaBlocksetID""
ORDER BY
""G"".""Path"", ""H"".""Index""
";
public void WriteFileset(Volumes.FilesetVolumeWriter filesetvolume, long filesetId)
2013-04-27 15:13:14 +02:00
{
using (var cmd = m_connection.CreateCommand(Transaction))
2013-04-27 15:13:14 +02:00
{
cmd.CommandText = LIST_FOLDERS_AND_SYMLINKS;
2013-04-27 15:13:14 +02:00
cmd.AddParameter(FOLDER_BLOCKSET_ID);
cmd.AddParameter(SYMLINK_BLOCKSET_ID);
cmd.AddParameter(filesetId);
string lastpath = null;
2013-04-27 15:13:14 +02:00
using (var rd = cmd.ExecuteReader())
while(rd.Read())
{
var blocksetID = rd.ConvertValueToInt64(0, -1);
var path = rd.GetValue(2).ToString();
var metalength = rd.ConvertValueToInt64(3, -1);
var metahash = rd.GetValue(4).ToString();
var metablockhash = rd.GetValue(6).ToString();
var metablocklisthash = rd.GetValue(7).ToString();
if (path == lastpath)
m_result.AddWarning(string.Format("Duplicate path detected: {0}!", path), null);
lastpath = path;
2013-04-27 15:13:14 +02:00
if (blocksetID == FOLDER_BLOCKSET_ID)
filesetvolume.AddDirectory(path, metahash, metalength, metablockhash, string.IsNullOrWhiteSpace(metablocklisthash) ? null : new string[] { metablocklisthash });
2013-04-27 15:13:14 +02:00
else if (blocksetID == SYMLINK_BLOCKSET_ID)
filesetvolume.AddSymlink(path, metahash, metalength, metablockhash, string.IsNullOrWhiteSpace(metablocklisthash) ? null : new string[] { metablocklisthash });
2013-04-27 15:13:14 +02:00
}
// TODO: Perhaps run the above query after recreate and compare count(*) with count(*) from filesetentry where id = x
2013-04-27 15:13:14 +02:00
cmd.CommandText = LIST_FILESETS;
2013-04-27 15:13:14 +02:00
cmd.Parameters.Clear();
cmd.AddParameter(filesetId);
using (var rd = cmd.ExecuteReader())
if (rd.Read())
{
var more = false;
do
{
var path = rd.GetValue(0).ToString();
var filehash = rd.GetValue(3).ToString();
2015-01-25 12:33:00 +01:00
var size = rd.ConvertValueToInt64(2);
var lastmodified = new DateTime(rd.ConvertValueToInt64(1, 0), DateTimeKind.Utc);
2013-04-27 15:13:14 +02:00
var metahash = rd.GetValue(4).ToString();
2015-01-25 12:33:00 +01:00
var metasize = rd.ConvertValueToInt64(5, -1);
var p = rd.GetValue(6);
var blrd = (p == null || p == DBNull.Value) ? null : new BlocklistHashEnumerable(rd);
var blockhash = rd.GetValue(7).ToString();
var blocksize = rd.ConvertValueToInt64(8, -1);
var metablockhash = rd.GetValue(9).ToString();
//var metablocksize = rd.ConvertValueToInt64(10, -1);
var metablocklisthash = rd.GetValue(11).ToString();
if (blockhash == filehash)
blockhash = null;
if (metablockhash == metahash)
metablockhash = null;
2013-04-27 15:13:14 +02:00
filesetvolume.AddFile(path, filehash, size, lastmodified, metahash, metasize, metablockhash, blockhash, blocksize, blrd, string.IsNullOrWhiteSpace(metablocklisthash) ? null : new string[] { metablocklisthash });
if (blrd == null)
more = rd.Read();
else
more = blrd.MoreData;
2013-04-27 15:13:14 +02:00
} while (more);
}
}
}
2013-05-11 12:03:15 +02:00
/// <summary>
/// Keeps a list of filenames in a temporary table with a single columne Path
///</summary>
public class FilteredFilenameTable : IDisposable
{
public string Tablename { get; private set; }
private System.Data.IDbConnection m_connection;
2013-05-13 22:32:05 +02:00
public FilteredFilenameTable(System.Data.IDbConnection connection, Library.Utility.IFilter filter, System.Data.IDbTransaction transaction)
2013-05-11 12:03:15 +02:00
{
m_connection = connection;
Tablename = "Filenames-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
2013-05-13 22:32:05 +02:00
var type = Library.Utility.FilterType.Regexp;
if (filter is Library.Utility.FilterExpression)
type = ((Library.Utility.FilterExpression)filter).Type;
// Bugfix: SQLite does not handle case-insensitive LIKE with non-ascii characters
if (type != Library.Utility.FilterType.Regexp && !Library.Utility.Utility.IsFSCaseSensitive && filter.ToString().Any(x => x > 127))
type = Library.Utility.FilterType.Regexp;
2013-05-13 22:32:05 +02:00
if (type == Library.Utility.FilterType.Regexp)
2013-05-11 12:03:15 +02:00
{
using(var cmd = m_connection.CreateCommand(transaction))
2013-05-11 12:03:15 +02:00
{
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" (""Path"" TEXT NOT NULL)", Tablename));
cmd.CommandText = string.Format(@"INSERT INTO ""{0}"" (""Path"") VALUES (?)", Tablename);
cmd.AddParameter();
using(var c2 = m_connection.CreateCommand(transaction))
using(var rd = c2.ExecuteReader(@"SELECT DISTINCT ""Path"" FROM ""File"" "))
while(rd.Read())
{
var p = rd.GetValue(0).ToString();
if(Library.Utility.FilterExpression.Matches(filter, p))
2013-05-11 12:03:15 +02:00
{
cmd.SetParameterValue(0, p);
cmd.ExecuteNonQuery();
2013-05-11 12:03:15 +02:00
}
}
2013-05-11 12:03:15 +02:00
}
}
else
{
var sb = new StringBuilder();
var args = new List<object>();
2013-05-13 22:32:05 +02:00
foreach(var f in ((Library.Utility.FilterExpression)filter).GetSimpleList())
2013-05-11 12:03:15 +02:00
{
if (f.Contains('*') || f.Contains('?'))
{
sb.Append(@"""Path"" LIKE ? OR ");
args.Add(f.Replace('*', '%').Replace('?', '_'));
}
else
{
sb.Append(@"""Path"" = ? OR ");
2013-05-11 12:03:15 +02:00
args.Add(f);
}
}
sb.Length = sb.Length - " OR ".Length;
using (var cmd = m_connection.CreateCommand(transaction))
2013-11-20 19:16:35 +01:00
{
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" (""Path"" TEXT NOT NULL)", Tablename));
2017-11-26 15:24:53 -08:00
cmd.ExecuteNonQuery(string.Format(@"INSERT INTO ""{0}"" SELECT DISTINCT ""Path"" FROM ""File"" WHERE " + sb, Tablename), args.ToArray());
2013-11-20 19:16:35 +01:00
}
2013-05-11 12:03:15 +02:00
}
}
public void Dispose()
{
if (Tablename != null)
try
{
using(var cmd = m_connection.CreateCommand())
cmd.ExecuteNonQuery(string.Format(@"DROP TABLE IF EXISTS ""{0}"" ", Tablename));
2013-05-11 12:03:15 +02:00
}
catch { }
2013-05-11 12:03:15 +02:00
finally { Tablename = null; }
}
}
public void RenameRemoteFile(string oldname, string newname)
{
using(var cmd = m_connection.CreateCommand(Transaction))
{
//Rename the old entry, to preserve ID links
var c = cmd.ExecuteNonQuery(@"UPDATE ""Remotevolume"" SET ""Name"" = ? WHERE ""Name"" = ?", newname, oldname);
if (c != 1)
throw new Exception(string.Format("Unexpected result from renaming \"{0}\" to \"{1}\", expected {2} got {3}", oldname, newname, 1, c));
// Grab the type of entry
var type = (RemoteVolumeType)Enum.Parse(typeof(RemoteVolumeType), cmd.ExecuteScalar(@"SELECT ""Type"" FROM ""Remotevolume"" WHERE ""Name"" = ?", newname).ToString(), true);
//Create a fake new entry with the old name and mark as deleting
// as this ensures we will remove it, if it shows up in some later listing
RegisterRemoteVolume(oldname, type, RemoteVolumeState.Deleting);
}
}
/// <summary>
/// Creates a timestamped backup operation to correctly associate the fileset with the time it was created.
/// </summary>
/// <param name="volumeid">The ID of the fileset volume to update</param>
/// <param name="timestamp">The timestamp of the operation to create</param>
/// <param name="transaction">An optional external transaction</param>
public virtual long CreateFileset(long volumeid, DateTime timestamp)
{
using (var cmd = m_connection.CreateCommand(Transaction))
return cmd.ExecuteScalarInt64(@"INSERT INTO ""Fileset"" (""OperationID"", ""Timestamp"", ""VolumeID"") VALUES (?, ?, ?); SELECT last_insert_rowid();", -1, m_operationid, NormalizeDateTimeToEpochSeconds(timestamp), volumeid);
}
public void AddIndexBlockLink(long indexVolumeID, long blockVolumeID)
{
m_insertIndexBlockLink.Transaction = Transaction;
m_insertIndexBlockLink.SetParameterValue(0, indexVolumeID);
m_insertIndexBlockLink.SetParameterValue(1, blockVolumeID);
m_insertIndexBlockLink.ExecuteNonQuery();
}
public IEnumerable<Tuple<string, byte[], int>> GetBlocklists(long volumeid, long blocksize, int hashsize)
{
using(var cmd = m_connection.CreateCommand(Transaction))
{
var sql = string.Format(@"SELECT ""A"".""Hash"", ""C"".""Hash"" FROM " +
2016-02-18 16:48:35 +01:00
@"(SELECT ""BlocklistHash"".""BlocksetID"", ""Block"".""Hash"", * FROM ""BlocklistHash"",""Block"" WHERE ""BlocklistHash"".""Hash"" = ""Block"".""Hash"" AND ""Block"".""VolumeID"" = ?) A, " +
@" ""BlocksetEntry"" B, ""Block"" C WHERE ""B"".""BlocksetID"" = ""A"".""BlocksetID"" AND " +
2016-03-26 13:14:05 +01:00
@" ""B"".""Index"" >= (""A"".""Index"" * {0}) AND ""B"".""Index"" < ((""A"".""Index"" + 1) * {0}) AND ""C"".""ID"" = ""B"".""BlockID"" " +
@" ORDER BY ""A"".""BlocksetID"", ""B"".""Index""",
2016-03-26 13:14:05 +01:00
blocksize / hashsize
);
string curHash = null;
int index = 0;
byte[] buffer = new byte[blocksize];
2016-02-18 16:48:35 +01:00
using(var rd = cmd.ExecuteReader(sql, volumeid))
while (rd.Read())
{
var blockhash = rd.GetValue(0).ToString();
if ((blockhash != curHash && curHash != null) || index + hashsize > buffer.Length)
{
yield return new Tuple<string, byte[], int>(curHash, buffer, index);
curHash = null;
index = 0;
buffer = new byte[blocksize];
}
var hash = Convert.FromBase64String(rd.GetValue(1).ToString());
Array.Copy(hash, 0, buffer, index, hashsize);
curHash = blockhash;
index += hashsize;
}
if (curHash != null)
yield return new Tuple<string, byte[], int>(curHash, buffer, index);
}
}
2015-02-15 23:26:52 +01:00
public void PurgeLogData(DateTime threshold)
{
using(var cmd = m_connection.CreateCommand(Transaction))
2015-02-15 23:26:52 +01:00
{
var t = NormalizeDateTimeToEpochSeconds(threshold);
cmd.ExecuteNonQuery(@"DELETE FROM ""LogData"" WHERE ""Timestamp"" < ?", t);
cmd.ExecuteNonQuery(@"DELETE FROM ""RemoteOperation"" WHERE ""Timestamp"" < ?", t);
}
}
public virtual void Dispose()
{
if (IsDisposed)
return;
DisposeAllFields<System.Data.IDbCommand>(this, false);
if (ShouldCloseConnection && m_connection != null)
{
if (m_connection.State == System.Data.ConnectionState.Open)
m_connection.Close();
m_connection.Dispose();
}
IsDisposed = true;
}
/// <summary>
/// Disposes all fields of a certain type, in the instance and its bases
/// </summary>
/// <typeparam name="T">The type of fields to find</typeparam>
/// <param name="item">The item to dispose</param>
/// <param name="throwExceptions"><c>True</c> if an aggregate exception should be thrown, or <c>false</c> if exceptions are silently captured</param>
public static void DisposeAllFields<T>(object item, bool throwExceptions)
where T : IDisposable
{
var typechain = new List<Type>();
var cur = item.GetType();
var exceptions = new List<Exception>();
while (cur != null && cur != typeof(object))
{
typechain.Add(cur);
cur = cur.BaseType;
}
var fields =
typechain.SelectMany(x =>
x.GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.FlattenHierarchy)
).Distinct().Where(x => x.FieldType.IsAssignableFrom(typeof(T)));
foreach (var p in fields)
try
{
var val = p.GetValue(item);
if (val != null)
((T)val).Dispose();
}
catch (Exception ex)
{
if (throwExceptions)
exceptions.Add(ex);
}
if (exceptions.Count > 0)
throw new AggregateException(exceptions);
}
public void WriteResults()
{
if (IsDisposed)
return;
if (m_connection != null && m_result != null)
{
m_result.FlushLog();
2016-12-01 23:59:54 +01:00
if (m_result.EndTime.Ticks == 0)
m_result.EndTime = DateTime.UtcNow;
LogMessage("Result",
Library.Utility.Utility.PrintSerializeObject(
m_result,
(StringBuilder)null,
(prop, item) =>
!typeof(IBackendProgressUpdater).IsAssignableFrom(prop.PropertyType) &&
!typeof(IMessageSink).IsAssignableFrom(prop.PropertyType) &&
!typeof(ILogWriter).IsAssignableFrom(prop.PropertyType) &&
prop.Name != "VerboseOutput" &&
prop.Name != "VerboseErrors" &&
!(prop.Name == "MainOperation" && item is BackendWriter) &&
!(prop.Name == "EndTime" && item is BackendWriter) &&
!(prop.Name == "Duration" && item is BackendWriter) &&
!(prop.Name == "BeginTime" && item is BackendWriter),
recurseobjects: true,
collectionlimit: 5
).ToString(),
null
);
}
CommitTransaction("WriteMessage");
}
}
}