Modifications to enable Sync Pausing for all operations
[pithos-ms-client] / trunk / Pithos.Core / Agents / DeleteAgent.cs
1 #region
2 /* -----------------------------------------------------------------------
3  * <copyright file="DeleteAgent.cs" company="GRNet">
4  * 
5  * Copyright 2011-2012 GRNET S.A. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or
8  * without modification, are permitted provided that the following
9  * conditions are met:
10  *
11  *   1. Redistributions of source code must retain the above
12  *      copyright notice, this list of conditions and the following
13  *      disclaimer.
14  *
15  *   2. Redistributions in binary form must reproduce the above
16  *      copyright notice, this list of conditions and the following
17  *      disclaimer in the documentation and/or other materials
18  *      provided with the distribution.
19  *
20  *
21  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  *
34  * The views and conclusions contained in the software and
35  * documentation are those of the authors and should not be
36  * interpreted as representing official policies, either expressed
37  * or implied, of GRNET S.A.
38  * </copyright>
39  * -----------------------------------------------------------------------
40  */
41 #endregion
42 using System.Collections.Concurrent;
43 using System.ComponentModel.Composition;
44 using System.Diagnostics.Contracts;
45 using System.IO;
46 using System.Net;
47 using System.Reflection;
48 using System.Threading.Tasks.Dataflow;
49 using Pithos.Interfaces;
50 using Pithos.Network;
51 using System;
52 using log4net;
53
54 namespace Pithos.Core.Agents
55 {
56
57     /// <summary>
58     /// The Delete Agent is used to delete files from the Pithos server with high priority, 
59     /// blocking the network agent through the ProceedEvent until all pending deletions complete
60     /// </summary>    
61     [Export]
62     public class DeleteAgent
63     {
64         private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
65
66         [Import]
67         public IStatusKeeper StatusKeeper { get; set; }
68         
69         public IStatusNotification StatusNotification { get; set; }
70
71         //A separate agent is used to execute delete actions immediatelly;
72         private readonly ActionBlock<CloudDeleteAction> _deleteAgent;
73
74         //The Proceed event signals the the network agent to proceed with processing.
75         //Essentially this event pauses the network agent to give priority to the deletion agent
76         //Initially the event is signalled because we don't need to pause
77         private readonly AsyncManualResetEvent _proceedEvent = new AsyncManualResetEvent(true);
78
79         public AsyncManualResetEvent ProceedEvent
80         {
81             get { return _proceedEvent; }
82         }
83
84         //Deleted file names are stored in memory so we can check that a file has already been deleted.
85         //and avoid sending duplicate delete commands
86         readonly ConcurrentDictionary<string, DateTime> _deletedFiles = new ConcurrentDictionary<string, DateTime>();
87
88         public DeleteAgent()
89         {
90             _deleteAgent = new ActionBlock<CloudDeleteAction>(message => ProcessDelete(message), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });
91
92         }
93
94         /// <summary>
95         /// Processes cloud delete actions
96         /// </summary>
97         /// <param name="action">The delete action to execute</param>
98         /// <returns></returns>
99         /// <remarks>
100         /// When a file/folder is deleted locally, we must delete it ASAP from the server and block any download
101         /// operations that may be in progress.
102         /// <para>
103         /// A separate agent is used to process deletes because the main agent may be busy with a long operation.
104         /// </para>
105         /// </remarks>
106         private void ProcessDelete(CloudDeleteAction action)
107         {
108             if (action == null)
109                 throw new ArgumentNullException("action");
110             if (action.AccountInfo == null)
111                 throw new ArgumentException("The action.AccountInfo is empty", "action");
112             Contract.EndContractBlock();
113
114             var accountInfo = action.AccountInfo;
115
116             using (log4net.ThreadContext.Stacks["Operation"].Push("ProcessDelete"))
117             {
118                 Log.InfoFormat("[ACTION] Start Processing {0}", action);
119
120                 var cloudFile = action.CloudFile;
121
122                 try
123                 {
124                     //Acquire a lock on the deleted file to prevent uploading/downloading operations from the normal
125                     //agent
126                     using (NetworkGate.Acquire(action.LocalFile.FullName, NetworkOperation.Deleting))
127                     {
128
129                         //Add the file URL to the deleted files list
130                         var key = GetFileKey(action.CloudFile);
131                         _deletedFiles[key] = DateTime.Now;
132
133                         _proceedEvent.Reset();
134                         // and then delete the file from the server
135                         DeleteCloudFile(accountInfo, cloudFile);
136
137                         Log.InfoFormat("[ACTION] End Delete {0}:{1}->{2}", action.Action, action.LocalFile,
138                                        action.CloudFile.Name);
139                     }
140                 }
141                 catch (WebException exc)
142                 {
143                     Log.ErrorFormat("[WEB ERROR] {0} : {1} -> {2} due to exception\r\n{3}", action.Action, action.LocalFile, action.CloudFile, exc);
144                 }
145                 catch (OperationCanceledException)
146                 {
147                     throw;
148                 }
149                 catch (DirectoryNotFoundException)
150                 {
151                     Log.ErrorFormat("{0} : {1} -> {2}  failed because the directory was not found.\n Rescheduling a delete",
152                         action.Action, action.LocalFile, action.CloudFile);
153                     //Repost a delete action for the missing file
154                     _deleteAgent.Post(action);
155                 }
156                 catch (FileNotFoundException)
157                 {
158                     Log.ErrorFormat("{0} : {1} -> {2}  failed because the file was not found.\n Rescheduling a delete",
159                         action.Action, action.LocalFile, action.CloudFile);
160                     //Post a delete action for the missing file
161                     _deleteAgent.Post(action);
162                 }
163                 catch (Exception exc)
164                 {
165                     Log.ErrorFormat("[REQUEUE] {0} : {1} -> {2} due to exception\r\n{3}",
166                                      action.Action, action.LocalFile, action.CloudFile, exc);
167
168                     _deleteAgent.Post(action);
169                 }
170                 finally
171                 {
172                     //Set the event when all delete actions are processed
173                     if (_deleteAgent.InputCount == 0)
174                         _proceedEvent.Set();
175
176                 }
177             }
178         }
179
180         //Returns true if an action concerns a file that was deleted
181         public bool IsDeletedFile(CloudAction action)
182         {
183             //Doesn't work for actions targeting shared files
184             if (action.IsShared)
185                 return false;
186             var key = GetFileKey(action.CloudFile);
187             DateTime entryDate;
188             if (_deletedFiles.TryGetValue(key, out entryDate))
189             {
190                 //If the delete entry was created after this action, abort the action
191                 if (entryDate > action.Created)
192                     return true;
193                 //Otherwise, remove the stale entry 
194                 _deletedFiles.TryRemove(key, out entryDate);
195             }
196             return false;
197         }
198
199         public void Post(CloudDeleteAction action)
200         {
201             _deleteAgent.Post(action);
202         }
203
204         private void DeleteCloudFile(AccountInfo accountInfo, ObjectInfo cloudFile)
205         {
206             if (accountInfo == null)
207                 throw new ArgumentNullException("accountInfo");
208             if (cloudFile == null)
209                 throw new ArgumentNullException("cloudFile");
210
211             if (String.IsNullOrWhiteSpace(cloudFile.Container))
212                 throw new ArgumentException("Invalid container", "cloudFile");
213             Contract.EndContractBlock();
214
215             var fileAgent = GetFileAgent(accountInfo);
216
217             using (ThreadContext.Stacks["Operation"].Push("DeleteCloudFile"))
218             {
219                 var fileName = cloudFile.RelativeUrlToFilePath(accountInfo.UserName);
220                 var info = fileAgent.GetFileSystemInfo(fileName);
221                 var fullPath = info.FullName.ToLower();
222
223                 StatusKeeper.SetFileOverlayStatus(fullPath, FileOverlayStatus.Modified);
224
225                 var account = cloudFile.Account ?? accountInfo.UserName;
226                 var container = cloudFile.Container;//?? FolderConstants.PithosContainer;
227
228                 var client = new CloudFilesClient(accountInfo);
229                 client.DeleteObject(account, container, cloudFile.Name);
230
231                 StatusKeeper.ClearFileStatus(fullPath);
232                 StatusNotification.Notify(new CloudNotification{Data=cloudFile});
233             }
234         }
235
236
237         private static string GetFileKey(ObjectInfo info)
238         {
239             var key = String.Format("{0}/{1}/{2}", info.Account, info.Container, info.Name);
240             return key;
241         }
242
243         private static FileAgent GetFileAgent(AccountInfo accountInfo)
244         {
245             return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);
246         }
247
248     }
249 }