Added option to log operations and expected backend state for debugging purposes.

git-svn-id: https://duplicati.googlecode.com/svn/trunk@1098 59da171f-624f-0410-aa54-27559c288bec
This commit is contained in:
kenneth@hexad.dk
2012-01-15 22:15:22 +00:00
parent 4494371de8
commit 72ff1b1bc3
10 changed files with 665 additions and 13 deletions
@@ -95,6 +95,7 @@
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
</Compile>
<Compile Include="IBackendInteraction.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Strings\CommandLineArgument.resx">
@@ -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
{
/// <summary>
/// An interface for logging interactions with the backend
/// </summary>
public interface IBackendInteraction
{
/// <summary>
/// Signals the start of a operation on the backend, such as cleanup or backup
/// </summary>
/// <param name='name'>The name of the operation</param>
void BeginOperation(string name);
/// <summary>
/// Signals the end of the operation
/// </summary>
void EndOperation();
/// <summary>
/// Registers a get operation
/// </summary>
/// <param name='entry'>The entry involved</param>
/// <param name='success'>True if the operation succeeeded, false otherwise</param>
/// <param name='errorMessage'>An error message if the operation did not succeeed</param>
void RegisterGet(IFileEntry entry, bool success, String errorMessage);
/// <summary>
/// Registers a put operation
/// </summary>
/// <param name='entry'>The entry involved</param>
/// <param name='success'>True if the operation succeeeded, false otherwise</param>
/// <param name='errorMessage'>An error message if the operation did not succeeed</param>
void RegisterPut(IFileEntry entry, bool success, String errorMessage);
/// <summary>
/// Registers a delete operation
/// </summary>
/// <param name='entry'>The entry involved</param>
/// <param name='success'>True if the operation succeeeded, false otherwise</param>
/// <param name='errorMessage'>An error message if the operation did not succeeed</param>
void RegisterDelete(IFileEntry entry, bool success, String errorMessage);
/// <summary>
/// Registers a create folder operation
/// </summary>
/// <param name='success'>True if the operation succeeeded, false otherwise</param>
/// <param name='errorMessage'>An error message if the operation did not succeeed</param>
void RegisterCreateFolder(bool success, String errorMessage);
/// <summary>
/// Registers a list operation
/// </summary>
/// <param name='files'>The entries returned, if the operation suceeded, null otherwise</param>
/// <param name='success'>True if the operation succeeeded, false otherwise</param>
/// <param name='errorMessage'>An error message if the operation did not succeeed</param>
void RegisterList(List<Duplicati.Library.Interface.IFileEntry> files, bool success, String errorMessage);
}
}
+67 -10
View File
@@ -170,7 +170,12 @@ namespace Duplicati.Library.Main
/// Gets the communication statistics assigned to the wrapper
/// </summary>
public CommunicationStatistics Statistics { get { return m_statistics; } }
/// <summary>
/// Recorder for remote operations
/// </summary>
private Duplicati.Library.Interface.IBackendInteraction m_backendInterfaceLogger;
/// <summary>
/// Class to represent hash failures
/// </summary>
@@ -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<Duplicati.Library.Interface.IFileEntry> 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
@@ -109,6 +109,8 @@
<DependentUpon>RSyncDir.resx</DependentUpon>
</Compile>
<Compile Include="VerificationFile.cs" />
<Compile Include="StateVerification\StateDatabase.cs" />
<Compile Include="StateVerification\LogEntryClasses.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Compression\Duplicati.Library.Compression.csproj">
@@ -191,4 +193,7 @@
<Target Name="AfterBuild">
</Target>
-->
<ItemGroup>
<Folder Include="StateVerification\" />
</ItemGroup>
</Project>
+2 -1
View File
@@ -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))
+16
View File
@@ -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
/// </summary>
public bool NoConnectionReuse { get { return GetBool("no-connection-reuse"); } }
/// <summary>
/// The path to a log database file, or null
/// </summary>
public string Backendlogdatabase
{
get
{
string value;
if (!m_options.TryGetValue("backend-log-database", out value))
value = null;
return value;
}
}
/// <summary>
/// Gets a list of modules, the key indicates if they are loaded
/// </summary>
@@ -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
{
/// <summary>
/// Represents a single invocation of Duplicati
/// </summary>
[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<LogEntryBase> Entries;
}
/// <summary>
/// The base for all operations performed on a remote folder
/// </summary>
[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;
}
/// <summary>
/// A single remote file
/// </summary>
[XmlType(TypeName="file")]
public class FileItem
{
[XmlAttribute("name")]
public string Name;
[XmlAttribute("size")]
public long Size;
[XmlAttribute("modified")]
public DateTime Modified;
}
/// <summary>
/// Representation of the LIST operation
/// </summary>
[XmlType(TypeName="list")]
public class ListLogEntry : LogEntryBase
{
[XmlElement("contents")]
public List<FileItem> Files;
}
/// <summary>
/// Representation of the file based entries
/// </summary>
public abstract class FileEntryType : LogEntryBase
{
[XmlElement("file")]
public FileItem File;
}
/// <summary>
/// Representation of a DELETE operation
/// </summary>
[XmlType(TypeName="delete")]
public class DeleteLogEntry : FileEntryType
{
}
/// <summary>
/// Representation of the GET operation
/// </summary>
[XmlType(TypeName="get")]
public class GetLogEntry : FileEntryType
{
}
/// <summary>
/// Representation of the PUT operation
/// </summary>
[XmlType(TypeName="put")]
public class PutLogEntry : FileEntryType
{
}
/// <summary>
/// Representation of the CREATEFOLDER operation
/// </summary>
[XmlType(TypeName="createfolder")]
public class CreateFolderLogEntry : LogEntryBase
{
}
}
@@ -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
{
/// <summary>
/// Possible backend operations
/// </summary>
public enum BackendOperation
{
Get,
Put,
List,
Delete,
CreateFolder
}
/// <summary>
/// Maintains local state information about the expected remote directory contents
/// </summary>
public class StateDatabase : Duplicati.Library.Interface.IBackendInteraction, IEnumerable<LogEntryBase>
{
private string m_filename;
private List<InvocationCollection> 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<InvocationCollection>), new XmlRootAttribute("duplicati-state-db"));
using (System.IO.FileStream fs = System.IO.File.OpenRead(m_filename))
m_invocations = (List<InvocationCollection>)s.Deserialize(fs);
}
else
{
m_invocations = new List<InvocationCollection>();
}
m_current = new InvocationCollection();
m_current.BeginTime = DateTime.Now;
m_current.Entries = new List<LogEntryBase>();
}
public void BeginOperation(string name)
{
m_current.Operation = name;
}
public void EndOperation()
{
m_current.EndTime = DateTime.Now;
Save();
}
private class ItemEnumerator : IEnumerator<LogEntryBase>
{
private List<InvocationCollection> m_invocations;
private InvocationCollection m_current;
private int m_i;
private int m_j;
private bool m_isCurrentAdded;
public ItemEnumerator(List<InvocationCollection> 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<FileItem> 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<FileItem> GetAndVerifyCurrentState()
{
//First we find the most recent LIST operation
List<FileItem> last_list = new List<FileItem>();
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<FileItem> 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<FileItem>(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<InvocationCollection>), 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<Duplicati.Library.Interface.IFileEntry> files, bool success, String errorMessage)
{
var ent = new ListLogEntry() {
Operation = BackendOperation.List,
LogMessage = errorMessage,
Success = success,
Files = files == null ? null : new List<FileItem>()
};
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<LogEntryBase> GetEnumerator ()
{
return new ItemEnumerator(m_invocations, m_current);
}
#endregion
#region IEnumerable implementation
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator ()
{
return GetEnumerator();
}
#endregion
}
}
+19 -1
View File
@@ -1229,5 +1229,23 @@ namespace Duplicati.Library.Main.Strings {
return ResourceManager.GetString("VssexcludewritersShort", resourceCulture);
}
}
}
/// <summary>
/// Looks up a localized string similar to Database log of remote operations.
/// </summary>
internal static string BackendlogdatabaseShort {
get {
return ResourceManager.GetString("BackendlogdatabaseShort", resourceCulture);
}
}
/// <summary>
/// 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..
/// </summary>
internal static string BackendlogdatabaseLong {
get {
return ResourceManager.GetString("BackendlogdatabaseLong", resourceCulture);
}
}
}
}
+7 -1
View File
@@ -505,6 +505,12 @@
<value>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</value>
</data>
<data name="UploadUnchangedBackupsShort" xml:space="preserve">
<value>Upload empty backup files </value>
<value>Upload empty backup files</value>
</data>
<data name="BackendlogdatabaseShort" xml:space="preserve">
<value>Database log of remote operations</value>
</data>
<data name="BackendlogdatabaseLong" xml:space="preserve">
<value>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.</value>
</data>
</root>