// Copyright (C) 2013, The Duplicati Team
// http://www.duplicati.com, opensource@duplicati.com
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace Duplicati.Library.Main.Database
{
public class LocalDeleteDatabase : LocalDatabase
{
///
/// An approximate size of a hash-string in memory (44 chars * 2 for unicode + 8 bytes for pointer = 104)
///
private const uint HASH_GUESS_SIZE = 128;
private System.Data.IDbCommand m_moveBlockToNewVolumeCommand;
public LocalDeleteDatabase(string path, bool isCompact)
: base(CreateConnection(path), isCompact ? "Compact" : "Delete")
{
InitializeCommands();
}
public LocalDeleteDatabase(LocalDatabase db)
: base(db)
{
InitializeCommands();
}
private void InitializeCommands()
{
m_moveBlockToNewVolumeCommand = m_connection.CreateCommand();
m_moveBlockToNewVolumeCommand.CommandText = @"UPDATE ""Block"" SET ""VolumeID"" = ? WHERE ""Hash"" = ? AND ""Size"" = ?";
m_moveBlockToNewVolumeCommand.AddParameters(3);
}
///
/// Deletes all but n backups from the database and remote storage.
///
/// The number of backups to keep
/// If set to true allow removing last backup, otherwise at least one backup is kept
public IEnumerable DeleteAllButN(int keep, bool allowRemovingLast, CommunicationStatistics stat, Options options, System.Data.IDbTransaction transaction)
{
IEnumerable result;
keep = Math.Max(allowRemovingLast ? 0 : 1, keep);
using(var cmd = m_connection.CreateCommand())
{
cmd.Transaction = transaction;
//We create a table with the operationIDs that are about to be deleted
var tmptablename = "DeletedOperations-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" AS SELECT ""ID"" FROM ""Fileset"" WHERE ""ID"" NOT IN (SELECT ""ID"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC LIMIT {1})", tmptablename, keep));
result = DropFromIDTable(cmd, tmptablename, stat);
cmd.ExecuteNonQuery(string.Format(@"DROP TABLE ""{0}"" ", tmptablename));
}
return result;
}
private long GetLastFilesetID(System.Data.IDbCommand cmd)
{
long id = -1;
var r = cmd.ExecuteScalar(@"SELECT ""ID"" FROM ""Fileset"" ORDER BY ""Timestamp"" DESC LIMIT 1");
if (r != null && r != DBNull.Value)
id = Convert.ToInt64(r);
return id;
}
///
/// Deletes all but n backups from the database and remote storage.
///
/// The number of backups to keep
/// If set to true allow removing last backup, otherwise at least one backup is kept
public IEnumerable DeleteOlderThan(DateTime limit, bool allowRemovingLast, CommunicationStatistics stat, Options options, System.Data.IDbTransaction transaction)
{
if (limit.Kind == DateTimeKind.Unspecified)
throw new Exception("Time must be either UTC or Local");
IEnumerable result;
using(var cmd = m_connection.CreateCommand())
{
cmd.Transaction = transaction;
long keepFilesetId = -1;
if (!allowRemovingLast)
keepFilesetId = GetLastFilesetID(cmd);
//We create a table with the operationIDs that are about to be deleted
var tmptablename = "DeletedOperations-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" AS SELECT ""ID"" FROM ""Fileset"" WHERE ""ID"" NOT IN (SELECT ""ID"" FROM ""Fileset"" WHERE ""Timestamp"" > ? OR ""ID"" = ? ORDER BY ""Timestamp"")", tmptablename), limit.ToUniversalTime(), keepFilesetId);
result = DropFromIDTable(cmd, tmptablename, stat);
cmd.ExecuteNonQuery(string.Format(@"DROP TABLE ""{0}"" ", tmptablename));
}
return result;
}
///
/// Deletes the specified filesets from the database and remote storage.
///
/// The filesets to remove
/// If set to true allow removing last backup, otherwise at least one backup is kept
public IEnumerable DeleteFilesets(string filesets, bool allowRemovingLast, CommunicationStatistics stat, Options options, System.Data.IDbTransaction transaction)
{
IEnumerable result;
using(var cmd = m_connection.CreateCommand())
{
cmd.Transaction = transaction;
long keepFilesetId = -1;
if (!allowRemovingLast)
keepFilesetId = GetLastFilesetID(cmd);
var tmptablename = "DeletedOperations-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" (""ID"" INTEGER NOT NULL)", tmptablename));
foreach(var d in filesets.Split(new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries))
{
var c = cmd.ExecuteNonQuery(string.Format(@"INSERT INTO ""{0}"" (ID) VALUES (SELECT DISTINCT ""Fileset"".""ID"" FROM ""Fileset"", ""RemoteVolume"" WHERE ""RemoteVolume"".""Name"" = ? AND ""RemoteVolume"".""Type"" = ? AND ""RemoteVolume"".""ID"" = ""Fileset"".""VolumeID"")", tmptablename), d, RemoteVolumeType.Files.ToString());
if (c != 1)
throw new Exception(string.Format("Failed to mark fileset {0}, query gave {1} volume(s)", d, c));
}
if (keepFilesetId >= 0)
{
var c0 = Convert.ToInt64(cmd.ExecuteScalar(@"SELECT COUNT(*) FROM ""{0}"" "));
var c1 = Convert.ToInt64(cmd.ExecuteScalar(@"SELECT COUNT(*) FROM ""Fileset"" "));
if (c1 - c0 <= 0)
cmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""{0}"" WHERE ""ID"" = ?", tmptablename), keepFilesetId);
}
result = DropFromIDTable(cmd, tmptablename, stat);
cmd.ExecuteNonQuery(string.Format(@"DROP TABLE ""{0}"" ", tmptablename));
}
return result;
}
///
/// Drops all entries related to operations listed in the table.
///
/// The command used to execute the queries
/// The name of a table with the operation IDs
/// A list of filesets to delete
private static IEnumerable DropFromIDTable(System.Data.IDbCommand cmd, string tmptablename, CommunicationStatistics stat)
{
//First we remove unwanted entries
var deleted = cmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""Fileset"" WHERE ""ID"" IN (SELECT ""ID"" FROM ""{0}"") ", tmptablename));
cmd.ExecuteNonQuery(string.Format(@"DELETE FROM ""FilesetEntry"" WHERE ""FilesetID"" IN (SELECT ""ID"" FROM ""{0}"") ", tmptablename));
//Then we delete anything that is no longer being referenced
cmd.ExecuteNonQuery(@"DELETE FROM ""File"" WHERE ""ID"" NOT IN (SELECT DISTINCT ""FileID"" FROM ""FilesetEntry"") ");
cmd.ExecuteNonQuery(@"DELETE FROM ""Metadataset"" WHERE ""ID"" NOT IN (SELECT DISTINCT ""MetadataID"" FROM ""File"") ");
cmd.ExecuteNonQuery(@"DELETE FROM ""Blockset"" WHERE ""ID"" NOT IN (SELECT DISTINCT ""BlocksetID"" FROM ""File"" UNION SELECT DISTINCT ""BlocksetID"" FROM ""Metadataset"") ");
cmd.ExecuteNonQuery(@"DELETE FROM ""BlocksetEntry"" WHERE ""BlocksetID"" NOT IN (SELECT DISTINCT ""ID"" FROM ""Blockset"") ");
cmd.ExecuteNonQuery(@"DELETE FROM ""BlocklistHash"" WHERE ""BlocksetID"" NOT IN (SELECT DISTINCT ""ID"" FROM ""Blockset"") ");
//We save the block info for the remote files, before we delete it
cmd.ExecuteNonQuery(@"INSERT INTO ""DeletedBlock"" (""Hash"", ""Size"", ""VolumeID"") SELECT ""Hash"", ""Size"", ""VolumeID"" FROM ""Block"" WHERE ""ID"" NOT IN (SELECT DISTINCT ""BlockID"" AS ""BlockID"" FROM ""BlocksetEntry"" UNION SELECT DISTINCT ""ID"" FROM ""Block"", ""BlocklistHash"" WHERE ""Block"".""Hash"" = ""BlocklistHash"".""Hash"") ");
cmd.ExecuteNonQuery(@"DELETE FROM ""Block"" WHERE ""ID"" NOT IN (SELECT DISTINCT ""BlockID"" FROM ""BlocksetEntry"" UNION SELECT DISTINCT ""ID"" FROM ""Block"", ""BlocklistHash"" WHERE ""Block"".""Hash"" = ""BlocklistHash"".""Hash"") ");
//Find all remote filesets that are no longer required, and mark them as delete
var updated = cmd.ExecuteNonQuery(@"UPDATE ""RemoteVolume"" SET ""State"" = ? WHERE ""Type"" = ? AND ""State"" IN (?, ?) AND ""ID"" NOT IN (SELECT ""VolumeID"" FROM ""Fileset"") ", RemoteVolumeState.Deleting.ToString(), RemoteVolumeType.Files.ToString(), RemoteVolumeState.Uploaded.ToString(), RemoteVolumeState.Verified.ToString());
if (deleted != updated)
throw new Exception(string.Format("Unexpected number of remote volumes marked as deleted. Found {0} filesets, but {1} volumes", deleted, updated));
var res = new List();
using (var rd = cmd.ExecuteReader(@"SELECT ""Name"" FROM ""RemoteVolume"" WHERE ""Type"" = ? AND ""State"" = ? ", RemoteVolumeType.Files.ToString(), RemoteVolumeState.Deleting.ToString()))
while (rd.Read())
res.Add(rd.GetValue(0).ToString());
return res;
}
private struct VolumeUsage
{
public string Name;
public long VolumeSize;
public long WastedSize;
public VolumeUsage(string name, long volumesize, long wastedsize)
{
this.Name = name;
this.VolumeSize = volumesize;
this.WastedSize = wastedsize;
}
}
///
/// Returns the number of bytes stored in each volume,
/// and the number of bytes no longer needed in each volume.
/// The sizes are the uncompressed values.
///
/// A list of tuples with name, datasize, wastedbytes.
private IEnumerable GetWastedSpaceReport(System.Data.IDbTransaction transaction)
{
var tmptablename = "UsageReport-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
var active = @"SELECT SUM(""Block"".""Size"") AS ""ActiveSize"", 0 AS ""InactiveSize"", ""Block"".""VolumeID"" AS ""VolumeID"", MIN(""FilesetEntry"".""Scantime"") AS ""SortScantime"" FROM ""FilesetEntry"", ""File"", ""BlocksetEntry"", ""Block"" WHERE ""FilesetEntry"".""FileID"" = ""File"".""ID"" AND ""File"".""BlocksetID"" = ""BlocksetEntry"".""BlocksetID"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" GROUP BY ""Block"".""VolumeID"" ";
var inactive = @"SELECT 0 AS ""ActiveSize"", SUM(""Size"") AS ""InactiveSize"", ""VolumeID"" AS ""VolumeID"", 0 AS ""SortScantime"" FROM ""DeletedBlock"" GROUP BY ""VolumeID"" ";
var combined = active + " UNION " + inactive;
var collected = @"SELECT ""VolumeID"" AS ""VolumeID"", SUM(""ActiveSize"") AS ""ActiveSize"", SUM(""InactiveSize"") AS ""InactiveSize"", MAX(""SortScantime"") AS ""SortScantime"" FROM (" + combined + @") GROUP BY ""VolumeID"" ";
var createtable = @"CREATE TEMPORARY TABLE """ + tmptablename + @""" AS " + collected;
using (var cmd = m_connection.CreateCommand())
{
cmd.Transaction = transaction;
try
{
cmd.ExecuteNonQuery(createtable);
var res = new List();
using (var rd = cmd.ExecuteReader(string.Format(@"SELECT ""A"".""Name"", ""B"".""ActiveSize"", ""B"".""InactiveSize"" FROM ""Remotevolume"" A, ""{0}"" B WHERE ""A"".""ID"" = ""B"".""VolumeID"" ORDER BY ""B"".""SortScantime"" ASC ", tmptablename)))
while (rd.Read())
res.Add(new VolumeUsage(rd.GetValue(0).ToString(), Convert.ToInt64(rd.GetValue(1)) + Convert.ToInt64(rd.GetValue(2)), Convert.ToInt64(rd.GetValue(2))));
return res;
}
finally
{
try { cmd.ExecuteNonQuery(string.Format(@"DROP TABLE ""{0}"" ", tmptablename)); }
catch { }
}
}
}
public interface ICompactReport
{
IEnumerable DeleteableVolumes { get; }
IEnumerable CompactableVolumes { get; }
bool ShouldReclaim { get; }
bool ShouldCompact { get; }
void ReportCompactData(CommunicationStatistics stat);
}
private class CompactReport : ICompactReport
{
private IEnumerable m_report;
private IEnumerable m_cleandelete;
private IEnumerable m_wastevolumes;
private IEnumerable m_smallvolumes;
private long m_deletablevolumes;
private long m_wastedspace;
private long m_smallspace;
private long m_maxwastesize;
private long m_volsize;
public CompactReport(long volsize, long maxwastesize, long volsizetolerance, IEnumerable report)
{
m_report = report;
m_cleandelete = from n in m_report where n.VolumeSize <= n.WastedSize select n;
m_wastevolumes = from n in m_report where n.WastedSize > volsizetolerance && !m_cleandelete.Contains(n) select n;
m_smallvolumes = from n in m_report where n.VolumeSize - volsizetolerance < volsize && !m_cleandelete.Contains(n) select n;
m_maxwastesize = maxwastesize;
m_volsize = volsize;
m_deletablevolumes = m_cleandelete.Count();
m_wastedspace = m_wastevolumes.Aggregate(0L, (a,x) => a + x.WastedSize);
m_smallspace = m_smallvolumes.Aggregate(0L, (a,x) => a + x.VolumeSize);
}
public void ReportCompactData(CommunicationStatistics stat)
{
stat.LogMessage("Found {0} fully deletable volume(s)", m_deletablevolumes);
stat.LogMessage("Found {0} small volumes(s) with a total size of {1}", m_smallvolumes.Count(), Library.Utility.Utility.FormatSizeString(m_smallspace));
stat.LogMessage("Found {0} volume(s) with a total of {1} wasted space", m_wastevolumes.Count(), Library.Utility.Utility.FormatSizeString(m_wastedspace));
if (m_deletablevolumes > 0)
stat.LogMessage("Compacting because there are {0} fully deletable volume(s)", m_deletablevolumes);
else if (m_wastedspace > m_maxwastesize && m_wastevolumes.Count() >= 2)
stat.LogMessage("Compacting because there are {0} wasted space and the limit is {1}", Library.Utility.Utility.FormatSizeString(m_wastedspace), Library.Utility.Utility.FormatSizeString(m_maxwastesize));
else if (m_smallspace > m_volsize)
stat.LogMessage("Compacting because there are {0} in small volumes and the volume size is {1}", Library.Utility.Utility.FormatSizeString(m_smallspace), Library.Utility.Utility.FormatSizeString(m_volsize));
else
stat.LogMessage("Not compacting");
}
public bool ShouldReclaim
{
get
{
return m_deletablevolumes > 0;
}
}
public bool ShouldCompact
{
get
{
return m_wastedspace > m_maxwastesize || m_smallspace > m_volsize;
}
}
public IEnumerable DeleteableVolumes
{
get { return from n in m_cleandelete select n.Name; }
}
public IEnumerable CompactableVolumes
{
get
{
//The order matters, we compact old volumes together first,
// as we anticipate old data will stay around, where never data
// is more likely to be discarded again
return m_wastevolumes.Union(m_smallvolumes).Select(x => x.Name).Distinct();
}
}
}
public ICompactReport GetCompactReport(long volsize, long maxwastesize, long volsizetolerance, System.Data.IDbTransaction transaction)
{
return new CompactReport(volsize, maxwastesize, volsizetolerance, GetWastedSpaceReport(transaction).ToList());
}
public interface IBlockQuery : IDisposable
{
bool UseBlock(string hash, long size);
}
private class BlockQuery : IBlockQuery
{
private System.Data.IDbCommand m_command;
private HashDatabaseProtector m_lookup;
public BlockQuery(System.Data.IDbConnection con, Options options, System.Data.IDbTransaction transaction)
{
m_command = con.CreateCommand();
m_command.Transaction = transaction;
if (options.BlockHashLookupMemory > 0)
{
m_lookup = new HashDatabaseProtector(HASH_GUESS_SIZE, (ulong)options.BlockHashLookupMemory);
using(var reader = m_command.ExecuteReader(@"SELECT ""Hash"", ""Size"" FROM ""Block"" "))
while (reader.Read())
{
var hash = reader.GetValue(0).ToString();
var size = Convert.ToInt64(reader.GetValue(1));
m_lookup.Add(HashPrefixLookup.DecodeBase64Hash(hash), hash, size);
}
}
m_command.Parameters.Clear();
m_command.CommandText = @"SELECT ""VolumeID"" FROM ""Block"" WHERE ""Hash"" = ? AND ""Size"" = ? ";
m_command.AddParameters(2);
}
public bool UseBlock(string hash, long size)
{
if (m_lookup != null)
{
long nsize;
switch(m_lookup.HasValue(HashPrefixLookup.DecodeBase64Hash(hash), hash, out nsize))
{
case HashLookupResult.Found:
if (nsize == size)
return true;
break;
case HashLookupResult.NotFound:
return false;
}
}
m_command.SetParameterValue(0, hash);
m_command.SetParameterValue(1, size);
var r = m_command.ExecuteScalar();
return r != null && r != DBNull.Value;
}
public void Dispose()
{
if (m_lookup != null)
try { m_lookup.Dispose(); }
finally { m_lookup = null; }
if (m_command != null)
try { m_command.Dispose(); }
finally { m_command = null; }
}
}
///
/// Builds a lookup table to enable faster response to block queries
///
/// The name of the volume to prepare for
public IBlockQuery CreateBlockQueryHelper(Options options, System.Data.IDbTransaction transaction)
{
return new BlockQuery(m_connection, options, transaction);
}
public void MoveBlockToNewVolume(string hash, long size, long volumeID, System.Data.IDbTransaction tr)
{
m_moveBlockToNewVolumeCommand.SetParameterValue(0, volumeID);
m_moveBlockToNewVolumeCommand.SetParameterValue(1, hash);
m_moveBlockToNewVolumeCommand.SetParameterValue(2, size);
m_moveBlockToNewVolumeCommand.Transaction = tr;
var c = m_moveBlockToNewVolumeCommand.ExecuteNonQuery();
if (c != 1)
throw new Exception("Unexpected update result");
}
///
/// Calculates the sequence in which files should be deleted based on their releations.
///
/// The deletable volumes.
/// Block volumes slated for deletion.
public IEnumerable GetDeletableVolumes(IEnumerable deleteableVolumes, System.Data.IDbTransaction transaction)
{
using(var cmd = m_connection.CreateCommand())
{
// Although the generated index volumes are always in pairs,
// this code handles many-to-many relations between
// index files and block volumes, should this be added later
var lookupBlock = new Dictionary>();
var lookupIndexfiles = new Dictionary>();
cmd.Transaction = transaction;
using(var rd = cmd.ExecuteReader(@"SELECT ""C"".""Name"", ""B"".""Name"", ""B"".""Hash"", ""B"".""Size"" FROM ""IndexBlockLink"" A, ""RemoteVolume"" B, ""RemoteVolume"" C WHERE ""A"".""IndexVolumeID"" = ""B"".""ID"" AND ""A"".""BlockVolumeID"" = ""C"".""ID"" "))
while(rd.Read())
{
var name = rd.GetValue(0).ToString();
List indexfileList;
if (!lookupBlock.TryGetValue(name, out indexfileList))
{
indexfileList = new List();
lookupBlock.Add(name, indexfileList);
}
var v = new RemoteVolume(rd.GetValue(1).ToString(), rd.GetValue(2).ToString(), Convert.ToInt64(rd.GetValue(3)));
indexfileList.Add(v);
List blockList;
if (!lookupIndexfiles.TryGetValue(v.Name, out blockList))
{
blockList = new List();
lookupIndexfiles.Add(v.Name, blockList);
}
blockList.Add(name);
}
foreach(var r in deleteableVolumes.Distinct())
{
// Return the input
yield return r;
List indexfileList;
if (lookupBlock.TryGetValue(r.Name, out indexfileList))
foreach(var sh in indexfileList)
{
List backref;
if (lookupIndexfiles.TryGetValue(sh.Name, out backref))
{
//If this is the last reference,
// remove the index file as well
if (backref.Remove(r.Name) && backref.Count == 0)
yield return sh;
}
}
}
}
}
}
}