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