2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Pithos.Interfaces;
16 namespace Pithos.Core.Agents
19 public class NetworkAgent
21 private Agent<CloudAction> _agent;
24 public IStatusKeeper StatusKeeper { get; set; }
26 public IStatusNotification StatusNotification { get; set; }
29 public FileAgent FileAgent {get;set;}
32 /* public int BlockSize { get; set; }
33 public string BlockHash { get; set; }*/
35 private static readonly ILog Log = LogManager.GetLogger("NetworkAgent");
37 private List<AccountInfo> _accounts=new List<AccountInfo>();
39 public void Start(/*int blockSize, string blockHash*/)
43 throw new ArgumentOutOfRangeException("blockSize");
44 if (String.IsNullOrWhiteSpace(blockHash))
45 throw new ArgumentOutOfRangeException("blockHash");
46 Contract.EndContractBlock();
50 BlockSize = blockSize;
51 BlockHash = blockHash;
55 _agent = Agent<CloudAction>.Start(inbox =>
60 var message = inbox.Receive();
61 var process=message.Then(Process,inbox.CancellationToken);
62 inbox.LoopAsync(process, loop);
68 private Task<object> Process(CloudAction action)
71 throw new ArgumentNullException("action");
72 if (action.AccountInfo==null)
73 throw new ArgumentException("The action.AccountInfo is empty","action");
74 Contract.EndContractBlock();
76 var accountInfo = action.AccountInfo;
78 using (log4net.ThreadContext.Stacks["NETWORK"].Push("PROCESS"))
80 Log.InfoFormat("[ACTION] Start Processing {0}:{1}->{2}", action.Action, action.LocalFile,
81 action.CloudFile.Name);
83 var localFile = action.LocalFile;
84 var cloudFile = action.CloudFile;
85 var downloadPath = (cloudFile == null)
87 : Path.Combine(accountInfo.AccountPath, cloudFile.RelativeUrlToFilePath(accountInfo.UserName));
91 var account = action.CloudFile.Account ?? accountInfo.UserName;
92 var container = action.CloudFile.Container ?? FolderConstants.PithosContainer;
94 switch (action.Action)
96 case CloudActionType.UploadUnconditional:
97 UploadCloudFile(accountInfo,account, container, localFile, action.LocalHash.Value, action.TopHash.Value);
99 case CloudActionType.DownloadUnconditional:
101 DownloadCloudFile(accountInfo, account, container, cloudFile,
104 case CloudActionType.DeleteCloud:
105 DeleteCloudFile(accountInfo, account, container, cloudFile.Name);
107 case CloudActionType.RenameCloud:
108 var moveAction = (CloudMoveAction)action;
109 RenameCloudFile(accountInfo, account, container, moveAction.OldFileName, moveAction.NewPath,
110 moveAction.NewFileName);
112 case CloudActionType.MustSynch:
114 if (!File.Exists(downloadPath))
116 DownloadCloudFile(accountInfo, account, container, cloudFile, downloadPath);
120 SyncFiles(accountInfo, action);
124 Log.InfoFormat("[ACTION] End Processing {0}:{1}->{2}", action.Action, action.LocalFile,
125 action.CloudFile.Name);
127 catch (OperationCanceledException)
131 catch (FileNotFoundException exc)
133 Log.ErrorFormat("{0} : {1} -> {2} failed because the file was not found.\n Rescheduling a delete",
134 action.Action, action.LocalFile, action.CloudFile, exc);
135 Post(new CloudDeleteAction(accountInfo,action.CloudFile,action.FileState));
137 catch (Exception exc)
139 Log.ErrorFormat("[REQUEUE] {0} : {1} -> {2} due to exception\r\n{3}",
140 action.Action, action.LocalFile, action.CloudFile, exc);
144 return CompletedTask<object>.Default;
148 private void SyncFiles(AccountInfo accountInfo,CloudAction action)
150 if (accountInfo == null)
151 throw new ArgumentNullException("accountInfo");
153 throw new ArgumentNullException("action");
154 if (action.LocalFile==null)
155 throw new ArgumentException("The action's local file is not specified","action");
156 if (!Path.IsPathRooted(action.LocalFile.FullName))
157 throw new ArgumentException("The action's local file path must be absolute","action");
158 if (action.CloudFile== null)
159 throw new ArgumentException("The action's cloud file is not specified", "action");
160 Contract.EndContractBlock();
162 var localFile = action.LocalFile;
163 var cloudFile = action.CloudFile;
164 var downloadPath=action.LocalFile.FullName.ToLower();
166 var account = cloudFile.Account;
167 //Use "pithos" by default if no container is specified
168 var container = cloudFile.Container ?? FolderConstants.PithosContainer;
170 var cloudUri = new Uri(cloudFile.Name, UriKind.Relative);
171 var cloudHash = cloudFile.Hash.ToLower();
172 var localHash = action.LocalHash.Value.ToLower();
173 var topHash = action.TopHash.Value.ToLower();
175 //Not enough to compare only the local hashes, also have to compare the tophashes
177 //If any of the hashes match, we are done
178 if ((cloudHash == localHash || cloudHash == topHash))
180 Log.InfoFormat("Skipping {0}, hashes match",downloadPath);
184 //The hashes DON'T match. We need to sync
185 var lastLocalTime = localFile.LastWriteTime;
186 var lastUpTime = cloudFile.Last_Modified;
188 //If the local file is newer upload it
189 if (lastUpTime <= lastLocalTime)
191 //It probably means it was changed while the app was down
192 UploadCloudFile(accountInfo,account, container, localFile, action.LocalHash.Value,
193 action.TopHash.Value);
197 //It the cloud file has a later date, it was modified by another user or computer.
198 //We need to check the local file's status
199 var status = StatusKeeper.GetFileStatus(downloadPath);
202 case FileStatus.Unchanged:
203 //If the local file's status is Unchanged, we can go on and download the newer cloud file
204 DownloadCloudFile(accountInfo,account, container,cloudFile,downloadPath);
206 case FileStatus.Modified:
207 //If the local file is Modified, we may have a conflict. In this case we should mark the file as Conflict
208 //We can't ensure that a file modified online since the last time will appear as Modified, unless we
209 //index all files before we start listening.
210 case FileStatus.Created:
211 //If the local file is Created, it means that the local and cloud files aren't related,
212 // yet they have the same name.
214 //In both cases we must mark the file as in conflict
215 ReportConflict(downloadPath);
218 //Other cases should never occur. Mark them as Conflict as well but log a warning
219 ReportConflict(downloadPath);
220 Log.WarnFormat("Unexcepted status {0} for file {1}->{2}", status,
221 downloadPath, action.CloudFile.Name);
227 private void ReportConflict(string downloadPath)
229 if (String.IsNullOrWhiteSpace(downloadPath))
230 throw new ArgumentNullException("downloadPath");
231 Contract.EndContractBlock();
233 StatusKeeper.SetFileOverlayStatus(downloadPath, FileOverlayStatus.Conflict);
234 var message = String.Format("Conflict detected for file {0}", downloadPath);
236 StatusNotification.NotifyChange(message, TraceLevel.Warning);
240 private Task<object> Process(CloudMoveAction action)
243 throw new ArgumentNullException("action");
244 Contract.EndContractBlock();
246 Log.InfoFormat("[ACTION] Start Processing {0}:{1}->{2}", action.Action, action.LocalFile, action.CloudFile.Name);
250 RenameCloudFile(action.OldFileName, action.NewPath, action.NewFileName);
251 Log.InfoFormat("[ACTION] End Processing {0}:{1}->{2}", action.Action, action.LocalFile, action.CloudFile.Name);
253 catch (OperationCanceledException)
257 catch (Exception exc)
259 Log.ErrorFormat("[REQUEUE] {0} : {1} -> {2} due to exception\r\n{3}",
260 action.Action, action.OldFileName, action.NewFileName, exc);
264 return CompletedTask<object>.Default;
269 public void Post(CloudAction cloudAction)
271 if (cloudAction == null)
272 throw new ArgumentNullException("cloudAction");
273 if (cloudAction.AccountInfo==null)
274 throw new ArgumentException("The CloudAction.AccountInfo is empty","cloudAction");
275 Contract.EndContractBlock();
277 //If the action targets a local file, add a treehash calculation
278 if (cloudAction.LocalFile != null)
280 var accountInfo = cloudAction.AccountInfo;
281 if (cloudAction.LocalFile.Length>accountInfo.BlockSize)
282 cloudAction.TopHash = new Lazy<string>(() => Signature.CalculateTreeHashAsync(cloudAction.LocalFile,
283 accountInfo.BlockSize, accountInfo.BlockHash).Result
284 .TopHash.ToHashString());
287 cloudAction.TopHash=new Lazy<string>(()=> cloudAction.LocalHash.Value);
291 _agent.Post(cloudAction);
294 class ObjectInfoByNameComparer:IEqualityComparer<ObjectInfo>
296 public bool Equals(ObjectInfo x, ObjectInfo y)
298 return x.Name.Equals(y.Name,StringComparison.InvariantCultureIgnoreCase);
301 public int GetHashCode(ObjectInfo obj)
303 return obj.Name.ToLower().GetHashCode();
309 //Remote files are polled periodically. Any changes are processed
310 public Task ProcessRemoteFiles(DateTime? since=null)
312 return Task<Task>.Factory.StartNewDelayed(10000, () =>
314 using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push("All accounts"))
316 //Next time we will check for all changes since the current check minus 1 second
317 //This is done to ensure there are no discrepancies due to clock differences
318 DateTime nextSince = DateTime.Now.AddSeconds(-1);
320 var tasks=from accountInfo in _accounts
321 select ProcessAccountFiles(accountInfo, since);
322 var process=Task.Factory.Iterate(tasks);
324 return process.ContinueWith(t =>
328 Log.Error("Error while processing accounts");
329 t.Exception.Handle(exc=>
331 Log.Error("Details:", exc);
335 ProcessRemoteFiles(nextSince);
341 public Task ProcessAccountFiles(AccountInfo accountInfo,DateTime? since=null)
343 if (accountInfo==null)
344 throw new ArgumentNullException("accountInfo");
345 if (String.IsNullOrWhiteSpace(accountInfo.AccountPath))
346 throw new ArgumentException("The AccountInfo.AccountPath is empty","accountInfo");
347 Contract.EndContractBlock();
349 using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push(accountInfo.UserName))
351 Log.Info("Scheduled");
352 var client=new CloudFilesClient(accountInfo);
354 //Get the list of server objects changed since the last check
355 var listObjects = Task<IList<ObjectInfo>>.Factory.StartNew(() =>
356 client.ListObjects(accountInfo.UserName, FolderConstants.PithosContainer, since));
357 //Get the list of deleted objects since the last check
358 var listTrash = Task<IList<ObjectInfo>>.Factory.StartNew(() =>
359 client.ListObjects(accountInfo.UserName, FolderConstants.TrashContainer, since));
361 var listShared = Task<IList<ObjectInfo>>.Factory.StartNew(() =>
362 client.ListSharedObjects());
364 var listAll = Task.Factory.TrackedSequence(
371 var enqueueFiles = listAll.ContinueWith(task =>
375 //ListObjects failed at this point, need to reschedule
376 Log.ErrorFormat("[FAIL] ListObjects for{0} in ProcessRemoteFiles with {0}", accountInfo.UserName,task.Exception);
379 using (log4net.ThreadContext.Stacks["SCHEDULE"].Push("Process Results"))
381 var remoteObjects = ((Task<IList<ObjectInfo>>) task.Result[0]).Result;
382 var trashObjects = ((Task<IList<ObjectInfo>>) task.Result[1]).Result;
383 var sharedObjects = ((Task<IList<ObjectInfo>>) task.Result[2]).Result;
385 //Items with the same name, hash may be both in the container and the trash
386 //Don't delete items that exist in the container
387 var realTrash = from trash in trashObjects
388 where !remoteObjects.Any(info => info.Hash == trash.Hash)
390 ProcessDeletedFiles(accountInfo,realTrash);
393 var remote = from info in remoteObjects.Union(sharedObjects)
395 where !name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase) &&
396 !name.StartsWith("fragments/", StringComparison.InvariantCultureIgnoreCase)
399 //Create a list of actions from the remote files
400 var allActions = ObjectsToActions(accountInfo,remote);
402 //And remove those that are already being processed by the agent
403 var distinctActions = allActions
404 .Except(_agent.GetEnumerable(), new PithosMonitor.LocalFileComparer())
407 //Queue all the actions
408 foreach (var message in distinctActions)
413 //Report the number of new files
414 var remoteCount = distinctActions.Count(action=>
415 action.Action==CloudActionType.DownloadUnconditional);
416 if ( remoteCount > 0)
417 StatusNotification.NotifyChange(String.Format("Processing {0} new files", remoteCount));
419 Log.Info("[LISTENER] End Processing");
423 var log = enqueueFiles.ContinueWith(t =>
427 Log.Error("[LISTENER] Exception", t.Exception);
431 Log.Info("[LISTENER] Finished");
438 //Creates an appropriate action for each server file
439 private IEnumerable<CloudAction> ObjectsToActions(AccountInfo accountInfo,IEnumerable<ObjectInfo> remote)
442 throw new ArgumentNullException();
443 Contract.EndContractBlock();
444 var fileAgent = GetFileAgent(accountInfo);
446 //In order to avoid multiple iterations over the files, we iterate only once
447 //over the remote files
448 foreach (var objectInfo in remote)
450 var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
451 //and remove any matching objects from the list, adding them to the commonObjects list
453 if (fileAgent.Exists(relativePath))
455 var localFile = fileAgent.GetFileInfo(relativePath);
456 var state = FileState.FindByFilePath(localFile.FullName);
457 //Common files should be checked on a per-case basis to detect differences, which is newer
459 yield return new CloudAction(accountInfo,CloudActionType.MustSynch,
460 localFile, objectInfo, state, accountInfo.BlockSize,
461 accountInfo.BlockHash);
465 //If there is no match we add them to the localFiles list
466 //but only if the file is not marked for deletion
467 var targetFile = Path.Combine(accountInfo.AccountPath, relativePath);
468 var fileStatus = StatusKeeper.GetFileStatus(targetFile);
469 if (fileStatus != FileStatus.Deleted)
471 //Remote files should be downloaded
472 yield return new CloudDownloadAction(accountInfo,objectInfo);
478 private static FileAgent GetFileAgent(AccountInfo accountInfo)
480 return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);
483 private void ProcessDeletedFiles(AccountInfo accountInfo,IEnumerable<ObjectInfo> trashObjects)
485 var fileAgent = GetFileAgent(accountInfo);
486 foreach (var trashObject in trashObjects)
488 var relativePath = trashObject.RelativeUrlToFilePath(accountInfo.UserName);
489 //and remove any matching objects from the list, adding them to the commonObjects list
490 fileAgent.Delete(relativePath);
495 private void RenameCloudFile(AccountInfo accountInfo,string account, string container,string oldFileName, string newPath, string newFileName)
497 if (accountInfo==null)
498 throw new ArgumentNullException("accountInfo");
499 if (String.IsNullOrWhiteSpace(account))
500 throw new ArgumentNullException("account");
501 if (String.IsNullOrWhiteSpace(container))
502 throw new ArgumentNullException("container");
503 if (String.IsNullOrWhiteSpace(oldFileName))
504 throw new ArgumentNullException("oldFileName");
505 if (String.IsNullOrWhiteSpace(oldFileName))
506 throw new ArgumentNullException("newPath");
507 if (String.IsNullOrWhiteSpace(oldFileName))
508 throw new ArgumentNullException("newFileName");
509 Contract.EndContractBlock();
510 //The local file is already renamed
511 this.StatusKeeper.SetFileOverlayStatus(newPath, FileOverlayStatus.Modified);
513 var client = new CloudFilesClient(accountInfo);
514 client.MoveObject(account, container, oldFileName, container, newFileName);
516 this.StatusKeeper.SetFileStatus(newPath, FileStatus.Unchanged);
517 this.StatusKeeper.SetFileOverlayStatus(newPath, FileOverlayStatus.Normal);
518 NativeMethods.RaiseChangeNotification(newPath);
521 private void DeleteCloudFile(AccountInfo accountInfo, string account, string container, string fileName)
523 if (accountInfo == null)
524 throw new ArgumentNullException("accountInfo");
525 if (String.IsNullOrWhiteSpace(account))
526 throw new ArgumentNullException("account");
527 if (String.IsNullOrWhiteSpace(container))
528 throw new ArgumentNullException("container");
529 if (String.IsNullOrWhiteSpace(container))
530 throw new ArgumentNullException("container");
532 if (String.IsNullOrWhiteSpace(fileName))
533 throw new ArgumentNullException("fileName");
534 if (Path.IsPathRooted(fileName))
535 throw new ArgumentException("The fileName should not be rooted","fileName");
536 Contract.EndContractBlock();
538 var fileAgent = GetFileAgent(accountInfo);
540 using ( log4net.ThreadContext.Stacks["DeleteCloudFile"].Push("Delete"))
542 var info = fileAgent.GetFileInfo(fileName);
543 var fullPath = info.FullName.ToLower();
544 this.StatusKeeper.SetFileOverlayStatus(fullPath, FileOverlayStatus.Modified);
546 var client = new CloudFilesClient(accountInfo);
547 client.DeleteObject(account, container, fileName);
549 this.StatusKeeper.ClearFileStatus(fullPath);
554 private void DownloadCloudFile(AccountInfo accountInfo, string account, string container,ObjectInfo cloudFile , string localPath)
556 if (accountInfo == null)
557 throw new ArgumentNullException("accountInfo");
558 if (String.IsNullOrWhiteSpace(account))
559 throw new ArgumentNullException("account");
560 if (String.IsNullOrWhiteSpace(container))
561 throw new ArgumentNullException("container");
562 if (cloudFile == null)
563 throw new ArgumentNullException("cloudFile");
564 if (String.IsNullOrWhiteSpace(localPath))
565 throw new ArgumentNullException("localPath");
566 if (!Path.IsPathRooted(localPath))
567 throw new ArgumentException("The localPath must be rooted", "localPath");
568 Contract.EndContractBlock();
570 Debug.Assert(cloudFile.Account==account);
571 Debug.Assert(cloudFile.Container == container);
573 var download=Task.Factory.Iterate(DownloadIterator(accountInfo,account,container, cloudFile, localPath));
577 private IEnumerable<Task> DownloadIterator(AccountInfo accountInfo, string account, string container, ObjectInfo cloudFile, string localPath)
579 if (accountInfo == null)
580 throw new ArgumentNullException("accountInfo");
581 if (String.IsNullOrWhiteSpace(account))
582 throw new ArgumentNullException("account");
583 if (String.IsNullOrWhiteSpace(container))
584 throw new ArgumentNullException("container");
586 throw new ArgumentNullException("cloudFile");
587 if (String.IsNullOrWhiteSpace(localPath))
588 throw new ArgumentNullException("localPath");
589 if (!Path.IsPathRooted(localPath))
590 throw new ArgumentException("The localPath must be rooted", "localPath");
591 Contract.EndContractBlock();
593 Uri relativeUrl = new Uri(cloudFile.Name, UriKind.Relative);
595 var url = relativeUrl.ToString();
596 if (cloudFile.Name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase))
599 //Are we already downloading or uploading the file?
600 using (var gate=NetworkGate.Acquire(localPath, NetworkOperation.Downloading))
604 //The file's hashmap will be stored in the same location with the extension .hashmap
605 //var hashPath = Path.Combine(FileAgent.FragmentsPath, relativePath + ".hashmap");
607 var client = new CloudFilesClient(accountInfo);
608 //Retrieve the hashmap from the server
609 var getHashMap = client.GetHashMap(account, container, url);
610 yield return getHashMap;
612 var serverHash=getHashMap.Result;
613 //If it's a small file
614 var downloadTask=(serverHash.Hashes.Count == 1 )
615 //Download it in one go
616 ? DownloadEntireFile(accountInfo,client, account, container, relativeUrl, localPath,serverHash)
617 //Otherwise download it block by block
618 : DownloadWithBlocks(accountInfo,client, account, container, relativeUrl, localPath, serverHash);
620 yield return downloadTask;
622 if (cloudFile.AllowedTo == "read")
624 var attributes=File.GetAttributes(localPath);
625 File.SetAttributes(localPath,attributes|FileAttributes.ReadOnly);
627 //Retrieve the object's metadata
628 var info=client.GetObjectInfo(account, container, url);
629 Debug.Assert(cloudFile==info);
631 StatusKeeper.StoreInfo(localPath, info);
633 //Notify listeners that a local file has changed
634 StatusNotification.NotifyChangedFile(localPath);
639 //Download a small file with a single GET operation
640 private Task DownloadEntireFile(AccountInfo accountInfo, CloudFilesClient client, string account, string container, Uri relativeUrl, string localPath,TreeHash serverHash)
643 throw new ArgumentNullException("client");
644 if (String.IsNullOrWhiteSpace(account))
645 throw new ArgumentNullException("account");
646 if (String.IsNullOrWhiteSpace(container))
647 throw new ArgumentNullException("container");
648 if (relativeUrl == null)
649 throw new ArgumentNullException("relativeUrl");
650 if (String.IsNullOrWhiteSpace(localPath))
651 throw new ArgumentNullException("localPath");
652 if (!Path.IsPathRooted(localPath))
653 throw new ArgumentException("The localPath must be rooted", "localPath");
654 Contract.EndContractBlock();
656 //If the file already exists
657 if (File.Exists(localPath))
659 //First check with MD5 as this is a small file
660 var localMD5 = Signature.CalculateMD5(localPath);
661 var cloudHash=serverHash.TopHash.ToHashString();
662 if (localMD5==cloudHash)
663 return CompletedTask.Default;
664 //Then check with a treehash
665 var localTreeHash = Signature.CalculateTreeHash(localPath, serverHash.BlockSize, serverHash.BlockHash);
666 var localHash = localTreeHash.TopHash.ToHashString();
667 if (localHash==cloudHash)
668 return CompletedTask.Default;
671 var fileAgent = GetFileAgent(accountInfo);
672 //Calculate the relative file path for the new file
673 var relativePath = relativeUrl.RelativeUriToFilePath();
674 //The file will be stored in a temporary location while downloading with an extension .download
675 var tempPath = Path.Combine(fileAgent.FragmentsPath, relativePath + ".download");
676 //Make sure the target folder exists. DownloadFileTask will not create the folder
677 var tempFolder = Path.GetDirectoryName(tempPath);
678 if (!Directory.Exists(tempFolder))
679 Directory.CreateDirectory(tempFolder);
681 //Download the object to the temporary location
682 var getObject = client.GetObject(account, container, relativeUrl.ToString(), tempPath).ContinueWith(t =>
684 t.PropagateExceptions();
685 //Create the local folder if it doesn't exist (necessary for shared objects)
686 var localFolder = Path.GetDirectoryName(localPath);
687 if (!Directory.Exists(localFolder))
688 Directory.CreateDirectory(localFolder);
689 //And move it to its actual location once downloading is finished
690 if (File.Exists(localPath))
691 File.Replace(tempPath,localPath,null,true);
693 File.Move(tempPath,localPath);
698 //Download a file asynchronously using blocks
699 public Task DownloadWithBlocks(AccountInfo accountInfo, CloudFilesClient client, string account, string container, Uri relativeUrl, string localPath, TreeHash serverHash)
702 throw new ArgumentNullException("client");
703 if (String.IsNullOrWhiteSpace(account))
704 throw new ArgumentNullException("account");
705 if (String.IsNullOrWhiteSpace(container))
706 throw new ArgumentNullException("container");
707 if (relativeUrl == null)
708 throw new ArgumentNullException("relativeUrl");
709 if (String.IsNullOrWhiteSpace(localPath))
710 throw new ArgumentNullException("localPath");
711 if (!Path.IsPathRooted(localPath))
712 throw new ArgumentException("The localPath must be rooted", "localPath");
713 if (serverHash == null)
714 throw new ArgumentNullException("serverHash");
715 Contract.EndContractBlock();
717 return Task.Factory.Iterate(BlockDownloadIterator(accountInfo,client,account,container, relativeUrl, localPath, serverHash));
720 private IEnumerable<Task> BlockDownloadIterator(AccountInfo accountInfo,CloudFilesClient client, string account, string container, Uri relativeUrl, string localPath, TreeHash serverHash)
723 throw new ArgumentNullException("client");
724 if (String.IsNullOrWhiteSpace(account))
725 throw new ArgumentNullException("account");
726 if (String.IsNullOrWhiteSpace(container))
727 throw new ArgumentNullException("container");
728 if (relativeUrl == null)
729 throw new ArgumentNullException("relativeUrl");
730 if (String.IsNullOrWhiteSpace(localPath))
731 throw new ArgumentNullException("localPath");
732 if (!Path.IsPathRooted(localPath))
733 throw new ArgumentException("The localPath must be rooted", "localPath");
735 throw new ArgumentNullException("serverHash");
736 Contract.EndContractBlock();
738 var fileAgent = GetFileAgent(accountInfo);
740 //Calculate the relative file path for the new file
741 var relativePath = relativeUrl.RelativeUriToFilePath();
742 var blockUpdater = new BlockUpdater(fileAgent.FragmentsPath, localPath, relativePath, serverHash);
746 //Calculate the file's treehash
747 var calcHash = Signature.CalculateTreeHashAsync(localPath, serverHash.BlockSize,serverHash.BlockHash);
748 yield return calcHash;
749 var treeHash = calcHash.Result;
751 //And compare it with the server's hash
752 var upHashes = serverHash.GetHashesAsStrings();
753 var localHashes = treeHash.HashDictionary;
754 for (int i = 0; i < upHashes.Length; i++)
756 //For every non-matching hash
757 var upHash = upHashes[i];
758 if (!localHashes.ContainsKey(upHash))
760 if (blockUpdater.UseOrphan(i, upHash))
762 Log.InfoFormat("[BLOCK GET] ORPHAN FOUND for {0} of {1} for {2}", i, upHashes.Length, localPath);
765 Log.InfoFormat("[BLOCK GET] START {0} of {1} for {2}", i, upHashes.Length, localPath);
766 var start = i*serverHash.BlockSize;
767 //To download the last block just pass a null for the end of the range
769 if (i < upHashes.Length - 1 )
770 end= ((i + 1)*serverHash.BlockSize) ;
772 //Download the missing block
773 var getBlock = client.GetBlock(account, container, relativeUrl, start, end);
774 yield return getBlock;
775 var block = getBlock.Result;
778 yield return blockUpdater.StoreBlock(i, block);
781 Log.InfoFormat("[BLOCK GET] FINISH {0} of {1} for {2}", i, upHashes.Length, localPath);
785 blockUpdater.Commit();
786 Log.InfoFormat("[BLOCK GET] COMPLETE {0}", localPath);
790 private void UploadCloudFile(AccountInfo accountInfo, string account, string container, FileInfo fileInfo, string hash, string topHash)
792 if (accountInfo == null)
793 throw new ArgumentNullException("accountInfo");
794 if (String.IsNullOrWhiteSpace(account))
795 throw new ArgumentNullException("account");
796 if (String.IsNullOrWhiteSpace(container))
797 throw new ArgumentNullException("container");
798 if (fileInfo == null)
799 throw new ArgumentNullException("fileInfo");
800 if (String.IsNullOrWhiteSpace(hash))
801 throw new ArgumentNullException("hash");
803 throw new ArgumentNullException("topHash");
804 Contract.EndContractBlock();
806 var upload = Task.Factory.Iterate(UploadIterator(accountInfo,account,container,fileInfo, hash.ToLower(), topHash.ToLower()));
810 private IEnumerable<Task> UploadIterator(AccountInfo accountInfo, string account, string container, FileInfo fileInfo, string hash, string topHash)
812 if (accountInfo == null)
813 throw new ArgumentNullException("accountInfo");
814 if (String.IsNullOrWhiteSpace(account))
815 throw new ArgumentNullException("account");
816 if (String.IsNullOrWhiteSpace(container))
817 throw new ArgumentNullException("container");
818 if (fileInfo == null)
819 throw new ArgumentNullException("fileInfo");
820 if (String.IsNullOrWhiteSpace(hash))
821 throw new ArgumentNullException("hash");
823 throw new ArgumentNullException("topHash");
824 Contract.EndContractBlock();
826 if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase))
829 var url = fileInfo.AsRelativeUrlTo(accountInfo.AccountPath);
831 var fullFileName = fileInfo.FullName;
832 using(var gate=NetworkGate.Acquire(fullFileName,NetworkOperation.Uploading))
834 //Abort if the file is already being uploaded or downloaded
838 var client = new CloudFilesClient(accountInfo);
839 //Even if GetObjectInfo times out, we can proceed with the upload
840 var info = client.GetObjectInfo(account, container, url);
841 var cloudHash = info.Hash.ToLower();
843 //If the file hashes match, abort the upload
844 if (hash == cloudHash || topHash ==cloudHash)
846 //but store any metadata changes
847 this.StatusKeeper.StoreInfo(fullFileName, info);
848 Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);
852 if (info.AllowedTo=="read")
855 //Mark the file as modified while we upload it
856 StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);
859 //If the file is larger than the block size, try a hashmap PUT
860 if (fileInfo.Length > accountInfo.BlockSize )
862 //To upload using a hashmap
863 //First, calculate the tree hash
864 var treeHash = Signature.CalculateTreeHashAsync(fileInfo.FullName, accountInfo.BlockSize,
865 accountInfo.BlockHash);
866 yield return treeHash;
868 yield return Task.Factory.Iterate(UploadWithHashMap(accountInfo,account,container,fileInfo,url,treeHash));
873 //Otherwise do a regular PUT
874 yield return client.PutObject(account, container, url, fullFileName, hash);
876 //If everything succeeds, change the file and overlay status to normal
877 this.StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal);
879 //Notify the Shell to update the overlays
880 NativeMethods.RaiseChangeNotification(fullFileName);
881 StatusNotification.NotifyChangedFile(fullFileName);
884 public IEnumerable<Task> UploadWithHashMap(AccountInfo accountInfo,string account,string container,FileInfo fileInfo,string url,Task<TreeHash> treeHash)
886 if (accountInfo == null)
887 throw new ArgumentNullException("accountInfo");
888 if (String.IsNullOrWhiteSpace(account))
889 throw new ArgumentNullException("account");
890 if (String.IsNullOrWhiteSpace(container))
891 throw new ArgumentNullException("container");
892 if (fileInfo == null)
893 throw new ArgumentNullException("fileInfo");
894 if (String.IsNullOrWhiteSpace(url))
895 throw new ArgumentNullException(url);
897 throw new ArgumentNullException("treeHash");
898 Contract.EndContractBlock();
900 var fullFileName = fileInfo.FullName;
902 var client = new CloudFilesClient(accountInfo);
903 //Send the hashmap to the server
904 var hashPut = client.PutHashMap(account, container, url, treeHash.Result);
905 yield return hashPut;
907 var missingHashes = hashPut.Result;
908 //If the server returns no missing hashes, we are done
909 while (missingHashes.Count > 0)
912 var buffer = new byte[accountInfo.BlockSize];
913 foreach (var missingHash in missingHashes)
915 //Find the proper block
916 var blockIndex = treeHash.Result.HashDictionary[missingHash];
917 var offset = blockIndex*accountInfo.BlockSize;
919 var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);
921 //And upload the block
922 var postBlock = client.PostBlock(account, container, buffer, 0, read);
924 //We have to handle possible exceptions in a continuation because
925 //*yield return* can't appear inside a try block
926 yield return postBlock.ContinueWith(t =>
928 exc => Log.ErrorFormat("[ERROR] uploading block {0} of {1}\n{2}", blockIndex, fullFileName, exc),
929 ()=>Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex,fullFileName)));
932 //Repeat until there are no more missing hashes
933 hashPut = client.PutHashMap(account, container, url, treeHash.Result);
934 yield return hashPut;
935 missingHashes = hashPut.Result;
940 public void AddAccount(AccountInfo accountInfo)
942 if (!_accounts.Contains(accountInfo))
943 _accounts.Add(accountInfo);