Convert all url usages to use the Uri class instead of raw strings.
[pithos-ms-client] / trunk / Pithos.Core / Agents / Downloader.cs
index 68b33c6..584141a 100644 (file)
-using System;
-using System.ComponentModel.Composition;
-using System.Diagnostics.Contracts;
-using System.IO;
-using System.Reflection;
-using System.Threading.Tasks;
-using Pithos.Interfaces;
-using Pithos.Network;
-using log4net;
-
-namespace Pithos.Core.Agents
-{
-    [Export(typeof(Downloader))]
-    public class Downloader
-    {
-        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
-
-        [Import]
-        private IStatusKeeper StatusKeeper { get; set; }
-
-        
-        public IStatusNotification StatusNotification { get; set; }
-
-        //Download a file.
-        public async Task DownloadCloudFile(AccountInfo accountInfo, ObjectInfo cloudFile, string filePath)
-        {
-            if (accountInfo == null)
-                throw new ArgumentNullException("accountInfo");
-            if (cloudFile == null)
-                throw new ArgumentNullException("cloudFile");
-            if (String.IsNullOrWhiteSpace(cloudFile.Account))
-                throw new ArgumentNullException("cloudFile");
-            if (String.IsNullOrWhiteSpace(cloudFile.Container))
-                throw new ArgumentNullException("cloudFile");
-            if (String.IsNullOrWhiteSpace(filePath))
-                throw new ArgumentNullException("filePath");
-            if (!Path.IsPathRooted(filePath))
-                throw new ArgumentException("The filePath must be rooted", "filePath");
-            Contract.EndContractBlock();
-
-            using (ThreadContext.Stacks["Operation"].Push("DownloadCloudFile"))
-            {
-
-                var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);
-                var relativeUrl = new Uri(cloudFile.Name, UriKind.Relative);
-
-                var url = relativeUrl.ToString();
-                if (cloudFile.Name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase))
-                    return;
-
-
-                //Are we already downloading or uploading the file? 
-                using (var gate = NetworkGate.Acquire(localPath, NetworkOperation.Downloading))
-                {
-                    if (gate.Failed)
-                        return;
-
-                    var client = new CloudFilesClient(accountInfo);
-                    var account = cloudFile.Account;
-                    var container = cloudFile.Container;
-
-                    if (cloudFile.IsDirectory)
-                    {
-                        if (!Directory.Exists(localPath))
-                            try
-                            {
-                                Directory.CreateDirectory(localPath);
-                                if (Log.IsDebugEnabled)
-                                    Log.DebugFormat("Created Directory [{0}]", localPath);
-                            }
-                            catch (IOException)
-                            {
-                                var localInfo = new FileInfo(localPath);
-                                if (localInfo.Exists && localInfo.Length == 0)
-                                {
-                                    Log.WarnFormat("Malformed directory object detected for [{0}]", localPath);
-                                    localInfo.Delete();
-                                    Directory.CreateDirectory(localPath);
-                                    if (Log.IsDebugEnabled)
-                                        Log.DebugFormat("Created Directory [{0}]", localPath);
-                                }
-                            }
-                    }
-                    else
-                    {
-                        var isChanged = IsObjectChanged(cloudFile, localPath);
-                        if (isChanged)
-                        {
-                            //Retrieve the hashmap from the server
-                            var serverHash = await client.GetHashMap(account, container, url);
-                            //If it's a small file
-                            if (serverHash.Hashes.Count == 1)
-                                //Download it in one go
-                                await
-                                    DownloadEntireFileAsync(accountInfo, client, cloudFile, relativeUrl, localPath);
-                            //Otherwise download it block by block
-                            else
-                                await
-                                    DownloadWithBlocks(accountInfo, client, cloudFile, relativeUrl, localPath,
-                                                       serverHash);
-
-                            if (cloudFile.AllowedTo == "read")
-                            {
-                                var attributes = File.GetAttributes(localPath);
-                                File.SetAttributes(localPath, attributes | FileAttributes.ReadOnly);
-                            }
-                        }
-                    }
-
-                    //Now we can store the object's metadata without worrying about ghost status entries
-                    StatusKeeper.StoreInfo(localPath, cloudFile);
-
-                }
-            }
-        }
-
-        //Download a file asynchronously using blocks
-        public async Task DownloadWithBlocks(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath, TreeHash serverHash)
-        {
-            if (client == null)
-                throw new ArgumentNullException("client");
-            if (cloudFile == null)
-                throw new ArgumentNullException("cloudFile");
-            if (relativeUrl == null)
-                throw new ArgumentNullException("relativeUrl");
-            if (String.IsNullOrWhiteSpace(filePath))
-                throw new ArgumentNullException("filePath");
-            if (!Path.IsPathRooted(filePath))
-                throw new ArgumentException("The filePath must be rooted", "filePath");
-            if (serverHash == null)
-                throw new ArgumentNullException("serverHash");
-            if (cloudFile.IsDirectory)
-                throw new ArgumentException("cloudFile is a directory, not a file", "cloudFile");
-            Contract.EndContractBlock();
-
-            var fileAgent = GetFileAgent(accountInfo);
-            var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);
-
-            //Calculate the relative file path for the new file
-            var relativePath = relativeUrl.RelativeUriToFilePath();
-            var blockUpdater = new BlockUpdater(fileAgent.CachePath, localPath, relativePath, serverHash);
-
-
-            StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing, String.Format("Calculating hashmap for {0} before download", Path.GetFileName(localPath)));
-            //Calculate the file's treehash
-            var treeHash = await Signature.CalculateTreeHashAsync(localPath, serverHash.BlockSize, serverHash.BlockHash, 2);
-
-            //And compare it with the server's hash
-            var upHashes = serverHash.GetHashesAsStrings();
-            var localHashes = treeHash.HashDictionary;
-            ReportDownloadProgress(Path.GetFileName(localPath), 0, upHashes.Length, cloudFile.Bytes);
-            for (int i = 0; i < upHashes.Length; i++)
-            {
-                //For every non-matching hash
-                var upHash = upHashes[i];
-                if (!localHashes.ContainsKey(upHash))
-                {
-                    StatusNotification.Notify(new CloudNotification { Data = cloudFile });
-
-                    if (blockUpdater.UseOrphan(i, upHash))
-                    {
-                        Log.InfoFormat("[BLOCK GET] ORPHAN FOUND for {0} of {1} for {2}", i, upHashes.Length, localPath);
-                        continue;
-                    }
-                    Log.InfoFormat("[BLOCK GET] START {0} of {1} for {2}", i, upHashes.Length, localPath);
-                    var start = i * serverHash.BlockSize;
-                    //To download the last block just pass a null for the end of the range
-                    long? end = null;
-                    if (i < upHashes.Length - 1)
-                        end = ((i + 1) * serverHash.BlockSize);
-
-                    //Download the missing block
-                    var block = await client.GetBlock(cloudFile.Account, cloudFile.Container, relativeUrl, start, end);
-
-                    //and store it
-                    blockUpdater.StoreBlock(i, block);
-
-
-                    Log.InfoFormat("[BLOCK GET] FINISH {0} of {1} for {2}", i, upHashes.Length, localPath);
-                }
-                ReportDownloadProgress(Path.GetFileName(localPath), i, upHashes.Length, cloudFile.Bytes);
-            }
-
-            //Want to avoid notifications if no changes were made
-            var hasChanges = blockUpdater.HasBlocks;
-            blockUpdater.Commit();
-
-            if (hasChanges)
-                //Notify listeners that a local file has changed
-                StatusNotification.NotifyChangedFile(localPath);
-
-            Log.InfoFormat("[BLOCK GET] COMPLETE {0}", localPath);
-        }
-
-        //Download a small file with a single GET operation
-        private async Task DownloadEntireFileAsync(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath)
-        {
-            if (client == null)
-                throw new ArgumentNullException("client");
-            if (cloudFile == null)
-                throw new ArgumentNullException("cloudFile");
-            if (relativeUrl == null)
-                throw new ArgumentNullException("relativeUrl");
-            if (String.IsNullOrWhiteSpace(filePath))
-                throw new ArgumentNullException("filePath");
-            if (!Path.IsPathRooted(filePath))
-                throw new ArgumentException("The localPath must be rooted", "filePath");
-            if (cloudFile.IsDirectory)
-                throw new ArgumentException("cloudFile is a directory, not a file", "cloudFile");
-            Contract.EndContractBlock();
-
-            var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);
-            StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing, String.Format("Downloading {0}", Path.GetFileName(localPath)));
-            StatusNotification.Notify(new CloudNotification { Data = cloudFile });
-
-            var fileAgent = GetFileAgent(accountInfo);
-            //Calculate the relative file path for the new file
-            var relativePath = relativeUrl.RelativeUriToFilePath();
-            //The file will be stored in a temporary location while downloading with an extension .download
-            var tempPath = Path.Combine(fileAgent.CachePath, relativePath + ".download");
-            //Make sure the target folder exists. DownloadFileTask will not create the folder
-            var tempFolder = Path.GetDirectoryName(tempPath);
-            if (!Directory.Exists(tempFolder))
-                Directory.CreateDirectory(tempFolder);
-
-            //Download the object to the temporary location
-            await client.GetObject(cloudFile.Account, cloudFile.Container, relativeUrl.ToString(), tempPath);
-
-            //Create the local folder if it doesn't exist (necessary for shared objects)
-            var localFolder = Path.GetDirectoryName(localPath);
-            if (!Directory.Exists(localFolder))
-                try
-                {
-                    Directory.CreateDirectory(localFolder);
-                }
-                catch (IOException)
-                {
-                    //A file may already exist that has the same name as the new folder.
-                    //This may be an artifact of the way Pithos handles directories
-                    var fileInfo = new FileInfo(localFolder);
-                    if (fileInfo.Exists && fileInfo.Length == 0)
-                    {
-                        Log.WarnFormat("Malformed directory object detected for [{0}]", localFolder);
-                        fileInfo.Delete();
-                        Directory.CreateDirectory(localFolder);
-                    }
-                    else
-                        throw;
-                }
-            //And move it to its actual location once downloading is finished
-            if (File.Exists(localPath))
-                File.Replace(tempPath, localPath, null, true);
-            else
-                File.Move(tempPath, localPath);
-            //Notify listeners that a local file has changed
-            StatusNotification.NotifyChangedFile(localPath);
-
-
-        }
-
-
-        private void ReportDownloadProgress(string fileName, int block, int totalBlocks, long fileSize)
-        {
-            StatusNotification.Notify(totalBlocks == 0
-                                          ? new ProgressNotification(fileName, "Downloading", 1, 1, fileSize)
-                                          : new ProgressNotification(fileName, "Downloading", block, totalBlocks, fileSize));
-        }
-
-        private bool IsObjectChanged(ObjectInfo cloudFile, string localPath)
-        {
-            //If the target is a directory, there are no changes to download
-            if (Directory.Exists(localPath))
-                return false;
-            //If the file doesn't exist, we have a chagne
-            if (!File.Exists(localPath))
-                return true;
-            //If there is no stored state, we have a change
-            var localState = StatusKeeper.GetStateByFilePath(localPath);
-            if (localState == null)
-                return true;
-
-            var info = new FileInfo(localPath);
-            var shortHash = info.ComputeShortHash();
-            //If the file is different from the stored state, we have a change
-            if (localState.ShortHash != shortHash)
-                return true;
-            //If the top hashes differ, we have a change
-            return (localState.Checksum != cloudFile.Hash);
-        }
-
-        private static FileAgent GetFileAgent(AccountInfo accountInfo)
-        {
-            return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);
-        }
-
-
-    }
-}
+using System;\r
+using System.ComponentModel.Composition;\r
+using System.Diagnostics.Contracts;\r
+using System.IO;\r
+using System.Reflection;\r
+using System.Threading;\r
+using System.Threading.Tasks;\r
+using Pithos.Interfaces;\r
+using Pithos.Network;\r
+using log4net;\r
+\r
+namespace Pithos.Core.Agents\r
+{\r
+    [Export(typeof(Downloader))]\r
+    public class Downloader\r
+    {\r
+        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);\r
+\r
+        [Import]\r
+        private IStatusKeeper StatusKeeper { get; set; }\r
+\r
+        [Import]\r
+        private IPithosSettings Settings { get; set; }\r
+        \r
+        public IStatusNotification StatusNotification { get; set; }\r
+\r
+/*\r
+        private CancellationTokenSource _cts=new CancellationTokenSource();\r
+\r
+        public void SignalStop()\r
+        {\r
+            _cts.Cancel();\r
+        }\r
+*/\r
+\r
+\r
+        //Download a file.\r
+        public async Task<TreeHash> DownloadCloudFile(AccountInfo accountInfo, ObjectInfo cloudFile, string filePath,CancellationToken cancellationToken)\r
+        {\r
+            if (accountInfo == null)\r
+                throw new ArgumentNullException("accountInfo");\r
+            if (cloudFile == null)\r
+                throw new ArgumentNullException("cloudFile");\r
+            if (String.IsNullOrWhiteSpace(cloudFile.Account))\r
+                throw new ArgumentNullException("cloudFile");\r
+            if (cloudFile.Container==null)\r
+                throw new ArgumentNullException("cloudFile");\r
+            if (cloudFile.Container.IsAbsoluteUri)\r
+                throw new ArgumentNullException("cloudFile");\r
+            if (String.IsNullOrWhiteSpace(filePath))\r
+                throw new ArgumentNullException("filePath");\r
+            if (!Path.IsPathRooted(filePath))\r
+                throw new ArgumentException("The filePath must be rooted", "filePath");\r
+            Contract.EndContractBlock();\r
+                using (ThreadContext.Stacks["Operation"].Push("DownloadCloudFile"))\r
+                {\r
+                   // var cancellationToken=_cts.Token;//  .ThrowIfCancellationRequested();\r
+\r
+                    //The file's treehash after download completes. For directories, the treehash is always the empty hash\r
+                    var finalHash = TreeHash.Empty;\r
+\r
+                    if (await WaitOrAbort(accountInfo,cloudFile, cancellationToken).ConfigureAwait(false))\r
+                        return finalHash;\r
+\r
+                    var fileName = Path.GetFileName(filePath);\r
+\r
+                    var info = FileInfoExtensions.FromPath(filePath).WithProperCapitalization();\r
+\r
+                    TreeHash localTreeHash;\r
+\r
+                    using (StatusNotification.GetNotifier("Hashing for Download {0}", "Hashed for Download {0}", fileName))\r
+                    {\r
+                        var state = StatusKeeper.GetStateByFilePath(filePath);\r
+                        var progress = new Progress<double>(d =>\r
+                            StatusNotification.Notify(new StatusNotification(String.Format("Hashing for Download {0} of {1}", d, fileName))));\r
+\r
+                        localTreeHash = StatusAgent.CalculateTreeHash(info, accountInfo, state, Settings.HashingParallelism, cancellationToken, progress);\r
+                    }\r
+                    \r
+                    var localPath = info.FullName;\r
+                    var relativeUrl = cloudFile.Name;\r
+\r
+                    var url = relativeUrl.ToString();\r
+                    if (url.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase))\r
+                        return finalHash;\r
+\r
+                    if (!Selectives.IsSelected(accountInfo,cloudFile))\r
+                        return finalHash;\r
+\r
+\r
+                    //Are we already downloading or uploading the file? \r
+                    using (var gate = NetworkGate.Acquire(localPath, NetworkOperation.Downloading))\r
+                    {\r
+                        if (gate.Failed)\r
+                            return finalHash;\r
+\r
+                        var client = new CloudFilesClient(accountInfo);\r
+                        var account = cloudFile.Account;\r
+                        var container = cloudFile.Container;\r
+\r
+                        if (cloudFile.IsDirectory)\r
+                        {\r
+                            if (!Directory.Exists(localPath))\r
+                                try\r
+                                {\r
+                                    Directory.CreateDirectory(localPath);\r
+                                    if (Log.IsDebugEnabled)\r
+                                        Log.DebugFormat("Created Directory [{0}]", localPath);\r
+                                }\r
+                                catch (IOException)\r
+                                {\r
+                                    var localInfo = new FileInfo(localPath);\r
+                                    if (localInfo.Exists && localInfo.Length == 0)\r
+                                    {\r
+                                        Log.WarnFormat("Malformed directory object detected for [{0}]", localPath);\r
+                                        localInfo.Delete();\r
+                                        Directory.CreateDirectory(localPath);\r
+                                        if (Log.IsDebugEnabled)\r
+                                            Log.DebugFormat("Created Directory [{0}]", localPath);\r
+                                    }\r
+                                }\r
+                        }\r
+                        else\r
+                        {\r
+                            var isChanged = IsObjectChanged(cloudFile, localPath,localTreeHash);\r
+                            if (isChanged)\r
+                            {\r
+                                //Retrieve the hashmap from the server\r
+                                var serverHash = await client.GetHashMap(account, container, relativeUrl).ConfigureAwait(false);\r
+                                //If it's a small file\r
+                                if (serverHash.Hashes.Count == 1)\r
+                                    //Download it in one go\r
+                                    await\r
+                                        DownloadEntireFileAsync(accountInfo, client, cloudFile, relativeUrl, localPath,cancellationToken);\r
+                                    //Otherwise download it block by block\r
+                                else\r
+                                    await\r
+                                        DownloadWithBlocks(accountInfo, client, cloudFile, relativeUrl, localPath,localTreeHash,\r
+                                                           serverHash,cancellationToken);\r
+\r
+                                if (!cloudFile.IsWritable(accountInfo.UserName))\r
+                                {\r
+                                    var attributes = File.GetAttributes(localPath);\r
+                                    File.SetAttributes(localPath, attributes | FileAttributes.ReadOnly);\r
+                                }\r
+\r
+                                //Once download completes, the final hash will be equal to the server hash\r
+                                finalHash = serverHash;\r
+\r
+                            }\r
+                        }\r
+\r
+                        //Now we can store the object's metadata without worrying about ghost status entries\r
+                        StatusKeeper.StoreInfo(localPath, cloudFile,finalHash);\r
+\r
+                    }\r
+                    return finalHash;\r
+                }\r
+           \r
+        }\r
+\r
+        //Download a file asynchronously using blocks\r
+        public async Task DownloadWithBlocks(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath, TreeHash localTreeHash,TreeHash serverHash, CancellationToken cancellationToken)\r
+        {\r
+            if (client == null)\r
+                throw new ArgumentNullException("client");\r
+            if (cloudFile == null)\r
+                throw new ArgumentNullException("cloudFile");\r
+            if (relativeUrl == null)\r
+                throw new ArgumentNullException("relativeUrl");\r
+            if (String.IsNullOrWhiteSpace(filePath))\r
+                throw new ArgumentNullException("filePath");\r
+            if (!Path.IsPathRooted(filePath))\r
+                throw new ArgumentException("The filePath must be rooted", "filePath");\r
+            if (serverHash == null)\r
+                throw new ArgumentNullException("serverHash");\r
+            if (cloudFile.IsDirectory)\r
+                throw new ArgumentException("cloudFile is a directory, not a file", "cloudFile");\r
+            Contract.EndContractBlock();\r
+\r
+            if (await WaitOrAbort(accountInfo, cloudFile, cancellationToken).ConfigureAwait(false))\r
+                return;\r
+\r
+            var fileAgent = GetFileAgent(accountInfo);\r
+            var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);\r
+\r
+            //Calculate the relative file path for the new file\r
+            var relativePath = relativeUrl.RelativeUriToFilePath();\r
+            var blockUpdater = new BlockUpdater(fileAgent.CachePath, localPath, relativePath, serverHash);\r
+\r
+            StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing, String.Format("Calculating hashmap for {0} before download", Path.GetFileName(localPath)));\r
+            //Calculate the file's treehash\r
+\r
+            var fileName = Path.GetFileName(localPath);\r
+            var progress = new Progress<double>(d =>\r
+                StatusNotification.Notify(new StatusNotification(String.Format("Hashing for Download {0} of {1}", d, fileName))));\r
+            \r
+            var treeHash = localTreeHash ?? Signature.CalculateTreeHashAsync(localPath, (int)serverHash.BlockSize, serverHash.BlockHash, Settings.HashingParallelism,cancellationToken,progress);\r
+\r
+            //And compare it with the server's hash\r
+            var upHashes = serverHash.GetHashesAsStrings();\r
+            var localHashes = treeHash.HashDictionary;\r
+            ReportDownloadProgress(Path.GetFileName(localPath), 0,0, upHashes.Length, cloudFile.Bytes);\r
+\r
+\r
+            long i = 0;\r
+            client.DownloadProgressChanged += (sender, args) =>\r
+                                ReportDownloadProgress(Path.GetFileName(localPath), i, args.ProgressPercentage, upHashes.Length, cloudFile.Bytes);\r
+\r
+\r
+            for (i = 0; i < upHashes.Length; i++)\r
+            {\r
+                if (await WaitOrAbort(accountInfo, cloudFile, cancellationToken).ConfigureAwait(false))\r
+                    return;\r
+\r
+                //For every non-matching hash\r
+                var upHash = upHashes[i];\r
+                if (!localHashes.ContainsKey(upHash))\r
+                {\r
+                    StatusNotification.Notify(new CloudNotification { Data = cloudFile });\r
+                    ReportDownloadProgress(Path.GetFileName(localPath), i, 0,upHashes.Length, cloudFile.Bytes);\r
+\r
+                    if (blockUpdater.UseOrphan(i, upHash))\r
+                    {\r
+                        Log.InfoFormat("[BLOCK GET] ORPHAN FOUND for {0} of {1} for {2}", i, upHashes.Length, localPath);\r
+                        continue;\r
+                    }\r
+                    Log.InfoFormat("[BLOCK GET] START {0} of {1} for {2}", i, upHashes.Length, localPath);\r
+                    long start = i * serverHash.BlockSize;\r
+                    //To download the last block just pass a null for the end of the range\r
+                    long? end = null;\r
+                    if (i < upHashes.Length - 1)\r
+                        end = ((i + 1) * serverHash.BlockSize);\r
+\r
+                    \r
+                    //Download the missing block\r
+                    byte[] block = await client.GetBlock(cloudFile.Account, cloudFile.Container, relativeUrl, start, end, cancellationToken).ConfigureAwait(false);\r
+\r
+                    //and store it\r
+                    blockUpdater.StoreBlock(i, block);\r
+\r
+\r
+                    Log.InfoFormat("[BLOCK GET] FINISH {0} of {1} for {2}", i, upHashes.Length, localPath);\r
+                }\r
+                ReportDownloadProgress(Path.GetFileName(localPath), i, 100,upHashes.Length, cloudFile.Bytes);\r
+            }\r
+\r
+            //Want to avoid notifications if no changes were made\r
+            var hasChanges = blockUpdater.HasBlocks;\r
+            blockUpdater.Commit();\r
+\r
+            if (hasChanges)\r
+                //Notify listeners that a local file has changed\r
+                StatusNotification.NotifyChangedFile(localPath);\r
+\r
+            Log.InfoFormat("[BLOCK GET] COMPLETE {0}", localPath);\r
+            \r
+        }\r
+\r
+        //Download a small file with a single GET operation\r
+        private async Task DownloadEntireFileAsync(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath, CancellationToken cancellationToken)\r
+        {\r
+            if (client == null)\r
+                throw new ArgumentNullException("client");\r
+            if (cloudFile == null)\r
+                throw new ArgumentNullException("cloudFile");\r
+            if (relativeUrl == null)\r
+                throw new ArgumentNullException("relativeUrl");\r
+            if (String.IsNullOrWhiteSpace(filePath))\r
+                throw new ArgumentNullException("filePath");\r
+            if (!Path.IsPathRooted(filePath))\r
+                throw new ArgumentException("The localPath must be rooted", "filePath");\r
+            if (cloudFile.IsDirectory)\r
+                throw new ArgumentException("cloudFile is a directory, not a file", "cloudFile");\r
+            Contract.EndContractBlock();\r
+\r
+            if (await WaitOrAbort(accountInfo, cloudFile, cancellationToken).ConfigureAwait(false))\r
+                return;\r
+\r
+            var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);\r
+            StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing, String.Format("Downloading {0}", Path.GetFileName(localPath)));\r
+            StatusNotification.Notify(new CloudNotification { Data = cloudFile });\r
+            ReportDownloadProgress(Path.GetFileName(localPath), 1, 0,1, cloudFile.Bytes);\r
+\r
+            var fileAgent = GetFileAgent(accountInfo);\r
+            //Calculate the relative file path for the new file\r
+            var relativePath = relativeUrl.RelativeUriToFilePath();\r
+            //The file will be stored in a temporary location while downloading with an extension .download\r
+            var tempPath = Path.Combine(fileAgent.CachePath, relativePath + ".download");\r
+            //Make sure the target folder exists. DownloadFileTask will not create the folder\r
+            var tempFolder = Path.GetDirectoryName(tempPath);\r
+            if (!Directory.Exists(tempFolder))\r
+                Directory.CreateDirectory(tempFolder);\r
+\r
+            //Download the object to the temporary location\r
+            await client.GetObject(cloudFile.Account, cloudFile.Container, relativeUrl, tempPath, cancellationToken).ConfigureAwait(false);\r
+\r
+            //Create the local folder if it doesn't exist (necessary for shared objects)\r
+            var localFolder = Path.GetDirectoryName(localPath);\r
+            if (!Directory.Exists(localFolder))\r
+                try\r
+                {\r
+                    Directory.CreateDirectory(localFolder);\r
+                }\r
+                catch (IOException)\r
+                {\r
+                    //A file may already exist that has the same name as the new folder.\r
+                    //This may be an artifact of the way Pithos handles directories\r
+                    var fileInfo = new FileInfo(localFolder);\r
+                    if (fileInfo.Exists && fileInfo.Length == 0)\r
+                    {\r
+                        Log.WarnFormat("Malformed directory object detected for [{0}]", localFolder);\r
+                        fileInfo.Delete();\r
+                        Directory.CreateDirectory(localFolder);\r
+                    }\r
+                    else\r
+                        throw;\r
+                }\r
+            //And move it to its actual location once downloading is finished\r
+            if (File.Exists(localPath))\r
+                File.Replace(tempPath, localPath, null, true);\r
+            else\r
+                File.Move(tempPath, localPath);\r
+            //Notify listeners that a local file has changed\r
+            StatusNotification.NotifyChangedFile(localPath);\r
+\r
+\r
+        }\r
+\r
+\r
+        private void ReportDownloadProgress(string fileName, long block, int blockPercentage,int totalBlocks, long fileSize)\r
+        {\r
+            StatusNotification.Notify(totalBlocks == 0\r
+                                          ? new ProgressNotification(fileName, "Downloading", 1, blockPercentage,1, fileSize)\r
+                                          : new ProgressNotification(fileName, "Downloading", block, blockPercentage, totalBlocks, fileSize));\r
+        }\r
+\r
+        private bool IsObjectChanged(ObjectInfo cloudFile, string localPath,TreeHash localTreeHash)\r
+        {\r
+            //If the target is a directory, there are no changes to download\r
+            if (Directory.Exists(localPath))\r
+                return false;\r
+            //If the file doesn't exist, we have a chagne\r
+            if (!File.Exists(localPath))\r
+                return true;\r
+            //If there is no stored state, we have a change\r
+            var localState = StatusKeeper.GetStateByFilePath(localPath);\r
+            if (localState == null)\r
+                return true;\r
+\r
+            var localHash= localTreeHash.TopHash.ToHashString();\r
+            //If the file is different from the stored state, we have a change\r
+            if (localState.Checksum != localHash)\r
+                return true;\r
+            //If the top hashes differ, we have a change\r
+            return (localState.Checksum != cloudFile.X_Object_Hash);\r
+        }\r
+\r
+        private static FileAgent GetFileAgent(AccountInfo accountInfo)\r
+        {\r
+            return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);\r
+        }\r
+\r
+        private async Task<bool> WaitOrAbort(AccountInfo account,ObjectInfo cloudFile, CancellationToken token)\r
+        {\r
+            token.ThrowIfCancellationRequested();\r
+            await UnpauseEvent.WaitAsync().ConfigureAwait(false);\r
+            var shouldAbort = !Selectives.IsSelected(account,cloudFile);\r
+            if (shouldAbort)\r
+                Log.InfoFormat("Aborting [{0}]", cloudFile.Uri);\r
+            return shouldAbort;\r
+        }\r
+\r
+        [Import]\r
+        public Selectives Selectives { get; set; }\r
+\r
+        public AsyncManualResetEvent UnpauseEvent { get; set; }\r
+    }\r
+}\r