diff --git a/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj b/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj
index 671c85e7b..7c6d31b6e 100644
--- a/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj
+++ b/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj
@@ -95,6 +95,7 @@
True
True
+
diff --git a/Duplicati/Library/Interface/IBackendInteraction.cs b/Duplicati/Library/Interface/IBackendInteraction.cs
new file mode 100644
index 000000000..1d81cf864
--- /dev/null
+++ b/Duplicati/Library/Interface/IBackendInteraction.cs
@@ -0,0 +1,82 @@
+#region Disclaimer / License
+// Copyright (C) 2011, Kenneth Skovhede
+// http://www.hexad.dk, opensource@hexad.dk
+//
+// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+#endregion
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Duplicati.Library.Interface
+{
+ ///
+ /// An interface for logging interactions with the backend
+ ///
+ public interface IBackendInteraction
+ {
+ ///
+ /// Signals the start of a operation on the backend, such as cleanup or backup
+ ///
+ /// The name of the operation
+ void BeginOperation(string name);
+
+ ///
+ /// Signals the end of the operation
+ ///
+ void EndOperation();
+
+ ///
+ /// Registers a get operation
+ ///
+ /// The entry involved
+ /// True if the operation succeeeded, false otherwise
+ /// An error message if the operation did not succeeed
+ void RegisterGet(IFileEntry entry, bool success, String errorMessage);
+
+ ///
+ /// Registers a put operation
+ ///
+ /// The entry involved
+ /// True if the operation succeeeded, false otherwise
+ /// An error message if the operation did not succeeed
+ void RegisterPut(IFileEntry entry, bool success, String errorMessage);
+
+ ///
+ /// Registers a delete operation
+ ///
+ /// The entry involved
+ /// True if the operation succeeeded, false otherwise
+ /// An error message if the operation did not succeeed
+ void RegisterDelete(IFileEntry entry, bool success, String errorMessage);
+
+ ///
+ /// Registers a create folder operation
+ ///
+ /// True if the operation succeeeded, false otherwise
+ /// An error message if the operation did not succeeed
+ void RegisterCreateFolder(bool success, String errorMessage);
+
+ ///
+ /// Registers a list operation
+ ///
+ /// The entries returned, if the operation suceeded, null otherwise
+ /// True if the operation succeeeded, false otherwise
+ /// An error message if the operation did not succeeed
+ void RegisterList(List files, bool success, String errorMessage);
+ }
+}
+
diff --git a/Duplicati/Library/Main/BackendWrapper.cs b/Duplicati/Library/Main/BackendWrapper.cs
index 3f9dde2b2..3e338507c 100644
--- a/Duplicati/Library/Main/BackendWrapper.cs
+++ b/Duplicati/Library/Main/BackendWrapper.cs
@@ -170,7 +170,12 @@ namespace Duplicati.Library.Main
/// Gets the communication statistics assigned to the wrapper
///
public CommunicationStatistics Statistics { get { return m_statistics; } }
-
+
+ ///
+ /// Recorder for remote operations
+ ///
+ private Duplicati.Library.Interface.IBackendInteraction m_backendInterfaceLogger;
+
///
/// Class to represent hash failures
///
@@ -223,7 +228,13 @@ namespace Duplicati.Library.Main
if (!string.IsNullOrEmpty(m_options.SignatureCachePath) && !System.IO.Directory.Exists(m_options.SignatureCachePath))
System.IO.Directory.CreateDirectory(m_options.SignatureCachePath);
-
+
+ if (!string.IsNullOrEmpty(m_options.Backendlogdatabase))
+ {
+ m_backendInterfaceLogger = new StateVerification.StateDatabase(m_options.Backendlogdatabase, statistics);
+ m_backendInterfaceLogger.BeginOperation(m_options.MainAction.ToString());
+ }
+
m_async = m_options.AsynchronousUpload;
if (m_async)
{
@@ -953,16 +964,23 @@ namespace Duplicati.Library.Main
{
ResetBackend();
m_statistics.AddNumberOfRemoteCalls(1);
- return m_backend.List();
+ List res = m_backend.List();
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterList(res, true, null);
+ return res;
}
- catch (System.Threading.ThreadAbortException)
+ catch (System.Threading.ThreadAbortException tex)
{
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterList(null, false, tex.ToString());
throw;
}
catch (Exception ex)
{
lastEx = ex;
m_statistics.LogRetryAttempt(ex.Message, ex);
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterList(null, false, ex.ToString());
DisposeBackend();
if (ex is Library.Interface.FolderMissingException && m_backendSupportsCreateFolder && m_options.AutocreateFolders)
@@ -990,15 +1008,21 @@ namespace Duplicati.Library.Main
m_statistics.AddNumberOfRemoteCalls(1);
m_backend.Delete(remote.Filename);
lastEx = null;
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterDelete(remote.Fileentry, true, null);
}
- catch (System.Threading.ThreadAbortException)
+ catch (System.Threading.ThreadAbortException tex)
{
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterDelete(remote.Fileentry, false, tex.ToString());
throw;
}
catch (Exception ex)
{
lastEx = ex;
m_statistics.LogRetryAttempt(ex.Message, ex);
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterDelete(remote.Fileentry, false, ex.ToString());
DisposeBackend();
retries--;
@@ -1206,6 +1230,15 @@ namespace Duplicati.Library.Main
lastEx = null;
tempfile.Protected = true; //Don't delete it
+
+ if (m_backendInterfaceLogger != null)
+ {
+
+ if (remote is ManifestEntry)
+ m_backendInterfaceLogger.RegisterGet(remote.Fileentry, true, System.IO.File.ReadAllText(tempfile));
+ else
+ m_backendInterfaceLogger.RegisterGet(remote.Fileentry, true, null);
+ }
}
finally
{
@@ -1219,14 +1252,18 @@ namespace Duplicati.Library.Main
}
}
}
- catch (System.Threading.ThreadAbortException)
+ catch (System.Threading.ThreadAbortException tex)
{
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterGet(remote.Fileentry, false, tex.ToString());
throw;
}
catch (Exception ex)
{
lastEx = ex;
m_statistics.LogRetryAttempt(ex.Message, ex);
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterGet(remote.Fileentry, false, ex.ToString());
DisposeBackend();
retries--;
@@ -1250,7 +1287,8 @@ namespace Duplicati.Library.Main
{
string remotename = remote.Filename;
m_statusmessage = string.Format(Strings.BackendWrapper.StatusMessageUploading, remotename, Utility.Utility.FormatSizeString(new System.IO.FileInfo(filename).Length));
-
+ Duplicati.Library.Interface.IFileEntry log_fe = new Duplicati.Library.Interface.FileEntry(remote.Filename, remote.Filesize, DateTime.Now, DateTime.Now);
+
try
{
int retries = m_options.NumberOfRetries;
@@ -1329,13 +1367,19 @@ namespace Duplicati.Library.Main
success = true;
lastEx = null;
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterPut(log_fe, true, null);
}
- catch (System.Threading.ThreadAbortException)
+ catch (System.Threading.ThreadAbortException tex)
{
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterPut(log_fe, false, tex.ToString());
throw;
}
catch (Exception ex)
{
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterPut(log_fe, false, ex.ToString());
DisposeBackend();
//Even if we can create the folder, we still count it as an error to prevent trouble with backends
@@ -1391,8 +1435,18 @@ namespace Duplicati.Library.Main
private void CreateFolderInternal()
{
- ResetBackend();
- (m_backend as Library.Interface.IBackend_v2).CreateFolder();
+ try
+ {
+ ResetBackend();
+ (m_backend as Library.Interface.IBackend_v2).CreateFolder();
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterCreateFolder(true, null);
+ } catch (Exception ex) {
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.RegisterCreateFolder(false, ex.ToString());
+
+ throw;
+ }
}
public void CreateFolder()
@@ -1624,6 +1678,9 @@ namespace Duplicati.Library.Main
ProtectedInvoke("DisposeInternal");
if (m_encryption != null)
m_encryption.Dispose();
+
+ if (m_backendInterfaceLogger != null)
+ m_backendInterfaceLogger.EndOperation();
}
#endregion
diff --git a/Duplicati/Library/Main/Duplicati.Library.Main.csproj b/Duplicati/Library/Main/Duplicati.Library.Main.csproj
index a0c4ab784..02770319a 100644
--- a/Duplicati/Library/Main/Duplicati.Library.Main.csproj
+++ b/Duplicati/Library/Main/Duplicati.Library.Main.csproj
@@ -109,6 +109,8 @@
RSyncDir.resx
+
+
@@ -191,4 +193,7 @@
-->
+
+
+
\ No newline at end of file
diff --git a/Duplicati/Library/Main/Interface.cs b/Duplicati/Library/Main/Interface.cs
index d8c4fecec..88fceacd8 100644
--- a/Duplicati/Library/Main/Interface.cs
+++ b/Duplicati/Library/Main/Interface.cs
@@ -335,7 +335,8 @@ namespace Duplicati.Library.Main
m_asyncReserved = ASYNC_RESERVED;
m_allowUploadProgress = false;
}
-
+
+ //Unused, but triggers errors in the encryption setup here
Library.Interface.IEncryption encryptionModule = m_options.NoEncryption ? null : DynamicLoader.EncryptionLoader.GetModule(m_options.EncryptionModule, m_options.Passphrase, m_options.RawOptions);
using (new Logging.Timer("Backup from " + string.Join(";", sources) + " to " + m_backend))
diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs
index 665f5c930..bccce1a06 100644
--- a/Duplicati/Library/Main/Options.cs
+++ b/Duplicati/Library/Main/Options.cs
@@ -203,6 +203,7 @@ namespace Duplicati.Library.Main
new CommandLineArgument("allow-sleep", CommandLineArgument.ArgumentType.Boolean, Strings.Options.AllowsleepShort, Strings.Options.AllowsleepShort, "false"),
new CommandLineArgument("no-connection-reuse", CommandLineArgument.ArgumentType.Boolean, Strings.Options.NoconnectionreuseShort, Strings.Options.NoconnectionreuseLong, "false"),
+ new CommandLineArgument("backend-log-database", CommandLineArgument.ArgumentType.Path, Strings.Options.BackendlogdatabaseShort, Strings.Options.BackendlogdatabaseLong),
});
}
}
@@ -978,6 +979,21 @@ namespace Duplicati.Library.Main
///
public bool NoConnectionReuse { get { return GetBool("no-connection-reuse"); } }
+ ///
+ /// The path to a log database file, or null
+ ///
+ public string Backendlogdatabase
+ {
+ get
+ {
+ string value;
+ if (!m_options.TryGetValue("backend-log-database", out value))
+ value = null;
+
+ return value;
+ }
+ }
+
///
/// Gets a list of modules, the key indicates if they are loaded
///
diff --git a/Duplicati/Library/Main/StateVerification/LogEntryClasses.cs b/Duplicati/Library/Main/StateVerification/LogEntryClasses.cs
new file mode 100644
index 000000000..17f2c75b7
--- /dev/null
+++ b/Duplicati/Library/Main/StateVerification/LogEntryClasses.cs
@@ -0,0 +1,132 @@
+#region Disclaimer / License
+// Copyright (C) 2011, Kenneth Skovhede
+// http://www.hexad.dk, opensource@hexad.dk
+//
+// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+#endregion
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Xml.Serialization;
+
+namespace Duplicati.Library.Main.StateVerification
+{
+ ///
+ /// Represents a single invocation of Duplicati
+ ///
+ [XmlType(TypeName="collection")]
+ [XmlRootAttribute(ElementName="duplicati-state-db")]
+ [Serializable()]
+ public class InvocationCollection
+ {
+ [XmlAttribute("begin")]
+ public DateTime BeginTime;
+ [XmlAttribute("end")]
+ public DateTime EndTime;
+ [XmlAttribute("operation")]
+ public string Operation;
+
+ [XmlElement("events")]
+ [XmlElement("list", typeof(ListLogEntry))]
+ [XmlElement("put", typeof(PutLogEntry))]
+ [XmlElement("get", typeof(GetLogEntry))]
+ [XmlElement("delete", typeof(DeleteLogEntry))]
+ [XmlElement("createfolder", typeof(CreateFolderLogEntry))]
+ public List Entries;
+ }
+
+ ///
+ /// The base for all operations performed on a remote folder
+ ///
+ [Serializable()]
+ public abstract class LogEntryBase
+ {
+ [XmlAttribute("op")]
+ public BackendOperation Operation;
+ [XmlElement("message")]
+ public string LogMessage;
+ [XmlAttribute("success")]
+ public bool Success;
+ [XmlAttribute("timestamp")]
+ public DateTime Timestamp = DateTime.Now;
+ }
+
+ ///
+ /// A single remote file
+ ///
+ [XmlType(TypeName="file")]
+ public class FileItem
+ {
+ [XmlAttribute("name")]
+ public string Name;
+ [XmlAttribute("size")]
+ public long Size;
+ [XmlAttribute("modified")]
+ public DateTime Modified;
+ }
+
+ ///
+ /// Representation of the LIST operation
+ ///
+ [XmlType(TypeName="list")]
+ public class ListLogEntry : LogEntryBase
+ {
+ [XmlElement("contents")]
+ public List Files;
+ }
+
+ ///
+ /// Representation of the file based entries
+ ///
+ public abstract class FileEntryType : LogEntryBase
+ {
+ [XmlElement("file")]
+ public FileItem File;
+ }
+
+ ///
+ /// Representation of a DELETE operation
+ ///
+ [XmlType(TypeName="delete")]
+ public class DeleteLogEntry : FileEntryType
+ {
+ }
+
+ ///
+ /// Representation of the GET operation
+ ///
+ [XmlType(TypeName="get")]
+ public class GetLogEntry : FileEntryType
+ {
+ }
+
+ ///
+ /// Representation of the PUT operation
+ ///
+ [XmlType(TypeName="put")]
+ public class PutLogEntry : FileEntryType
+ {
+ }
+
+ ///
+ /// Representation of the CREATEFOLDER operation
+ ///
+ [XmlType(TypeName="createfolder")]
+ public class CreateFolderLogEntry : LogEntryBase
+ {
+ }
+}
+
diff --git a/Duplicati/Library/Main/StateVerification/StateDatabase.cs b/Duplicati/Library/Main/StateVerification/StateDatabase.cs
new file mode 100644
index 000000000..82e6e496a
--- /dev/null
+++ b/Duplicati/Library/Main/StateVerification/StateDatabase.cs
@@ -0,0 +1,334 @@
+#region Disclaimer / License
+// Copyright (C) 2011, Kenneth Skovhede
+// http://www.hexad.dk, opensource@hexad.dk
+//
+// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+#endregion
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Xml.Serialization;
+
+namespace Duplicati.Library.Main.StateVerification
+{
+ ///
+ /// Possible backend operations
+ ///
+ public enum BackendOperation
+ {
+ Get,
+ Put,
+ List,
+ Delete,
+ CreateFolder
+ }
+
+ ///
+ /// Maintains local state information about the expected remote directory contents
+ ///
+ public class StateDatabase : Duplicati.Library.Interface.IBackendInteraction, IEnumerable
+ {
+ private string m_filename;
+ private List m_invocations;
+ private InvocationCollection m_current;
+ private CommunicationStatistics m_stats;
+
+ public StateDatabase(string filename, CommunicationStatistics stats)
+ {
+ m_filename = filename;
+ m_stats = stats;
+
+ if (System.IO.File.Exists(filename))
+ {
+ XmlSerializer s = new XmlSerializer(typeof(List), new XmlRootAttribute("duplicati-state-db"));
+ using (System.IO.FileStream fs = System.IO.File.OpenRead(m_filename))
+ m_invocations = (List)s.Deserialize(fs);
+ }
+ else
+ {
+ m_invocations = new List();
+ }
+
+ m_current = new InvocationCollection();
+ m_current.BeginTime = DateTime.Now;
+ m_current.Entries = new List();
+ }
+
+ public void BeginOperation(string name)
+ {
+ m_current.Operation = name;
+ }
+
+ public void EndOperation()
+ {
+ m_current.EndTime = DateTime.Now;
+ Save();
+ }
+
+
+ private class ItemEnumerator : IEnumerator
+ {
+ private List m_invocations;
+ private InvocationCollection m_current;
+ private int m_i;
+ private int m_j;
+ private bool m_isCurrentAdded;
+
+ public ItemEnumerator(List invocations, InvocationCollection current)
+ {
+ m_invocations = invocations;
+ m_current = current;
+ m_isCurrentAdded = invocations.Count > 0 && invocations[invocations.Count - 1] == current;
+ this.Reset();
+ }
+
+ #region IEnumerator[LogEntryBase] implementation
+ public LogEntryBase Current
+ {
+ get
+ {
+ if (m_i >= m_invocations.Count + (m_isCurrentAdded ? 0 : 1))
+ return null;
+
+ InvocationCollection ic = (m_i >= m_invocations.Count) ? m_current : m_invocations[m_i];
+ if (m_j >= ic.Entries.Count)
+ return null;
+
+ return ic.Entries[m_j];
+ }
+ }
+ #endregion
+
+ #region IEnumerator implementation
+ public bool MoveNext ()
+ {
+ if (m_i >= m_invocations.Count + (m_isCurrentAdded ? 0 : 1))
+ return false;
+
+ InvocationCollection ic = (m_i >= m_invocations.Count) ? m_current : m_invocations[m_i];
+ m_j++;
+
+ if (m_j >= ic.Entries.Count)
+ {
+ m_i++;
+ m_j = -1;
+ return MoveNext();
+ }
+
+ return true;
+ }
+
+ public void Reset ()
+ {
+ m_i = 0;
+ m_j = -1;
+ }
+
+ object System.Collections.IEnumerator.Current
+ {
+ get
+ {
+ return this.Current;
+ }
+ }
+ #endregion
+
+ #region IDisposable implementation
+ public void Dispose ()
+ {
+ }
+ #endregion
+ }
+
+ private int FindFileItem(List files, string name)
+ {
+ for(int i = 0; i < files.Count; i++)
+ if (files[i].Name.Equals(name, StringComparison.InvariantCulture))
+ return i;
+
+ return -1;
+ }
+
+ public List GetAndVerifyCurrentState()
+ {
+ //First we find the most recent LIST operation
+ List last_list = new List();
+ foreach(LogEntryBase leb in this)
+ {
+ if (leb.Success)
+ {
+ if (leb.Operation == BackendOperation.List)
+ {
+ //If there was a list operation before, check if we have the same contents
+ List new_list = ((ListLogEntry)leb).Files;
+ if (last_list != null && last_list.Count != 0)
+ {
+ StringBuilder changeMessages = new StringBuilder();
+
+ foreach(FileItem fi in new_list)
+ {
+ int ix = FindFileItem(last_list, fi.Name);
+ if (ix == -1)
+ changeMessages.AppendLine("A file was added unexpectedly: " + fi.Name);
+ else
+ last_list.RemoveAt(ix);
+ }
+
+ foreach(FileItem fi in last_list)
+ changeMessages.AppendLine("A file was removed unexpectedly: " + fi.Name);
+
+ if (changeMessages.Length > 0 && m_stats != null)
+ {
+ m_stats.LogWarning("Change found in list @" + leb.Timestamp
+ + Environment.NewLine +
+ changeMessages.ToString(), null);
+ }
+ }
+
+ last_list = new List(new_list);
+ }
+
+ if (last_list != null)
+ {
+ if (leb.Operation == BackendOperation.Put)
+ {
+ //Check for overwrite
+ int ix = FindFileItem(last_list, ((PutLogEntry)leb).File.Name);
+ if (ix != -1)
+ last_list.RemoveAt(ix);
+ last_list.Add(((PutLogEntry)leb).File);
+
+ }
+ else if (leb.Operation == BackendOperation.Delete)
+ {
+ string name = ((DeleteLogEntry)leb).File.Name;
+ int ix = FindFileItem(last_list, name);
+
+ if (ix == -1)
+ {
+ if (m_stats != null)
+ m_stats.LogWarning("Delete @" + leb.Timestamp + " for file not found: " + name, null);
+ }
+ else
+ last_list.RemoveAt(ix);
+ }
+ }
+ }
+ }
+
+ return last_list;
+ }
+
+ public void Save()
+ {
+ if (m_invocations.Count == 0 || m_invocations[m_invocations.Count -1] != m_current)
+ m_invocations.Add(m_current);
+
+ XmlSerializer s = new XmlSerializer(typeof(List), new XmlRootAttribute("duplicati-state-db"));
+ using (System.IO.FileStream fs = System.IO.File.OpenWrite(m_filename))
+ s.Serialize(fs, m_invocations);
+ }
+
+ public void RegisterGet(Duplicati.Library.Interface.IFileEntry entry, bool success, String errorMessage)
+ {
+ m_current.Entries.Add(new GetLogEntry() {
+ Operation = BackendOperation.Get,
+ Success = success,
+ LogMessage = errorMessage,
+ File = new FileItem() {
+ Name = entry.Name,
+ Size = entry.Size,
+ Modified = entry.LastModification
+ }
+ });
+ }
+
+ public void RegisterDelete(Duplicati.Library.Interface.IFileEntry entry, bool success, String errorMessage)
+ {
+ m_current.Entries.Add(new DeleteLogEntry() {
+ Operation = BackendOperation.Delete,
+ Success = success,
+ LogMessage = errorMessage,
+ File = new FileItem() {
+ Name = entry.Name,
+ Size = entry.Size,
+ Modified = entry.LastModification
+ }
+ });
+ }
+
+ public void RegisterPut(Duplicati.Library.Interface.IFileEntry entry, bool success, String errorMessage)
+ {
+ m_current.Entries.Add(new PutLogEntry() {
+ Operation = BackendOperation.Put,
+ Success = success,
+ LogMessage = errorMessage,
+ File = new FileItem() {
+ Name = entry.Name,
+ Size = entry.Size,
+ Modified = entry.LastModification
+ }
+ });
+ }
+
+ public void RegisterCreateFolder(bool success, String errorMessage)
+ {
+ m_current.Entries.Add(new CreateFolderLogEntry() {
+ Operation = BackendOperation.CreateFolder,
+ Success = success,
+ LogMessage = errorMessage
+ });
+ }
+
+ public void RegisterList(List files, bool success, String errorMessage)
+ {
+ var ent = new ListLogEntry() {
+ Operation = BackendOperation.List,
+ LogMessage = errorMessage,
+ Success = success,
+ Files = files == null ? null : new List()
+ };
+
+ if (files != null)
+ foreach(var f in files)
+ ent.Files.Add(new FileItem() {
+ Name = f.Name,
+ Size = f.Size,
+ Modified = f.LastModification
+ });
+
+ m_current.Entries.Add(ent);
+
+ //As we have added the entry, we can just run the normal verification process
+ GetAndVerifyCurrentState();
+ }
+
+ #region IEnumerable[LogEntryBase] implementation
+ public IEnumerator GetEnumerator ()
+ {
+ return new ItemEnumerator(m_invocations, m_current);
+ }
+ #endregion
+
+ #region IEnumerable implementation
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator ()
+ {
+ return GetEnumerator();
+ }
+ #endregion
+ }
+}
+
diff --git a/Duplicati/Library/Main/Strings/Options.Designer.cs b/Duplicati/Library/Main/Strings/Options.Designer.cs
index a2cf2e339..57639e761 100644
--- a/Duplicati/Library/Main/Strings/Options.Designer.cs
+++ b/Duplicati/Library/Main/Strings/Options.Designer.cs
@@ -1229,5 +1229,23 @@ namespace Duplicati.Library.Main.Strings {
return ResourceManager.GetString("VssexcludewritersShort", resourceCulture);
}
}
- }
+
+ ///
+ /// Looks up a localized string similar to Database log of remote operations.
+ ///
+ internal static string BackendlogdatabaseShort {
+ get {
+ return ResourceManager.GetString("BackendlogdatabaseShort", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Use this option to specify a path to a file that will store information about backend operations. This option can be used to track down errors by comparing the backend state with the expected backend state..
+ ///
+ internal static string BackendlogdatabaseLong {
+ get {
+ return ResourceManager.GetString("BackendlogdatabaseLong", resourceCulture);
+ }
+ }
+}
}
diff --git a/Duplicati/Library/Main/Strings/Options.resx b/Duplicati/Library/Main/Strings/Options.resx
index 6f6846788..d605547af 100644
--- a/Duplicati/Library/Main/Strings/Options.resx
+++ b/Duplicati/Library/Main/Strings/Options.resx
@@ -505,6 +505,12 @@
If no files have changed, Duplicati will not upload a backup set. If the backup data is used to verify that a backup was executed, this option will make Duplicati upload a backupset even if it is empty
- Upload empty backup files
+ Upload empty backup files
+
+
+ Database log of remote operations
+
+
+ Use this option to specify a path to a file that will store information about backend operations. This option can be used to track down errors by comparing the backend state with the expected backend state.
\ No newline at end of file