using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.IO;
using Duplicati.Library.Modules.Builtin.ResultSerialization;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Main.Database
{
internal class LocalDatabase : IDisposable
{
///
/// The tag used for logging
///
private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(LocalDatabase));
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_removedeletedremotevolumeCommand;
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;
private readonly System.Data.IDbCommand m_insertremotelogCommand;
private readonly System.Data.IDbCommand m_insertIndexBlockLink;
private readonly System.Data.IDbCommand m_findpathprefixCommand;
private readonly System.Data.IDbCommand m_insertpathprefixCommand;
protected BasicResults m_result;
public const long FOLDER_BLOCKSET_ID = -100;
public const long SYMLINK_BLOCKSET_ID = -200;
public DateTime OperationTimestamp { get; private set; }
internal System.Data.IDbConnection Connection { get { return m_connection; } }
public bool IsDisposed { get; private set; }
public bool ShouldCloseConnection { get; set; }
protected static System.Data.IDbConnection CreateConnection(string path)
{
path = System.IO.Path.GetFullPath(path);
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);
try
{
Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(c, path, typeof(LocalDatabase));
}
catch
{
//Don't leak database connections when something goes wrong
c.Dispose();
throw;
}
return c;
}
///
/// Creates a new database instance and starts a new operation
///
/// The path to the database
/// The name of the operation
public LocalDatabase(string path, string operation, bool shouldclose)
: this(CreateConnection(path), operation)
{
ShouldCloseConnection = shouldclose;
}
///
/// Creates a new database instance and starts a new operation
///
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;
}
///
/// Creates a new database instance and starts a new operation
///
/// The name of the operation
public LocalDatabase(System.Data.IDbConnection connection, string operation)
: this(connection)
{
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, Library.Utility.Utility.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_removedeletedremotevolumeCommand = connection.CreateCommand();
m_selectremotevolumeIdCommand = connection.CreateCommand();
m_createremotevolumeCommand = connection.CreateCommand();
m_insertIndexBlockLink = connection.CreateCommand();
m_findpathprefixCommand = connection.CreateCommand();
m_insertpathprefixCommand = 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_removedeletedremotevolumeCommand.CommandText = $@"DELETE FROM ""Remotevolume"" WHERE ""State"" == ""{RemoteVolumeState.Deleted.ToString()}"" AND (""DeleteGraceTime"" < ? OR LENGTH(""DeleteGraceTime"") > 12) "; // >12 is to handle removal of old records that were in ticks
m_removedeletedremotevolumeCommand.AddParameter();
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);
m_findpathprefixCommand.CommandText = @"SELECT ""ID"" FROM ""PathPrefix"" WHERE ""Prefix"" = ?";
m_findpathprefixCommand.AddParameter();
m_insertpathprefixCommand.CommandText = @"INSERT INTO ""PathPrefix"" (""Prefix"") VALUES (?); SELECT last_insert_rowid(); ";
m_insertpathprefixCommand.AddParameter();
}
internal void SetResult(BasicResults result)
{
m_result = result;
}
///
/// Creates a DateTime instance by adding the specified number of seconds to the EPOCH value
///
public static DateTime ParseFromEpochSeconds(long seconds)
{
return Library.Utility.Utility.EPOCH.AddSeconds(seconds);
}
public void UpdateRemoteVolume(string name, RemoteVolumeState state, long size, string hash, System.Data.IDbTransaction transaction = null)
{
UpdateRemoteVolume(name, state, size, hash, false, transaction);
}
public void UpdateRemoteVolume(string name, RemoteVolumeState state, long size, string hash, bool suppressCleanup, System.Data.IDbTransaction transaction = null)
{
UpdateRemoteVolume(name, state, size, hash, suppressCleanup, new TimeSpan(0), transaction);
}
public void UpdateRemoteVolume(string name, RemoteVolumeState state, long size, string hash, bool suppressCleanup, TimeSpan deleteGraceTime, System.Data.IDbTransaction transaction = null)
{
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)
{
throw new Exception($"Unexpected number of remote volumes detected: {c}!");
}
if (deleteGraceTime.Ticks > 0)
{
using (var cmd = m_connection.CreateCommand(transaction))
{
if ((c = cmd.ExecuteNonQuery(
@"UPDATE ""RemoteVolume"" SET ""DeleteGraceTime"" = ? WHERE ""Name"" = ? ",
Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(DateTime.UtcNow + deleteGraceTime),
name)) != 1)
{
throw new Exception($"Unexpected number of updates when recording remote volume updates: {c}!");
}
}
}
if (!suppressCleanup && state == RemoteVolumeState.Deleted)
{
RemoveRemoteVolume(name, transaction);
}
}
public IEnumerable> FilesetTimes
{
get
{
using (var cmd = m_connection.CreateCommand())
using(var rd = cmd.ExecuteReader(@"SELECT ""ID"", ""Timestamp"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC"))
while (rd.Read())
yield return new KeyValuePair(rd.GetInt64(0), ParseFromEpochSeconds(rd.GetInt64(1)).ToLocalTime());
}
}
public Tuple GetFilelistWhereClause(DateTime time, long[] versions, IEnumerable> filesetslist = null, bool singleTimeMatch = false)
{
var filesets = (filesetslist ?? this.FilesetTimes).ToArray();
StringBuilder query = new StringBuilder();
var args = new List