Replaced RestClient with HttpClient in PostBlock
[pithos-ms-client] / trunk / Pithos.Core / Agents / Uploader.cs
index 094d2f9..222fbb8 100644 (file)
@@ -1,10 +1,8 @@
 using System;\r
-using System.Collections.Generic;\r
 using System.ComponentModel.Composition;\r
 using System.Diagnostics;\r
 using System.Diagnostics.Contracts;\r
 using System.IO;\r
-using System.Linq;\r
 using System.Net;\r
 using System.Reflection;\r
 using System.Threading;\r
@@ -23,7 +21,9 @@ namespace Pithos.Core.Agents
         [Import]\r
         private IStatusKeeper StatusKeeper { get; set; }\r
 \r
-        \r
+        [Import]\r
+        private IPithosSettings Settings { get; set; }\r
+\r
         public IStatusNotification StatusNotification { get; set; }\r
 \r
         \r
@@ -33,7 +33,7 @@ namespace Pithos.Core.Agents
             _cts.Cancel();\r
         }*/\r
 \r
-        public async Task UploadCloudFile(CloudAction action,CancellationToken cancellationToken)\r
+        public async Task UploadCloudFile(CloudUploadAction action,CancellationToken cancellationToken)\r
         {\r
             if (action == null)\r
                 throw new ArgumentNullException("action");\r
@@ -43,40 +43,60 @@ namespace Pithos.Core.Agents
             {\r
                 try\r
                 {\r
-                    await UnpauseEvent.WaitAsync();\r
+                    await UnpauseEvent.WaitAsync().ConfigureAwait(false);\r
 \r
                     var fileInfo = action.LocalFile;\r
 \r
                     if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase))\r
                         return;\r
 \r
-                    if (!Selectives.IsSelected(action.AccountInfo, fileInfo))\r
+                    if (!Selectives.IsSelected(action.AccountInfo, fileInfo) && !action.IsCreation)\r
                         return;\r
 \r
+\r
                     //Try to load the action's local state, if it is empty\r
                     if (action.FileState == null)\r
                         action.FileState = StatusKeeper.GetStateByFilePath(fileInfo.FullName);\r
-                    if (action.FileState == null)\r
+\r
+                    TreeHash localTreeHash;\r
+                    using (StatusNotification.GetNotifier("Merkle Hashing for Upload {0}", "Merkle Hashed for Upload {0}", fileInfo.Name))\r
                     {\r
-                        Log.WarnFormat("File [{0}] has no local state. It was probably created by a download action", fileInfo.FullName);\r
-                        return;\r
-                    }\r
+                        //TODO: Load the stored treehash if appropriate\r
+                        //TODO: WHO updates LastMD5?\r
 \r
-                    var latestState = action.FileState;\r
+                        var progress = new Progress<double>(d => StatusNotification.Notify(\r
+                            new StatusNotification(String.Format("Merkle Hashing for Upload {0:p} of {1}", d, fileInfo.Name))));\r
 \r
-                    //Do not upload files in conflict\r
-                    if (latestState.FileStatus == FileStatus.Conflict)\r
-                    {\r
-                        Log.InfoFormat("Skipping file in conflict [{0}]", fileInfo.FullName);\r
-                        return;\r
+                        //If the action's Treehash is already calculated, use it instead of reprocessing\r
+                        localTreeHash = action.TreeHash.IsValueCreated\r
+                            ? action.TreeHash.Value \r
+                            : StatusAgent.CalculateTreeHash(fileInfo, action.AccountInfo, action.FileState, Settings.HashingParallelism, cancellationToken, progress);\r
                     }\r
-                    //Do not upload files when we have no permission\r
-                    if (latestState.FileStatus == FileStatus.Forbidden)\r
+\r
+\r
+                    if (action.FileState != null)\r
                     {\r
-                        Log.InfoFormat("Skipping forbidden file [{0}]", fileInfo.FullName);\r
-                        return;\r
-                    }\r
+                        /*\r
+                                                Log.WarnFormat("File [{0}] has no local state. It was probably created by a download action", fileInfo.FullName);\r
+                                                return;\r
+                        */\r
+\r
 \r
+                        var latestState = action.FileState;\r
+\r
+                        //Do not upload files in conflict\r
+                        if (latestState.FileStatus == FileStatus.Conflict)\r
+                        {\r
+                            Log.InfoFormat("Skipping file in conflict [{0}]", fileInfo.FullName);\r
+                            return;\r
+                        }\r
+                        //Do not upload files when we have no permission\r
+                        if (latestState.FileStatus == FileStatus.Forbidden)\r
+                        {\r
+                            Log.InfoFormat("Skipping forbidden file [{0}]", fileInfo.FullName);\r
+                            return;\r
+                        }\r
+                    }\r
                     //Are we targeting our own account or a sharer account?\r
                     var relativePath = fileInfo.AsRelativeTo(action.AccountInfo.AccountPath);\r
                     var accountInfo = relativePath.StartsWith(FolderConstants.OthersFolder) \r
@@ -105,68 +125,116 @@ namespace Pithos.Core.Agents
                             //If this a shared file\r
                             if (!cloudFile.Account.Equals(action.AccountInfo.UserName,StringComparison.InvariantCultureIgnoreCase))\r
                             {\r
-                                //If this is a read-only file, do not upload changes\r
+                                \r
+/*\r
                                 if (!cloudInfo.IsWritable(action.AccountInfo.UserName))\r
                                 {\r
                                     MakeFileReadOnly(fullFileName);\r
+                                    StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");\r
                                     return;\r
                                 }\r
+*/\r
 \r
-                                //If the file is new, can we upload it?\r
-                                if ( !cloudInfo.Exists && !client.CanUpload(account, cloudFile))\r
+                                //If this is a read-only file, do not upload changes\r
+                                if ( !cloudInfo.IsWritable(action.AccountInfo.UserName) ||\r
+                                    //If the file is new, but we can't upload it\r
+                                    (!cloudInfo.Exists && !client.CanUpload(account, cloudFile)) )\r
                                 {\r
                                     MakeFileReadOnly(fullFileName);\r
+                                    StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");\r
                                     return;\r
                                 }\r
 \r
                             }\r
 \r
-                            await UnpauseEvent.WaitAsync();\r
+                            await UnpauseEvent.WaitAsync().ConfigureAwait(false);\r
 \r
-                            if (fileInfo is DirectoryInfo)\r
+                            fileInfo.Refresh();\r
+                            //Does the file still exist or was it deleted/renamed?\r
+                            if (fileInfo.Exists)\r
                             {\r
-                                //If the directory doesn't exist the Hash property will be empty\r
-                                if (String.IsNullOrWhiteSpace(cloudInfo.Hash))\r
-                                    //Go on and create the directory\r
-                                    await client.PutObject(account, cloudFile.Container, cloudFile.Name, fullFileName,\r
-                                                         String.Empty, "application/directory");\r
-                            }\r
-                            else\r
-                            {\r
-\r
-                                var cloudHash = cloudInfo.Hash.ToLower();\r
-\r
-                                string topHash;\r
-                                TreeHash treeHash;\r
-                                using(StatusNotification.GetNotifier("Hashing {0} for Upload", "Finished hashing {0}",fileInfo.Name))\r
+                                if (fileInfo is DirectoryInfo)\r
                                 {\r
-                                    treeHash = action.TreeHash.Value;\r
-                                    topHash = treeHash.TopHash.ToHashString();\r
+                                    //If the directory doesn't exist the Hash property will be empty\r
+                                    if (String.IsNullOrWhiteSpace(cloudInfo.X_Object_Hash))\r
+                                        //Go on and create the directory\r
+                                        await\r
+                                            client.PutObject(account, cloudFile.Container, cloudFile.Name, fullFileName,\r
+                                                             Signature.MERKLE_EMPTY, ObjectInfo.CONTENT_TYPE_DIRECTORY);\r
+                                    //If the upload is in response to a Folder create with Selective Sync enabled\r
+                                    if (action.IsCreation)\r
+                                    {\r
+                                        //Add the folder to the Selected URls\r
+                                        var selectiveUri = client.RootAddressUri.Combine(cloudFile.Uri);\r
+                                        Selectives.AddUri(accountInfo, selectiveUri);\r
+                                        Selectives.Save(accountInfo);\r
+                                    }\r
                                 }\r
-\r
-                                //If the file hashes match, abort the upload\r
-                                if (cloudInfo != ObjectInfo.Empty && topHash == cloudHash)\r
+                                else\r
                                 {\r
-                                    //but store any metadata changes \r
-                                    StatusKeeper.StoreInfo(fullFileName, cloudInfo);\r
-                                    Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);\r
-                                    return;\r
-                                }\r
 \r
+                                    var cloudHash = cloudInfo.X_Object_Hash.ToLower();\r
 \r
-                                //Mark the file as modified while we upload it\r
-                                StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);\r
-                                //And then upload it\r
+                                    string topHash;\r
+                                    TreeHash treeHash;\r
+                                    using (\r
+                                        StatusNotification.GetNotifier("Hashing {0} for Upload", "Finished hashing {0}",\r
+                                                                       fileInfo.Name))\r
+                                    {\r
+                                        treeHash = localTreeHash ?? action.TreeHash.Value;\r
+                                        topHash = treeHash.TopHash.ToHashString();\r
+                                    }\r
 \r
-                                //Upload even small files using the Hashmap. The server may already contain\r
-                                //the relevant block                                \r
 \r
-                                \r
 \r
-                                await UploadWithHashMap(accountInfo, cloudFile, fileInfo as FileInfo, cloudFile.Name, treeHash,cancellationToken);\r
+                                    //If the file hashes match, abort the upload\r
+                                    if (cloudInfo != ObjectInfo.Empty && (topHash == cloudHash))\r
+                                    {\r
+                                        //but store any metadata changes \r
+                                        StatusKeeper.StoreInfo(fullFileName, cloudInfo,treeHash);\r
+                                        Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);\r
+                                        return;\r
+                                    }\r
+\r
+\r
+                                    //Mark the file as modified while we upload it\r
+                                    StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);\r
+                                    //And then upload it\r
+\r
+                                    //Upload even small files using the Hashmap. The server may already contain\r
+                                    //the relevant block                                \r
+\r
+\r
+\r
+                                    await\r
+                                        UploadWithHashMap(accountInfo, cloudFile, fileInfo as FileInfo, cloudFile.Name,\r
+                                                          treeHash, cancellationToken).ConfigureAwait(false);\r
+                                }\r
+\r
+                                var currentInfo = client.GetObjectInfo(cloudFile.Account, cloudFile.Container,\r
+                                                                       cloudFile.Name);\r
+\r
+                                StatusKeeper.StoreInfo(fullFileName, currentInfo, localTreeHash);\r
+                                //Ensure the status is cleared\r
+                                StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged,\r
+                                                              FileOverlayStatus.Normal, "");\r
+/*\r
+                                //If there is no stored ObjectID in the file state, add it\r
+                                //TODO: Why not just update everything, then change the state?\r
+                                if (action.FileState == null || action.FileState.ObjectID == null)\r
+                                {\r
+                                    \r
+                                }\r
+                                else\r
+                                    //If everything succeeds, change the file and overlay status to normal\r
+                                    StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged,\r
+                                                              FileOverlayStatus.Normal, "");\r
+*/\r
+                            }\r
+                            else\r
+                            {\r
+                                StatusKeeper.ClearFileStatus(fullFileName);\r
                             }\r
-                            //If everything succeeds, change the file and overlay status to normal\r
-                            StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");\r
                         }\r
                         catch (WebException exc)\r
                         {\r
@@ -210,13 +278,14 @@ namespace Pithos.Core.Agents
             }\r
         }\r
 \r
+\r
         private static void MakeFileReadOnly(string fullFileName)\r
         {\r
             var attributes = File.GetAttributes(fullFileName);\r
             //Do not make any modifications if not necessary\r
             if (attributes.HasFlag(FileAttributes.ReadOnly))\r
                 return;\r
-            File.SetAttributes(fullFileName, attributes | FileAttributes.ReadOnly);\r
+            File.SetAttributes(fullFileName, attributes | FileAttributes.ReadOnly);            \r
         }\r
 \r
         private static AccountInfo GetSharerAccount(string relativePath, AccountInfo accountInfo)\r
@@ -241,7 +310,7 @@ namespace Pithos.Core.Agents
         }\r
 \r
 \r
-        public async Task UploadWithHashMap(AccountInfo accountInfo, ObjectInfo cloudFile, FileInfo fileInfo, string url, TreeHash treeHash, CancellationToken token)\r
+        public async Task UploadWithHashMap(AccountInfo accountInfo, ObjectInfo cloudFile, FileInfo fileInfo, Uri uri, TreeHash treeHash, CancellationToken token)\r
         {\r
             if (accountInfo == null)\r
                 throw new ArgumentNullException("accountInfo");\r
@@ -249,89 +318,104 @@ namespace Pithos.Core.Agents
                 throw new ArgumentNullException("cloudFile");\r
             if (fileInfo == null)\r
                 throw new ArgumentNullException("fileInfo");\r
-            if (String.IsNullOrWhiteSpace(url))\r
-                throw new ArgumentNullException(url);\r
+            if (uri==null)\r
+                throw new ArgumentNullException("uri");\r
             if (treeHash == null)\r
                 throw new ArgumentNullException("treeHash");\r
-            if (String.IsNullOrWhiteSpace(cloudFile.Container))\r
+            if (cloudFile.Container==null)\r
                 throw new ArgumentException("Invalid container", "cloudFile");\r
+            if (cloudFile.Container.IsAbsoluteUri)\r
+                throw new ArgumentException("Container URI must be relative", "cloudFile");\r
             Contract.EndContractBlock();\r
 \r
-           \r
-            using (StatusNotification.GetNotifier("Uploading {0}", "Finished Uploading {0}", fileInfo.Name))\r
-            {\r
-                if (await WaitOrAbort(cloudFile, token)) \r
-                    return;\r
 \r
-                var fullFileName = fileInfo.GetProperCapitalization();\r
+            if (await WaitOrAbort(accountInfo, cloudFile, token).ConfigureAwait(false))\r
+                return;\r
 \r
-                var account = cloudFile.Account ?? accountInfo.UserName;\r
-                var container = cloudFile.Container;\r
+            var fullFileName = fileInfo.GetProperCapitalization();\r
 \r
+            var account = cloudFile.Account ?? accountInfo.UserName;\r
+            var container = cloudFile.Container;\r
 \r
-                var client = new CloudFilesClient(accountInfo);\r
-                //Send the hashmap to the server            \r
-                var missingHashes = await client.PutHashMap(account, container, url, treeHash);\r
-                int block = 0;\r
-                ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);\r
-                //If the server returns no missing hashes, we are done\r
-                while (missingHashes.Count > 0)\r
-                {\r
+            int block = 0;\r
 \r
-                    if (await WaitOrAbort(cloudFile, token))\r
-                        return;\r
+            var client = new CloudFilesClient(accountInfo);\r
+            //Send the hashmap to the server            \r
+            var missingHashes = await client.PutHashMap(account, container, uri, treeHash).ConfigureAwait(false);\r
+            ReportUploadProgress(fileInfo.Name, block, 0, missingHashes.Count, fileInfo.Length);\r
+            //If the server returns no missing hashes, we are done\r
 \r
+            client.UploadProgressChanged += (sender, args) =>\r
+                                            ReportUploadProgress(fileInfo.Name, block, args.ProgressPercentage,\r
+                                                                 missingHashes.Count, fileInfo.Length);\r
 \r
-                    var buffer = new byte[accountInfo.BlockSize];\r
-                    foreach (var missingHash in missingHashes)\r
-                    {\r
-                        if (await WaitOrAbort(cloudFile, token))\r
-                            return;\r
 \r
+            while (missingHashes.Count > 0)\r
+            {\r
+                block = 0;\r
+\r
+                if (await WaitOrAbort(accountInfo, cloudFile, token).ConfigureAwait(false))\r
+                    return;\r
 \r
-                        //Find the proper block\r
-                        var blockIndex = treeHash.HashDictionary[missingHash];\r
-                        long offset = blockIndex*accountInfo.BlockSize;\r
 \r
-                        var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);\r
+                var buffer = new byte[accountInfo.BlockSize];\r
+                foreach (var missingHash in missingHashes)\r
+                {\r
+                    if (await WaitOrAbort(accountInfo, cloudFile, token).ConfigureAwait(false))\r
+                        return;\r
 \r
-                        try\r
-                        {\r
-                            //And upload the block                \r
-                            await client.PostBlock(account, container, buffer, 0, read);\r
-                            Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);\r
-                        }\r
-                        catch (Exception exc)\r
-                        {\r
-                            Log.Error(String.Format("Uploading block {0} of {1}", blockIndex, fullFileName), exc);\r
-                        }\r
-                        ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);\r
-                    }\r
 \r
-                    token.ThrowIfCancellationRequested();\r
-                    //Repeat until there are no more missing hashes                \r
-                    missingHashes = await client.PutHashMap(account, container, url, treeHash);\r
+                    //Find the proper block\r
+                    long blockIndex = treeHash.HashDictionary[missingHash];\r
+                    long offset = blockIndex*accountInfo.BlockSize;\r
+                    Debug.Assert(offset >= 0,\r
+                                 String.Format("Negative Offset! BlockIndex {0} BlockSize {1}", blockIndex,\r
+                                               accountInfo.BlockSize));\r
+\r
+                    var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);\r
+\r
+                    try\r
+                    {\r
+                        //And upload the block                \r
+                        await client.PostBlock(account, container, buffer, 0, read,missingHash, token).ConfigureAwait(false);\r
+                        token.ThrowIfCancellationRequested();\r
+                        Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);\r
+                    }\r
+                    catch (TaskCanceledException)\r
+                    {\r
+                        throw new OperationCanceledException(token);\r
+                    }\r
+                    catch (Exception exc)\r
+                    {\r
+                        Log.Error(String.Format("Uploading block {0} of {1}", blockIndex, fullFileName), exc);\r
+                    }\r
+                    ReportUploadProgress(fileInfo.Name, block++, 100, missingHashes.Count, fileInfo.Length);\r
                 }\r
 \r
-                ReportUploadProgress(fileInfo.Name, missingHashes.Count, missingHashes.Count, fileInfo.Length);\r
+                token.ThrowIfCancellationRequested();\r
+                //Repeat until there are no more missing hashes                \r
+                missingHashes = await client.PutHashMap(account, container, uri, treeHash).ConfigureAwait(false);\r
             }\r
+\r
+            ReportUploadProgress(fileInfo.Name, missingHashes.Count, 0, missingHashes.Count, fileInfo.Length);\r
+\r
         }\r
 \r
-        private async Task<bool> WaitOrAbort(ObjectInfo cloudFile, CancellationToken token)\r
+        private async Task<bool> WaitOrAbort(AccountInfo account,ObjectInfo cloudFile, CancellationToken token)\r
         {\r
             token.ThrowIfCancellationRequested();\r
-            await UnpauseEvent.WaitAsync();\r
-            var shouldAbort = !Selectives.IsSelected(cloudFile);\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
-        private void ReportUploadProgress(string fileName, int block, int totalBlocks, long fileSize)\r
+        private void ReportUploadProgress(string fileName, int block, int blockPercentage, int totalBlocks, long fileSize)\r
         {\r
             StatusNotification.Notify(totalBlocks == 0\r
-                                          ? new ProgressNotification(fileName, "Uploading", 1, 1, fileSize)\r
-                                          : new ProgressNotification(fileName, "Uploading", block, totalBlocks, fileSize));\r
+                                          ? new ProgressNotification(fileName, "Uploading", 1,blockPercentage, 1, fileSize)\r
+                                          : new ProgressNotification(fileName, "Uploading", block, blockPercentage, totalBlocks, fileSize));\r
         }\r
 \r
 \r