Revision d78d765c trunk/Pithos.Core/Agents/NetworkAgent.cs

b/trunk/Pithos.Core/Agents/NetworkAgent.cs
65 65
        private Agent<CloudAction> _agent;
66 66

  
67 67
        [System.ComponentModel.Composition.Import]
68
        private DeleteAgent _deleteAgent { get; set; }
68
        private DeleteAgent DeleteAgent { get; set; }
69 69

  
70 70
        [System.ComponentModel.Composition.Import]
71 71
        public IStatusKeeper StatusKeeper { get; set; }
......
77 77
            set
78 78
            {
79 79
                _statusNotification = value;
80
                _deleteAgent.StatusNotification = value;
80
                DeleteAgent.StatusNotification = value;
81
                Uploader.StatusNotification = value;
82
                Downloader.StatusNotification = value;
81 83
            }
82 84
        }
83 85

  
......
85 87
        [System.ComponentModel.Composition.Import]
86 88
        public IPithosSettings Settings { get; set; }
87 89

  
90
        [System.ComponentModel.Composition.Import]
91
        public Uploader Uploader { get; set; }
92

  
93
        [System.ComponentModel.Composition.Import]
94
        public Downloader Downloader { get; set; }
95

  
88 96
        //The Proceed signals the poll agent that it can proceed with polling. 
89 97
        //Essentially it stops the poll agent to give priority to the network agent
90 98
        //Initially the event is signalled because we don't need to pause
......
109 117
                Action loop = null;
110 118
                loop = () =>
111 119
                {
112
                    _deleteAgent.ProceedEvent.Wait();
120
                    DeleteAgent.ProceedEvent.Wait();
113 121
                    var message = inbox.Receive();
114 122
                    var process=message.Then(Process,inbox.CancellationToken);
115 123
                    inbox.LoopAsync(process, loop);
......
130 138

  
131 139

  
132 140

  
133
            using (log4net.ThreadContext.Stacks["Operation"].Push(action.ToString()))
141
            using (ThreadContext.Stacks["Operation"].Push(action.ToString()))
134 142
            {                
135 143

  
136 144
                var cloudFile = action.CloudFile;
......
140 148
                {
141 149
                    StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,"Processing");
142 150
                    _proceedEvent.Reset();
143
                    //UpdateStatus(PithosStatus.Syncing);
151
                    
144 152
                    var accountInfo = action.AccountInfo;
145 153

  
146 154
                    if (action.Action == CloudActionType.DeleteCloud)
147 155
                    {                        
148 156
                        //Redirect deletes to the delete agent 
149
                        _deleteAgent.Post((CloudDeleteAction)action);
157
                        DeleteAgent.Post((CloudDeleteAction)action);
150 158
                    }
151
                    if (_deleteAgent.IsDeletedFile(action))
159
                    if (DeleteAgent.IsDeletedFile(action))
152 160
                    {
153 161
                        //Clear the status of already deleted files to avoid reprocessing
154 162
                        if (action.LocalFile != null)
......
160 168
                        {
161 169
                            case CloudActionType.UploadUnconditional:
162 170
                                //Abort if the file was deleted before we reached this point
163
                                await UploadCloudFile(action);
171
                                await Uploader.UploadCloudFile(action);
164 172
                                break;
165 173
                            case CloudActionType.DownloadUnconditional:
166
                                await DownloadCloudFile(accountInfo, cloudFile, downloadPath);
174
                                await Downloader.DownloadCloudFile(accountInfo, cloudFile, downloadPath);
167 175
                                break;
168 176
                            case CloudActionType.RenameCloud:
169 177
                                var moveAction = (CloudMoveAction)action;
......
175 183
                            case CloudActionType.MustSynch:
176 184
                                if (!File.Exists(downloadPath) && !Directory.Exists(downloadPath))
177 185
                                {
178
                                    await DownloadCloudFile(accountInfo, cloudFile, downloadPath);
186
                                    await Downloader.DownloadCloudFile(accountInfo, cloudFile, downloadPath);
179 187
                                }
180 188
                                else
181 189
                                {
......
187 195
                    Log.InfoFormat("End Processing {0}:{1}->{2}", action.Action, action.LocalFile,
188 196
                                           action.CloudFile.Name);
189 197
                }
198
/*
190 199
                catch (WebException exc)
191
                {
200
                {                    
192 201
                    Log.ErrorFormat("[WEB ERROR] {0} : {1} -> {2} due to exception\r\n{3}", action.Action, action.LocalFile, action.CloudFile, exc);
202
                    
203
                    
204
                    //Actions that resulted in server errors should be retried                    
205
                    var response = exc.Response as HttpWebResponse;
206
                    if (response != null && response.StatusCode >= HttpStatusCode.InternalServerError)
207
                    {
208
                        _agent.Post(action);
209
                        Log.WarnFormat("[REQUEUE] {0} : {1} -> {2}", action.Action, action.LocalFile, action.CloudFile);
210
                    }
193 211
                }
212
*/
194 213
                catch (OperationCanceledException)
195 214
                {
196 215
                    throw;
......
274 293
                FileInfoExtensions.GetProperFilePathCapitalization(previousFile.FullName):
275 294
                FileInfoExtensions.GetProperDirectoryCapitalization(previousFile.FullName);                
276 295
            
277
            using (var gateOld = NetworkGate.Acquire(previousFullPath, NetworkOperation.Renaming))
278
            using (var gateNew = NetworkGate.Acquire(newPath,NetworkOperation.Renaming))
296
            using (NetworkGate.Acquire(previousFullPath, NetworkOperation.Renaming))
297
            using (NetworkGate.Acquire(newPath,NetworkOperation.Renaming)) 
279 298
            using (new SessionScope(FlushAction.Auto))
280 299
            {
281 300
                if (isFile)
......
307 326
            using (ThreadContext.Stacks["Operation"].Push("SyncFiles"))
308 327
            {
309 328

  
310
                var localFile = action.LocalFile;
329
                //var localFile = action.LocalFile;
311 330
                var cloudFile = action.CloudFile;
312 331
                var downloadPath = action.LocalFile.GetProperCapitalization();
313 332

  
......
322 341
                //
323 342

  
324 343
                //If the hashes match, we are done
325
                if (cloudHash == localHash)
344
                if (cloudFile != ObjectInfo.Empty && cloudHash == localHash)
326 345
                {
327 346
                    Log.InfoFormat("Skipping {0}, hashes match", downloadPath);
328 347
                    return;
......
333 352
                // If the previous tophash matches the local tophash, the file was only changed on the server. 
334 353
                if (localHash == previousCloudHash)
335 354
                {
336
                    await DownloadCloudFile(accountInfo, cloudFile, downloadPath);
355
                    await Downloader.DownloadCloudFile(accountInfo, cloudFile, downloadPath);
337 356
                }
338 357
                else
339 358
                {
......
365 384
                throw new ArgumentException("The CloudAction.AccountInfo is empty","cloudAction");
366 385
            Contract.EndContractBlock();
367 386

  
368
            _deleteAgent.ProceedEvent.Wait();
387
            DeleteAgent.ProceedEvent.Wait();
369 388
/*
370 389

  
371 390
            //If the action targets a local file, add a treehash calculation
......
394 413
*/
395 414
            
396 415
            if (cloudAction is CloudDeleteAction)
397
                _deleteAgent.Post((CloudDeleteAction)cloudAction);
416
                DeleteAgent.Post((CloudDeleteAction)cloudAction);
398 417
            else
399 418
                _agent.Post(cloudAction);
400 419
        }
......
407 426

  
408 427
        public Task GetDeleteAwaiter()
409 428
        {
410
            return _deleteAgent.ProceedEvent.WaitAsync();
429
            return DeleteAgent.ProceedEvent.WaitAsync();
411 430
        }
412 431
        public CancellationToken CancellationToken
413 432
        {
414 433
            get { return _agent.CancellationToken; }
415 434
        }
416 435

  
417
        private static FileAgent GetFileAgent(AccountInfo accountInfo)
418
        {
419
            return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);
420
        }
421

  
422 436

  
423 437

  
424 438
        private void RenameCloudFile(AccountInfo accountInfo,CloudMoveAction action)
......
471 485
            }
472 486
        }
473 487

  
474
        //Download a file.
475
        private async Task DownloadCloudFile(AccountInfo accountInfo, ObjectInfo cloudFile , string filePath)
476
        {
477
            if (accountInfo == null)
478
                throw new ArgumentNullException("accountInfo");
479
            if (cloudFile == null)
480
                throw new ArgumentNullException("cloudFile");
481
            if (String.IsNullOrWhiteSpace(cloudFile.Account))
482
                throw new ArgumentNullException("cloudFile");
483
            if (String.IsNullOrWhiteSpace(cloudFile.Container))
484
                throw new ArgumentNullException("cloudFile");
485
            if (String.IsNullOrWhiteSpace(filePath))
486
                throw new ArgumentNullException("filePath");
487
            if (!Path.IsPathRooted(filePath))
488
                throw new ArgumentException("The filePath must be rooted", "filePath");
489
            Contract.EndContractBlock();
490

  
491
            using (ThreadContext.Stacks["Operation"].Push("DownloadCloudFile"))
492
            {
493

  
494
                var localPath = Interfaces.FileInfoExtensions.GetProperFilePathCapitalization(filePath);
495
                var relativeUrl = new Uri(cloudFile.Name, UriKind.Relative);
496

  
497
                var url = relativeUrl.ToString();
498
                if (cloudFile.Name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase))
499
                    return;
500

  
501

  
502
                //Are we already downloading or uploading the file? 
503
                using (var gate = NetworkGate.Acquire(localPath, NetworkOperation.Downloading))
504
                {
505
                    if (gate.Failed)
506
                        return;
507

  
508
                    var client = new CloudFilesClient(accountInfo);
509
                    var account = cloudFile.Account;
510
                    var container = cloudFile.Container;
511

  
512
                    if (cloudFile.IsDirectory)
513
                    {
514
                        if (!Directory.Exists(localPath))
515
                            try
516
                            {
517
                                Directory.CreateDirectory(localPath);
518
                                if (Log.IsDebugEnabled)
519
                                    Log.DebugFormat("Created Directory [{0}]",localPath);
520
                            }
521
                            catch (IOException)
522
                            {
523
                                var localInfo = new FileInfo(localPath);
524
                                if (localInfo.Exists && localInfo.Length == 0)
525
                                {
526
                                    Log.WarnFormat("Malformed directory object detected for [{0}]",localPath);
527
                                    localInfo.Delete();
528
                                    Directory.CreateDirectory(localPath);
529
                                    if (Log.IsDebugEnabled)
530
                                        Log.DebugFormat("Created Directory [{0}]", localPath);
531
                                }
532
                            }
533
                    }
534
                    else
535
                    {
536
                        var isChanged = IsObjectChanged(cloudFile, localPath);
537
                        if (isChanged)
538
                        {
539
                            //Retrieve the hashmap from the server
540
                            var serverHash = await client.GetHashMap(account, container, url);
541
                            //If it's a small file
542
                            if (serverHash.Hashes.Count == 1)
543
                                //Download it in one go
544
                                await
545
                                    DownloadEntireFileAsync(accountInfo, client, cloudFile, relativeUrl, localPath,
546
                                                            serverHash);
547
                            //Otherwise download it block by block
548
                            else
549
                                await
550
                                    DownloadWithBlocks(accountInfo, client, cloudFile, relativeUrl, localPath,
551
                                                       serverHash);
552

  
553
                            if (cloudFile.AllowedTo == "read")
554
                            {
555
                                var attributes = File.GetAttributes(localPath);
556
                                File.SetAttributes(localPath, attributes | FileAttributes.ReadOnly);
557
                            }
558
                        }
559
                    }
560

  
561
                    //Now we can store the object's metadata without worrying about ghost status entries
562
                    StatusKeeper.StoreInfo(localPath, cloudFile);
563

  
564
                }
565
            }
566
        }
567

  
568
        private bool IsObjectChanged(ObjectInfo cloudFile, string localPath)
569
        {
570
            //If the target is a directory, there are no changes to download
571
            if (Directory.Exists(localPath))
572
                return false;
573
            //If the file doesn't exist, we have a chagne
574
            if (!File.Exists(localPath)) 
575
                return true;
576
            //If there is no stored state, we have a change
577
            var localState = StatusKeeper.GetStateByFilePath(localPath);
578
            if (localState == null)
579
                return true;
580

  
581
            var info = new FileInfo(localPath);
582
            var shortHash = info.ComputeShortHash();
583
            //If the file is different from the stored state, we have a change
584
            if (localState.ShortHash != shortHash)
585
                return true;
586
            //If the top hashes differ, we have a change
587
            return (localState.Checksum != cloudFile.Hash);
588
        }
589

  
590
        //Download a small file with a single GET operation
591
        private async Task DownloadEntireFileAsync(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath,TreeHash serverHash)
592
        {
593
            if (client == null)
594
                throw new ArgumentNullException("client");
595
            if (cloudFile==null)
596
                throw new ArgumentNullException("cloudFile");
597
            if (relativeUrl == null)
598
                throw new ArgumentNullException("relativeUrl");
599
            if (String.IsNullOrWhiteSpace(filePath))
600
                throw new ArgumentNullException("filePath");
601
            if (!Path.IsPathRooted(filePath))
602
                throw new ArgumentException("The localPath must be rooted", "filePath");
603
            if (cloudFile.IsDirectory)
604
                throw new ArgumentException("cloudFile is a directory, not a file","cloudFile");
605
            Contract.EndContractBlock();
606

  
607
            var localPath = Pithos.Interfaces.FileInfoExtensions.GetProperFilePathCapitalization(filePath);
608
            StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,String.Format("Downloading {0}",Path.GetFileName(localPath)));
609
            StatusNotification.Notify(new CloudNotification { Data = cloudFile });
610

  
611
            var fileAgent = GetFileAgent(accountInfo);
612
            //Calculate the relative file path for the new file
613
            var relativePath = relativeUrl.RelativeUriToFilePath();
614
            //The file will be stored in a temporary location while downloading with an extension .download
615
            var tempPath = Path.Combine(fileAgent.CachePath, relativePath + ".download");
616
            //Make sure the target folder exists. DownloadFileTask will not create the folder
617
            var tempFolder = Path.GetDirectoryName(tempPath);
618
            if (!Directory.Exists(tempFolder))
619
                Directory.CreateDirectory(tempFolder);
620

  
621
            //Download the object to the temporary location
622
            await client.GetObject(cloudFile.Account, cloudFile.Container, relativeUrl.ToString(), tempPath);
623

  
624
            //Create the local folder if it doesn't exist (necessary for shared objects)
625
            var localFolder = Path.GetDirectoryName(localPath);
626
            if (!Directory.Exists(localFolder))
627
                try
628
                {
629
                    Directory.CreateDirectory(localFolder);
630
                }
631
                catch (IOException)
632
                {
633
                    //A file may already exist that has the same name as the new folder.
634
                    //This may be an artifact of the way Pithos handles directories
635
                    var fileInfo = new FileInfo(localFolder);
636
                    if (fileInfo.Exists && fileInfo.Length == 0)
637
                    {
638
                        Log.WarnFormat("Malformed directory object detected for [{0}]", localFolder);
639
                        fileInfo.Delete();
640
                        Directory.CreateDirectory(localFolder);
641
                    }
642
                    else 
643
                        throw;
644
                }
645
            //And move it to its actual location once downloading is finished
646
            if (File.Exists(localPath))
647
                File.Replace(tempPath,localPath,null,true);
648
            else
649
                File.Move(tempPath,localPath);
650
            //Notify listeners that a local file has changed
651
            StatusNotification.NotifyChangedFile(localPath);
652

  
653
                       
654
        }
655 488

  
656
        //Download a file asynchronously using blocks
657
        public async Task DownloadWithBlocks(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath, TreeHash serverHash)
658
        {
659
            if (client == null)
660
                throw new ArgumentNullException("client");
661
            if (cloudFile == null)
662
                throw new ArgumentNullException("cloudFile");
663
            if (relativeUrl == null)
664
                throw new ArgumentNullException("relativeUrl");
665
            if (String.IsNullOrWhiteSpace(filePath))
666
                throw new ArgumentNullException("filePath");
667
            if (!Path.IsPathRooted(filePath))
668
                throw new ArgumentException("The filePath must be rooted", "filePath");
669
            if (serverHash == null)
670
                throw new ArgumentNullException("serverHash");
671
            if (cloudFile.IsDirectory)
672
                throw new ArgumentException("cloudFile is a directory, not a file", "cloudFile");
673
            Contract.EndContractBlock();
674
            
675
           var fileAgent = GetFileAgent(accountInfo);
676
            var localPath = Interfaces.FileInfoExtensions.GetProperFilePathCapitalization(filePath);
677
            
678
            //Calculate the relative file path for the new file
679
            var relativePath = relativeUrl.RelativeUriToFilePath();
680
            var blockUpdater = new BlockUpdater(fileAgent.CachePath, localPath, relativePath, serverHash);
681 489

  
682
            
683
            StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing,String.Format("Calculating hashmap for {0} before download",Path.GetFileName(localPath)));
684
            //Calculate the file's treehash
685
            var treeHash = await Signature.CalculateTreeHashAsync(localPath, serverHash.BlockSize, serverHash.BlockHash, 2);
686
                
687
            //And compare it with the server's hash
688
            var upHashes = serverHash.GetHashesAsStrings();
689
            var localHashes = treeHash.HashDictionary;
690
            ReportDownloadProgress(Path.GetFileName(localPath),0,upHashes.Length,cloudFile.Bytes);
691
            for (int i = 0; i < upHashes.Length; i++)
692
            {
693
                //For every non-matching hash
694
                var upHash = upHashes[i];
695
                if (!localHashes.ContainsKey(upHash))
696
                {
697
                    StatusNotification.Notify(new CloudNotification { Data = cloudFile });
698

  
699
                    if (blockUpdater.UseOrphan(i, upHash))
700
                    {
701
                        Log.InfoFormat("[BLOCK GET] ORPHAN FOUND for {0} of {1} for {2}", i, upHashes.Length, localPath);
702
                        continue;
703
                    }
704
                    Log.InfoFormat("[BLOCK GET] START {0} of {1} for {2}", i, upHashes.Length, localPath);
705
                    var start = i*serverHash.BlockSize;
706
                    //To download the last block just pass a null for the end of the range
707
                    long? end = null;
708
                    if (i < upHashes.Length - 1 )
709
                        end= ((i + 1)*serverHash.BlockSize) ;
710
                            
711
                    //Download the missing block
712
                    var block = await client.GetBlock(cloudFile.Account, cloudFile.Container, relativeUrl, start, end);
713

  
714
                    //and store it
715
                    blockUpdater.StoreBlock(i, block);
716

  
717

  
718
                    Log.InfoFormat("[BLOCK GET] FINISH {0} of {1} for {2}", i, upHashes.Length, localPath);
719
                }
720
                ReportDownloadProgress(Path.GetFileName(localPath), i, upHashes.Length, cloudFile.Bytes);
721
            }
722

  
723
            //Want to avoid notifications if no changes were made
724
            var hasChanges = blockUpdater.HasBlocks;
725
            blockUpdater.Commit();
726
            
727
            if (hasChanges)
728
                //Notify listeners that a local file has changed
729
                StatusNotification.NotifyChangedFile(localPath);
730

  
731
            Log.InfoFormat("[BLOCK GET] COMPLETE {0}", localPath);            
732
        }
733

  
734

  
735
        private async Task UploadCloudFile(CloudAction action)
736
        {
737
            if (action == null)
738
                throw new ArgumentNullException("action");           
739
            Contract.EndContractBlock();
740
            using(ThreadContext.Stacks["Operation"].Push("UploadCloudFile"))
741
            {
742
                try
743
                {
744

  
745
                    var accountInfo = action.AccountInfo;
746

  
747
                    var fileInfo = action.LocalFile;
748

  
749
                    if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase))
750
                        return;
751
                    
752
                    //
753
                    if (action.FileState == null)
754
                        action.FileState = StatusKeeper.GetStateByFilePath(fileInfo.FullName);
755
                    if (action.FileState == null)
756
                    {
757
                        Log.WarnFormat("File [{0}] has no local state. It was probably created by a download action",fileInfo.FullName);
758
                        return;
759
                    }
760
                    //Do not upload files in conflict
761
                    if (action.FileState.FileStatus == FileStatus.Conflict )
762
                    {
763
                        Log.InfoFormat("Skipping file in conflict [{0}]",fileInfo.FullName);
764
                        return;
765
                    }
766
                    if (action.FileState.FileStatus == FileStatus.Forbidden)
767
                    {
768
                        Log.InfoFormat("Skipping forbidden file [{0}]",fileInfo.FullName);
769
                        return;
770
                    }
771

  
772
                    var relativePath = fileInfo.AsRelativeTo(accountInfo.AccountPath);
773
                    if (relativePath.StartsWith(FolderConstants.OthersFolder))
774
                    {
775
                        var parts = relativePath.Split('\\');
776
                        var accountName = parts[1];
777
                        var oldName = accountInfo.UserName;
778
                        var absoluteUri = accountInfo.StorageUri.AbsoluteUri;
779
                        var nameIndex = absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
780
                        var root = absoluteUri.Substring(0, nameIndex);
781

  
782
                        accountInfo = new AccountInfo
783
                                          {
784
                                              UserName = accountName,
785
                                              AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
786
                                              StorageUri = new Uri(root + accountName),
787
                                              BlockHash = accountInfo.BlockHash,
788
                                              BlockSize = accountInfo.BlockSize,
789
                                              Token = accountInfo.Token
790
                                          };
791
                    }
792

  
793

  
794
                    var fullFileName = fileInfo.GetProperCapitalization();
795
                    using (var gate = NetworkGate.Acquire(fullFileName, NetworkOperation.Uploading))
796
                    {
797
                        //Abort if the file is already being uploaded or downloaded
798
                        if (gate.Failed)
799
                            return;
800

  
801
                        var cloudFile = action.CloudFile;
802
                        var account = cloudFile.Account ?? accountInfo.UserName;
803
                        try
804
                        {
805

  
806
                        var client = new CloudFilesClient(accountInfo);
807
                        //Even if GetObjectInfo times out, we can proceed with the upload            
808
                        var cloudInfo = client.GetObjectInfo(account, cloudFile.Container, cloudFile.Name);
809

  
810
                        //If this is a read-only file, do not upload changes
811
                        if (cloudInfo.AllowedTo == "read")
812
                            return;
813

  
814
                        //TODO: Check how a directory hash is calculated -> All dirs seem to have the same hash
815
                            if (fileInfo is DirectoryInfo)
816
                            {
817
                                //If the directory doesn't exist the Hash property will be empty
818
                                if (String.IsNullOrWhiteSpace(cloudInfo.Hash))
819
                                    //Go on and create the directory
820
                                    await
821
                                        client.PutObject(account, cloudFile.Container, cloudFile.Name, fullFileName,
822
                                                         String.Empty, "application/directory");
823
                            }
824
                            else
825
                            {
826

  
827
                                var cloudHash = cloudInfo.Hash.ToLower();
828

  
829
                                StatusNotification.Notify(new StatusNotification(String.Format("Hashing {0} for Upload",fileInfo.Name)));
830

  
831
                                //TODO: This is the same as the calculation for the Local Hash!
832
                                //First, calculate the tree hash
833
/*
834
                                var treeHash = await Signature.CalculateTreeHashAsync(fullFileName, accountInfo.BlockSize,
835
                                                                                      accountInfo.BlockHash, 2);
836
*/
837

  
838

  
839
                                var treeHash = action.TreeHash.Value;
840
                                var topHash = treeHash.TopHash.ToHashString();
841
                                
842
                                //var topHash = action.TopHash.Value;
843

  
844
                                //If the file hashes match, abort the upload
845
                                if (topHash == cloudHash /*|| topHash == cloudHash*/)
846
                                {
847
                                    //but store any metadata changes 
848
                                    StatusKeeper.StoreInfo(fullFileName, cloudInfo);
849
                                    Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);
850
                                    return;
851
                                }
852

  
853

  
854
                                //Mark the file as modified while we upload it
855
                                StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);
856
                                //And then upload it
857

  
858
                                //Upload even small files using the Hashmap. The server may already contain
859
                                //the relevant block
860

  
861
                                //TODO: If the upload fails with a 403, abort it and mark conflict
862

  
863
                                await
864
                                    UploadWithHashMap(accountInfo, cloudFile, fileInfo as FileInfo, cloudFile.Name, treeHash);
865
                            }
866
                            //If everything succeeds, change the file and overlay status to normal
867
                            StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal);
868
                        }
869
                        catch (WebException exc)
870
                        {
871
                            var response=(exc.Response as HttpWebResponse);
872
                            if (response == null)
873
                                throw;
874
                            if (response.StatusCode == HttpStatusCode.Forbidden)
875
                            {
876
                                StatusKeeper.SetFileState(fileInfo.FullName,FileStatus.Forbidden, FileOverlayStatus.Conflict);                                
877
                            }
878
                        }
879
                    }
880
                    //Notify the Shell to update the overlays
881
                    NativeMethods.RaiseChangeNotification(fullFileName);
882
                    StatusNotification.NotifyChangedFile(fullFileName);
883
                }
884
                catch (AggregateException ex)
885
                {
886
                    var exc = ex.InnerException as WebException;
887
                    if (exc == null)
888
                        throw ex.InnerException;
889
                    if (HandleUploadWebException(action, exc))
890
                        return;
891
                    throw;
892
                }
893
                catch (WebException ex)
894
                {
895
                    if (HandleUploadWebException(action, ex))
896
                        return;
897
                    throw;
898
                }
899
                catch (Exception ex)
900
                {
901
                    Log.Error("Unexpected error while uploading file", ex);
902
                    throw;
903
                }
904
            }
905
        }
906

  
907

  
908

  
909
        private bool HandleUploadWebException(CloudAction action, WebException exc)
910
        {
911
            var response = exc.Response as HttpWebResponse;
912
            if (response == null)
913
                throw exc;
914
            if (response.StatusCode == HttpStatusCode.Unauthorized)
915
            {
916
                Log.Error("Not allowed to upload file", exc);
917
                var message = String.Format("Not allowed to uplad file {0}", action.LocalFile.FullName);
918
                StatusKeeper.SetFileState(action.LocalFile.FullName, FileStatus.Unchanged, FileOverlayStatus.Normal);
919
                StatusNotification.NotifyChange(message, TraceLevel.Warning);
920
                return true;
921
            }
922
            return false;
923
        }
924

  
925
        public async Task UploadWithHashMap(AccountInfo accountInfo,ObjectInfo cloudFile,FileInfo fileInfo,string url,TreeHash treeHash)
926
        {
927
            if (accountInfo == null)
928
                throw new ArgumentNullException("accountInfo");
929
            if (cloudFile==null)
930
                throw new ArgumentNullException("cloudFile");
931
            if (fileInfo == null)
932
                throw new ArgumentNullException("fileInfo");
933
            if (String.IsNullOrWhiteSpace(url))
934
                throw new ArgumentNullException(url);
935
            if (treeHash==null)
936
                throw new ArgumentNullException("treeHash");
937
            if (String.IsNullOrWhiteSpace(cloudFile.Container) )
938
                throw new ArgumentException("Invalid container","cloudFile");
939
            Contract.EndContractBlock();
940

  
941
            StatusNotification.Notify(new StatusNotification(String.Format("Uploading {0}", fileInfo.Name)));
942

  
943
            var fullFileName = fileInfo.GetProperCapitalization();
944

  
945
            var account = cloudFile.Account ?? accountInfo.UserName;
946
            var container = cloudFile.Container ;
947

  
948
            var client = new CloudFilesClient(accountInfo);            
949
            //Send the hashmap to the server            
950
            var missingHashes =  await client.PutHashMap(account, container, url, treeHash);
951
            int block = 0;
952
            ReportUploadProgress(fileInfo.Name,block++, missingHashes.Count, fileInfo.Length);
953
            //If the server returns no missing hashes, we are done
954
            while (missingHashes.Count > 0)
955
            {
956

  
957
                var buffer = new byte[accountInfo.BlockSize];
958
                foreach (var missingHash in missingHashes)
959
                {
960
                    //Find the proper block
961
                    var blockIndex = treeHash.HashDictionary[missingHash];
962
                    long offset = blockIndex*accountInfo.BlockSize;
963

  
964
                    var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);
965

  
966
                    try
967
                    {
968
                        //And upload the block                
969
                        await client.PostBlock(account, container, buffer, 0, read);
970
                        Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);
971
                    }
972
                    catch (Exception exc)
973
                    {
974
                        Log.Error(String.Format("Uploading block {0} of {1}", blockIndex, fullFileName), exc);
975
                    }
976
                    ReportUploadProgress(fileInfo.Name,block++, missingHashes.Count, fileInfo.Length);
977
                }
978

  
979
                //Repeat until there are no more missing hashes                
980
                missingHashes = await client.PutHashMap(account, container, url, treeHash);
981
            }
982

  
983
            ReportUploadProgress(fileInfo.Name, missingHashes.Count, missingHashes.Count, fileInfo.Length);
984
        }
985

  
986
        private void ReportUploadProgress(string fileName,int block, int totalBlocks, long fileSize)
987
        {
988
            StatusNotification.Notify(totalBlocks == 0
989
                                          ? new ProgressNotification(fileName, "Uploading", 1, 1, fileSize)
990
                                          : new ProgressNotification(fileName, "Uploading", block, totalBlocks, fileSize));
991
        }
992

  
993
        private void ReportDownloadProgress(string fileName,int block, int totalBlocks, long fileSize)
994
        {
995
            StatusNotification.Notify(totalBlocks == 0
996
                                          ? new ProgressNotification(fileName, "Downloading", 1, 1, fileSize)
997
                                          : new ProgressNotification(fileName, "Downloading", block, totalBlocks, fileSize));
998
        }
999 490
    }
1000 491

  
1001 492
   

Also available in: Unified diff