root / trunk / Pithos.Core / Agents / FileSystemWatcherAdapter.cs @ b666b39a
History | View | Annotate | Download (11.4 kB)
1 | 255f5f86 | Panagiotis Kanavos | #region |
---|---|---|---|
2 | 255f5f86 | Panagiotis Kanavos | /* ----------------------------------------------------------------------- |
3 | 255f5f86 | Panagiotis Kanavos | * <copyright file="FileSystemWatcherAdapter.cs" company="GRNet"> |
4 | 255f5f86 | Panagiotis Kanavos | * |
5 | 255f5f86 | Panagiotis Kanavos | * Copyright 2011-2012 GRNET S.A. All rights reserved. |
6 | 255f5f86 | Panagiotis Kanavos | * |
7 | 255f5f86 | Panagiotis Kanavos | * Redistribution and use in source and binary forms, with or |
8 | 255f5f86 | Panagiotis Kanavos | * without modification, are permitted provided that the following |
9 | 255f5f86 | Panagiotis Kanavos | * conditions are met: |
10 | 255f5f86 | Panagiotis Kanavos | * |
11 | 255f5f86 | Panagiotis Kanavos | * 1. Redistributions of source code must retain the above |
12 | 255f5f86 | Panagiotis Kanavos | * copyright notice, this list of conditions and the following |
13 | 255f5f86 | Panagiotis Kanavos | * disclaimer. |
14 | 255f5f86 | Panagiotis Kanavos | * |
15 | 255f5f86 | Panagiotis Kanavos | * 2. Redistributions in binary form must reproduce the above |
16 | 255f5f86 | Panagiotis Kanavos | * copyright notice, this list of conditions and the following |
17 | 255f5f86 | Panagiotis Kanavos | * disclaimer in the documentation and/or other materials |
18 | 255f5f86 | Panagiotis Kanavos | * provided with the distribution. |
19 | 255f5f86 | Panagiotis Kanavos | * |
20 | 255f5f86 | Panagiotis Kanavos | * |
21 | 255f5f86 | Panagiotis Kanavos | * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
22 | 255f5f86 | Panagiotis Kanavos | * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
23 | 255f5f86 | Panagiotis Kanavos | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
24 | 255f5f86 | Panagiotis Kanavos | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
25 | 255f5f86 | Panagiotis Kanavos | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
26 | 255f5f86 | Panagiotis Kanavos | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
27 | 255f5f86 | Panagiotis Kanavos | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
28 | 255f5f86 | Panagiotis Kanavos | * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
29 | 255f5f86 | Panagiotis Kanavos | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
30 | 255f5f86 | Panagiotis Kanavos | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
31 | 255f5f86 | Panagiotis Kanavos | * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
32 | 255f5f86 | Panagiotis Kanavos | * POSSIBILITY OF SUCH DAMAGE. |
33 | 255f5f86 | Panagiotis Kanavos | * |
34 | 255f5f86 | Panagiotis Kanavos | * The views and conclusions contained in the software and |
35 | 255f5f86 | Panagiotis Kanavos | * documentation are those of the authors and should not be |
36 | 255f5f86 | Panagiotis Kanavos | * interpreted as representing official policies, either expressed |
37 | 255f5f86 | Panagiotis Kanavos | * or implied, of GRNET S.A. |
38 | 255f5f86 | Panagiotis Kanavos | * </copyright> |
39 | 255f5f86 | Panagiotis Kanavos | * ----------------------------------------------------------------------- |
40 | 255f5f86 | Panagiotis Kanavos | */ |
41 | 255f5f86 | Panagiotis Kanavos | #endregion |
42 | 78ebfd2d | Panagiotis Kanavos | using System.Diagnostics.Contracts; |
43 | 78ebfd2d | Panagiotis Kanavos | using System.IO; |
44 | 78ebfd2d | Panagiotis Kanavos | using System.Threading.Tasks; |
45 | 139ac1e8 | Panagiotis Kanavos | using Pithos.Interfaces; |
46 | 78ebfd2d | Panagiotis Kanavos | |
47 | 78ebfd2d | Panagiotis Kanavos | namespace Pithos.Core.Agents |
48 | 78ebfd2d | Panagiotis Kanavos | { |
49 | 78ebfd2d | Panagiotis Kanavos | using System; |
50 | 78ebfd2d | Panagiotis Kanavos | using System.Collections.Generic; |
51 | 78ebfd2d | Panagiotis Kanavos | using System.Linq; |
52 | 78ebfd2d | Panagiotis Kanavos | using System.Text; |
53 | 78ebfd2d | Panagiotis Kanavos | |
54 | 78ebfd2d | Panagiotis Kanavos | /// <summary> |
55 | 78ebfd2d | Panagiotis Kanavos | /// TODO: Update summary. |
56 | 78ebfd2d | Panagiotis Kanavos | /// </summary> |
57 | 78ebfd2d | Panagiotis Kanavos | public class FileSystemWatcherAdapter |
58 | 78ebfd2d | Panagiotis Kanavos | { |
59 | 78ebfd2d | Panagiotis Kanavos | public event FileSystemEventHandler Changed; |
60 | 78ebfd2d | Panagiotis Kanavos | public event FileSystemEventHandler Created; |
61 | 78ebfd2d | Panagiotis Kanavos | public event FileSystemEventHandler Deleted; |
62 | 78ebfd2d | Panagiotis Kanavos | public event RenamedEventHandler Renamed; |
63 | 78ebfd2d | Panagiotis Kanavos | public event MovedEventHandler Moved; |
64 | 78ebfd2d | Panagiotis Kanavos | |
65 | 78ebfd2d | Panagiotis Kanavos | public FileSystemWatcherAdapter(FileSystemWatcher watcher) |
66 | 78ebfd2d | Panagiotis Kanavos | { |
67 | 78ebfd2d | Panagiotis Kanavos | if (watcher==null) |
68 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentNullException("watcher"); |
69 | 78ebfd2d | Panagiotis Kanavos | Contract.EndContractBlock(); |
70 | 78ebfd2d | Panagiotis Kanavos | |
71 | 78ebfd2d | Panagiotis Kanavos | watcher.Changed += OnChangeOrCreate; |
72 | 78ebfd2d | Panagiotis Kanavos | watcher.Created += OnChangeOrCreate; |
73 | 78ebfd2d | Panagiotis Kanavos | watcher.Deleted += OnDeleted; |
74 | 78ebfd2d | Panagiotis Kanavos | watcher.Renamed += OnRename; |
75 | 78ebfd2d | Panagiotis Kanavos | |
76 | 78ebfd2d | Panagiotis Kanavos | } |
77 | 78ebfd2d | Panagiotis Kanavos | |
78 | 78ebfd2d | Panagiotis Kanavos | private string _cachedDeletedFullPath; |
79 | 78ebfd2d | Panagiotis Kanavos | private const int PropagateDelay = 10; |
80 | 78ebfd2d | Panagiotis Kanavos | |
81 | 78ebfd2d | Panagiotis Kanavos | private static void OnTimeout(object state) |
82 | 78ebfd2d | Panagiotis Kanavos | { |
83 | 78ebfd2d | Panagiotis Kanavos | throw new NotImplementedException(); |
84 | 78ebfd2d | Panagiotis Kanavos | } |
85 | 78ebfd2d | Panagiotis Kanavos | |
86 | 78ebfd2d | Panagiotis Kanavos | private void OnDeleted(object sender, FileSystemEventArgs e) |
87 | 78ebfd2d | Panagiotis Kanavos | { |
88 | 78ebfd2d | Panagiotis Kanavos | if (sender == null) |
89 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentNullException("sender"); |
90 | 78ebfd2d | Panagiotis Kanavos | if (e.ChangeType != WatcherChangeTypes.Deleted) |
91 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentException("e"); |
92 | 78ebfd2d | Panagiotis Kanavos | if (string.IsNullOrWhiteSpace(e.FullPath)) |
93 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentException("e"); |
94 | 78ebfd2d | Panagiotis Kanavos | Contract.Ensures(!String.IsNullOrWhiteSpace(_cachedDeletedFullPath)); |
95 | 78ebfd2d | Panagiotis Kanavos | Contract.EndContractBlock(); |
96 | 78ebfd2d | Panagiotis Kanavos | |
97 | 78ebfd2d | Panagiotis Kanavos | //Handle any previously deleted event |
98 | 78ebfd2d | Panagiotis Kanavos | PropagateCachedDeleted(sender); |
99 | 78ebfd2d | Panagiotis Kanavos | |
100 | 78ebfd2d | Panagiotis Kanavos | //A delete event may be an actual delete event or the first event in a move action. |
101 | 78ebfd2d | Panagiotis Kanavos | //To decide which action occured, we need to wait for the next action, so |
102 | 78ebfd2d | Panagiotis Kanavos | //we store the file path and return . |
103 | 78ebfd2d | Panagiotis Kanavos | //A delete action will not be followed by any other event, so we need to add a watchdog |
104 | 78ebfd2d | Panagiotis Kanavos | //that will declare a Delete action after a short amount of time |
105 | 78ebfd2d | Panagiotis Kanavos | |
106 | 78ebfd2d | Panagiotis Kanavos | //TODO: Moving a folder to the recycle bin results in a single delete event for the entire folder and its contents |
107 | 78ebfd2d | Panagiotis Kanavos | // as this is actually a MOVE operation |
108 | 78ebfd2d | Panagiotis Kanavos | //Deleting by Shift+Delete results in a delete event for each file followed by the delete of the folder itself |
109 | 78ebfd2d | Panagiotis Kanavos | _cachedDeletedFullPath = e.FullPath; |
110 | 139ac1e8 | Panagiotis Kanavos | |
111 | 78ebfd2d | Panagiotis Kanavos | //TODO: This requires synchronization of the _cachedDeletedFullPath field |
112 | 78ebfd2d | Panagiotis Kanavos | //TODO: This creates a new task for each file even though we can cancel any existing tasks if a new event arrives |
113 | 78ebfd2d | Panagiotis Kanavos | //Maybe, use a timer instead of a task |
114 | 78ebfd2d | Panagiotis Kanavos | |
115 | 78ebfd2d | Panagiotis Kanavos | TaskEx.Delay(PropagateDelay).ContinueWith(t => |
116 | 78ebfd2d | Panagiotis Kanavos | { |
117 | 78ebfd2d | Panagiotis Kanavos | var myPath = e.FullPath; |
118 | 78ebfd2d | Panagiotis Kanavos | if (this._cachedDeletedFullPath==myPath) |
119 | 78ebfd2d | Panagiotis Kanavos | PropagateCachedDeleted(sender); |
120 | 78ebfd2d | Panagiotis Kanavos | }); |
121 | 78ebfd2d | Panagiotis Kanavos | } |
122 | 78ebfd2d | Panagiotis Kanavos | |
123 | 78ebfd2d | Panagiotis Kanavos | private void OnRename(object sender, RenamedEventArgs e) |
124 | 78ebfd2d | Panagiotis Kanavos | { |
125 | 78ebfd2d | Panagiotis Kanavos | if (sender == null) |
126 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentNullException("sender"); |
127 | 78ebfd2d | Panagiotis Kanavos | Contract.Ensures(_cachedDeletedFullPath == null); |
128 | 78ebfd2d | Panagiotis Kanavos | Contract.EndContractBlock(); |
129 | 78ebfd2d | Panagiotis Kanavos | |
130 | 78ebfd2d | Panagiotis Kanavos | try |
131 | 78ebfd2d | Panagiotis Kanavos | { |
132 | 78ebfd2d | Panagiotis Kanavos | //Propagate any previous cached delete event |
133 | 78ebfd2d | Panagiotis Kanavos | PropagateCachedDeleted(sender); |
134 | 78ebfd2d | Panagiotis Kanavos | |
135 | 78ebfd2d | Panagiotis Kanavos | if (Renamed!=null) |
136 | 78ebfd2d | Panagiotis Kanavos | Renamed(sender, e); |
137 | 78ebfd2d | Panagiotis Kanavos | |
138 | 78ebfd2d | Panagiotis Kanavos | } |
139 | 78ebfd2d | Panagiotis Kanavos | finally |
140 | 78ebfd2d | Panagiotis Kanavos | { |
141 | 78ebfd2d | Panagiotis Kanavos | _cachedDeletedFullPath = null; |
142 | 78ebfd2d | Panagiotis Kanavos | } |
143 | 78ebfd2d | Panagiotis Kanavos | } |
144 | 78ebfd2d | Panagiotis Kanavos | |
145 | 78ebfd2d | Panagiotis Kanavos | private void OnChangeOrCreate(object sender, FileSystemEventArgs e) |
146 | 78ebfd2d | Panagiotis Kanavos | { |
147 | 78ebfd2d | Panagiotis Kanavos | if (sender == null) |
148 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentNullException("sender"); |
149 | 78ebfd2d | Panagiotis Kanavos | if (!(e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed)) |
150 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentException("e"); |
151 | 78ebfd2d | Panagiotis Kanavos | Contract.Ensures(_cachedDeletedFullPath == null); |
152 | 78ebfd2d | Panagiotis Kanavos | Contract.EndContractBlock(); |
153 | 78ebfd2d | Panagiotis Kanavos | |
154 | 78ebfd2d | Panagiotis Kanavos | try |
155 | 78ebfd2d | Panagiotis Kanavos | { |
156 | 78ebfd2d | Panagiotis Kanavos | //A Move action results in a sequence of a Delete and a Create or Change event |
157 | 78ebfd2d | Panagiotis Kanavos | //If the actual action is a Move, raise a Move event instead of the actual event |
158 | 78ebfd2d | Panagiotis Kanavos | if (HandleMoved(sender, e)) |
159 | 78ebfd2d | Panagiotis Kanavos | return; |
160 | 78ebfd2d | Panagiotis Kanavos | |
161 | 78ebfd2d | Panagiotis Kanavos | //Otherwise, propagate the Delete event if it exists |
162 | 78ebfd2d | Panagiotis Kanavos | PropagateCachedDeleted(sender); |
163 | 78ebfd2d | Panagiotis Kanavos | //and propagate the actual event |
164 | 78ebfd2d | Panagiotis Kanavos | var actualEvent = e.ChangeType == WatcherChangeTypes.Created ? Created : Changed; |
165 | 78ebfd2d | Panagiotis Kanavos | if (actualEvent != null) |
166 | 78ebfd2d | Panagiotis Kanavos | actualEvent(sender, e); |
167 | 78ebfd2d | Panagiotis Kanavos | } |
168 | 78ebfd2d | Panagiotis Kanavos | finally |
169 | 78ebfd2d | Panagiotis Kanavos | { |
170 | 78ebfd2d | Panagiotis Kanavos | //Finally, make sure the cached path is cleared |
171 | 78ebfd2d | Panagiotis Kanavos | _cachedDeletedFullPath = null; |
172 | 78ebfd2d | Panagiotis Kanavos | } |
173 | 78ebfd2d | Panagiotis Kanavos | |
174 | 78ebfd2d | Panagiotis Kanavos | } |
175 | 78ebfd2d | Panagiotis Kanavos | |
176 | 78ebfd2d | Panagiotis Kanavos | private bool HandleMoved(object sender, FileSystemEventArgs e) |
177 | 78ebfd2d | Panagiotis Kanavos | { |
178 | 78ebfd2d | Panagiotis Kanavos | if (sender == null) |
179 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentNullException("sender"); |
180 | 78ebfd2d | Panagiotis Kanavos | if (!(e.ChangeType == WatcherChangeTypes.Created || |
181 | 78ebfd2d | Panagiotis Kanavos | e.ChangeType == WatcherChangeTypes.Changed)) |
182 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentException("e"); |
183 | 78ebfd2d | Panagiotis Kanavos | Contract.EndContractBlock(); |
184 | 78ebfd2d | Panagiotis Kanavos | |
185 | 78ebfd2d | Panagiotis Kanavos | //TODO: If a file is deleted and another file with the same name is created, it will be detected as a MOVE |
186 | 78ebfd2d | Panagiotis Kanavos | //instead of a sequence of independent actions |
187 | 78ebfd2d | Panagiotis Kanavos | //One way to detect this would be to request that the full paths are NOT the same |
188 | 78ebfd2d | Panagiotis Kanavos | |
189 | 78ebfd2d | Panagiotis Kanavos | var oldName = Path.GetFileName(_cachedDeletedFullPath); |
190 | 78ebfd2d | Panagiotis Kanavos | //NOTE: e.Name is a path relative to the watched path. We MUST call Path.GetFileName to get the actual path |
191 | 78ebfd2d | Panagiotis Kanavos | var newName = Path.GetFileName(e.Name); |
192 | 78ebfd2d | Panagiotis Kanavos | //If the last deleted filename is equal to the current and the action is create, we have a MOVE operation |
193 | 78ebfd2d | Panagiotis Kanavos | var hasMoved = (_cachedDeletedFullPath != e.FullPath && oldName == newName); |
194 | 78ebfd2d | Panagiotis Kanavos | |
195 | 78ebfd2d | Panagiotis Kanavos | if (!hasMoved) |
196 | 78ebfd2d | Panagiotis Kanavos | return false; |
197 | 78ebfd2d | Panagiotis Kanavos | |
198 | 78ebfd2d | Panagiotis Kanavos | try |
199 | 78ebfd2d | Panagiotis Kanavos | { |
200 | 78ebfd2d | Panagiotis Kanavos | //If the actual action is a Move, raise a Move event instead of the actual event |
201 | 78ebfd2d | Panagiotis Kanavos | var newDirectory = Path.GetDirectoryName(e.FullPath); |
202 | 78ebfd2d | Panagiotis Kanavos | var oldDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
203 | 139ac1e8 | Panagiotis Kanavos | |
204 | 78ebfd2d | Panagiotis Kanavos | if (Moved != null) |
205 | 139ac1e8 | Panagiotis Kanavos | { |
206 | 78ebfd2d | Panagiotis Kanavos | Moved(sender, new MovedEventArgs(newDirectory, newName, oldDirectory, oldName)); |
207 | 139ac1e8 | Panagiotis Kanavos | //If the moved item is a dictionary, we need to raise a change event for each child item |
208 | 139ac1e8 | Panagiotis Kanavos | //When a directory is moved within the same volume, Windows raises events only for the directory object, |
209 | 139ac1e8 | Panagiotis Kanavos | //not its children. This happens because the move actually changes a single directory entry. It doesn't |
210 | 139ac1e8 | Panagiotis Kanavos | //affect the entries of the children. |
211 | 139ac1e8 | Panagiotis Kanavos | var directory = new DirectoryInfo(e.FullPath); |
212 | 139ac1e8 | Panagiotis Kanavos | if (directory.Exists) |
213 | 139ac1e8 | Panagiotis Kanavos | { |
214 | 139ac1e8 | Panagiotis Kanavos | foreach (var child in directory.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) |
215 | 139ac1e8 | Panagiotis Kanavos | { |
216 | 139ac1e8 | Panagiotis Kanavos | var newChildDirectory = Path.GetDirectoryName(child.FullName); |
217 | 139ac1e8 | Panagiotis Kanavos | |
218 | 139ac1e8 | Panagiotis Kanavos | var relativePath=child.AsRelativeTo(newDirectory); |
219 | 139ac1e8 | Panagiotis Kanavos | var relativeFolder = Path.GetDirectoryName(relativePath); |
220 | 139ac1e8 | Panagiotis Kanavos | var oldChildDirectory = Path.Combine(oldDirectory, relativeFolder); |
221 | 139ac1e8 | Panagiotis Kanavos | Moved(sender,new MovedEventArgs(newChildDirectory,child.Name,oldChildDirectory,child.Name)); |
222 | 139ac1e8 | Panagiotis Kanavos | } |
223 | 139ac1e8 | Panagiotis Kanavos | } |
224 | 139ac1e8 | Panagiotis Kanavos | |
225 | 139ac1e8 | Panagiotis Kanavos | } |
226 | 139ac1e8 | Panagiotis Kanavos | |
227 | 78ebfd2d | Panagiotis Kanavos | } |
228 | 78ebfd2d | Panagiotis Kanavos | finally |
229 | 78ebfd2d | Panagiotis Kanavos | { |
230 | 78ebfd2d | Panagiotis Kanavos | _cachedDeletedFullPath = null; |
231 | 78ebfd2d | Panagiotis Kanavos | } |
232 | 78ebfd2d | Panagiotis Kanavos | return true; |
233 | 78ebfd2d | Panagiotis Kanavos | } |
234 | 78ebfd2d | Panagiotis Kanavos | |
235 | 78ebfd2d | Panagiotis Kanavos | private void PropagateCachedDeleted(object sender) |
236 | 78ebfd2d | Panagiotis Kanavos | { |
237 | 78ebfd2d | Panagiotis Kanavos | if (sender == null) |
238 | 78ebfd2d | Panagiotis Kanavos | throw new ArgumentNullException("sender"); |
239 | 78ebfd2d | Panagiotis Kanavos | Contract.Ensures(_cachedDeletedFullPath == null); |
240 | 78ebfd2d | Panagiotis Kanavos | Contract.EndContractBlock(); |
241 | 78ebfd2d | Panagiotis Kanavos | |
242 | 78ebfd2d | Panagiotis Kanavos | //Nothing to handle if there is no cached deleted file |
243 | 78ebfd2d | Panagiotis Kanavos | if (String.IsNullOrWhiteSpace(_cachedDeletedFullPath)) |
244 | 78ebfd2d | Panagiotis Kanavos | return; |
245 | 78ebfd2d | Panagiotis Kanavos | |
246 | 78ebfd2d | Panagiotis Kanavos | var deletedFileName = Path.GetFileName(_cachedDeletedFullPath); |
247 | 78ebfd2d | Panagiotis Kanavos | var deletedFileDirectory = Path.GetDirectoryName(_cachedDeletedFullPath); |
248 | 78ebfd2d | Panagiotis Kanavos | |
249 | 139ac1e8 | Panagiotis Kanavos | //Only a single file Delete event is raised when moving a file to the Recycle Bin, as this is actually a MOVE operation |
250 | 139ac1e8 | Panagiotis Kanavos | //In this case we need to raise the proper events for all child objects of the deleted directory. |
251 | 139ac1e8 | Panagiotis Kanavos | //UNFORTUNATELY, this can't be detected here, eg. by retrieving the child objects, because they are already deleted |
252 | 139ac1e8 | Panagiotis Kanavos | //This should be done at a higher level, eg by checking the stored state |
253 | 139ac1e8 | Panagiotis Kanavos | if (Deleted != null) |
254 | 139ac1e8 | Panagiotis Kanavos | Deleted(sender,new FileSystemEventArgs(WatcherChangeTypes.Deleted, deletedFileDirectory, deletedFileName)); |
255 | 78ebfd2d | Panagiotis Kanavos | |
256 | 78ebfd2d | Panagiotis Kanavos | _cachedDeletedFullPath = null; |
257 | 78ebfd2d | Panagiotis Kanavos | } |
258 | 78ebfd2d | Panagiotis Kanavos | } |
259 | 78ebfd2d | Panagiotis Kanavos | } |