Files
duplicati/Duplicati/Library/Main/BackendWrapper.cs
T
kenneth.skovhede@gmail.com b81d63ea73 Fixed a bug with async uploads that caused some files to no be uploaded.
Fixed a protential race condition with warning messages and file counts (cosmetic only) when using async transfers.
Spelling correction, Seperator -> Separator.
Changed so a restore operation can also supply advanced options.
Changed the format of timestamps in filenames to follow the Duplicity convention, which removes the need for --time-separator and --use-short-filenames.
Moved the URL utility into Core, to avoid compilation errors with Monodevelop.
Reorganized the runner to better support the LiveControls on all operations rather than only for backup/restore.

Update issue #210
Status: Fixed
I have now changed the time format to follow the Duplicity naming convention.
This means that all filenames are now generated with the time set as "yyyyMMddTHHmmssZ".
This format is more portable across all platforms, and easily sortable/readable.
The drawback is that the Z indicates UTC time, so if one looks at the filenames, 
the times are not local. The motivation for this is to avoid trouble
with daylight savings time offsets.

There is full backwards compatibility as long as backups have not used more than
one character for --time-separator (only possible via commandline).
This new format means that the options --time-separator and --use-short-filenames
are now deprecated and removed from the user interface. It is possible to
access them from the advanced options page, along with the --use-old-filenames option.

If anyone needs these options for some reason, let me know, otherwise they will be removed
in a later version.



git-svn-id: https://duplicati.googlecode.com/svn/trunk@523 59da171f-624f-0410-aa54-27559c288bec
2010-09-25 12:02:14 +00:00

1187 lines
49 KiB
C#

#region Disclaimer / License
// Copyright (C) 2010, 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.Main
{
/// <summary>
/// Internal class that ensures retry operations, tracks statistics and performs asyncronous operations
/// </summary>
internal class BackendWrapper : IDisposable
{
/// <summary>
/// The actual backend that this class is wrapping
/// </summary>
private Duplicati.Library.Interface.IBackend m_backend;
/// <summary>
/// The statistics gathering object
/// </summary>
private CommunicationStatistics m_statistics;
/// <summary>
/// The set of options used by Duplicati
/// </summary>
private Options m_options;
/// <summary>
/// A list of pending async operations
/// </summary>
private Queue<KeyValuePair<BackupEntryBase, string>> m_pendingOperations;
/// <summary>
/// This event is set after an item has been removed from the queue
/// </summary>
private System.Threading.AutoResetEvent m_asyncItemProcessed;
/// <summary>
/// This event is set after an item has been placed into the queue
/// </summary>
private System.Threading.AutoResetEvent m_asyncItemReady;
/// <summary>
/// A flag used to signal the termination of the async thread
/// </summary>
private volatile bool m_asyncTerminate = false;
/// <summary>
/// The lock for items in the processing queue
/// </summary>
private object m_queuelock;
/// <summary>
/// A value describing if the backend wrapper is performing asynchronous operations
/// </summary>
private bool m_async;
/// <summary>
/// The thread performing asynchronous operations
/// </summary>
private System.Threading.Thread m_workerThread;
/// <summary>
/// The exception, if any, encountered by the worker thread
/// </summary>
private Exception m_workerException;
/// <summary>
/// The number of manifests uploaded asynchronously
/// </summary>
private int m_manifestUploads = 0;
/// <summary>
/// Temporary variable for progress reporting
/// </summary>
private string m_statusmessage;
/// <summary>
/// The cache filename strategy used
/// </summary>
/// <returns>A cache filename strategy object</returns>
public static FilenameStrategy CreateCacheFilenameStrategy() { return new FilenameStrategy("dpl"); }
/// <summary>
/// The filename strategy used to generate and parse filenames
/// </summary>
private FilenameStrategy m_filenamestrategy;
/// <summary>
/// A local version of the cache filename strategy
/// </summary>
private FilenameStrategy m_cachefilenamestrategy = BackendWrapper.CreateCacheFilenameStrategy();
/// <summary>
/// A list of items that should be removed
/// </summary>
private List<BackupEntryBase> m_orphans;
/// <summary>
/// The progress reporting event
/// </summary>
public event RSync.RSyncDir.ProgressEventDelegate ProgressEvent;
/// <summary>
/// An event that is raised after an async item has been uploaded, used to pause the uploads
/// </summary>
public event EventHandler AsyncItemProcessedEvent;
/// <summary>
/// If encryption is used, this is the instance that performs it
/// </summary>
private Duplicati.Library.Interface.IEncryption m_encryption = null;
/// <summary>
/// The number of bytes added to a file when encrypted
/// </summary>
private long m_encryptionSizeOverhead = 0;
/// <summary>
/// Gets the number of bytes added to a file when encrypted and transfered
/// </summary>
public long FileSizeOverhead { get { return m_encryptionSizeOverhead; } }
/// <summary>
/// Class to represent hash failures
/// </summary>
public class HashMismathcException : Exception
{
/// <summary>
/// Default constructor, sets a generic string as the message
/// </summary>
public HashMismathcException() : base() { }
/// <summary>
/// Constructor with non-default message
/// </summary>
/// <param name="message">The exception message</param>
public HashMismathcException(string message) : base(message) { }
/// <summary>
/// Constructor with non-default message and inner exception details
/// </summary>
/// <param name="message">The exception message</param>
/// <param name="innerException">The exception that caused this exception</param>
public HashMismathcException(string message, Exception innerException) : base(message, innerException) { }
}
/// <summary>
/// Constructs a new BackendWrapper
/// </summary>
/// <param name="statistics">The statistics logging module, may be null</param>
/// <param name="backend">The url to the backend to wrap</param>
/// <param name="options">A set of backend options</param>
public BackendWrapper(CommunicationStatistics statistics, string backend, Options options)
{
m_statistics = statistics;
m_options = options;
m_filenamestrategy = new FilenameStrategy(m_options);
m_backend = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(backend, m_options.RawOptions);
if (m_backend == null)
throw new Exception(string.Format(Strings.BackendWrapper.BackendNotFoundError, m_backend));
if (!m_options.NoEncryption)
{
if (string.IsNullOrEmpty(m_options.Passphrase))
throw new Exception(Strings.BackendWrapper.PassphraseMissingError);
m_encryption = DynamicLoader.EncryptionLoader.GetModule(m_options.EncryptionModule, m_options.Passphrase, m_options.RawOptions);
m_encryptionSizeOverhead = m_encryption.SizeOverhead(m_options.VolumeSize);
}
if (m_options.AutoCleanup)
m_orphans = new List<BackupEntryBase>();
if (!string.IsNullOrEmpty(m_options.SignatureCachePath) && !System.IO.Directory.Exists(m_options.SignatureCachePath))
System.IO.Directory.CreateDirectory(m_options.SignatureCachePath);
m_async = m_options.AsynchronousUpload;
if (m_async)
{
//If we are using async operations, the entire class is actually threadsafe,
//utilizing a common exclusive lock on all operations. But the implementation does
//not prevent starvation, so it should not be called by multiple threads.
m_pendingOperations = new Queue<KeyValuePair<BackupEntryBase, string>>();
m_asyncItemProcessed = new System.Threading.AutoResetEvent(false);
m_asyncItemReady = new System.Threading.AutoResetEvent(false);
m_workerThread = new System.Threading.Thread(ProcessQueue);
m_workerThread.Name = "AsyncUploaderThread";
m_queuelock = new object();
m_workerThread.Start();
}
}
public void AddOrphan(BackupEntryBase entry)
{
if (m_orphans != null)
m_orphans.Add(entry);
else
Logging.Log.WriteMessage(string.Format(Strings.BackendWrapper.PartialFileFoundMessage, entry.Filename), Duplicati.Library.Logging.LogMessageType.Warning);
}
public ManifestEntry GetBackupSet(string timelimit)
{
if (string.IsNullOrEmpty(timelimit))
timelimit = "now";
return GetBackupSet(Core.Timeparser.ParseTimeInterval(timelimit, DateTime.Now));
}
/// <summary>
/// Returns all files in the target backend
/// </summary>
/// <returns>All files in the target backend</returns>
public List<Library.Interface.IFileEntry> List()
{
return m_backend.List();
}
/// <summary>
/// Gets the manifest entry that represents the most recent full backup, with a list of incrementals in the chain.
/// </summary>
/// <param name="timelimit">The oldest allowed time for a backup, set to DateTime.Now to get the most recent entry</param>
/// <returns>The manifest entry with incrementals that contains all changes up to the timelimit</returns>
public ManifestEntry GetBackupSet(DateTime timelimit)
{
List<ManifestEntry> backups = GetBackupSets();
if (backups.Count == 0)
throw new Exception(Strings.BackendWrapper.NoBackupsFoundError);
ManifestEntry bestFit = backups[0];
List<ManifestEntry> additions = new List<ManifestEntry>();
foreach (ManifestEntry be in backups)
if (be.Time <= timelimit)
{
bestFit = be;
additions.Clear();
foreach (ManifestEntry bex in be.Incrementals)
if (bex.Time <= timelimit)
additions.Add(bex);
}
if (bestFit.Volumes.Count == 0)
throw new Exception(Strings.BackendWrapper.FilenameParseError);
bestFit.Incrementals.Clear();
bestFit.Incrementals.AddRange(additions);
return bestFit;
}
/// <summary>
/// Parses the filelist into a list of full backups, each with a chain of incrementals attached
/// </summary>
/// <param name="files">The list of filenames found on the backend</param>
/// <returns>A list of full backups</returns>
public List<ManifestEntry> SortAndPairSets(List<Duplicati.Library.Interface.IFileEntry> files)
{
Sorter sortHelper = new Sorter();
files.Sort(sortHelper);
for(int i = 1; i < files.Count; i++)
if (files[i].Name == files[i - 1].Name)
{
if (m_statistics != null)
m_statistics.LogWarning(string.Format(Strings.BackendWrapper.DuplicateFileEntryWarning, files[i].Name), null);
files.RemoveAt(i);
i--;
}
List<ManifestEntry> incrementals = new List<ManifestEntry>();
List<ManifestEntry> fulls = new List<ManifestEntry>();
Dictionary<string, List<SignatureEntry>> signatures = new Dictionary<string, List<SignatureEntry>>();
Dictionary<string, List<ContentEntry>> contents = new Dictionary<string, List<ContentEntry>>();
//First we parse all files into their respective classes
foreach (Duplicati.Library.Interface.IFileEntry fe in files)
{
BackupEntryBase be = m_filenamestrategy.ParseFilename(fe);
if (be == null)
{
if (m_statistics != null && m_statistics.VerboseErrors && !fe.IsFolder)
m_statistics.LogWarning(string.Format(Strings.BackendWrapper.UnmatchedFilenameWarning, fe.Name), null);
continue; //Non-duplicati files
}
if (!string.IsNullOrEmpty(be.EncryptionMode) && Array.IndexOf<string>(DynamicLoader.EncryptionLoader.Keys, be.EncryptionMode) < 0)
continue;
if (be is PayloadEntryBase && Array.IndexOf<string>(DynamicLoader.CompressionLoader.Keys,((PayloadEntryBase)be).Compression) < 0)
continue;
if (be is ManifestEntry)
{
if (((ManifestEntry)be).IsFull)
fulls.Add((ManifestEntry)be);
else
incrementals.Add((ManifestEntry)be);
}
else if (be is ContentEntry)
{
string key = be.TimeString;
if (!contents.ContainsKey(key))
contents[key] = new List<ContentEntry>();
contents[key].Add((ContentEntry)be);
}
else if (be is SignatureEntry)
{
string key = be.TimeString;
if (!signatures.ContainsKey(key))
signatures[key] = new List<SignatureEntry>();
signatures[key].Add((SignatureEntry)be);
}
else
throw new Exception(string.Format(Strings.BackendWrapper.InvalidEntryTypeError, be.GetType().FullName));
}
fulls.Sort(sortHelper);
incrementals.Sort(sortHelper);
//Pair up the manifests in primary/alternate pairs
foreach(List<ManifestEntry> mfl in new List<ManifestEntry>[] {fulls, incrementals})
for (int i = 0; i < mfl.Count - 1; i++)
{
if (mfl[i].TimeString == mfl[i + 1].TimeString && mfl[i].IsPrimary != mfl[i + 1].IsPrimary)
{
if (mfl[i].IsPrimary)
{
mfl[i].Alternate = mfl[i + 1];
mfl.RemoveAt(i + 1);
}
else
{
mfl[i + 1].Alternate = mfl[i];
mfl.RemoveAt(i);
}
}
}
//Attach volumes to the full backups
for(int i = 0; i < fulls.Count; i++)
{
ManifestEntry be = fulls[i];
string key = be.TimeString;
if (contents.ContainsKey(key) && signatures.ContainsKey(key))
{
List<SignatureEntry> signature = signatures[key];
List<ContentEntry> content = contents[key];
signature.Sort(sortHelper);
content.Sort(sortHelper);
int volCount = Math.Min(content.Count, signature.Count);
for (int j = 0; j < volCount; j++)
if (signature[0].Volumenumber == (j + 1) && content[0].Volumenumber == (j + 1))
{
be.Volumes.Add(new KeyValuePair<SignatureEntry, ContentEntry>(signature[0], content[0]));
signature.RemoveAt(0);
content.RemoveAt(0);
}
}
fulls[i] = SwapManifestAlternates(be);
}
//Attach volumes to the incrementals, and attach incrementals to the fulls
int index = 0;
foreach (ManifestEntry be in incrementals)
{
string key = be.TimeString;
if (contents.ContainsKey(key) && signatures.ContainsKey(key))
{
List<SignatureEntry> signature = signatures[key];
List<ContentEntry> content = contents[key];
signature.Sort(sortHelper);
content.Sort(sortHelper);
int volCount = Math.Min(content.Count, signature.Count);
for (int j = 0; j < volCount; j++)
if (signature[0].Volumenumber == (j + 1) && content[0].Volumenumber == (j + 1))
{
be.Volumes.Add(new KeyValuePair<SignatureEntry, ContentEntry>(signature[0], content[0]));
signature.RemoveAt(0);
content.RemoveAt(0);
}
}
if (index >= fulls.Count || be.Time <= fulls[index].Time)
{
if (m_orphans == null)
Logging.Log.WriteMessage(string.Format(Strings.BackendWrapper.OrphanIncrementalFoundMessage, be.Filename), Duplicati.Library.Logging.LogMessageType.Warning);
else
m_orphans.Add(be);
continue;
}
else
{
while (index < fulls.Count - 1 && be.Time > fulls[index + 1].Time)
index++;
fulls[index].Incrementals.Add(SwapManifestAlternates(be));
}
}
foreach (List<ContentEntry> lb in contents.Values)
foreach (ContentEntry be in lb)
AddOrphan(be);
foreach (List<SignatureEntry> lb in signatures.Values)
foreach (SignatureEntry be in lb)
AddOrphan(be);
if (m_statistics != null)
{
foreach (ManifestEntry me in fulls)
{
if (me.Volumes.Count == 0)
m_statistics.LogWarning(string.Format(Strings.BackendWrapper.EmptyManifestWarning, me.Filename), null);
foreach (ManifestEntry me2 in me.Incrementals)
if (me2.Volumes.Count == 0)
m_statistics.LogWarning(string.Format(Strings.BackendWrapper.EmptyManifestWarning, me.Filename), null);
}
}
return fulls;
}
private ManifestEntry SwapManifestAlternates(ManifestEntry m)
{
if (m.Volumes.Count % 2 != 1 && m.Alternate != null)
{
m.Alternate.Incrementals.Clear();
m.Alternate.Incrementals.AddRange(m.Incrementals);
m.Alternate.Volumes.Clear();
m.Alternate.Volumes.AddRange(m.Volumes);
m.Incrementals.Clear();
m.Volumes.Clear();
m.Alternate.Alternate = m;
return m.Alternate;
}
return m;
}
public List<ManifestEntry> GetBackupSets()
{
using (new Logging.Timer("Getting and sorting filelist from " + m_backend.DisplayName))
return SortAndPairSets((List<Duplicati.Library.Interface.IFileEntry>)ProtectedInvoke("ListInternal"));
}
public void Put(BackupEntryBase remote, string filename)
{
if (!m_async)
PutInternal(remote, filename);
else
{
bool waitForCompletion;
lock (m_queuelock)
{
if (m_workerException != null)
throw m_workerException;
m_pendingOperations.Enqueue(new KeyValuePair<BackupEntryBase, string>( remote, filename ));
m_asyncItemReady.Set();
//The *3 is there because a volume consists of 3 files (signature, content and manifest)
waitForCompletion = m_options.AsynchronousUploadLimit > 0 && m_pendingOperations.Count > (m_options.AsynchronousUploadLimit * 3);
}
while (waitForCompletion)
{
m_asyncItemProcessed.WaitOne(1000 * 5, false);
lock (m_queuelock)
{
if (m_workerException != null)
throw m_workerException;
waitForCompletion = m_options.AsynchronousUploadLimit > 0 && m_pendingOperations.Count > (m_options.AsynchronousUploadLimit * 3);
}
}
}
}
public void Get(BackupEntryBase remote, string filename, string filehash)
{
ProtectedInvoke("GetInternal", remote, filename, filehash);
}
public void Delete(BackupEntryBase remote)
{
ProtectedInvoke("DeleteInternal", remote);
}
/// <summary>
/// This method invokes another method in such a way that no more than one thread is ever
/// executing on the same backend at any time.
/// </summary>
/// <param name="methodname">The method to invoke</param>
/// <param name="arguments">The methods arguments</param>
/// <returns>The return value of the invoked function</returns>
private object ProtectedInvoke(string methodname, params object[] arguments)
{
System.Reflection.MethodInfo method = this.GetType().GetMethod(methodname);
if (method == null)
method = this.GetType().GetMethod(methodname, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (method == null)
throw new Exception(string.Format(Strings.BackendWrapper.FunctionLookupError, methodname));
//If the code is not async, just invoke it
if (!m_async)
{
try
{
return method.Invoke(this, arguments);
}
catch (System.Reflection.TargetInvocationException tex)
{
//Unwrap those annoying invocation exceptions
if (tex.InnerException != null)
throw tex.InnerException;
else
throw;
}
}
else
{
while (true)
{
//Get the lock
lock (m_queuelock)
{
//If the lock is free, just run, but keep the lock
if (m_workerThread == null || !m_workerThread.IsAlive || m_pendingOperations.Count == 0)
{
try
{
return method.Invoke(this, arguments);
}
catch (System.Reflection.TargetInvocationException tex)
{
//Unwrap those annoying invocation exceptions
if (tex.InnerException != null)
throw tex.InnerException;
else
throw;
}
}
}
//Otherwise, wait for the worker to signal completion
//We wait without holding the lock, because the worker cannot signal completion while
//we hold the lock
m_workerThread.Join(1000);
//Now re-acquire the lock, and re-check the state of the event
}
}
}
private void DeleteSignatureCacheCopy(BackupEntryBase entry)
{
if (!string.IsNullOrEmpty(m_options.SignatureCachePath))
{
string cacheFilename = m_cachefilenamestrategy.GenerateFilename(entry);
if (System.IO.File.Exists(cacheFilename))
try { System.IO.File.Delete(cacheFilename); }
catch { }
}
}
public void DeleteOrphans()
{
if (m_orphans == null)
return;
foreach (BackupEntryBase be in m_orphans)
{
Logging.Log.WriteMessage(string.Format(Strings.BackendWrapper.RemovingLeftoverFileMessage, be.Filename), Duplicati.Library.Logging.LogMessageType.Information);
if (m_options.Force)
{
if (m_statistics != null)
m_statistics.LogWarning(string.Format(Strings.BackendWrapper.RemoveOrphanFileWarning, be.Filename), null);
m_backend.Delete(be.Filename);
DeleteSignatureCacheCopy(be);
}
if (be is ManifestEntry)
foreach (KeyValuePair<SignatureEntry, ContentEntry> bex in ((ManifestEntry)be).Volumes)
{
Logging.Log.WriteMessage(string.Format(Strings.BackendWrapper.RemovingLeftoverFileMessage, bex.Key.Filename), Duplicati.Library.Logging.LogMessageType.Information);
Logging.Log.WriteMessage(string.Format(Strings.BackendWrapper.RemovingLeftoverFileMessage, bex.Value.Filename), Duplicati.Library.Logging.LogMessageType.Information);
if (m_options.Force)
{
if (m_statistics != null)
{
m_statistics.LogWarning(string.Format(Strings.BackendWrapper.RemoveOrphanFileWarning, bex.Key.Filename), null);
m_statistics.LogWarning(string.Format(Strings.BackendWrapper.RemoveOrphanFileWarning, bex.Value.Filename), null);
}
m_backend.Delete(bex.Key.Filename);
m_backend.Delete(bex.Value.Filename);
DeleteSignatureCacheCopy(bex.Key);
}
}
}
if (!m_options.Force && m_orphans.Count > 0)
Logging.Log.WriteMessage(Strings.BackendWrapper.FilesNotForceRemovedMessage, Duplicati.Library.Logging.LogMessageType.Information);
}
private List<Duplicati.Library.Interface.IFileEntry> ListInternal()
{
int retries = m_options.NumberOfRetries;
Exception lastEx = null;
do
{
try
{
m_statistics.AddNumberOfRemoteCalls(1);
return m_backend.List();
}
catch (System.Threading.ThreadAbortException)
{
throw;
}
catch (Exception ex)
{
lastEx = ex;
m_statistics.LogError(ex.Message, ex);
retries--;
if (retries > 0 && m_options.RetryDelay.Ticks > 0)
System.Threading.Thread.Sleep(m_options.RetryDelay);
}
} while (retries > 0);
throw new Exception(string.Format(Strings.BackendWrapper.FileListingError, lastEx.Message), lastEx);
}
private void DeleteInternal(BackupEntryBase remote)
{
int retries = m_options.NumberOfRetries;
Exception lastEx = null;
do
{
try
{
m_statistics.AddNumberOfRemoteCalls(1);
m_backend.Delete(remote.Filename);
lastEx = null;
}
catch (System.Threading.ThreadAbortException)
{
throw;
}
catch (Exception ex)
{
lastEx = ex;
m_statistics.LogError(ex.Message, ex);
retries--;
if (retries > 0 && m_options.RetryDelay.Ticks > 0)
System.Threading.Thread.Sleep(m_options.RetryDelay);
}
} while (lastEx != null && retries > 0);
if (lastEx != null)
throw new Exception(string.Format(Strings.BackendWrapper.FileDeleteError, lastEx.Message), lastEx);
if (remote is SignatureEntry && !string.IsNullOrEmpty(m_options.SignatureCachePath))
{
try
{
if (System.IO.File.Exists(System.IO.Path.Combine(m_options.SignatureCachePath, m_cachefilenamestrategy.GenerateFilename(remote))))
System.IO.File.Delete(System.IO.Path.Combine(m_options.SignatureCachePath, m_cachefilenamestrategy.GenerateFilename(remote)));
}
catch {}
}
}
private void GetInternal(BackupEntryBase remote, string filename, string filehash)
{
int retries = m_options.NumberOfRetries;
Exception lastEx = null;
m_statusmessage = string.Format(Strings.BackendWrapper.StatusMessageDownloading, remote.Filename);
do
{
try
{
if (!string.IsNullOrEmpty(m_options.SignatureCachePath) && remote is SignatureEntry)
{
string cachefilename = System.IO.Path.Combine(m_options.SignatureCachePath, m_cachefilenamestrategy.GenerateFilename(remote));
if (System.IO.File.Exists(cachefilename))
if (filehash != null && Core.Utility.CalculateHash(cachefilename) == filehash)
{
//TODO: Don't copy, but just return it as write protected
System.IO.File.Copy(cachefilename, filename, true); //TODO: Warn on hash mismatch?
return;
}
}
Core.TempFile tempfile = null;
try
{
if (!string.IsNullOrEmpty(remote.EncryptionMode))
tempfile = new Duplicati.Library.Core.TempFile();
else
tempfile = new Duplicati.Library.Core.TempFile(filename);
m_statistics.AddNumberOfRemoteCalls(1);
if (m_backend is Duplicati.Library.Interface.IStreamingBackend && !m_options.DisableStreamingTransfers)
{
using (System.IO.FileStream fs = System.IO.File.Open(tempfile, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
using (Core.ProgressReportingStream pgs = new Duplicati.Library.Core.ProgressReportingStream(fs, remote.Fileentry.Size))
using (Core.ThrottledStream ts = new Duplicati.Library.Core.ThrottledStream(pgs, m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond))
{
pgs.Progress += new Duplicati.Library.Core.ProgressReportingStream.ProgressDelegate(pgs_Progress);
ts.Callback += new Duplicati.Library.Core.ThrottledStream.ThrottledStreamCallback(ThrottledStream_Callback);
((Duplicati.Library.Interface.IStreamingBackend)m_backend).Get(remote.Filename, ts);
}
}
else
{
if (!m_async && ProgressEvent != null)
ProgressEvent(50, m_statusmessage);
m_backend.Get(remote.Filename, tempfile);
if (!m_async && ProgressEvent != null)
ProgressEvent(100, m_statusmessage);
}
if (!string.IsNullOrEmpty(remote.EncryptionMode))
{
try
{
using (Library.Interface.IEncryption enc = DynamicLoader.EncryptionLoader.GetModule(remote.EncryptionMode, m_options.Passphrase, m_options.RawOptions))
enc.Decrypt(tempfile, filename);
}
catch (Exception ex)
{
//If we fail here, make sure that we throw a crypto exception
if (ex is System.Security.Cryptography.CryptographicException)
throw;
else
throw new System.Security.Cryptography.CryptographicException(ex.Message, ex);
}
tempfile.Dispose(); //Remove the encrypted file
//Wrap the new file as a temp file
tempfile = new Duplicati.Library.Core.TempFile(filename);
}
if (filehash != null && Core.Utility.CalculateHash(tempfile) != filehash)
throw new HashMismathcException(string.Format(Strings.BackendWrapper.HashMismatchError, remote.Filename, filehash, Core.Utility.CalculateHash(tempfile)));
if (!string.IsNullOrEmpty(m_options.SignatureCachePath) && remote is SignatureEntry)
{
string cachefilename = System.IO.Path.Combine(m_options.SignatureCachePath, m_cachefilenamestrategy.GenerateFilename(remote));
System.IO.File.Copy(tempfile, cachefilename);
}
lastEx = null;
tempfile.Protected = true; //Don't delete it
}
finally
{
try
{
if (tempfile != null)
tempfile.Dispose();
}
catch
{
}
}
}
catch (System.Threading.ThreadAbortException)
{
throw;
}
catch (Exception ex)
{
lastEx = ex;
m_statistics.LogError(ex.Message, ex);
retries--;
if (retries > 0 && m_options.RetryDelay.Ticks > 0)
System.Threading.Thread.Sleep(m_options.RetryDelay);
}
} while (lastEx != null && retries > 0);
if (lastEx != null)
if (lastEx is HashMismathcException)
throw lastEx;
else
throw new Exception(string.Format(Strings.BackendWrapper.FileDownloadError, lastEx.Message), lastEx);
m_statistics.AddBytesDownloaded(new System.IO.FileInfo(filename).Length);
}
private void PutInternal(BackupEntryBase remote, string filename)
{
string remotename = GenerateFilename(remote);
m_statusmessage = string.Format(Strings.BackendWrapper.StatusMessageUploading, remotename, Core.Utility.FormatSizeString(new System.IO.FileInfo(filename).Length));
string encryptedFile = filename;
try
{
if (m_encryption != null)
{
using (Core.TempFile tf = new Duplicati.Library.Core.TempFile()) //If exception is thrown, tf will be deleted
{
m_encryption.Encrypt(filename, tf);
tf.Protected = true; //Done, keep file
encryptedFile = tf;
}
}
int retries = m_options.NumberOfRetries;
bool success = false;
Exception lastEx = null;
do
{
try
{
m_statistics.AddNumberOfRemoteCalls(1);
if (m_backend is Library.Interface.IStreamingBackend && !m_options.DisableStreamingTransfers)
{
#if DEBUG_THROTTLE
DateTime begin = DateTime.Now;
#endif
using (System.IO.FileStream fs = System.IO.File.Open(encryptedFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read))
using (Core.ProgressReportingStream pgs = new Duplicati.Library.Core.ProgressReportingStream(fs, fs.Length))
{
pgs.Progress += new Duplicati.Library.Core.ProgressReportingStream.ProgressDelegate(pgs_Progress);
using (Core.ThrottledStream ts = new Core.ThrottledStream(pgs, m_options.MaxUploadPrSecond, m_options.MaxDownloadPrSecond))
{
ts.Callback += new Duplicati.Library.Core.ThrottledStream.ThrottledStreamCallback(ThrottledStream_Callback);
((Library.Interface.IStreamingBackend)m_backend).Put(remotename, ts);
}
}
#if DEBUG_THROTTLE
TimeSpan duration = DateTime.Now - begin;
long size = new System.IO.FileInfo(encryptedFile).Length;
Console.WriteLine("Transferred " + Core.Utility.FormatSizeString(size) + " in " + duration.TotalSeconds.ToString() + ", yielding : " + ((size / (double)1024.0) / duration.TotalSeconds) + " kb/s");
#endif
}
else
{
if (ProgressEvent != null)
ProgressEvent(50, m_statusmessage);
m_backend.Put(remotename, encryptedFile);
if (ProgressEvent != null)
ProgressEvent(50, m_statusmessage);
}
if (remote is SignatureEntry && !string.IsNullOrEmpty(m_options.SignatureCachePath))
System.IO.File.Copy(filename, System.IO.Path.Combine(m_options.SignatureCachePath, m_cachefilenamestrategy.GenerateFilename(remote)), true);
if (remote is ManifestEntry)
{
if (m_queuelock != null)
{
lock (m_queuelock)
m_manifestUploads++;
}
else
m_manifestUploads++;
}
success = true;
lastEx = null;
}
catch (System.Threading.ThreadAbortException)
{
throw;
}
catch (Exception ex)
{
lastEx = ex;
m_statistics.LogError(ex.Message, ex);
retries--;
if (retries > 0 && m_options.RetryDelay.Ticks > 0)
System.Threading.Thread.Sleep(m_options.RetryDelay);
}
} while (!success && retries > 0);
if (!success)
throw new Exception(string.Format(Strings.BackendWrapper.FileUploadError, lastEx == null ? "<null>" : lastEx.Message), lastEx);
m_statistics.AddBytesUploaded(new System.IO.FileInfo(filename).Length);
}
finally
{
//We delete the file here, because the backend leaves the file,
//in the case where we use async methods
try
{
if (System.IO.File.Exists(filename))
System.IO.File.Delete(filename);
}
catch { }
try
{
if (System.IO.File.Exists(encryptedFile))
System.IO.File.Delete(encryptedFile);
}
catch { }
try
{
if (ProgressEvent != null)
ProgressEvent(-1, "");
}
catch
{
}
}
}
/// <summary>
/// A callback from the throttled stream, used to change speed based on user adjustments
/// </summary>
/// <param name="sender">The stream that raised the event</param>
void ThrottledStream_Callback(Core.ThrottledStream sender)
{
sender.ReadSpeed = m_options.MaxUploadPrSecond;
sender.WriteSpeed = m_options.MaxDownloadPrSecond;
}
/// <summary>
/// Internal helper to consistenly name remote files beyond what the filenamestrategy supports
/// </summary>
/// <param name="remote">The entry to create a filename for</param>
/// <returns>A filename with extensions</returns>
private string GenerateFilename(BackupEntryBase remote)
{
string remotename = m_filenamestrategy.GenerateFilename(remote);
if (remote is ManifestEntry)
remotename += ".manifest";
else
remotename += "." + m_options.CompressionModule;
if (m_encryption != null)
remotename += "." + m_encryption.FilenameExtension;
return remotename;
}
private void pgs_Progress(int progress)
{
if (ProgressEvent != null)
ProgressEvent(progress, m_statusmessage);
}
/// <summary>
/// Worker Thread entry that empties the request queue
/// </summary>
/// <param name="dummy">Unused required parameter</param>
private void ProcessQueue(object dummy)
{
try
{
lock (m_queuelock)
m_workerException = null;
while (!m_asyncTerminate)
{
KeyValuePair<BackupEntryBase, string> args = new KeyValuePair<BackupEntryBase,string>(null, null);
while (args.Key == null)
{
if (m_asyncTerminate)
return;
//Obtain the lock for the queue
lock (m_queuelock)
if (m_pendingOperations.Count > 0)
args = m_pendingOperations.Peek();
if (args.Key == null)
m_asyncItemReady.WaitOne(1000, false);
}
//Pause if requested
if (AsyncItemProcessedEvent != null)
AsyncItemProcessedEvent(this, null);
PutInternal(args.Key, args.Value);
lock (m_queuelock)
{
m_pendingOperations.Dequeue();
m_asyncItemProcessed.Set();
}
}
}
catch (Exception ex)
{
lock (m_queuelock)
{
m_workerException = ex;
m_asyncItemProcessed.Set();
}
}
}
/// <summary>
/// Gets the number of uploads performed asynchronously
/// </summary>
public int ManifestUploads
{
get
{
if (m_queuelock != null)
{
lock (m_queuelock)
return m_manifestUploads;
}
else
return m_manifestUploads;
}
}
/// <summary>
/// This function attemtps to forcefully abort all ongoing async operations
/// </summary>
public void AbortAll()
{
//If we are not running async, just return
if (!m_async)
return;
List<KeyValuePair<BackupEntryBase, string>> pending = new List<KeyValuePair<BackupEntryBase, string>>();
m_asyncTerminate = true;
if (m_workerThread != null && m_workerThread.IsAlive)
m_workerThread.Join(1000);
lock (m_queuelock)
{
if (m_workerThread != null && m_workerThread.IsAlive)
m_workerThread.Abort();
//Extract all unprocessed items
while (m_pendingOperations.Count > 0)
pending.Add(m_pendingOperations.Dequeue());
}
//Clean up all temporary files from pending operations
foreach (KeyValuePair<BackupEntryBase, string> p in pending)
try
{
if (System.IO.File.Exists(p.Value))
System.IO.File.Delete(p.Value);
}
catch { }
}
/// <summary>
/// This function suspends the calling thread until the
/// ongoing transfer has completed, then disables asynchronous
/// transfers and returns the non-transferred items
/// </summary>
public List<KeyValuePair<BackupEntryBase, string>> ExtractPendingUploads()
{
List<KeyValuePair<BackupEntryBase, string>> work = null;
while (true)
{
lock (m_queuelock)
{
if (m_workerException != null)
throw m_workerException;
//On the first run, we empty the queue and signal the stop
if (work == null)
{
m_asyncTerminate = true;
work = new List<KeyValuePair<BackupEntryBase, string>>();
if (m_pendingOperations.Count > 0)
{
while (m_pendingOperations.Count != 0)
work.Add(m_pendingOperations.Dequeue());
//The top entry is probably being completed by the thread
m_pendingOperations.Enqueue(work[0]);
}
}
//Make sure the worker is awake to see the terminate message
m_asyncItemReady.Set();
//When the thread completes, disable asynchronous transfers and return the unfinished work
if (m_workerThread == null || !m_workerThread.IsAlive)
{
if (m_workerException != null)
throw m_workerException;
//If the thread did indeed complete the entry, remove it from the pending list
if (m_pendingOperations.Count == 0 && work.Count > 0)
work.RemoveAt(0);
m_async = false;
return work;
}
}
m_workerThread.Join(1000 * 5);
}
}
private void DisposeInternal()
{
if (m_backend != null)
{
m_backend.Dispose();
m_backend = null;
}
}
#region IDisposable Members
public void Dispose()
{
if (m_async && m_queuelock != null)
AbortAll();
if (m_backend != null)
ProtectedInvoke("DisposeInternal");
if (m_encryption != null)
m_encryption.Dispose();
}
#endregion
}
}