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.Security.Cryptography;\r
using System.Threading;\r
using System.Threading.Tasks;\r
using Pithos.Interfaces;\r
[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
{\r
try\r
{\r
- await UnpauseEvent.WaitAsync();\r
+ await UnpauseEvent.WaitAsync().ConfigureAwait(false);\r
\r
var fileInfo = action.LocalFile;\r
\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
\r
}\r
\r
- await UnpauseEvent.WaitAsync();\r
+ await UnpauseEvent.WaitAsync().ConfigureAwait(false);\r
\r
- if (fileInfo is DirectoryInfo)\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
+ fileInfo.Refresh();\r
+ //Does the file still exist or was it deleted/renamed?\r
+ if (fileInfo.Exists)\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
+ else\r
+ {\r
\r
+ var cloudHash = cloudInfo.X_Object_Hash.ToLower();\r
\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
- //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);\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
- await StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);\r
- //And then upload it\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
- //Upload even small files using the Hashmap. The server may already contain\r
- //the relevant block \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
- await UploadWithHashMap(accountInfo, cloudFile, fileInfo as FileInfo, cloudFile.Name, treeHash,cancellationToken);\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
}\r
}\r
\r
+\r
private static void MakeFileReadOnly(string fullFileName)\r
{\r
var attributes = File.GetAttributes(fullFileName);\r
}\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
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(accountInfo,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(accountInfo,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(accountInfo,cloudFile, token))\r
- return;\r
\r
+ while (missingHashes.Count > 0)\r
+ {\r
+ block = 0;\r
\r
- //Find the proper block\r
- var blockIndex = treeHash.HashDictionary[missingHash];\r
- long offset = blockIndex*accountInfo.BlockSize;\r
+ if (await WaitOrAbort(accountInfo, cloudFile, token).ConfigureAwait(false))\r
+ return;\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, token);\r
- Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);\r
- }\r
- catch (TaskCanceledException exc)\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++, missingHashes.Count, fileInfo.Length);\r
- }\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
+\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
- token.ThrowIfCancellationRequested();\r
- //Repeat until there are no more missing hashes \r
- missingHashes = await client.PutHashMap(account, container, url, treeHash);\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(AccountInfo account,ObjectInfo cloudFile, CancellationToken token)\r
{\r
token.ThrowIfCancellationRequested();\r
- await UnpauseEvent.WaitAsync();\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