2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.ComponentModel.Composition;
5 using System.Diagnostics;
6 using System.Diagnostics.Contracts;
11 using System.Threading;
12 using System.Threading.Tasks;
13 using Pithos.Interfaces;
17 namespace Pithos.Core.Agents
20 public class NetworkAgent
22 private Agent<CloudAction> _agent;
25 public IStatusKeeper StatusKeeper { get; set; }
27 public IStatusNotification StatusNotification { get; set; }
30 public FileAgent FileAgent {get;set;}
33 /* public int BlockSize { get; set; }
34 public string BlockHash { get; set; }*/
36 private static readonly ILog Log = LogManager.GetLogger("NetworkAgent");
38 private List<AccountInfo> _accounts=new List<AccountInfo>();
40 public void Start(/*int blockSize, string blockHash*/)
44 throw new ArgumentOutOfRangeException("blockSize");
45 if (String.IsNullOrWhiteSpace(blockHash))
46 throw new ArgumentOutOfRangeException("blockHash");
47 Contract.EndContractBlock();
51 BlockSize = blockSize;
52 BlockHash = blockHash;
56 _agent = Agent<CloudAction>.Start(inbox =>
61 var message = inbox.Receive();
62 var process=message.Then(Process,inbox.CancellationToken);
63 inbox.LoopAsync(process, loop);
69 private async Task Process(CloudAction action)
72 throw new ArgumentNullException("action");
73 if (action.AccountInfo==null)
74 throw new ArgumentException("The action.AccountInfo is empty","action");
75 Contract.EndContractBlock();
77 var accountInfo = action.AccountInfo;
79 using (log4net.ThreadContext.Stacks["NETWORK"].Push("PROCESS"))
81 Log.InfoFormat("[ACTION] Start Processing {0}", action);
83 var localFile = action.LocalFile;
84 var cloudFile = action.CloudFile;
85 var downloadPath = action.GetDownloadPath();
90 switch (action.Action)
92 case CloudActionType.UploadUnconditional:
93 await UploadCloudFile(action);
95 case CloudActionType.DownloadUnconditional:
97 await DownloadCloudFile(accountInfo, cloudFile,downloadPath);
99 case CloudActionType.DeleteCloud:
100 DeleteCloudFile(accountInfo, cloudFile, cloudFile.Name);
102 case CloudActionType.RenameCloud:
103 var moveAction = (CloudMoveAction)action;
104 RenameCloudFile(accountInfo, moveAction);
106 case CloudActionType.MustSynch:
108 if (!File.Exists(downloadPath))
110 await DownloadCloudFile(accountInfo, cloudFile, downloadPath);
114 await SyncFiles(accountInfo, action);
118 Log.InfoFormat("[ACTION] End Processing {0}:{1}->{2}", action.Action, action.LocalFile,
119 action.CloudFile.Name);
121 catch (OperationCanceledException)
125 catch (FileNotFoundException exc)
127 Log.ErrorFormat("{0} : {1} -> {2} failed because the file was not found.\n Rescheduling a delete",
128 action.Action, action.LocalFile, action.CloudFile, exc);
129 //Post a delete action for the missing file
130 Post(new CloudDeleteAction(action));
132 catch (Exception exc)
134 Log.ErrorFormat("[REQUEUE] {0} : {1} -> {2} due to exception\r\n{3}",
135 action.Action, action.LocalFile, action.CloudFile, exc);
142 private async Task SyncFiles(AccountInfo accountInfo,CloudAction action)
144 if (accountInfo == null)
145 throw new ArgumentNullException("accountInfo");
147 throw new ArgumentNullException("action");
148 if (action.LocalFile==null)
149 throw new ArgumentException("The action's local file is not specified","action");
150 if (!Path.IsPathRooted(action.LocalFile.FullName))
151 throw new ArgumentException("The action's local file path must be absolute","action");
152 if (action.CloudFile== null)
153 throw new ArgumentException("The action's cloud file is not specified", "action");
154 Contract.EndContractBlock();
156 var localFile = action.LocalFile;
157 var cloudFile = action.CloudFile;
158 var downloadPath=action.LocalFile.FullName.ToLower();
160 var cloudHash = cloudFile.Hash.ToLower();
161 var localHash = action.LocalHash.Value.ToLower();
162 var topHash = action.TopHash.Value.ToLower();
164 //Not enough to compare only the local hashes, also have to compare the tophashes
166 //If any of the hashes match, we are done
167 if ((cloudHash == localHash || cloudHash == topHash))
169 Log.InfoFormat("Skipping {0}, hashes match",downloadPath);
173 //The hashes DON'T match. We need to sync
174 var lastLocalTime = localFile.LastWriteTime;
175 var lastUpTime = cloudFile.Last_Modified;
177 //If the local file is newer upload it
178 if (lastUpTime <= lastLocalTime)
180 //It probably means it was changed while the app was down
181 UploadCloudFile(action);
185 //It the cloud file has a later date, it was modified by another user or computer.
186 //We need to check the local file's status
187 var status = StatusKeeper.GetFileStatus(downloadPath);
190 case FileStatus.Unchanged:
191 //If the local file's status is Unchanged, we can go on and download the newer cloud file
192 await DownloadCloudFile(accountInfo,cloudFile,downloadPath);
194 case FileStatus.Modified:
195 //If the local file is Modified, we may have a conflict. In this case we should mark the file as Conflict
196 //We can't ensure that a file modified online since the last time will appear as Modified, unless we
197 //index all files before we start listening.
198 case FileStatus.Created:
199 //If the local file is Created, it means that the local and cloud files aren't related,
200 // yet they have the same name.
202 //In both cases we must mark the file as in conflict
203 ReportConflict(downloadPath);
206 //Other cases should never occur. Mark them as Conflict as well but log a warning
207 ReportConflict(downloadPath);
208 Log.WarnFormat("Unexcepted status {0} for file {1}->{2}", status,
209 downloadPath, action.CloudFile.Name);
215 private void ReportConflict(string downloadPath)
217 if (String.IsNullOrWhiteSpace(downloadPath))
218 throw new ArgumentNullException("downloadPath");
219 Contract.EndContractBlock();
221 StatusKeeper.SetFileOverlayStatus(downloadPath, FileOverlayStatus.Conflict);
222 var message = String.Format("Conflict detected for file {0}", downloadPath);
224 StatusNotification.NotifyChange(message, TraceLevel.Warning);
227 public void Post(CloudAction cloudAction)
229 if (cloudAction == null)
230 throw new ArgumentNullException("cloudAction");
231 if (cloudAction.AccountInfo==null)
232 throw new ArgumentException("The CloudAction.AccountInfo is empty","cloudAction");
233 Contract.EndContractBlock();
235 //If the action targets a local file, add a treehash calculation
236 if (cloudAction.LocalFile != null)
238 var accountInfo = cloudAction.AccountInfo;
239 if (cloudAction.LocalFile.Length>accountInfo.BlockSize)
240 cloudAction.TopHash = new Lazy<string>(() => Signature.CalculateTreeHashAsync(cloudAction.LocalFile,
241 accountInfo.BlockSize, accountInfo.BlockHash).Result
242 .TopHash.ToHashString());
245 cloudAction.TopHash=new Lazy<string>(()=> cloudAction.LocalHash.Value);
249 _agent.Post(cloudAction);
252 class ObjectInfoByNameComparer:IEqualityComparer<ObjectInfo>
254 public bool Equals(ObjectInfo x, ObjectInfo y)
256 return x.Name.Equals(y.Name,StringComparison.InvariantCultureIgnoreCase);
259 public int GetHashCode(ObjectInfo obj)
261 return obj.Name.ToLower().GetHashCode();
267 //Remote files are polled periodically. Any changes are processed
268 public Task ProcessRemoteFiles(DateTime? since=null)
270 return Task<Task>.Factory.StartNewDelayed(10000, () =>
272 using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push("All accounts"))
274 //Next time we will check for all changes since the current check minus 1 second
275 //This is done to ensure there are no discrepancies due to clock differences
276 DateTime nextSince = DateTime.Now.AddSeconds(-1);
278 var tasks=from accountInfo in _accounts
279 select ProcessAccountFiles(accountInfo, since);
280 var process=Task.Factory.Iterate(tasks);
282 return process.ContinueWith(t =>
286 Log.Error("Error while processing accounts");
287 t.Exception.Handle(exc=>
289 Log.Error("Details:", exc);
293 ProcessRemoteFiles(nextSince);
299 public Task ProcessAccountFiles(AccountInfo accountInfo,DateTime? since=null)
301 if (accountInfo==null)
302 throw new ArgumentNullException("accountInfo");
303 if (String.IsNullOrWhiteSpace(accountInfo.AccountPath))
304 throw new ArgumentException("The AccountInfo.AccountPath is empty","accountInfo");
305 Contract.EndContractBlock();
307 using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push(accountInfo.UserName))
309 Log.Info("Scheduled");
310 var client=new CloudFilesClient(accountInfo);
312 var containers = client.ListContainers(accountInfo.UserName);
314 CreateContainerFolders(accountInfo, containers);
317 //Get the list of server objects changed since the last check
318 var listObjects = from container in containers
319 select Task<IList<ObjectInfo>>.Factory.StartNew(_ =>
320 client.ListObjects(accountInfo.UserName, container.Name, since),container.Name);
322 var listAll = Task.Factory.WhenAll(listObjects.ToArray());
326 //Get the list of deleted objects since the last check
328 var listTrash = Task<IList<ObjectInfo>>.Factory.StartNew(() =>
329 client.ListObjects(accountInfo.UserName, FolderConstants.TrashContainer, since));
331 var listShared = Task<IList<ObjectInfo>>.Factory.StartNew(() =>
332 client.ListSharedObjects(since));
334 var listAll = Task.Factory.TrackedSequence(
342 var enqueueFiles = listAll.ContinueWith(task =>
346 //ListObjects failed at this point, need to reschedule
347 Log.ErrorFormat("[FAIL] ListObjects for{0} in ProcessRemoteFiles with {0}", accountInfo.UserName,task.Exception);
350 using (log4net.ThreadContext.Stacks["SCHEDULE"].Push("Process Results"))
352 var dict=task.Result.ToDictionary(t=> t.AsyncState);
354 //Get all non-trash objects. Remember, the container name is stored in AsyncState
355 var remoteObjects = from objectList in task.Result
356 where (string)objectList.AsyncState != "trash"
357 from obj in objectList.Result
360 var trashObjects = dict["trash"].Result;
361 //var sharedObjects = ((Task<IList<ObjectInfo>>) task.Result[2]).Result;
363 //Items with the same name, hash may be both in the container and the trash
364 //Don't delete items that exist in the container
365 var realTrash = from trash in trashObjects
366 where !remoteObjects.Any(info => info.Hash == trash.Hash)
368 ProcessDeletedFiles(accountInfo,realTrash);
371 var remote = from info in remoteObjects//.Union(sharedObjects)
373 where !name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase) &&
374 !name.StartsWith(FolderConstants.CacheFolder +"/", StringComparison.InvariantCultureIgnoreCase)
377 //Create a list of actions from the remote files
378 var allActions = ObjectsToActions(accountInfo,remote);
380 //And remove those that are already being processed by the agent
381 var distinctActions = allActions
382 .Except(_agent.GetEnumerable(), new PithosMonitor.LocalFileComparer())
385 //Queue all the actions
386 foreach (var message in distinctActions)
391 //Report the number of new files
392 var remoteCount = distinctActions.Count(action=>
393 action.Action==CloudActionType.DownloadUnconditional);
395 if ( remoteCount > 0)
396 StatusNotification.NotifyChange(String.Format("Processing {0} new files", remoteCount));
399 Log.Info("[LISTENER] End Processing");
403 var log = enqueueFiles.ContinueWith(t =>
407 Log.Error("[LISTENER] Exception", t.Exception);
411 Log.Info("[LISTENER] Finished");
418 private static void CreateContainerFolders(AccountInfo accountInfo, IList<ContainerInfo> containers)
420 var containerPaths = from container in containers
421 let containerPath = Path.Combine(accountInfo.AccountPath, container.Name)
422 where container.Name != FolderConstants.TrashContainer && !Directory.Exists(containerPath)
423 select containerPath;
425 foreach (var path in containerPaths)
427 Directory.CreateDirectory(path);
431 //Creates an appropriate action for each server file
432 private IEnumerable<CloudAction> ObjectsToActions(AccountInfo accountInfo,IEnumerable<ObjectInfo> remote)
435 throw new ArgumentNullException();
436 Contract.EndContractBlock();
437 var fileAgent = GetFileAgent(accountInfo);
439 //In order to avoid multiple iterations over the files, we iterate only once
440 //over the remote files
441 foreach (var objectInfo in remote)
443 var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
444 //and remove any matching objects from the list, adding them to the commonObjects list
446 if (fileAgent.Exists(relativePath))
448 var localFile = fileAgent.GetFileInfo(relativePath);
449 var state = FileState.FindByFilePath(localFile.FullName);
450 //Common files should be checked on a per-case basis to detect differences, which is newer
452 yield return new CloudAction(accountInfo,CloudActionType.MustSynch,
453 localFile, objectInfo, state, accountInfo.BlockSize,
454 accountInfo.BlockHash);
458 //If there is no match we add them to the localFiles list
459 //but only if the file is not marked for deletion
460 var targetFile = Path.Combine(accountInfo.AccountPath, relativePath);
461 var fileStatus = StatusKeeper.GetFileStatus(targetFile);
462 if (fileStatus != FileStatus.Deleted)
464 //Remote files should be downloaded
465 yield return new CloudDownloadAction(accountInfo,objectInfo);
471 private static FileAgent GetFileAgent(AccountInfo accountInfo)
473 return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);
476 private void ProcessDeletedFiles(AccountInfo accountInfo,IEnumerable<ObjectInfo> trashObjects)
478 var fileAgent = GetFileAgent(accountInfo);
479 foreach (var trashObject in trashObjects)
481 var barePath = trashObject.RelativeUrlToFilePath(accountInfo.UserName);
482 //HACK: Assume only the "pithos" container is used. Must find out what happens when
483 //deleting a file from a different container
484 var relativePath = Path.Combine("pithos", barePath);
485 fileAgent.Delete(relativePath);
490 private void RenameCloudFile(AccountInfo accountInfo,CloudMoveAction action)
492 if (accountInfo==null)
493 throw new ArgumentNullException("accountInfo");
495 throw new ArgumentNullException("action");
496 if (action.CloudFile==null)
497 throw new ArgumentException("CloudFile","action");
498 if (action.LocalFile==null)
499 throw new ArgumentException("LocalFile","action");
500 if (action.OldLocalFile==null)
501 throw new ArgumentException("OldLocalFile","action");
502 if (action.OldCloudFile==null)
503 throw new ArgumentException("OldCloudFile","action");
504 Contract.EndContractBlock();
506 var newFilePath = action.LocalFile.FullName;
507 //The local file is already renamed
508 this.StatusKeeper.SetFileOverlayStatus(newFilePath, FileOverlayStatus.Modified);
511 var account = action.CloudFile.Account ?? accountInfo.UserName;
512 var container = action.CloudFile.Container;
514 var client = new CloudFilesClient(accountInfo);
515 client.MoveObject(account, container, action.OldCloudFile.Name, container, action.CloudFile.Name);
517 this.StatusKeeper.SetFileStatus(newFilePath, FileStatus.Unchanged);
518 this.StatusKeeper.SetFileOverlayStatus(newFilePath, FileOverlayStatus.Normal);
519 NativeMethods.RaiseChangeNotification(newFilePath);
522 private void DeleteCloudFile(AccountInfo accountInfo, ObjectInfo cloudFile,string fileName)
524 if (accountInfo == null)
525 throw new ArgumentNullException("accountInfo");
527 throw new ArgumentNullException("cloudFile");
529 if (String.IsNullOrWhiteSpace(fileName))
530 throw new ArgumentNullException("fileName");
531 if (Path.IsPathRooted(fileName))
532 throw new ArgumentException("The fileName should not be rooted","fileName");
533 if (String.IsNullOrWhiteSpace(cloudFile.Container))
534 throw new ArgumentException("Invalid container", "cloudFile");
535 Contract.EndContractBlock();
537 var fileAgent = GetFileAgent(accountInfo);
539 using ( log4net.ThreadContext.Stacks["DeleteCloudFile"].Push("Delete"))
541 var info = fileAgent.GetFileInfo(fileName);
542 var fullPath = info.FullName.ToLower();
543 this.StatusKeeper.SetFileOverlayStatus(fullPath, FileOverlayStatus.Modified);
545 var account = cloudFile.Account ?? accountInfo.UserName;
546 var container = cloudFile.Container ;//?? FolderConstants.PithosContainer;
548 var client = new CloudFilesClient(accountInfo);
549 client.DeleteObject(account, container, fileName);
551 this.StatusKeeper.ClearFileStatus(fullPath);
556 private async Task DownloadCloudFile(AccountInfo accountInfo, ObjectInfo cloudFile , string localPath)
558 if (accountInfo == null)
559 throw new ArgumentNullException("accountInfo");
560 if (cloudFile == null)
561 throw new ArgumentNullException("cloudFile");
562 if (String.IsNullOrWhiteSpace(cloudFile.Account))
563 throw new ArgumentNullException("cloudFile");
564 if (String.IsNullOrWhiteSpace(cloudFile.Container))
565 throw new ArgumentNullException("cloudFile");
566 if (String.IsNullOrWhiteSpace(localPath))
567 throw new ArgumentNullException("localPath");
568 if (!Path.IsPathRooted(localPath))
569 throw new ArgumentException("The localPath must be rooted", "localPath");
570 Contract.EndContractBlock();
572 Uri relativeUrl = new Uri(cloudFile.Name, UriKind.Relative);
574 var url = relativeUrl.ToString();
575 if (cloudFile.Name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase))
578 //Are we already downloading or uploading the file?
579 using (var gate=NetworkGate.Acquire(localPath, NetworkOperation.Downloading))
583 //The file's hashmap will be stored in the same location with the extension .hashmap
584 //var hashPath = Path.Combine(FileAgent.CachePath, relativePath + ".hashmap");
586 var client = new CloudFilesClient(accountInfo);
587 var account = cloudFile.Account;
588 var container = cloudFile.Container;
590 //Retrieve the hashmap from the server
591 var serverHash = await client.GetHashMap(account, container, url);
592 //If it's a small file
593 if (serverHash.Hashes.Count == 1 )
594 //Download it in one go
595 await DownloadEntireFileAsync(accountInfo, client, cloudFile, relativeUrl, localPath, serverHash);
596 //Otherwise download it block by block
598 await DownloadWithBlocks(accountInfo,client, cloudFile, relativeUrl, localPath, serverHash);
600 if (cloudFile.AllowedTo == "read")
602 var attributes=File.GetAttributes(localPath);
603 File.SetAttributes(localPath,attributes|FileAttributes.ReadOnly);
606 //Now we can store the object's metadata without worrying about ghost status entries
607 StatusKeeper.StoreInfo(localPath, cloudFile);
612 //Download a small file with a single GET operation
613 private async Task DownloadEntireFileAsync(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string localPath,TreeHash serverHash)
616 throw new ArgumentNullException("client");
618 throw new ArgumentNullException("cloudFile");
619 if (relativeUrl == null)
620 throw new ArgumentNullException("relativeUrl");
621 if (String.IsNullOrWhiteSpace(localPath))
622 throw new ArgumentNullException("localPath");
623 if (!Path.IsPathRooted(localPath))
624 throw new ArgumentException("The localPath must be rooted", "localPath");
625 Contract.EndContractBlock();
627 //If the file already exists
628 if (File.Exists(localPath))
630 //First check with MD5 as this is a small file
631 var localMD5 = Signature.CalculateMD5(localPath);
632 var cloudHash=serverHash.TopHash.ToHashString();
633 if (localMD5==cloudHash)
635 //Then check with a treehash
636 var localTreeHash = Signature.CalculateTreeHash(localPath, serverHash.BlockSize, serverHash.BlockHash);
637 var localHash = localTreeHash.TopHash.ToHashString();
638 if (localHash==cloudHash)
642 var fileAgent = GetFileAgent(accountInfo);
643 //Calculate the relative file path for the new file
644 var relativePath = relativeUrl.RelativeUriToFilePath();
645 //The file will be stored in a temporary location while downloading with an extension .download
646 var tempPath = Path.Combine(fileAgent.CachePath, relativePath + ".download");
647 //Make sure the target folder exists. DownloadFileTask will not create the folder
648 var tempFolder = Path.GetDirectoryName(tempPath);
649 if (!Directory.Exists(tempFolder))
650 Directory.CreateDirectory(tempFolder);
652 //Download the object to the temporary location
653 await client.GetObject(cloudFile.Account, cloudFile.Container, relativeUrl.ToString(), tempPath).ContinueWith(t =>
655 t.PropagateExceptions();
656 //Create the local folder if it doesn't exist (necessary for shared objects)
657 var localFolder = Path.GetDirectoryName(localPath);
658 if (!Directory.Exists(localFolder))
659 Directory.CreateDirectory(localFolder);
660 //And move it to its actual location once downloading is finished
661 if (File.Exists(localPath))
662 File.Replace(tempPath,localPath,null,true);
664 File.Move(tempPath,localPath);
665 //Notify listeners that a local file has changed
666 StatusNotification.NotifyChangedFile(localPath);
671 //Download a file asynchronously using blocks
672 public async Task DownloadWithBlocks(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string localPath, TreeHash serverHash)
675 throw new ArgumentNullException("client");
676 if (cloudFile == null)
677 throw new ArgumentNullException("cloudFile");
678 if (relativeUrl == null)
679 throw new ArgumentNullException("relativeUrl");
680 if (String.IsNullOrWhiteSpace(localPath))
681 throw new ArgumentNullException("localPath");
682 if (!Path.IsPathRooted(localPath))
683 throw new ArgumentException("The localPath must be rooted", "localPath");
684 if (serverHash == null)
685 throw new ArgumentNullException("serverHash");
686 Contract.EndContractBlock();
688 var fileAgent = GetFileAgent(accountInfo);
690 //Calculate the relative file path for the new file
691 var relativePath = relativeUrl.RelativeUriToFilePath();
692 var blockUpdater = new BlockUpdater(fileAgent.CachePath, localPath, relativePath, serverHash);
696 //Calculate the file's treehash
697 var treeHash = await Signature.CalculateTreeHashAsync(localPath, serverHash.BlockSize, serverHash.BlockHash);
699 //And compare it with the server's hash
700 var upHashes = serverHash.GetHashesAsStrings();
701 var localHashes = treeHash.HashDictionary;
702 for (int i = 0; i < upHashes.Length; i++)
704 //For every non-matching hash
705 var upHash = upHashes[i];
706 if (!localHashes.ContainsKey(upHash))
708 if (blockUpdater.UseOrphan(i, upHash))
710 Log.InfoFormat("[BLOCK GET] ORPHAN FOUND for {0} of {1} for {2}", i, upHashes.Length, localPath);
713 Log.InfoFormat("[BLOCK GET] START {0} of {1} for {2}", i, upHashes.Length, localPath);
714 var start = i*serverHash.BlockSize;
715 //To download the last block just pass a null for the end of the range
717 if (i < upHashes.Length - 1 )
718 end= ((i + 1)*serverHash.BlockSize) ;
720 //Download the missing block
721 var block = await client.GetBlock(cloudFile.Account, cloudFile.Container, relativeUrl, start, end);
724 blockUpdater.StoreBlock(i, block);
727 Log.InfoFormat("[BLOCK GET] FINISH {0} of {1} for {2}", i, upHashes.Length, localPath);
731 //Want to avoid notifications if no changes were made
732 var hasChanges = blockUpdater.HasBlocks;
733 blockUpdater.Commit();
736 //Notify listeners that a local file has changed
737 StatusNotification.NotifyChangedFile(localPath);
739 Log.InfoFormat("[BLOCK GET] COMPLETE {0}", localPath);
743 private async Task UploadCloudFile(CloudAction action)
746 throw new ArgumentNullException("action");
747 Contract.EndContractBlock();
752 throw new ArgumentNullException("action");
753 Contract.EndContractBlock();
755 var accountInfo = action.AccountInfo;
757 var fileInfo = action.LocalFile;
759 if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase))
762 var relativePath = fileInfo.AsRelativeTo(accountInfo.AccountPath);
763 if (relativePath.StartsWith(FolderConstants.OthersFolder))
765 var parts = relativePath.Split('\\');
766 var accountName = parts[1];
767 var oldName = accountInfo.UserName;
768 var absoluteUri = accountInfo.StorageUri.AbsoluteUri;
769 var nameIndex = absoluteUri.IndexOf(oldName);
770 var root = absoluteUri.Substring(0, nameIndex);
772 accountInfo = new AccountInfo
774 UserName = accountName,
775 AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
776 StorageUri = new Uri(root + accountName),
777 BlockHash = accountInfo.BlockHash,
778 BlockSize = accountInfo.BlockSize,
779 Token = accountInfo.Token
784 var fullFileName = fileInfo.FullName;
785 using (var gate = NetworkGate.Acquire(fullFileName, NetworkOperation.Uploading))
787 //Abort if the file is already being uploaded or downloaded
791 var cloudFile = action.CloudFile;
792 var account = cloudFile.Account ?? accountInfo.UserName;
794 var client = new CloudFilesClient(accountInfo);
795 //Even if GetObjectInfo times out, we can proceed with the upload
796 var info = client.GetObjectInfo(account, cloudFile.Container, cloudFile.Name);
797 var cloudHash = info.Hash.ToLower();
799 var hash = action.LocalHash.Value;
800 var topHash = action.TopHash.Value;
802 //If the file hashes match, abort the upload
803 if (hash == cloudHash || topHash == cloudHash)
805 //but store any metadata changes
806 this.StatusKeeper.StoreInfo(fullFileName, info);
807 Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);
811 if (info.AllowedTo == "read")
814 //Mark the file as modified while we upload it
815 StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);
818 //Upload even small files using the Hashmap. The server may already containt
819 //the relevant folder
821 //First, calculate the tree hash
822 var treeHash = await Signature.CalculateTreeHashAsync(fileInfo.FullName, accountInfo.BlockSize,
823 accountInfo.BlockHash);
825 await UploadWithHashMap(accountInfo, cloudFile, fileInfo, cloudFile.Name, treeHash);
827 //If everything succeeds, change the file and overlay status to normal
828 this.StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal);
830 //Notify the Shell to update the overlays
831 NativeMethods.RaiseChangeNotification(fullFileName);
832 StatusNotification.NotifyChangedFile(fullFileName);
834 catch (AggregateException ex)
836 var exc = ex.InnerException as WebException;
838 throw ex.InnerException;
839 if (HandleUploadWebException(action, exc))
843 catch (WebException ex)
845 if (HandleUploadWebException(action, ex))
851 Log.Error("Unexpected error while uploading file", ex);
857 private bool HandleUploadWebException(CloudAction action, WebException exc)
859 var response = exc.Response as HttpWebResponse;
860 if (response == null)
862 if (response.StatusCode == HttpStatusCode.Unauthorized)
864 Log.Error("Not allowed to upload file", exc);
865 var message = String.Format("Not allowed to uplad file {0}", action.LocalFile.FullName);
866 StatusKeeper.SetFileState(action.LocalFile.FullName, FileStatus.Unchanged, FileOverlayStatus.Normal);
867 StatusNotification.NotifyChange(message, TraceLevel.Warning);
873 public async Task UploadWithHashMap(AccountInfo accountInfo,ObjectInfo cloudFile,FileInfo fileInfo,string url,TreeHash treeHash)
875 if (accountInfo == null)
876 throw new ArgumentNullException("accountInfo");
878 throw new ArgumentNullException("cloudFile");
879 if (fileInfo == null)
880 throw new ArgumentNullException("fileInfo");
881 if (String.IsNullOrWhiteSpace(url))
882 throw new ArgumentNullException(url);
884 throw new ArgumentNullException("treeHash");
885 if (String.IsNullOrWhiteSpace(cloudFile.Container) )
886 throw new ArgumentException("Invalid container","cloudFile");
887 Contract.EndContractBlock();
889 var fullFileName = fileInfo.FullName;
891 var account = cloudFile.Account ?? accountInfo.UserName;
892 var container = cloudFile.Container ;
894 var client = new CloudFilesClient(accountInfo);
895 //Send the hashmap to the server
896 var missingHashes = await client.PutHashMap(account, container, url, treeHash);
897 //If the server returns no missing hashes, we are done
898 while (missingHashes.Count > 0)
901 var buffer = new byte[accountInfo.BlockSize];
902 foreach (var missingHash in missingHashes)
904 //Find the proper block
905 var blockIndex = treeHash.HashDictionary[missingHash];
906 var offset = blockIndex*accountInfo.BlockSize;
908 var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);
912 //And upload the block
913 await client.PostBlock(account, container, buffer, 0, read);
914 Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);
916 catch (Exception exc)
918 Log.ErrorFormat("[ERROR] uploading block {0} of {1}\n{2}", blockIndex, fullFileName, exc);
923 //Repeat until there are no more missing hashes
924 missingHashes = await client.PutHashMap(account, container, url, treeHash);
929 public void AddAccount(AccountInfo accountInfo)
931 if (!_accounts.Contains(accountInfo))
932 _accounts.Add(accountInfo);