2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="PollAgent.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\r
7 * Redistribution and use in source and binary forms, with or
\r
8 * without modification, are permitted provided that the following
\r
9 * conditions are met:
\r
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\r
15 * 2. Redistributions in binary form must reproduce the above
\r
16 * copyright notice, this list of conditions and the following
\r
17 * disclaimer in the documentation and/or other materials
\r
18 * provided with the distribution.
\r
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
\r
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
\r
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
\r
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
\r
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
\r
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
\r
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
\r
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
\r
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
32 * POSSIBILITY OF SUCH DAMAGE.
\r
34 * The views and conclusions contained in the software and
\r
35 * documentation are those of the authors and should not be
\r
36 * interpreted as representing official policies, either expressed
\r
37 * or implied, of GRNET S.A.
\r
39 * -----------------------------------------------------------------------
\r
43 using System.Collections.Concurrent;
\r
44 using System.ComponentModel.Composition;
\r
45 using System.Diagnostics;
\r
46 using System.Diagnostics.Contracts;
\r
48 using System.Linq.Expressions;
\r
49 using System.Reflection;
\r
50 using System.Threading;
\r
51 using System.Threading.Tasks;
\r
52 using Castle.ActiveRecord;
\r
53 using Pithos.Interfaces;
\r
54 using Pithos.Network;
\r
57 namespace Pithos.Core.Agents
\r
60 using System.Collections.Generic;
\r
63 [DebuggerDisplay("{FilePath} C:{C} L:{L} S:{S}")]
\r
64 public class StateTuple
\r
66 public string FilePath { get; private set; }
\r
70 get { return FileState==null?null:FileState.Checksum; }
\r
73 public string C { get; set; }
\r
77 get { return ObjectInfo== null ? null : ObjectInfo.Hash; }
\r
80 private FileSystemInfo _fileInfo;
\r
81 public FileSystemInfo FileInfo
\r
83 get { return _fileInfo; }
\r
87 FilePath = value.FullName;
\r
91 public FileState FileState { get; set; }
\r
92 public ObjectInfo ObjectInfo{ get; set; }
\r
94 public StateTuple() { }
\r
96 public StateTuple(FileSystemInfo info)
\r
106 /// PollAgent periodically polls the server to detect object changes. The agent retrieves a listing of all
\r
107 /// objects and compares it with a previously cached version to detect differences.
\r
108 /// New files are downloaded, missing files are deleted from the local file system and common files are compared
\r
109 /// to determine the appropriate action
\r
112 public class PollAgent
\r
114 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
116 [System.ComponentModel.Composition.Import]
\r
117 public IStatusKeeper StatusKeeper { get; set; }
\r
119 [System.ComponentModel.Composition.Import]
\r
120 public IPithosSettings Settings { get; set; }
\r
122 [System.ComponentModel.Composition.Import]
\r
123 public NetworkAgent NetworkAgent { get; set; }
\r
125 [System.ComponentModel.Composition.Import]
\r
126 public Selectives Selectives { get; set; }
\r
128 public IStatusNotification StatusNotification { get; set; }
\r
130 private CancellationTokenSource _currentOperationCancellation = new CancellationTokenSource();
\r
132 public void CancelCurrentOperation()
\r
134 //What does it mean to cancel the current upload/download?
\r
135 //Obviously, the current operation will be cancelled by throwing
\r
136 //a cancellation exception.
\r
138 //The default behavior is to retry any operations that throw.
\r
139 //Obviously this is not what we want in this situation.
\r
140 //The cancelled operation should NOT bea retried.
\r
142 //This can be done by catching the cancellation exception
\r
143 //and avoiding the retry.
\r
146 //Have to reset the cancellation source - it is not possible to reset the source
\r
147 //Have to prevent a case where an operation requests a token from the old source
\r
148 var oldSource = Interlocked.Exchange(ref _currentOperationCancellation, new CancellationTokenSource());
\r
149 oldSource.Cancel();
\r
161 _unPauseEvent.Set();
\r
164 _unPauseEvent.Reset();
\r
169 private bool _firstPoll = true;
\r
171 //The Sync Event signals a manual synchronisation
\r
172 private readonly AsyncManualResetEvent _syncEvent = new AsyncManualResetEvent();
\r
174 private readonly AsyncManualResetEvent _unPauseEvent = new AsyncManualResetEvent(true);
\r
176 private readonly ConcurrentDictionary<string, DateTime> _lastSeen = new ConcurrentDictionary<string, DateTime>();
\r
177 private readonly ConcurrentDictionary<Uri, AccountInfo> _accounts = new ConcurrentDictionary<Uri,AccountInfo>();
\r
181 /// Start a manual synchronization
\r
183 public void SynchNow()
\r
190 /// Remote files are polled periodically. Any changes are processed
\r
192 /// <param name="since"></param>
\r
193 /// <returns></returns>
\r
194 public async Task PollRemoteFiles(DateTime? since = null)
\r
196 if (Log.IsDebugEnabled)
\r
197 Log.DebugFormat("Polling changes after [{0}]",since);
\r
199 Debug.Assert(Thread.CurrentThread.IsBackground, "Polling Ended up in the main thread!");
\r
203 using (ThreadContext.Stacks["Retrieve Remote"].Push("All accounts"))
\r
205 //If this poll fails, we will retry with the same since value
\r
206 var nextSince = since;
\r
209 await _unPauseEvent.WaitAsync();
\r
210 UpdateStatus(PithosStatus.PollSyncing);
\r
212 var tasks = from accountInfo in _accounts.Values
\r
213 select ProcessAccountFiles(accountInfo, since);
\r
215 var nextTimes=await TaskEx.WhenAll(tasks.ToList());
\r
217 _firstPoll = false;
\r
218 //Reschedule the poll with the current timestamp as a "since" value
\r
220 if (nextTimes.Length>0)
\r
221 nextSince = nextTimes.Min();
\r
222 if (Log.IsDebugEnabled)
\r
223 Log.DebugFormat("Next Poll at [{0}]",nextSince);
\r
225 catch (Exception ex)
\r
227 Log.ErrorFormat("Error while processing accounts\r\n{0}", ex);
\r
228 //In case of failure retry with the same "since" value
\r
231 UpdateStatus(PithosStatus.PollComplete);
\r
232 //The multiple try blocks are required because we can't have an await call
\r
233 //inside a finally block
\r
234 //TODO: Find a more elegant solution for reschedulling in the event of an exception
\r
237 //Wait for the polling interval to pass or the Sync event to be signalled
\r
238 nextSince = await WaitForScheduledOrManualPoll(nextSince);
\r
242 //Ensure polling is scheduled even in case of error
\r
243 TaskEx.Run(() => PollRemoteFiles(nextSince));
\r
249 /// Wait for the polling period to expire or a manual sync request
\r
251 /// <param name="since"></param>
\r
252 /// <returns></returns>
\r
253 private async Task<DateTime?> WaitForScheduledOrManualPoll(DateTime? since)
\r
255 var sync = _syncEvent.WaitAsync();
\r
256 var wait = TaskEx.Delay(TimeSpan.FromSeconds(Settings.PollingInterval), NetworkAgent.CancellationToken);
\r
258 var signaledTask = await TaskEx.WhenAny(sync, wait);
\r
260 //Pausing takes precedence over manual sync or awaiting
\r
261 _unPauseEvent.Wait();
\r
263 //Wait for network processing to finish before polling
\r
264 var pauseTask=NetworkAgent.ProceedEvent.WaitAsync();
\r
265 await TaskEx.WhenAll(signaledTask, pauseTask);
\r
267 //If polling is signalled by SynchNow, ignore the since tag
\r
268 if (sync.IsCompleted)
\r
270 //TODO: Must convert to AutoReset
\r
271 _syncEvent.Reset();
\r
277 public async Task<DateTime?> ProcessAccountFiles(AccountInfo accountInfo, DateTime? since = null)
\r
279 if (accountInfo == null)
\r
280 throw new ArgumentNullException("accountInfo");
\r
281 if (String.IsNullOrWhiteSpace(accountInfo.AccountPath))
\r
282 throw new ArgumentException("The AccountInfo.AccountPath is empty", "accountInfo");
\r
283 Contract.EndContractBlock();
\r
286 using (ThreadContext.Stacks["Retrieve Remote"].Push(accountInfo.UserName))
\r
289 await NetworkAgent.GetDeleteAwaiter();
\r
291 Log.Info("Scheduled");
\r
292 var client = new CloudFilesClient(accountInfo);
\r
294 //We don't need to check the trash container
\r
295 var containers = client.ListContainers(accountInfo.UserName)
\r
296 .Where(c=>c.Name!="trash")
\r
300 CreateContainerFolders(accountInfo, containers);
\r
302 //The nextSince time fallback time is the same as the current.
\r
303 //If polling succeeds, the next Since time will be the smallest of the maximum modification times
\r
304 //of the shared and account objects
\r
305 var nextSince = since;
\r
309 //Wait for any deletions to finish
\r
310 await NetworkAgent.GetDeleteAwaiter();
\r
311 //Get the poll time now. We may miss some deletions but it's better to keep a file that was deleted
\r
312 //than delete a file that was created while we were executing the poll
\r
314 //Get the list of server objects changed since the last check
\r
315 //The name of the container is passed as state in order to create a dictionary of tasks in a subsequent step
\r
316 var listObjects = (from container in containers
\r
317 select Task<IList<ObjectInfo>>.Factory.StartNew(_ =>
\r
318 client.ListObjects(accountInfo.UserName, container.Name, since), container.Name)).ToList();
\r
320 var listShared = Task<IList<ObjectInfo>>.Factory.StartNew(_ =>
\r
321 client.ListSharedObjects(since), "shared");
\r
322 listObjects.Add(listShared);
\r
323 var listTasks = await Task.Factory.WhenAll(listObjects.ToArray());
\r
325 using (ThreadContext.Stacks["SCHEDULE"].Push("Process Results"))
\r
327 var dict = listTasks.ToDictionary(t => t.AsyncState);
\r
329 //Get all non-trash objects. Remember, the container name is stored in AsyncState
\r
330 var remoteObjects = (from objectList in listTasks
\r
331 where (string)objectList.AsyncState != "trash"
\r
332 from obj in objectList.Result
\r
333 orderby obj.Bytes ascending
\r
334 select obj).ToList();
\r
336 //Get the latest remote object modification date, only if it is after
\r
337 //the original since date
\r
338 nextSince = GetLatestDateAfter(nextSince, remoteObjects);
\r
340 var sharedObjects = dict["shared"].Result;
\r
341 nextSince = GetLatestDateBefore(nextSince, sharedObjects);
\r
343 //DON'T process trashed files
\r
344 //If some files are deleted and added again to a folder, they will be deleted
\r
345 //even though they are new.
\r
346 //We would have to check file dates and hashes to ensure that a trashed file
\r
347 //can be deleted safely from the local hard drive.
\r
349 //Items with the same name, hash may be both in the container and the trash
\r
350 //Don't delete items that exist in the container
\r
351 var realTrash = from trash in trashObjects
\r
353 !remoteObjects.Any(
\r
354 info => info.Name == trash.Name && info.Hash == trash.Hash)
\r
356 ProcessTrashedFiles(accountInfo, realTrash);
\r
359 var cleanRemotes = (from info in remoteObjects.Union(sharedObjects)
\r
360 let name = info.Name??""
\r
361 where !name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase) &&
\r
362 !name.StartsWith(FolderConstants.CacheFolder + "/",
\r
363 StringComparison.InvariantCultureIgnoreCase)
\r
364 select info).ToList();
\r
367 StatusKeeper.CleanupOrphanStates();
\r
368 StatusKeeper.CleanupStaleStates(accountInfo, cleanRemotes);
\r
370 //var differencer = _differencer.PostSnapshot(accountInfo, cleanRemotes);
\r
372 var filterUris = Selectives.SelectiveUris[accountInfo.AccountKey];
\r
375 //Get the local files here
\r
376 var agent = AgentLocator<FileAgent>.Get(accountInfo.AccountPath);
\r
378 var files = LoadLocalFileTuples(accountInfo);
\r
380 var states = FileState.Queryable.ToList();
\r
382 var infos = (from remote in cleanRemotes
\r
383 let path = remote.RelativeUrlToFilePath(accountInfo.UserName)
\r
384 let info=agent.GetFileSystemInfo(path)
\r
385 select Tuple.Create(info.FullName,remote))
\r
388 var token = _currentOperationCancellation.Token;
\r
390 var tuples = MergeSources(infos, files, states).ToList();
\r
393 foreach (var tuple in tuples)
\r
395 await _unPauseEvent.WaitAsync();
\r
397 SyncSingleItem(accountInfo, tuple, agent, token);
\r
405 MarkSuspectedDeletes(accountInfo, cleanRemotes);
\r
410 Log.Info("[LISTENER] End Processing");
\r
413 catch (Exception ex)
\r
415 Log.ErrorFormat("[FAIL] ListObjects for{0} in ProcessRemoteFiles with {1}", accountInfo.UserName, ex);
\r
419 Log.Info("[LISTENER] Finished");
\r
424 private static List<Tuple<FileSystemInfo, string>> LoadLocalFileTuples(AccountInfo accountInfo)
\r
426 using (ThreadContext.Stacks["Account Files Hashing"].Push(accountInfo.UserName))
\r
429 var localInfos = AgentLocator<FileAgent>.Get(accountInfo.AccountPath).EnumerateFileSystemInfos();
\r
430 //Use the queue to retry locked file hashing
\r
431 var fileQueue = new Queue<FileSystemInfo>(localInfos);
\r
433 var results = new List<Tuple<FileSystemInfo, string>>();
\r
435 while (fileQueue.Count > 0)
\r
437 var file = fileQueue.Dequeue();
\r
438 using (ThreadContext.Stacks["File"].Push(file.FullName))
\r
442 var hash = (file is DirectoryInfo)
\r
443 ? "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
\r
444 : Signature.CalculateTreeHash(file, accountInfo.BlockSize,
\r
445 accountInfo.BlockHash)
\r
447 TopHash.ToHashString();
\r
448 results.Add(Tuple.Create(file, hash));
\r
450 catch (IOException exc)
\r
452 Log.WarnFormat("[HASH] File in use, will retry [{0}]", exc);
\r
453 fileQueue.Enqueue(file);
\r
462 private void SyncSingleItem(AccountInfo accountInfo, StateTuple tuple, FileAgent agent, CancellationToken token)
\r
464 Log.DebugFormat("Sync [{0}] C:[{1}] L:[{2}] S:[{3}]",tuple.FilePath,tuple.C,tuple.L,tuple.S);
\r
466 var localFilePath = tuple.FilePath;
\r
467 //Don't use the tuple info, it may have been deleted
\r
468 var localInfo = FileInfoExtensions.FromPath(localFilePath);
\r
470 // Local file unchanged? If both C and L are null, make sure it's because
\r
471 //both the file is missing and the state checksum is not missing
\r
472 if (tuple.C == tuple.L && (localInfo.Exists || tuple.FileState==null))
\r
475 //Server unchanged?
\r
476 if (tuple.S == tuple.L)
\r
478 // No server changes
\r
483 //Different from server
\r
484 if (Selectives.IsSelected(accountInfo, localFilePath))
\r
486 //Does the server file exist?
\r
487 if (tuple.S == null)
\r
489 //Server file doesn't exist
\r
490 //deleteObjectFromLocal()
\r
491 StatusKeeper.SetFileState(localFilePath, FileStatus.Deleted,
\r
492 FileOverlayStatus.Deleted, "");
\r
493 agent.Delete(localFilePath);
\r
494 //updateRecord(Remove C, L)
\r
495 StatusKeeper.ClearFileStatus(localFilePath);
\r
499 //Server file exists
\r
500 //downloadServerObject() // Result: L = S
\r
501 StatusKeeper.SetFileState(localFilePath, FileStatus.Modified,
\r
502 FileOverlayStatus.Modified, "");
\r
503 NetworkAgent.Downloader.DownloadCloudFile(accountInfo,
\r
505 localFilePath, token).Wait(token);
\r
506 //updateRecord( L = S )
\r
507 StatusKeeper.UpdateFileChecksum(localFilePath, tuple.FileState==null?"":tuple.FileState.ShortHash,
\r
508 tuple.ObjectInfo.Hash);
\r
510 StatusKeeper.SetFileState(localFilePath, FileStatus.Unchanged,
\r
511 FileOverlayStatus.Normal, "");
\r
518 //Local changes found
\r
520 //Server unchanged?
\r
521 if (tuple.S == tuple.L)
\r
523 //The FileAgent selective sync checks for new root folder files
\r
524 if (!agent.Ignore(localFilePath))
\r
526 if ((tuple.C == null || !localInfo.Exists) && tuple.ObjectInfo != null)
\r
528 //deleteObjectFromServer()
\r
529 DeleteCloudFile(accountInfo, tuple);
\r
530 //updateRecord( Remove L, S)
\r
534 //uploadLocalObject() // Result: S = C, L = S
\r
535 var isUnselected = agent.IsUnselectedRootFolder(tuple.FilePath);
\r
537 //Debug.Assert(tuple.FileState !=null);
\r
538 var action = new CloudUploadAction(accountInfo, localInfo, tuple.FileState,
\r
539 accountInfo.BlockSize, accountInfo.BlockHash,
\r
540 "Poll", isUnselected);
\r
541 NetworkAgent.Uploader.UploadCloudFile(action, token).Wait(token);
\r
544 //updateRecord( S = C )
\r
545 StatusKeeper.SetFileState(localFilePath, FileStatus.Unchanged,
\r
546 FileOverlayStatus.Normal, "");
\r
549 ProcessChildren(accountInfo, tuple, agent, token);
\r
556 if (Selectives.IsSelected(accountInfo, localFilePath))
\r
558 if (tuple.C == tuple.S)
\r
560 // (Identical Changes) Result: L = S
\r
562 StatusKeeper.UpdateFileChecksum(localFilePath, tuple.FileState == null ? "" : tuple.FileState.ShortHash,
\r
563 tuple.ObjectInfo.Hash);
\r
564 StatusKeeper.SetFileState(localFilePath, FileStatus.Unchanged,
\r
565 FileOverlayStatus.Normal, "");
\r
569 if ((tuple.C == null || !localInfo.Exists) && tuple.ObjectInfo != null )
\r
571 //deleteObjectFromServer()
\r
572 DeleteCloudFile(accountInfo, tuple);
\r
573 //updateRecord(Remove L, S)
\r
577 ReportConflictForMismatch(localFilePath);
\r
578 //identifyAsConflict() // Manual action required
\r
586 private void DeleteCloudFile(AccountInfo accountInfo, StateTuple tuple)
\r
588 StatusKeeper.SetFileState(tuple.FilePath, FileStatus.Deleted,
\r
589 FileOverlayStatus.Deleted, "");
\r
590 NetworkAgent.DeleteAgent.DeleteCloudFile(accountInfo, tuple.ObjectInfo);
\r
591 StatusKeeper.ClearFileStatus(tuple.FilePath);
\r
594 private void ProcessChildren(AccountInfo accountInfo, StateTuple tuple, FileAgent agent, CancellationToken token)
\r
597 var dirInfo = tuple.FileInfo as DirectoryInfo;
\r
598 var folderTuples = from folder in dirInfo.EnumerateDirectories("*", SearchOption.AllDirectories)
\r
599 select new StateTuple(folder);
\r
600 var fileTuples = from file in dirInfo.EnumerateFiles("*", SearchOption.AllDirectories)
\r
601 select new StateTuple(file);
\r
603 //Process folders first, to ensure folders appear on the sever as soon as possible
\r
604 folderTuples.ApplyAction(t => SyncSingleItem(accountInfo, t, agent, token));
\r
606 fileTuples.ApplyAction(t => SyncSingleItem(accountInfo, t, agent, token));
\r
609 private static IEnumerable<StateTuple> MergeSources(
\r
610 IEnumerable<Tuple<string, ObjectInfo>> infos,
\r
611 IEnumerable<Tuple<FileSystemInfo, string>> files,
\r
612 IEnumerable<FileState> states)
\r
614 var dct = new Dictionary<string, StateTuple>();
\r
615 foreach (var file in files)
\r
617 var fsInfo = file.Item1;
\r
618 var fileHash = file.Item2;
\r
619 dct[fsInfo.FullName] = new StateTuple {FileInfo = fsInfo, C = fileHash};
\r
621 foreach (var state in states)
\r
623 StateTuple hashTuple;
\r
624 if (dct.TryGetValue(state.FilePath, out hashTuple))
\r
626 hashTuple.FileState = state;
\r
630 var fsInfo = FileInfoExtensions.FromPath(state.FilePath);
\r
631 dct[state.FilePath] = new StateTuple {FileInfo = fsInfo, FileState = state};
\r
634 foreach (var info in infos)
\r
636 StateTuple hashTuple;
\r
637 var filePath = info.Item1;
\r
638 var objectInfo = info.Item2;
\r
639 if (dct.TryGetValue(filePath, out hashTuple))
\r
641 hashTuple.ObjectInfo = objectInfo;
\r
645 var fsInfo = FileInfoExtensions.FromPath(filePath);
\r
646 dct[filePath] = new StateTuple {FileInfo = fsInfo, ObjectInfo = objectInfo};
\r
653 /// Returns the latest LastModified date from the list of objects, but only if it is before
\r
654 /// than the threshold value
\r
656 /// <param name="threshold"></param>
\r
657 /// <param name="cloudObjects"></param>
\r
658 /// <returns></returns>
\r
659 private static DateTime? GetLatestDateBefore(DateTime? threshold, IList<ObjectInfo> cloudObjects)
\r
661 DateTime? maxDate = null;
\r
662 if (cloudObjects!=null && cloudObjects.Count > 0)
\r
663 maxDate = cloudObjects.Max(obj => obj.Last_Modified);
\r
664 if (maxDate == null || maxDate == DateTime.MinValue)
\r
666 if (threshold == null || threshold == DateTime.MinValue || threshold > maxDate)
\r
672 /// Returns the latest LastModified date from the list of objects, but only if it is after
\r
673 /// the threshold value
\r
675 /// <param name="threshold"></param>
\r
676 /// <param name="cloudObjects"></param>
\r
677 /// <returns></returns>
\r
678 private static DateTime? GetLatestDateAfter(DateTime? threshold, IList<ObjectInfo> cloudObjects)
\r
680 DateTime? maxDate = null;
\r
681 if (cloudObjects!=null && cloudObjects.Count > 0)
\r
682 maxDate = cloudObjects.Max(obj => obj.Last_Modified);
\r
683 if (maxDate == null || maxDate == DateTime.MinValue)
\r
685 if (threshold == null || threshold == DateTime.MinValue || threshold < maxDate)
\r
690 //readonly AccountsDifferencer _differencer = new AccountsDifferencer();
\r
691 private Dictionary<Uri, List<Uri>> _selectiveUris = new Dictionary<Uri, List<Uri>>();
\r
692 private bool _pause;
\r
695 /// Deletes local files that are not found in the list of cloud files
\r
697 /// <param name="accountInfo"></param>
\r
698 /// <param name="cloudFiles"></param>
\r
699 private void ProcessDeletedFiles(AccountInfo accountInfo, IEnumerable<ObjectInfo> cloudFiles)
\r
701 if (accountInfo == null)
\r
702 throw new ArgumentNullException("accountInfo");
\r
703 if (String.IsNullOrWhiteSpace(accountInfo.AccountPath))
\r
704 throw new ArgumentException("The AccountInfo.AccountPath is empty", "accountInfo");
\r
705 if (cloudFiles == null)
\r
706 throw new ArgumentNullException("cloudFiles");
\r
707 Contract.EndContractBlock();
\r
709 var deletedFiles = new List<FileSystemInfo>();
\r
710 foreach (var objectInfo in cloudFiles)
\r
712 if (Log.IsDebugEnabled)
\r
713 Log.DebugFormat("Handle deleted [{0}]", objectInfo.Uri);
\r
714 var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
\r
715 var item = FileAgent.GetFileAgent(accountInfo).GetFileSystemInfo(relativePath);
\r
716 if (Log.IsDebugEnabled)
\r
717 Log.DebugFormat("Will delete [{0}] for [{1}]", item.FullName, objectInfo.Uri);
\r
720 if ((item.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
\r
722 item.Attributes = item.Attributes & ~FileAttributes.ReadOnly;
\r
727 Log.DebugFormat("Deleting {0}", item.FullName);
\r
729 var directory = item as DirectoryInfo;
\r
730 if (directory != null)
\r
731 directory.Delete(true);
\r
734 Log.DebugFormat("Deleted [{0}] for [{1}]", item.FullName, objectInfo.Uri);
\r
736 _lastSeen.TryRemove(item.FullName, out lastDate);
\r
737 deletedFiles.Add(item);
\r
739 StatusKeeper.SetFileState(item.FullName, FileStatus.Deleted, FileOverlayStatus.Deleted, "File Deleted");
\r
741 Log.InfoFormat("[{0}] files were deleted", deletedFiles.Count);
\r
742 StatusNotification.NotifyForFiles(deletedFiles, String.Format("{0} files were deleted", deletedFiles.Count),
\r
747 private void MarkSuspectedDeletes(AccountInfo accountInfo, IEnumerable<ObjectInfo> cloudFiles)
\r
749 //Only consider files that are not being modified, ie they are in the Unchanged state
\r
750 var deleteCandidates = FileState.Queryable.Where(state =>
\r
751 state.FilePath.StartsWith(accountInfo.AccountPath)
\r
752 && state.FileStatus == FileStatus.Unchanged).ToList();
\r
755 //TODO: filesToDelete must take into account the Others container
\r
756 var filesToDelete = (from deleteCandidate in deleteCandidates
\r
757 let localFile = FileInfoExtensions.FromPath(deleteCandidate.FilePath)
\r
758 let relativeFilePath = localFile.AsRelativeTo(accountInfo.AccountPath)
\r
760 !cloudFiles.Any(r => r.RelativeUrlToFilePath(accountInfo.UserName) == relativeFilePath)
\r
761 select localFile).ToList();
\r
764 //Set the status of missing files to Conflict
\r
765 foreach (var item in filesToDelete)
\r
767 //Try to acquire a gate on the file, to take into account files that have been dequeued
\r
768 //and are being processed
\r
769 using (var gate = NetworkGate.Acquire(item.FullName, NetworkOperation.Deleting))
\r
773 StatusKeeper.SetFileState(item.FullName, FileStatus.Conflict, FileOverlayStatus.Deleted,
\r
774 "Local file missing from server");
\r
777 UpdateStatus(PithosStatus.HasConflicts);
\r
778 StatusNotification.NotifyConflicts(filesToDelete,
\r
780 "{0} local files are missing from Pithos, possibly because they were deleted",
\r
781 filesToDelete.Count));
\r
782 StatusNotification.NotifyForFiles(filesToDelete, String.Format("{0} files were deleted", filesToDelete.Count),
\r
786 private void ReportConflictForMismatch(string localFilePath)
\r
788 if (String.IsNullOrWhiteSpace(localFilePath))
\r
789 throw new ArgumentNullException("localFilePath");
\r
790 Contract.EndContractBlock();
\r
792 StatusKeeper.SetFileState(localFilePath, FileStatus.Conflict, FileOverlayStatus.Conflict, "File changed at the server");
\r
793 UpdateStatus(PithosStatus.HasConflicts);
\r
794 var message = String.Format("Conflict detected for file {0}", localFilePath);
\r
796 StatusNotification.NotifyChange(message, TraceLevel.Warning);
\r
802 /// Creates a Sync action for each changed server file
\r
804 /// <param name="accountInfo"></param>
\r
805 /// <param name="changes"></param>
\r
806 /// <returns></returns>
\r
807 private IEnumerable<CloudAction> ChangesToActions(AccountInfo accountInfo, IEnumerable<ObjectInfo> changes)
\r
809 if (changes == null)
\r
810 throw new ArgumentNullException();
\r
811 Contract.EndContractBlock();
\r
812 var fileAgent = FileAgent.GetFileAgent(accountInfo);
\r
814 //In order to avoid multiple iterations over the files, we iterate only once
\r
815 //over the remote files
\r
816 foreach (var objectInfo in changes)
\r
818 var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
\r
819 //If a directory object already exists, we may need to sync it
\r
820 if (fileAgent.Exists(relativePath))
\r
822 var localFile = fileAgent.GetFileSystemInfo(relativePath);
\r
823 //We don't need to sync directories
\r
824 if (objectInfo.IsDirectory && localFile is DirectoryInfo)
\r
826 using (new SessionScope(FlushAction.Never))
\r
828 var state = StatusKeeper.GetStateByFilePath(localFile.FullName);
\r
829 _lastSeen[localFile.FullName] = DateTime.Now;
\r
830 //Common files should be checked on a per-case basis to detect differences, which is newer
\r
832 yield return new CloudAction(accountInfo, CloudActionType.MustSynch,
\r
833 localFile, objectInfo, state, accountInfo.BlockSize,
\r
834 accountInfo.BlockHash,"Poll Changes");
\r
839 //Remote files should be downloaded
\r
840 yield return new CloudDownloadAction(accountInfo, objectInfo,"Poll Changes");
\r
846 /// Creates a Local Move action for each moved server file
\r
848 /// <param name="accountInfo"></param>
\r
849 /// <param name="moves"></param>
\r
850 /// <returns></returns>
\r
851 private IEnumerable<CloudAction> MovesToActions(AccountInfo accountInfo, IEnumerable<ObjectInfo> moves)
\r
854 throw new ArgumentNullException();
\r
855 Contract.EndContractBlock();
\r
856 var fileAgent = FileAgent.GetFileAgent(accountInfo);
\r
858 //In order to avoid multiple iterations over the files, we iterate only once
\r
859 //over the remote files
\r
860 foreach (var objectInfo in moves)
\r
862 var previousRelativepath = objectInfo.Previous.RelativeUrlToFilePath(accountInfo.UserName);
\r
863 //If the previous file already exists, we can execute a Move operation
\r
864 if (fileAgent.Exists(previousRelativepath))
\r
866 var previousFile = fileAgent.GetFileSystemInfo(previousRelativepath);
\r
867 using (new SessionScope(FlushAction.Never))
\r
869 var state = StatusKeeper.GetStateByFilePath(previousFile.FullName);
\r
870 _lastSeen[previousFile.FullName] = DateTime.Now;
\r
872 //For each moved object we need to move both the local file and update
\r
873 yield return new CloudAction(accountInfo, CloudActionType.RenameLocal,
\r
874 previousFile, objectInfo, state, accountInfo.BlockSize,
\r
875 accountInfo.BlockHash,"Poll Moves");
\r
876 //For modified files, we need to download the changes as well
\r
877 if (objectInfo.Hash!=objectInfo.PreviousHash)
\r
878 yield return new CloudDownloadAction(accountInfo,objectInfo, "Poll Moves");
\r
881 //If the previous file does not exist, we need to download it in the new location
\r
884 //Remote files should be downloaded
\r
885 yield return new CloudDownloadAction(accountInfo, objectInfo, "Poll Moves");
\r
892 /// Creates a download action for each new server file
\r
894 /// <param name="accountInfo"></param>
\r
895 /// <param name="creates"></param>
\r
896 /// <returns></returns>
\r
897 private IEnumerable<CloudAction> CreatesToActions(AccountInfo accountInfo, IEnumerable<ObjectInfo> creates)
\r
899 if (creates == null)
\r
900 throw new ArgumentNullException();
\r
901 Contract.EndContractBlock();
\r
902 var fileAgent = FileAgent.GetFileAgent(accountInfo);
\r
904 //In order to avoid multiple iterations over the files, we iterate only once
\r
905 //over the remote files
\r
906 foreach (var objectInfo in creates)
\r
908 if (Log.IsDebugEnabled)
\r
909 Log.DebugFormat("[NEW INFO] {0}",objectInfo.Uri);
\r
911 var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
\r
913 //If the object already exists, we should check before uploading or downloading
\r
914 if (fileAgent.Exists(relativePath))
\r
916 var localFile= fileAgent.GetFileSystemInfo(relativePath);
\r
917 var state = StatusKeeper.GetStateByFilePath(localFile.WithProperCapitalization().FullName);
\r
918 yield return new CloudAction(accountInfo, CloudActionType.MustSynch,
\r
919 localFile, objectInfo, state, accountInfo.BlockSize,
\r
920 accountInfo.BlockHash,"Poll Creates");
\r
924 //Remote files should be downloaded
\r
925 yield return new CloudDownloadAction(accountInfo, objectInfo,"Poll Creates");
\r
932 /// Notify the UI to update the visual status
\r
934 /// <param name="status"></param>
\r
935 private void UpdateStatus(PithosStatus status)
\r
939 StatusNotification.SetPithosStatus(status);
\r
940 //StatusNotification.Notify(new Notification());
\r
942 catch (Exception exc)
\r
944 //Failure is not critical, just log it
\r
945 Log.Warn("Error while updating status", exc);
\r
949 private static void CreateContainerFolders(AccountInfo accountInfo, IEnumerable<ContainerInfo> containers)
\r
951 var containerPaths = from container in containers
\r
952 let containerPath = Path.Combine(accountInfo.AccountPath, container.Name)
\r
953 where container.Name != FolderConstants.TrashContainer && !Directory.Exists(containerPath)
\r
954 select containerPath;
\r
956 foreach (var path in containerPaths)
\r
958 Directory.CreateDirectory(path);
\r
962 public void AddAccount(AccountInfo accountInfo)
\r
964 //Avoid adding a duplicate accountInfo
\r
965 _accounts.TryAdd(accountInfo.AccountKey, accountInfo);
\r
968 public void RemoveAccount(AccountInfo accountInfo)
\r
970 AccountInfo account;
\r
971 _accounts.TryRemove(accountInfo.AccountKey, out account);
\r
973 SnapshotDifferencer differencer;
\r
974 _differencer.Differencers.TryRemove(accountInfo.AccountKey, out differencer);
\r
978 public void SetSelectivePaths(AccountInfo accountInfo,Uri[] added, Uri[] removed)
\r
980 AbortRemovedPaths(accountInfo,removed);
\r
981 DownloadNewPaths(accountInfo,added);
\r
984 private void DownloadNewPaths(AccountInfo accountInfo, Uri[] added)
\r
986 var client = new CloudFilesClient(accountInfo);
\r
987 foreach (var folderUri in added)
\r
994 var segmentsCount = folderUri.Segments.Length;
\r
995 //Is this an account URL?
\r
996 if (segmentsCount < 3)
\r
998 //Is this a container or folder URL?
\r
999 if (segmentsCount == 3)
\r
1001 account = folderUri.Segments[1].TrimEnd('/');
\r
1002 container = folderUri.Segments[2].TrimEnd('/');
\r
1006 account = folderUri.Segments[2].TrimEnd('/');
\r
1007 container = folderUri.Segments[3].TrimEnd('/');
\r
1009 IList<ObjectInfo> items;
\r
1010 if (segmentsCount > 3)
\r
1013 var folder = String.Join("", folderUri.Segments.Splice(4));
\r
1014 items = client.ListObjects(account, container, folder);
\r
1019 items = client.ListObjects(account, container);
\r
1021 var actions = CreatesToActions(accountInfo, items);
\r
1022 foreach (var action in actions)
\r
1024 NetworkAgent.Post(action);
\r
1027 catch (Exception exc)
\r
1029 Log.WarnFormat("Listing of new selective path [{0}] failed with \r\n{1}", folderUri, exc);
\r
1033 //Need to get a listing of each of the URLs, then post them to the NetworkAgent
\r
1034 //CreatesToActions(accountInfo,)
\r
1036 /* NetworkAgent.Post();*/
\r
1039 private void AbortRemovedPaths(AccountInfo accountInfo, Uri[] removed)
\r
1041 /*this.NetworkAgent.*/
\r