Statistics
| Branch: | Revision:

root / trunk / Pithos.Core / Agents / PollAgent.cs @ b666b39a

History | View | Annotate | Download (26.3 kB)

1 aa7ac00e Panagiotis Kanavos
#region
2 aa7ac00e Panagiotis Kanavos
/* -----------------------------------------------------------------------
3 aa7ac00e Panagiotis Kanavos
 * <copyright file="PollAgent.cs" company="GRNet">
4 aa7ac00e Panagiotis Kanavos
 * 
5 aa7ac00e Panagiotis Kanavos
 * Copyright 2011-2012 GRNET S.A. All rights reserved.
6 aa7ac00e Panagiotis Kanavos
 *
7 aa7ac00e Panagiotis Kanavos
 * Redistribution and use in source and binary forms, with or
8 aa7ac00e Panagiotis Kanavos
 * without modification, are permitted provided that the following
9 aa7ac00e Panagiotis Kanavos
 * conditions are met:
10 aa7ac00e Panagiotis Kanavos
 *
11 aa7ac00e Panagiotis Kanavos
 *   1. Redistributions of source code must retain the above
12 aa7ac00e Panagiotis Kanavos
 *      copyright notice, this list of conditions and the following
13 aa7ac00e Panagiotis Kanavos
 *      disclaimer.
14 aa7ac00e Panagiotis Kanavos
 *
15 aa7ac00e Panagiotis Kanavos
 *   2. Redistributions in binary form must reproduce the above
16 aa7ac00e Panagiotis Kanavos
 *      copyright notice, this list of conditions and the following
17 aa7ac00e Panagiotis Kanavos
 *      disclaimer in the documentation and/or other materials
18 aa7ac00e Panagiotis Kanavos
 *      provided with the distribution.
19 aa7ac00e Panagiotis Kanavos
 *
20 aa7ac00e Panagiotis Kanavos
 *
21 aa7ac00e Panagiotis Kanavos
 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22 aa7ac00e Panagiotis Kanavos
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 aa7ac00e Panagiotis Kanavos
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 aa7ac00e Panagiotis Kanavos
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25 aa7ac00e Panagiotis Kanavos
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 aa7ac00e Panagiotis Kanavos
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 aa7ac00e Panagiotis Kanavos
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28 aa7ac00e Panagiotis Kanavos
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 aa7ac00e Panagiotis Kanavos
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 aa7ac00e Panagiotis Kanavos
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 aa7ac00e Panagiotis Kanavos
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 aa7ac00e Panagiotis Kanavos
 * POSSIBILITY OF SUCH DAMAGE.
33 aa7ac00e Panagiotis Kanavos
 *
34 aa7ac00e Panagiotis Kanavos
 * The views and conclusions contained in the software and
35 aa7ac00e Panagiotis Kanavos
 * documentation are those of the authors and should not be
36 aa7ac00e Panagiotis Kanavos
 * interpreted as representing official policies, either expressed
37 aa7ac00e Panagiotis Kanavos
 * or implied, of GRNET S.A.
38 aa7ac00e Panagiotis Kanavos
 * </copyright>
39 aa7ac00e Panagiotis Kanavos
 * -----------------------------------------------------------------------
40 aa7ac00e Panagiotis Kanavos
 */
41 aa7ac00e Panagiotis Kanavos
#endregion
42 aa7ac00e Panagiotis Kanavos
43 aa7ac00e Panagiotis Kanavos
using System.Collections.Concurrent;
44 aa7ac00e Panagiotis Kanavos
using System.ComponentModel.Composition;
45 aa7ac00e Panagiotis Kanavos
using System.Diagnostics;
46 aa7ac00e Panagiotis Kanavos
using System.Diagnostics.Contracts;
47 aa7ac00e Panagiotis Kanavos
using System.IO;
48 aa7ac00e Panagiotis Kanavos
using System.Threading;
49 aa7ac00e Panagiotis Kanavos
using System.Threading.Tasks;
50 aa7ac00e Panagiotis Kanavos
using System.Threading.Tasks.Dataflow;
51 aa7ac00e Panagiotis Kanavos
using Castle.ActiveRecord;
52 aa7ac00e Panagiotis Kanavos
using Pithos.Interfaces;
53 aa7ac00e Panagiotis Kanavos
using Pithos.Network;
54 aa7ac00e Panagiotis Kanavos
using log4net;
55 aa7ac00e Panagiotis Kanavos
56 aa7ac00e Panagiotis Kanavos
namespace Pithos.Core.Agents
57 aa7ac00e Panagiotis Kanavos
{
58 aa7ac00e Panagiotis Kanavos
    using System;
59 aa7ac00e Panagiotis Kanavos
    using System.Collections.Generic;
60 aa7ac00e Panagiotis Kanavos
    using System.Linq;
61 aa7ac00e Panagiotis Kanavos
    using System.Text;
62 aa7ac00e Panagiotis Kanavos
63 aa7ac00e Panagiotis Kanavos
    /// <summary>
64 aa7ac00e Panagiotis Kanavos
    /// PollAgent periodically polls the server to detect object changes. The agent retrieves a listing of all
65 aa7ac00e Panagiotis Kanavos
    /// objects and compares it with a previously cached version to detect differences. 
66 aa7ac00e Panagiotis Kanavos
    /// New files are downloaded, missing files are deleted from the local file system and common files are compared
67 aa7ac00e Panagiotis Kanavos
    /// to determine the appropriate action
68 aa7ac00e Panagiotis Kanavos
    /// </summary>
69 aa7ac00e Panagiotis Kanavos
    [Export]
70 aa7ac00e Panagiotis Kanavos
    public class PollAgent
71 aa7ac00e Panagiotis Kanavos
    {
72 aa7ac00e Panagiotis Kanavos
        private static readonly ILog Log = LogManager.GetLogger("PollAgent");
73 aa7ac00e Panagiotis Kanavos
74 aa7ac00e Panagiotis Kanavos
        [System.ComponentModel.Composition.Import]
75 aa7ac00e Panagiotis Kanavos
        public IStatusKeeper StatusKeeper { get; set; }
76 aa7ac00e Panagiotis Kanavos
77 aa7ac00e Panagiotis Kanavos
        [System.ComponentModel.Composition.Import]
78 aa7ac00e Panagiotis Kanavos
        public IPithosSettings Settings { get; set; }
79 aa7ac00e Panagiotis Kanavos
80 aa7ac00e Panagiotis Kanavos
        [System.ComponentModel.Composition.Import]
81 aa7ac00e Panagiotis Kanavos
        public NetworkAgent NetworkAgent { get; set; }
82 aa7ac00e Panagiotis Kanavos
83 aa7ac00e Panagiotis Kanavos
        public IStatusNotification StatusNotification { get; set; }
84 aa7ac00e Panagiotis Kanavos
85 aa7ac00e Panagiotis Kanavos
        private bool _firstPoll = true;
86 aa7ac00e Panagiotis Kanavos
87 aa7ac00e Panagiotis Kanavos
        //The Sync Event signals a manual synchronisation
88 aa7ac00e Panagiotis Kanavos
        private readonly AsyncManualResetEvent _syncEvent = new AsyncManualResetEvent();
89 aa7ac00e Panagiotis Kanavos
90 aa7ac00e Panagiotis Kanavos
        private ConcurrentDictionary<string, DateTime> _lastSeen = new ConcurrentDictionary<string, DateTime>();
91 aa7ac00e Panagiotis Kanavos
        private readonly ConcurrentBag<AccountInfo> _accounts = new ConcurrentBag<AccountInfo>();
92 aa7ac00e Panagiotis Kanavos
93 aa7ac00e Panagiotis Kanavos
94 aa7ac00e Panagiotis Kanavos
        /// <summary>
95 aa7ac00e Panagiotis Kanavos
        /// Start a manual synchronization
96 aa7ac00e Panagiotis Kanavos
        /// </summary>
97 aa7ac00e Panagiotis Kanavos
        public void SynchNow()
98 aa7ac00e Panagiotis Kanavos
        {            
99 aa7ac00e Panagiotis Kanavos
            _syncEvent.Set();
100 aa7ac00e Panagiotis Kanavos
        }
101 aa7ac00e Panagiotis Kanavos
102 fec5da06 Panagiotis Kanavos
        /// <summary>
103 fec5da06 Panagiotis Kanavos
        /// Remote files are polled periodically. Any changes are processed
104 fec5da06 Panagiotis Kanavos
        /// </summary>
105 fec5da06 Panagiotis Kanavos
        /// <param name="since"></param>
106 fec5da06 Panagiotis Kanavos
        /// <returns></returns>
107 aa7ac00e Panagiotis Kanavos
        public async Task PollRemoteFiles(DateTime? since = null)
108 aa7ac00e Panagiotis Kanavos
        {
109 aa7ac00e Panagiotis Kanavos
            Debug.Assert(Thread.CurrentThread.IsBackground, "Polling Ended up in the main thread!");
110 aa7ac00e Panagiotis Kanavos
111 aa7ac00e Panagiotis Kanavos
            UpdateStatus(PithosStatus.Syncing);
112 aa7ac00e Panagiotis Kanavos
            StatusNotification.Notify(new PollNotification());
113 aa7ac00e Panagiotis Kanavos
114 aa7ac00e Panagiotis Kanavos
            using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push("All accounts"))
115 aa7ac00e Panagiotis Kanavos
            {
116 aa7ac00e Panagiotis Kanavos
                //If this poll fails, we will retry with the same since value
117 aa7ac00e Panagiotis Kanavos
                var nextSince = since;
118 aa7ac00e Panagiotis Kanavos
                try
119 aa7ac00e Panagiotis Kanavos
                {
120 aa7ac00e Panagiotis Kanavos
                    //Next time we will check for all changes since the current check minus 1 second
121 aa7ac00e Panagiotis Kanavos
                    //This is done to ensure there are no discrepancies due to clock differences
122 aa7ac00e Panagiotis Kanavos
                    var current = DateTime.Now.AddSeconds(-1);
123 aa7ac00e Panagiotis Kanavos
124 aa7ac00e Panagiotis Kanavos
                    var tasks = from accountInfo in _accounts
125 aa7ac00e Panagiotis Kanavos
                                select ProcessAccountFiles(accountInfo, since);
126 aa7ac00e Panagiotis Kanavos
127 aa7ac00e Panagiotis Kanavos
                    await TaskEx.WhenAll(tasks.ToList());
128 aa7ac00e Panagiotis Kanavos
129 aa7ac00e Panagiotis Kanavos
                    _firstPoll = false;
130 aa7ac00e Panagiotis Kanavos
                    //Reschedule the poll with the current timestamp as a "since" value
131 aa7ac00e Panagiotis Kanavos
                    nextSince = current;
132 aa7ac00e Panagiotis Kanavos
                }
133 aa7ac00e Panagiotis Kanavos
                catch (Exception ex)
134 aa7ac00e Panagiotis Kanavos
                {
135 aa7ac00e Panagiotis Kanavos
                    Log.ErrorFormat("Error while processing accounts\r\n{0}", ex);
136 aa7ac00e Panagiotis Kanavos
                    //In case of failure retry with the same "since" value
137 aa7ac00e Panagiotis Kanavos
                }
138 aa7ac00e Panagiotis Kanavos
139 aa7ac00e Panagiotis Kanavos
                UpdateStatus(PithosStatus.InSynch);
140 fec5da06 Panagiotis Kanavos
                //The multiple try blocks are required because we can't have an await call
141 fec5da06 Panagiotis Kanavos
                //inside a finally block
142 fec5da06 Panagiotis Kanavos
                //TODO: Find a more elegant solution for reschedulling in the event of an exception
143 fec5da06 Panagiotis Kanavos
                try
144 fec5da06 Panagiotis Kanavos
                {
145 fec5da06 Panagiotis Kanavos
                    //Wait for the polling interval to pass or the Sync event to be signalled
146 fec5da06 Panagiotis Kanavos
                    nextSince = await WaitForScheduledOrManualPoll(nextSince);
147 fec5da06 Panagiotis Kanavos
                }
148 fec5da06 Panagiotis Kanavos
                finally
149 fec5da06 Panagiotis Kanavos
                {
150 fec5da06 Panagiotis Kanavos
                    //Ensure polling is scheduled even in case of error
151 fec5da06 Panagiotis Kanavos
                    TaskEx.Run(() => PollRemoteFiles(nextSince));                        
152 fec5da06 Panagiotis Kanavos
                }
153 aa7ac00e Panagiotis Kanavos
            }
154 aa7ac00e Panagiotis Kanavos
        }
155 aa7ac00e Panagiotis Kanavos
156 aa7ac00e Panagiotis Kanavos
        /// <summary>
157 aa7ac00e Panagiotis Kanavos
        /// Wait for the polling period to expire or a manual sync request
158 aa7ac00e Panagiotis Kanavos
        /// </summary>
159 aa7ac00e Panagiotis Kanavos
        /// <param name="since"></param>
160 aa7ac00e Panagiotis Kanavos
        /// <returns></returns>
161 aa7ac00e Panagiotis Kanavos
        private async Task<DateTime?> WaitForScheduledOrManualPoll(DateTime? since)
162 aa7ac00e Panagiotis Kanavos
        {
163 aa7ac00e Panagiotis Kanavos
            var sync = _syncEvent.WaitAsync();
164 aa7ac00e Panagiotis Kanavos
            var wait = TaskEx.Delay(TimeSpan.FromSeconds(Settings.PollingInterval), NetworkAgent.CancellationToken);
165 aa7ac00e Panagiotis Kanavos
            var signaledTask = await TaskEx.WhenAny(sync, wait);
166 aa7ac00e Panagiotis Kanavos
167 aa7ac00e Panagiotis Kanavos
            //Wait for network processing to finish before polling
168 38ac43a6 Panagiotis Kanavos
            var pauseTask=NetworkAgent.ProceedEvent.WaitAsync();
169 aa7ac00e Panagiotis Kanavos
            await TaskEx.WhenAll(signaledTask, pauseTask);
170 aa7ac00e Panagiotis Kanavos
171 aa7ac00e Panagiotis Kanavos
            //If polling is signalled by SynchNow, ignore the since tag
172 aa7ac00e Panagiotis Kanavos
            if (sync.IsCompleted)
173 aa7ac00e Panagiotis Kanavos
            {
174 aa7ac00e Panagiotis Kanavos
                //TODO: Must convert to AutoReset
175 aa7ac00e Panagiotis Kanavos
                _syncEvent.Reset();
176 aa7ac00e Panagiotis Kanavos
                return null;
177 aa7ac00e Panagiotis Kanavos
            }
178 aa7ac00e Panagiotis Kanavos
            return since;
179 aa7ac00e Panagiotis Kanavos
        }
180 aa7ac00e Panagiotis Kanavos
181 aa7ac00e Panagiotis Kanavos
        public async Task ProcessAccountFiles(AccountInfo accountInfo, DateTime? since = null)
182 aa7ac00e Panagiotis Kanavos
        {
183 aa7ac00e Panagiotis Kanavos
            if (accountInfo == null)
184 aa7ac00e Panagiotis Kanavos
                throw new ArgumentNullException("accountInfo");
185 aa7ac00e Panagiotis Kanavos
            if (String.IsNullOrWhiteSpace(accountInfo.AccountPath))
186 aa7ac00e Panagiotis Kanavos
                throw new ArgumentException("The AccountInfo.AccountPath is empty", "accountInfo");
187 aa7ac00e Panagiotis Kanavos
            Contract.EndContractBlock();
188 aa7ac00e Panagiotis Kanavos
189 aa7ac00e Panagiotis Kanavos
190 aa7ac00e Panagiotis Kanavos
            using (log4net.ThreadContext.Stacks["Retrieve Remote"].Push(accountInfo.UserName))
191 aa7ac00e Panagiotis Kanavos
            {
192 aa7ac00e Panagiotis Kanavos
                await NetworkAgent.GetDeleteAwaiter();
193 aa7ac00e Panagiotis Kanavos
194 aa7ac00e Panagiotis Kanavos
                Log.Info("Scheduled");
195 aa7ac00e Panagiotis Kanavos
                var client = new CloudFilesClient(accountInfo);
196 aa7ac00e Panagiotis Kanavos
197 99e6329f Panagiotis Kanavos
                //We don't need to check the trash container
198 99e6329f Panagiotis Kanavos
                var containers = client.ListContainers(accountInfo.UserName).Where(c=>c.Name!="trash");
199 aa7ac00e Panagiotis Kanavos
200 aa7ac00e Panagiotis Kanavos
201 aa7ac00e Panagiotis Kanavos
                CreateContainerFolders(accountInfo, containers);
202 aa7ac00e Panagiotis Kanavos
203 aa7ac00e Panagiotis Kanavos
                try
204 aa7ac00e Panagiotis Kanavos
                {
205 759bd3c4 Panagiotis Kanavos
                    //Wait for any deletions to finish
206 aa7ac00e Panagiotis Kanavos
                    await NetworkAgent.GetDeleteAwaiter();
207 aa7ac00e Panagiotis Kanavos
                    //Get the poll time now. We may miss some deletions but it's better to keep a file that was deleted
208 aa7ac00e Panagiotis Kanavos
                    //than delete a file that was created while we were executing the poll                    
209 aa7ac00e Panagiotis Kanavos
                    var pollTime = DateTime.Now;
210 aa7ac00e Panagiotis Kanavos
211 aa7ac00e Panagiotis Kanavos
                    //Get the list of server objects changed since the last check
212 aa7ac00e Panagiotis Kanavos
                    //The name of the container is passed as state in order to create a dictionary of tasks in a subsequent step
213 aa7ac00e Panagiotis Kanavos
                    var listObjects = (from container in containers
214 aa7ac00e Panagiotis Kanavos
                                       select Task<IList<ObjectInfo>>.Factory.StartNew(_ =>
215 aa7ac00e Panagiotis Kanavos
                                             client.ListObjects(accountInfo.UserName, container.Name, since), container.Name)).ToList();
216 126f90b3 Panagiotis Kanavos
                    //BUG: Can't detect difference between no changes or no objects
217 126f90b3 Panagiotis Kanavos
                    //ListObjects returns nothing if there are no changes since the last check time (since value)                    
218 99e6329f Panagiotis Kanavos
                    //TODO: Must detect the difference between no server objects and no change
219 99e6329f Panagiotis Kanavos
220 99e6329f Panagiotis Kanavos
                    //NOTE: One option is to "mark" all result lists with their container name, or 
221 99e6329f Panagiotis Kanavos
                    //rather the url of the container
222 99e6329f Panagiotis Kanavos
                    //Another option 
223 99e6329f Panagiotis Kanavos
224 99e6329f Panagiotis Kanavos
                    var listShared = Task<IList<ObjectInfo>>.Factory.StartNew(_ => 
225 99e6329f Panagiotis Kanavos
                        client.ListSharedObjects(since), "shared");
226 aa7ac00e Panagiotis Kanavos
                    listObjects.Add(listShared);
227 aa7ac00e Panagiotis Kanavos
                    var listTasks = await Task.Factory.WhenAll(listObjects.ToArray());
228 aa7ac00e Panagiotis Kanavos
229 aa7ac00e Panagiotis Kanavos
                    using (log4net.ThreadContext.Stacks["SCHEDULE"].Push("Process Results"))
230 aa7ac00e Panagiotis Kanavos
                    {
231 aa7ac00e Panagiotis Kanavos
                        var dict = listTasks.ToDictionary(t => t.AsyncState);
232 aa7ac00e Panagiotis Kanavos
233 aa7ac00e Panagiotis Kanavos
                        //Get all non-trash objects. Remember, the container name is stored in AsyncState
234 aa7ac00e Panagiotis Kanavos
                        var remoteObjects = from objectList in listTasks
235 aa7ac00e Panagiotis Kanavos
                                            where (string)objectList.AsyncState != "trash"
236 aa7ac00e Panagiotis Kanavos
                                            from obj in objectList.Result
237 aa7ac00e Panagiotis Kanavos
                                            select obj;
238 aa7ac00e Panagiotis Kanavos
239 aa7ac00e Panagiotis Kanavos
                        var sharedObjects = dict["shared"].Result;
240 aa7ac00e Panagiotis Kanavos
241 aa7ac00e Panagiotis Kanavos
                        //DON'T process trashed files
242 aa7ac00e Panagiotis Kanavos
                        //If some files are deleted and added again to a folder, they will be deleted
243 aa7ac00e Panagiotis Kanavos
                        //even though they are new.
244 aa7ac00e Panagiotis Kanavos
                        //We would have to check file dates and hashes to ensure that a trashed file
245 aa7ac00e Panagiotis Kanavos
                        //can be deleted safely from the local hard drive.
246 aa7ac00e Panagiotis Kanavos
                        /*
247 aa7ac00e Panagiotis Kanavos
                        //Items with the same name, hash may be both in the container and the trash
248 aa7ac00e Panagiotis Kanavos
                        //Don't delete items that exist in the container
249 aa7ac00e Panagiotis Kanavos
                        var realTrash = from trash in trashObjects
250 aa7ac00e Panagiotis Kanavos
                                        where
251 aa7ac00e Panagiotis Kanavos
                                            !remoteObjects.Any(
252 aa7ac00e Panagiotis Kanavos
                                                info => info.Name == trash.Name && info.Hash == trash.Hash)
253 aa7ac00e Panagiotis Kanavos
                                        select trash;
254 aa7ac00e Panagiotis Kanavos
                        ProcessTrashedFiles(accountInfo, realTrash);
255 aa7ac00e Panagiotis Kanavos
*/
256 aa7ac00e Panagiotis Kanavos
257 aa7ac00e Panagiotis Kanavos
                        var cleanRemotes = (from info in remoteObjects.Union(sharedObjects)
258 92f18b56 Panagiotis Kanavos
                                            let name = info.Name??""
259 aa7ac00e Panagiotis Kanavos
                                            where !name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase) &&
260 aa7ac00e Panagiotis Kanavos
                                                  !name.StartsWith(FolderConstants.CacheFolder + "/",
261 aa7ac00e Panagiotis Kanavos
                                                                   StringComparison.InvariantCultureIgnoreCase)
262 aa7ac00e Panagiotis Kanavos
                                            select info).ToList();
263 aa7ac00e Panagiotis Kanavos
264 aa7ac00e Panagiotis Kanavos
                        var differencer = _differencer.PostSnapshot(accountInfo, cleanRemotes);
265 aa7ac00e Panagiotis Kanavos
266 fec5da06 Panagiotis Kanavos
                        ProcessDeletedFiles(accountInfo, differencer.Deleted.FilterDirectlyBelow(SelectiveUris), pollTime);
267 759bd3c4 Panagiotis Kanavos
268 759bd3c4 Panagiotis Kanavos
                        // @@@ NEED To add previous state here as well, To compare with previous hash
269 759bd3c4 Panagiotis Kanavos
270 759bd3c4 Panagiotis Kanavos
                        
271 aa7ac00e Panagiotis Kanavos
272 aa7ac00e Panagiotis Kanavos
                        //Create a list of actions from the remote files
273 b666b39a Panagiotis Kanavos
                        var allActions = MovesToActions(accountInfo,differencer.Moved.FilterDirectlyBelow(SelectiveUris))
274 b666b39a Panagiotis Kanavos
                                        .Union(
275 b666b39a Panagiotis Kanavos
                                        ChangesToActions(accountInfo, differencer.Changed.FilterDirectlyBelow(SelectiveUris)))
276 aa7ac00e Panagiotis Kanavos
                                        .Union(
277 fec5da06 Panagiotis Kanavos
                                        CreatesToActions(accountInfo, differencer.Created.FilterDirectlyBelow(SelectiveUris)));
278 aa7ac00e Panagiotis Kanavos
279 aa7ac00e Panagiotis Kanavos
                        //And remove those that are already being processed by the agent
280 aa7ac00e Panagiotis Kanavos
                        var distinctActions = allActions
281 aa7ac00e Panagiotis Kanavos
                            .Except(NetworkAgent.GetEnumerable(), new PithosMonitor.LocalFileComparer())
282 aa7ac00e Panagiotis Kanavos
                            .ToList();
283 aa7ac00e Panagiotis Kanavos
284 aa7ac00e Panagiotis Kanavos
                        //Queue all the actions
285 aa7ac00e Panagiotis Kanavos
                        foreach (var message in distinctActions)
286 aa7ac00e Panagiotis Kanavos
                        {
287 aa7ac00e Panagiotis Kanavos
                            NetworkAgent.Post(message);
288 aa7ac00e Panagiotis Kanavos
                        }
289 aa7ac00e Panagiotis Kanavos
290 aa7ac00e Panagiotis Kanavos
                        Log.Info("[LISTENER] End Processing");
291 aa7ac00e Panagiotis Kanavos
                    }
292 aa7ac00e Panagiotis Kanavos
                }
293 aa7ac00e Panagiotis Kanavos
                catch (Exception ex)
294 aa7ac00e Panagiotis Kanavos
                {
295 aa7ac00e Panagiotis Kanavos
                    Log.ErrorFormat("[FAIL] ListObjects for{0} in ProcessRemoteFiles with {1}", accountInfo.UserName, ex);
296 aa7ac00e Panagiotis Kanavos
                    return;
297 aa7ac00e Panagiotis Kanavos
                }
298 aa7ac00e Panagiotis Kanavos
299 aa7ac00e Panagiotis Kanavos
                Log.Info("[LISTENER] Finished");
300 aa7ac00e Panagiotis Kanavos
301 aa7ac00e Panagiotis Kanavos
            }
302 aa7ac00e Panagiotis Kanavos
        }
303 aa7ac00e Panagiotis Kanavos
304 aa7ac00e Panagiotis Kanavos
        AccountsDifferencer _differencer = new AccountsDifferencer();
305 759bd3c4 Panagiotis Kanavos
        private List<Uri> _selectiveUris=new List<Uri>();
306 aa7ac00e Panagiotis Kanavos
307 aa7ac00e Panagiotis Kanavos
        /// <summary>
308 aa7ac00e Panagiotis Kanavos
        /// Deletes local files that are not found in the list of cloud files
309 aa7ac00e Panagiotis Kanavos
        /// </summary>
310 aa7ac00e Panagiotis Kanavos
        /// <param name="accountInfo"></param>
311 aa7ac00e Panagiotis Kanavos
        /// <param name="cloudFiles"></param>
312 aa7ac00e Panagiotis Kanavos
        /// <param name="pollTime"></param>
313 aa7ac00e Panagiotis Kanavos
        private void ProcessDeletedFiles(AccountInfo accountInfo, IEnumerable<ObjectInfo> cloudFiles, DateTime pollTime)
314 aa7ac00e Panagiotis Kanavos
        {
315 aa7ac00e Panagiotis Kanavos
            if (accountInfo == null)
316 aa7ac00e Panagiotis Kanavos
                throw new ArgumentNullException("accountInfo");
317 aa7ac00e Panagiotis Kanavos
            if (String.IsNullOrWhiteSpace(accountInfo.AccountPath))
318 aa7ac00e Panagiotis Kanavos
                throw new ArgumentException("The AccountInfo.AccountPath is empty", "accountInfo");
319 aa7ac00e Panagiotis Kanavos
            if (cloudFiles == null)
320 aa7ac00e Panagiotis Kanavos
                throw new ArgumentNullException("cloudFiles");
321 aa7ac00e Panagiotis Kanavos
            Contract.EndContractBlock();
322 aa7ac00e Panagiotis Kanavos
323 aa7ac00e Panagiotis Kanavos
            //On the first run
324 aa7ac00e Panagiotis Kanavos
            if (_firstPoll)
325 aa7ac00e Panagiotis Kanavos
            {
326 aa7ac00e Panagiotis Kanavos
                //Only consider files that are not being modified, ie they are in the Unchanged state            
327 aa7ac00e Panagiotis Kanavos
                var deleteCandidates = FileState.Queryable.Where(state =>
328 aa7ac00e Panagiotis Kanavos
                    state.FilePath.StartsWith(accountInfo.AccountPath)
329 aa7ac00e Panagiotis Kanavos
                    && state.FileStatus == FileStatus.Unchanged).ToList();
330 aa7ac00e Panagiotis Kanavos
331 aa7ac00e Panagiotis Kanavos
332 aa7ac00e Panagiotis Kanavos
                //TODO: filesToDelete must take into account the Others container            
333 aa7ac00e Panagiotis Kanavos
                var filesToDelete = (from deleteCandidate in deleteCandidates
334 aa7ac00e Panagiotis Kanavos
                                     let localFile = FileInfoExtensions.FromPath(deleteCandidate.FilePath)
335 aa7ac00e Panagiotis Kanavos
                                     let relativeFilePath = localFile.AsRelativeTo(accountInfo.AccountPath)
336 aa7ac00e Panagiotis Kanavos
                                     where
337 aa7ac00e Panagiotis Kanavos
                                         !cloudFiles.Any(r => r.RelativeUrlToFilePath(accountInfo.UserName) == relativeFilePath)
338 aa7ac00e Panagiotis Kanavos
                                     select localFile).ToList();
339 aa7ac00e Panagiotis Kanavos
340 aa7ac00e Panagiotis Kanavos
341 aa7ac00e Panagiotis Kanavos
342 aa7ac00e Panagiotis Kanavos
                //Set the status of missing files to Conflict
343 aa7ac00e Panagiotis Kanavos
                foreach (var item in filesToDelete)
344 aa7ac00e Panagiotis Kanavos
                {
345 aa7ac00e Panagiotis Kanavos
                    //Try to acquire a gate on the file, to take into account files that have been dequeued
346 aa7ac00e Panagiotis Kanavos
                    //and are being processed
347 aa7ac00e Panagiotis Kanavos
                    using (var gate = NetworkGate.Acquire(item.FullName, NetworkOperation.Deleting))
348 aa7ac00e Panagiotis Kanavos
                    {
349 aa7ac00e Panagiotis Kanavos
                        if (gate.Failed)
350 aa7ac00e Panagiotis Kanavos
                            continue;
351 aa7ac00e Panagiotis Kanavos
                        StatusKeeper.SetFileState(item.FullName, FileStatus.Conflict, FileOverlayStatus.Deleted);
352 aa7ac00e Panagiotis Kanavos
                    }
353 aa7ac00e Panagiotis Kanavos
                }
354 aa7ac00e Panagiotis Kanavos
                UpdateStatus(PithosStatus.HasConflicts);
355 aa7ac00e Panagiotis Kanavos
                StatusNotification.NotifyConflicts(filesToDelete, String.Format("{0} local files are missing from Pithos, possibly because they were deleted", filesToDelete.Count));
356 aa7ac00e Panagiotis Kanavos
                StatusNotification.NotifyForFiles(filesToDelete, String.Format("{0} files were deleted", filesToDelete.Count), TraceLevel.Info);
357 aa7ac00e Panagiotis Kanavos
            }
358 aa7ac00e Panagiotis Kanavos
            else
359 aa7ac00e Panagiotis Kanavos
            {
360 aa7ac00e Panagiotis Kanavos
                var deletedFiles = new List<FileSystemInfo>();
361 aa7ac00e Panagiotis Kanavos
                foreach (var objectInfo in cloudFiles)
362 aa7ac00e Panagiotis Kanavos
                {
363 aa7ac00e Panagiotis Kanavos
                    var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
364 fbbbe99b Panagiotis Kanavos
                    var item = FileAgent.GetFileAgent(accountInfo).GetFileSystemInfo(relativePath);
365 aa7ac00e Panagiotis Kanavos
                    if (item.Exists)
366 aa7ac00e Panagiotis Kanavos
                    {
367 aa7ac00e Panagiotis Kanavos
                        if ((item.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
368 aa7ac00e Panagiotis Kanavos
                        {
369 aa7ac00e Panagiotis Kanavos
                            item.Attributes = item.Attributes & ~FileAttributes.ReadOnly;
370 aa7ac00e Panagiotis Kanavos
371 aa7ac00e Panagiotis Kanavos
                        }
372 aa7ac00e Panagiotis Kanavos
                        item.Delete();
373 aa7ac00e Panagiotis Kanavos
                        DateTime lastDate;
374 aa7ac00e Panagiotis Kanavos
                        _lastSeen.TryRemove(item.FullName, out lastDate);
375 aa7ac00e Panagiotis Kanavos
                        deletedFiles.Add(item);
376 aa7ac00e Panagiotis Kanavos
                    }
377 aa7ac00e Panagiotis Kanavos
                    StatusKeeper.SetFileState(item.FullName, FileStatus.Deleted, FileOverlayStatus.Deleted);
378 aa7ac00e Panagiotis Kanavos
                }
379 aa7ac00e Panagiotis Kanavos
                StatusNotification.NotifyForFiles(deletedFiles, String.Format("{0} files were deleted", deletedFiles.Count), TraceLevel.Info);
380 aa7ac00e Panagiotis Kanavos
            }
381 aa7ac00e Panagiotis Kanavos
382 aa7ac00e Panagiotis Kanavos
        }
383 aa7ac00e Panagiotis Kanavos
384 b666b39a Panagiotis Kanavos
        /// <summary>
385 b666b39a Panagiotis Kanavos
        /// Creates a Sync action for each changed server file
386 b666b39a Panagiotis Kanavos
        /// </summary>
387 b666b39a Panagiotis Kanavos
        /// <param name="accountInfo"></param>
388 b666b39a Panagiotis Kanavos
        /// <param name="changes"></param>
389 b666b39a Panagiotis Kanavos
        /// <returns></returns>
390 aa7ac00e Panagiotis Kanavos
        private IEnumerable<CloudAction> ChangesToActions(AccountInfo accountInfo, IEnumerable<ObjectInfo> changes)
391 aa7ac00e Panagiotis Kanavos
        {
392 aa7ac00e Panagiotis Kanavos
            if (changes == null)
393 aa7ac00e Panagiotis Kanavos
                throw new ArgumentNullException();
394 aa7ac00e Panagiotis Kanavos
            Contract.EndContractBlock();
395 fbbbe99b Panagiotis Kanavos
            var fileAgent = FileAgent.GetFileAgent(accountInfo);
396 aa7ac00e Panagiotis Kanavos
397 aa7ac00e Panagiotis Kanavos
            //In order to avoid multiple iterations over the files, we iterate only once
398 aa7ac00e Panagiotis Kanavos
            //over the remote files
399 aa7ac00e Panagiotis Kanavos
            foreach (var objectInfo in changes)
400 aa7ac00e Panagiotis Kanavos
            {
401 aa7ac00e Panagiotis Kanavos
                var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
402 b666b39a Panagiotis Kanavos
                //If a directory object already exists, we may need to sync it
403 aa7ac00e Panagiotis Kanavos
                if (fileAgent.Exists(relativePath))
404 aa7ac00e Panagiotis Kanavos
                {
405 aa7ac00e Panagiotis Kanavos
                    var localFile = fileAgent.GetFileSystemInfo(relativePath);
406 b666b39a Panagiotis Kanavos
                    //We don't need to sync directories
407 aa7ac00e Panagiotis Kanavos
                    if (objectInfo.Content_Type == @"application/directory" && localFile is DirectoryInfo)
408 aa7ac00e Panagiotis Kanavos
                        continue;
409 aa7ac00e Panagiotis Kanavos
                    using (new SessionScope(FlushAction.Never))
410 aa7ac00e Panagiotis Kanavos
                    {
411 aa7ac00e Panagiotis Kanavos
                        var state = StatusKeeper.GetStateByFilePath(localFile.FullName);
412 aa7ac00e Panagiotis Kanavos
                        _lastSeen[localFile.FullName] = DateTime.Now;
413 aa7ac00e Panagiotis Kanavos
                        //Common files should be checked on a per-case basis to detect differences, which is newer
414 aa7ac00e Panagiotis Kanavos
415 aa7ac00e Panagiotis Kanavos
                        yield return new CloudAction(accountInfo, CloudActionType.MustSynch,
416 aa7ac00e Panagiotis Kanavos
                                                     localFile, objectInfo, state, accountInfo.BlockSize,
417 aa7ac00e Panagiotis Kanavos
                                                     accountInfo.BlockHash);
418 aa7ac00e Panagiotis Kanavos
                    }
419 aa7ac00e Panagiotis Kanavos
                }
420 aa7ac00e Panagiotis Kanavos
                else
421 aa7ac00e Panagiotis Kanavos
                {
422 aa7ac00e Panagiotis Kanavos
                    //Remote files should be downloaded
423 aa7ac00e Panagiotis Kanavos
                    yield return new CloudDownloadAction(accountInfo, objectInfo);
424 aa7ac00e Panagiotis Kanavos
                }
425 aa7ac00e Panagiotis Kanavos
            }
426 aa7ac00e Panagiotis Kanavos
        }
427 aa7ac00e Panagiotis Kanavos
428 b666b39a Panagiotis Kanavos
        /// <summary>
429 b666b39a Panagiotis Kanavos
        /// Creates a Local Move action for each moved server file
430 b666b39a Panagiotis Kanavos
        /// </summary>
431 b666b39a Panagiotis Kanavos
        /// <param name="accountInfo"></param>
432 b666b39a Panagiotis Kanavos
        /// <param name="moves"></param>
433 b666b39a Panagiotis Kanavos
        /// <returns></returns>
434 b666b39a Panagiotis Kanavos
        private IEnumerable<CloudAction> MovesToActions(AccountInfo accountInfo, IEnumerable<ObjectInfo> moves)
435 b666b39a Panagiotis Kanavos
        {
436 b666b39a Panagiotis Kanavos
            if (moves == null)
437 b666b39a Panagiotis Kanavos
                throw new ArgumentNullException();
438 b666b39a Panagiotis Kanavos
            Contract.EndContractBlock();
439 b666b39a Panagiotis Kanavos
            var fileAgent = FileAgent.GetFileAgent(accountInfo);
440 b666b39a Panagiotis Kanavos
441 b666b39a Panagiotis Kanavos
            //In order to avoid multiple iterations over the files, we iterate only once
442 b666b39a Panagiotis Kanavos
            //over the remote files
443 b666b39a Panagiotis Kanavos
            foreach (var objectInfo in moves)
444 b666b39a Panagiotis Kanavos
            {
445 b666b39a Panagiotis Kanavos
                var previousRelativepath = objectInfo.Previous.RelativeUrlToFilePath(accountInfo.UserName);
446 b666b39a Panagiotis Kanavos
                //If the previous file already exists, we can execute a Move operation
447 b666b39a Panagiotis Kanavos
                if (fileAgent.Exists(previousRelativepath))
448 b666b39a Panagiotis Kanavos
                {
449 b666b39a Panagiotis Kanavos
                    var previousFile = fileAgent.GetFileSystemInfo(previousRelativepath);
450 b666b39a Panagiotis Kanavos
                    using (new SessionScope(FlushAction.Never))
451 b666b39a Panagiotis Kanavos
                    {
452 b666b39a Panagiotis Kanavos
                        var state = StatusKeeper.GetStateByFilePath(previousFile.FullName);
453 b666b39a Panagiotis Kanavos
                        _lastSeen[previousFile.FullName] = DateTime.Now;
454 b666b39a Panagiotis Kanavos
455 b666b39a Panagiotis Kanavos
                        //For each moved object we need to move both the local file and update                                                
456 b666b39a Panagiotis Kanavos
                        yield return new CloudAction(accountInfo, CloudActionType.RenameLocal,
457 b666b39a Panagiotis Kanavos
                                                     previousFile, objectInfo, state, accountInfo.BlockSize,
458 b666b39a Panagiotis Kanavos
                                                     accountInfo.BlockHash);
459 b666b39a Panagiotis Kanavos
                        //For modified files, we need to download the changes as well
460 b666b39a Panagiotis Kanavos
                        if (objectInfo.Hash!=objectInfo.PreviousHash)
461 b666b39a Panagiotis Kanavos
                            yield return new CloudDownloadAction(accountInfo,objectInfo);
462 b666b39a Panagiotis Kanavos
                    }
463 b666b39a Panagiotis Kanavos
                }
464 b666b39a Panagiotis Kanavos
                //If the previous file does not exist, we need to download it in the new location
465 b666b39a Panagiotis Kanavos
                else
466 b666b39a Panagiotis Kanavos
                {
467 b666b39a Panagiotis Kanavos
                    //Remote files should be downloaded
468 b666b39a Panagiotis Kanavos
                    yield return new CloudDownloadAction(accountInfo, objectInfo);
469 b666b39a Panagiotis Kanavos
                }
470 b666b39a Panagiotis Kanavos
            }
471 b666b39a Panagiotis Kanavos
        }
472 b666b39a Panagiotis Kanavos
473 b666b39a Panagiotis Kanavos
474 b666b39a Panagiotis Kanavos
        /// <summary>
475 b666b39a Panagiotis Kanavos
        /// Creates a download action for each new server file
476 b666b39a Panagiotis Kanavos
        /// </summary>
477 b666b39a Panagiotis Kanavos
        /// <param name="accountInfo"></param>
478 b666b39a Panagiotis Kanavos
        /// <param name="creates"></param>
479 b666b39a Panagiotis Kanavos
        /// <returns></returns>
480 aa7ac00e Panagiotis Kanavos
        private IEnumerable<CloudAction> CreatesToActions(AccountInfo accountInfo, IEnumerable<ObjectInfo> creates)
481 aa7ac00e Panagiotis Kanavos
        {
482 aa7ac00e Panagiotis Kanavos
            if (creates == null)
483 aa7ac00e Panagiotis Kanavos
                throw new ArgumentNullException();
484 aa7ac00e Panagiotis Kanavos
            Contract.EndContractBlock();
485 fbbbe99b Panagiotis Kanavos
            var fileAgent = FileAgent.GetFileAgent(accountInfo);
486 aa7ac00e Panagiotis Kanavos
487 aa7ac00e Panagiotis Kanavos
            //In order to avoid multiple iterations over the files, we iterate only once
488 aa7ac00e Panagiotis Kanavos
            //over the remote files
489 aa7ac00e Panagiotis Kanavos
            foreach (var objectInfo in creates)
490 aa7ac00e Panagiotis Kanavos
            {
491 aa7ac00e Panagiotis Kanavos
                var relativePath = objectInfo.RelativeUrlToFilePath(accountInfo.UserName);
492 b666b39a Panagiotis Kanavos
                //If the object already exists, we probably have a conflict
493 aa7ac00e Panagiotis Kanavos
                if (fileAgent.Exists(relativePath))
494 aa7ac00e Panagiotis Kanavos
                {
495 aa7ac00e Panagiotis Kanavos
                    //If a directory object already exists, we don't need to perform any other action                    
496 aa7ac00e Panagiotis Kanavos
                    var localFile = fileAgent.GetFileSystemInfo(relativePath);
497 aa7ac00e Panagiotis Kanavos
                    StatusKeeper.SetFileState(localFile.FullName, FileStatus.Conflict, FileOverlayStatus.Conflict);
498 aa7ac00e Panagiotis Kanavos
                }
499 aa7ac00e Panagiotis Kanavos
                else
500 aa7ac00e Panagiotis Kanavos
                {
501 aa7ac00e Panagiotis Kanavos
                    //Remote files should be downloaded
502 aa7ac00e Panagiotis Kanavos
                    yield return new CloudDownloadAction(accountInfo, objectInfo);
503 aa7ac00e Panagiotis Kanavos
                }
504 aa7ac00e Panagiotis Kanavos
            }
505 aa7ac00e Panagiotis Kanavos
        }
506 aa7ac00e Panagiotis Kanavos
507 aa7ac00e Panagiotis Kanavos
        private void ProcessTrashedFiles(AccountInfo accountInfo, IEnumerable<ObjectInfo> trashObjects)
508 aa7ac00e Panagiotis Kanavos
        {
509 fbbbe99b Panagiotis Kanavos
            var fileAgent = FileAgent.GetFileAgent(accountInfo);
510 aa7ac00e Panagiotis Kanavos
            foreach (var trashObject in trashObjects)
511 aa7ac00e Panagiotis Kanavos
            {
512 aa7ac00e Panagiotis Kanavos
                var barePath = trashObject.RelativeUrlToFilePath(accountInfo.UserName);
513 aa7ac00e Panagiotis Kanavos
                //HACK: Assume only the "pithos" container is used. Must find out what happens when
514 aa7ac00e Panagiotis Kanavos
                //deleting a file from a different container
515 aa7ac00e Panagiotis Kanavos
                var relativePath = Path.Combine("pithos", barePath);
516 aa7ac00e Panagiotis Kanavos
                fileAgent.Delete(relativePath);
517 aa7ac00e Panagiotis Kanavos
            }
518 aa7ac00e Panagiotis Kanavos
        }
519 aa7ac00e Panagiotis Kanavos
520 fec5da06 Panagiotis Kanavos
        /// <summary>
521 fec5da06 Panagiotis Kanavos
        /// Notify the UI to update the visual status
522 fec5da06 Panagiotis Kanavos
        /// </summary>
523 fec5da06 Panagiotis Kanavos
        /// <param name="status"></param>
524 aa7ac00e Panagiotis Kanavos
        private void UpdateStatus(PithosStatus status)
525 aa7ac00e Panagiotis Kanavos
        {
526 fec5da06 Panagiotis Kanavos
            try
527 fec5da06 Panagiotis Kanavos
            {
528 fec5da06 Panagiotis Kanavos
                StatusKeeper.SetPithosStatus(status);
529 fec5da06 Panagiotis Kanavos
                StatusNotification.Notify(new Notification());
530 fec5da06 Panagiotis Kanavos
            }
531 fec5da06 Panagiotis Kanavos
            catch (Exception exc)
532 fec5da06 Panagiotis Kanavos
            {
533 fec5da06 Panagiotis Kanavos
                //Failure is not critical, just log it
534 fec5da06 Panagiotis Kanavos
                Log.Warn("Error while updating status", exc);
535 fec5da06 Panagiotis Kanavos
            }
536 aa7ac00e Panagiotis Kanavos
        }
537 aa7ac00e Panagiotis Kanavos
538 aa7ac00e Panagiotis Kanavos
        private static void CreateContainerFolders(AccountInfo accountInfo, IEnumerable<ContainerInfo> containers)
539 aa7ac00e Panagiotis Kanavos
        {
540 aa7ac00e Panagiotis Kanavos
            var containerPaths = from container in containers
541 aa7ac00e Panagiotis Kanavos
                                 let containerPath = Path.Combine(accountInfo.AccountPath, container.Name)
542 aa7ac00e Panagiotis Kanavos
                                 where container.Name != FolderConstants.TrashContainer && !Directory.Exists(containerPath)
543 aa7ac00e Panagiotis Kanavos
                                 select containerPath;
544 aa7ac00e Panagiotis Kanavos
545 aa7ac00e Panagiotis Kanavos
            foreach (var path in containerPaths)
546 aa7ac00e Panagiotis Kanavos
            {
547 aa7ac00e Panagiotis Kanavos
                Directory.CreateDirectory(path);
548 aa7ac00e Panagiotis Kanavos
            }
549 aa7ac00e Panagiotis Kanavos
        }
550 759bd3c4 Panagiotis Kanavos
551 fec5da06 Panagiotis Kanavos
        public void SetSyncUris(Uri[] uris)
552 fec5da06 Panagiotis Kanavos
        {            
553 fec5da06 Panagiotis Kanavos
            SelectiveUris=uris.ToList();
554 759bd3c4 Panagiotis Kanavos
        }
555 759bd3c4 Panagiotis Kanavos
556 759bd3c4 Panagiotis Kanavos
        protected List<Uri> SelectiveUris
557 759bd3c4 Panagiotis Kanavos
        {
558 759bd3c4 Panagiotis Kanavos
            get { return _selectiveUris;}
559 759bd3c4 Panagiotis Kanavos
            set { _selectiveUris = value; }
560 759bd3c4 Panagiotis Kanavos
        }
561 fec5da06 Panagiotis Kanavos
562 fec5da06 Panagiotis Kanavos
        public void AddAccount(AccountInfo accountInfo)
563 fec5da06 Panagiotis Kanavos
        {
564 fec5da06 Panagiotis Kanavos
            if (!_accounts.Contains(accountInfo))
565 fec5da06 Panagiotis Kanavos
                _accounts.Add(accountInfo);
566 fec5da06 Panagiotis Kanavos
        }
567 aa7ac00e Panagiotis Kanavos
    }
568 aa7ac00e Panagiotis Kanavos
}