Files
duplicati/Duplicati/Library/Main/ForestHash/Database/LocalDeleteDatabase.cs
T
2013-04-08 22:28:28 +02:00

494 lines
20 KiB
C#

// 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.ForestHash.Database
{
public class LocalDeleteDatabase : LocalDatabase
{
/// <summary>
/// An approximate size of a hash-string in memory (44 chars * 2 for unicode + 8 bytes for pointer = 104)
/// </summary>
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);
}
/// <summary>
/// Deletes all but n backups from the database and remote storage.
/// </summary>
/// <param name="keep">The number of backups to keep</param>
/// <param name="allowRemovingLast">If set to <c>true</c> allow removing last backup, otherwise at least one backup is kept</param>
public IEnumerable<string> DeleteAllButN(int keep, bool allowRemovingLast, CommunicationStatistics stat, FhOptions options, System.Data.IDbTransaction transaction)
{
IEnumerable<string> 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-" + 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;
}
/// <summary>
/// Deletes all but n backups from the database and remote storage.
/// </summary>
/// <param name="keep">The number of backups to keep</param>
/// <param name="allowRemovingLast">If set to <c>true</c> allow removing last backup, otherwise at least one backup is kept</param>
public IEnumerable<string> DeleteOlderThan(DateTime limit, bool allowRemovingLast, CommunicationStatistics stat, FhOptions options, System.Data.IDbTransaction transaction)
{
if (limit.Kind == DateTimeKind.Unspecified)
throw new Exception("Time must be either UTC or Local");
IEnumerable<string> 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-" + 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;
}
/// <summary>
/// Deletes the specified filesets from the database and remote storage.
/// </summary>
/// <param name="dates">The filesets to remove</param>
/// <param name="allowRemovingLast">If set to <c>true</c> allow removing last backup, otherwise at least one backup is kept</param>
public IEnumerable<string> DeleteFilesets(string filesets, bool allowRemovingLast, CommunicationStatistics stat, FhOptions options, System.Data.IDbTransaction transaction)
{
IEnumerable<string> result;
using(var cmd = m_connection.CreateCommand())
{
cmd.Transaction = transaction;
long keepFilesetId = -1;
if (!allowRemovingLast)
keepFilesetId = GetLastFilesetID(cmd);
var tmptablename = "DeletedOperations-" + 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;
}
/// <summary>
/// Drops all entries related to operations listed in the table.
/// </summary>
/// <param name="cmd">The command used to execute the queries</param>
/// <param name="tmptablename">The name of a table with the operation IDs</param>
/// <returns>A list of filesets to delete</returns>
private static IEnumerable<string> 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"") ");
//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 ""ID"" FROM ""Blockset"") ");
cmd.ExecuteNonQuery(@"DELETE FROM ""Block"" WHERE ""ID"" NOT IN (SELECT DISTINCT ""ID"" FROM ""Blockset"") ");
//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<string>();
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns>A list of tuples with name, datasize, wastedbytes.</returns>
private IEnumerable<VolumeUsage> GetWastedSpaceReport(System.Data.IDbTransaction transaction)
{
var tmptablename = "UsageReport-" + Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
var active = @"SELECT SUM(""Size"") AS ""ActiveSize"", 0 AS ""InactiveSize"", ""VolumeID"" AS ""VolumeID"" FROM ""Block"" GROUP BY ""VolumeID"" ";
var inactive = @"SELECT 0 AS ""ActiveSize"", SUM(""Size"") AS ""InactiveSize"", ""VolumeID"" AS ""VolumeID"" FROM ""DeletedBlock"" GROUP BY ""VolumeID"" ";
var combined = active + " UNION " + inactive;
var collected = @"SELECT ""VolumeID"" AS ""VolumeID"", SUM(""ActiveSize"") AS ""ActiveSize"", SUM(""InactiveSize"") AS ""InactiveSize"" 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<VolumeUsage>();
using (var rd = cmd.ExecuteReader(string.Format(@"SELECT ""Remotevolume"".""Name"", ""{0}"".""ActiveSize"", ""{0}"".""InactiveSize"" FROM ""Remotevolume"", ""{0}"" WHERE ""Remotevolume"".""ID"" = ""{0}"".""VolumeID"" ", 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<string> DeleteableVolumes { get; }
IEnumerable<string> CompactableVolumes { get; }
bool ShouldReclaim { get; }
bool ShouldCompact { get; }
void ReportCompactData(CommunicationStatistics stat);
}
private class CompactReport : ICompactReport
{
private IEnumerable<VolumeUsage> m_report;
private IEnumerable<VolumeUsage> m_cleandelete;
private IEnumerable<VolumeUsage> m_wastevolumes;
private IEnumerable<VolumeUsage> 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<VolumeUsage> 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(), Utility.Utility.FormatSizeString(m_smallspace));
stat.LogMessage("Found {0} volume(s) with a total of {1} wasted space", m_wastevolumes.Count(), 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}", Utility.Utility.FormatSizeString(m_wastedspace), 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}", Utility.Utility.FormatSizeString(m_smallspace), 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_wastevolumes.Count() >= 2) || m_smallspace > m_volsize;
}
}
public IEnumerable<string> DeleteableVolumes
{
get { return from n in m_cleandelete select n.Name; }
}
public IEnumerable<string> 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<string, long> m_lookup;
public BlockQuery(System.Data.IDbConnection con, FhOptions options, System.Data.IDbTransaction transaction)
{
m_command = con.CreateCommand();
m_command.Transaction = transaction;
if (options.FhBlockHashLookupMemory > 0)
{
m_lookup = new HashDatabaseProtector<string, long>(HASH_GUESS_SIZE, (ulong)options.FhBlockHashLookupMemory);
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; }
}
}
/// <summary>
/// Builds a lookup table to enable faster response to block queries
/// </summary>
/// <param name="volumename">The name of the volume to prepare for</param>
public IBlockQuery CreateBlockQueryHelper(FhOptions 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");
}
/// <summary>
/// Calculates the sequence in which files should be deleted based on their releations.
/// </summary>
/// <returns>The deletable volumes.</returns>
/// <param name="deleteableVolumes">Block volumes slated for deletion.</param>
public IEnumerable<IRemoteVolume> GetDeletableVolumes(IEnumerable<IRemoteVolume> deleteableVolumes, System.Data.IDbTransaction transaction)
{
using(var cmd = m_connection.CreateCommand())
{
// Although the generated shadow volumes are always in pairs,
// this code handles many-to-many relations between
// shadow files and block volumes, should this be added later
var lookupBlock = new Dictionary<string, List<IRemoteVolume>>();
var lookupShadow = new Dictionary<string, List<string>>();
cmd.Transaction = transaction;
using(var rd = cmd.ExecuteReader(@"SELECT ""C"".""Name"", ""B"".""Name"", ""B"".""Hash"", ""B"".""Size"" FROM ""ShadowBlockLink"" A, ""RemoteVolume"" B, ""RemoteVolume"" C WHERE ""A"".""ShadowVolumeID"" = ""B"".""ID"" AND ""A"".""BlockVolumeID"" = ""C"".""ID"" "))
while(rd.Read())
{
var name = rd.GetValue(0).ToString();
List<IRemoteVolume> shadowList;
if (!lookupBlock.TryGetValue(name, out shadowList))
{
shadowList = new List<IRemoteVolume>();
lookupBlock.Add(name, shadowList);
}
var v = new RemoteVolume(rd.GetValue(1).ToString(), rd.GetValue(2).ToString(), Convert.ToInt64(rd.GetValue(3)));
shadowList.Add(v);
List<string> blockList;
if (!lookupShadow.TryGetValue(v.Name, out blockList))
{
blockList = new List<string>();
lookupShadow.Add(v.Name, blockList);
}
blockList.Add(name);
}
foreach(var r in deleteableVolumes.Distinct())
{
// Return the input
yield return r;
List<IRemoteVolume> shadowList;
if (lookupBlock.TryGetValue(r.Name, out shadowList))
foreach(var sh in shadowList)
{
List<string> backref;
if (lookupShadow.TryGetValue(sh.Name, out backref))
{
//If this is the last reference,
// remove the shadow file as well
if (backref.Remove(r.Name) && backref.Count == 0)
yield return sh;
}
}
}
}
}
}
}