PageRenderTime 69ms CodeModel.GetById 35ms app.highlight 28ms RepoModel.GetById 1ms app.codeStats 0ms

/SparkleShare/SparkleControllerBase.cs

http://github.com/hbons/SparkleShare
C# | 796 lines | 534 code | 218 blank | 44 comment | 75 complexity | 50287f45928e08fdeb7a7c89781f0c79 MD5 | raw file
  1//   SparkleShare, a collaboration and sharing tool.
  2//   Copyright (C) 2010  Hylke Bons <hylkebons@gmail.com>
  3//
  4//   This program is free software: you can redistribute it and/or modify
  5//   it under the terms of the GNU General Public License as published by
  6//   the Free Software Foundation, either version 3 of the License, or
  7//   (at your option) any later version.
  8//
  9//   This program is distributed in the hope that it will be useful,
 10//   but WITHOUT ANY WARRANTY; without even the implied warranty of
 11//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12//   GNU General Public License for more details.
 13//
 14//   You should have received a copy of the GNU General Public License
 15//   along with this program. If not, see <http://www.gnu.org/licenses/>.
 16
 17
 18using System;
 19using System.Collections.Generic;
 20using System.IO;
 21using System.Linq;
 22using System.Threading;
 23
 24using SparkleLib;
 25
 26namespace SparkleShare {
 27    
 28    public abstract class SparkleControllerBase {
 29        
 30        public SparkleRepoBase [] Repositories {
 31            get {
 32                lock (this.repo_lock)
 33                    return this.repositories.GetRange (0, this.repositories.Count).ToArray ();
 34            }
 35        }
 36        
 37        
 38        private void AddRepository (SparkleRepoBase repo)
 39        {
 40            lock (this.repo_lock) {
 41                this.repositories.Add (repo);
 42                this.repositories.Sort ((x, y) => string.Compare (x.Name, y.Name));
 43            }
 44        }
 45        
 46        
 47        private void RemoveRepository (SparkleRepoBase repo)
 48        {
 49            lock (this.repo_lock)
 50                this.repositories.Remove (repo);
 51        }
 52        
 53        
 54        public SparkleRepoBase GetRepoByName (string name)
 55        {
 56            lock (this.repo_lock) {
 57                foreach (SparkleRepoBase repo in this.repositories)
 58                    if (repo.Name.Equals (name))
 59                        return repo;
 60            }
 61            
 62            return null;
 63        }
 64        
 65        
 66        public SparkleConfig Config { get; private set; }
 67        public bool RepositoriesLoaded { get; private set; }
 68        public string FoldersPath { get; private set; }
 69        
 70        public double ProgressPercentage = 0.0;
 71        public double ProgressSpeedUp    = 0.0;
 72        public double ProgressSpeedDown  = 0.0;
 73        
 74        
 75        public event ShowSetupWindowEventHandler ShowSetupWindowEvent = delegate { };
 76        public delegate void ShowSetupWindowEventHandler (PageType page_type);
 77
 78        public event ShowNoteWindowEventHandler ShowNoteWindowEvent = delegate { };
 79        public delegate void ShowNoteWindowEventHandler (string project);
 80
 81        public event Action ShowAboutWindowEvent = delegate { };
 82        public event Action ShowEventLogWindowEvent = delegate { };
 83        
 84        public event FolderFetchedEventHandler FolderFetched = delegate { };
 85        public delegate void FolderFetchedEventHandler (string remote_url, string [] warnings);
 86        
 87        public event FolderFetchErrorHandler FolderFetchError = delegate { };
 88        public delegate void FolderFetchErrorHandler (string remote_url, string [] errors);
 89        
 90        public event FolderFetchingHandler FolderFetching = delegate { };
 91        public delegate void FolderFetchingHandler (double percentage, double speed);
 92        
 93        
 94        public event Action FolderListChanged = delegate { };
 95        public event Action OnIdle = delegate { };
 96        public event Action OnSyncing = delegate { };
 97        public event Action OnError = delegate { };
 98        
 99        
100        public event InviteReceivedHandler InviteReceived = delegate { };
101        public delegate void InviteReceivedHandler (SparkleInvite invite);
102        
103        public event NotificationRaisedEventHandler NotificationRaised = delegate { };
104        public delegate void NotificationRaisedEventHandler (SparkleChangeSet change_set);
105        
106        public event AlertNotificationRaisedEventHandler AlertNotificationRaised = delegate { };
107        public delegate void AlertNotificationRaisedEventHandler (string title, string message);
108        
109        
110        public bool FirstRun {
111            get { return Config.User.Email.Equals ("Unknown"); }
112        }
113        
114        public List<string> Folders {
115            get {
116                List<string> folders = Config.Folders;
117                return folders;
118            }
119        }
120        
121        public SparkleUser CurrentUser {
122            get { return Config.User; }
123            set { Config.User = value; }
124        }
125        
126        public bool NotificationsEnabled {
127            get {
128                string notifications_enabled = Config.GetConfigOption ("notifications");
129                
130                if (string.IsNullOrEmpty (notifications_enabled)) {
131                    Config.SetConfigOption ("notifications", bool.TrueString);
132                    return true;
133                    
134                } else {
135                    return notifications_enabled.Equals (bool.TrueString);
136                }
137            }
138        }
139        
140        public bool AvatarsEnabled {
141            get {
142                string fetch_avatars_option = Config.GetConfigOption ("fetch_avatars");
143                
144                if (fetch_avatars_option != null && fetch_avatars_option.Equals (bool.FalseString))
145                    return false;
146                
147                return true;
148            }
149        }
150        
151        
152        // Path where the plugins are kept
153        public abstract string PluginsPath { get; }
154        
155        // Enables SparkleShare to start automatically at login
156        public abstract void CreateStartupItem ();
157        
158        // Installs the sparkleshare:// protocol handler
159        public abstract void InstallProtocolHandler ();
160        
161        // Adds the SparkleShare folder to the user's
162        // list of bookmarked places
163        public abstract void AddToBookmarks ();
164        
165        // Creates the SparkleShare folder in the user's home folder
166        public abstract bool CreateSparkleShareFolder ();
167        
168        // Opens the SparkleShare folder or an (optional) subfolder
169        public abstract void OpenFolder (string path);
170        
171        // Opens a file with the appropriate application
172        public abstract void OpenFile (string path);
173        
174        // Opens a file with the appropriate application
175        public virtual void OpenWebsite (string url) { }
176        
177        // Copies text to the clipboard
178        public abstract void CopyToClipboard (string text);
179        
180        public abstract string EventLogHTML { get; }
181        public abstract string DayEntryHTML { get; }
182        public abstract string EventEntryHTML { get; }
183        
184        
185        private SparkleFetcherBase fetcher;
186        private FileSystemWatcher watcher;
187        private Object repo_lock = new Object ();
188        private Object check_repos_lock = new Object ();
189        private List<SparkleRepoBase> repositories = new List<SparkleRepoBase> ();
190        private bool lost_folders_path = false;
191        
192        
193        public SparkleControllerBase ()
194        {
195            
196            string app_data_path = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
197            string config_path   = Path.Combine (app_data_path, "sparkleshare");
198            
199            Config                      = new SparkleConfig (config_path, "config.xml");
200            SparkleConfig.DefaultConfig = Config;
201            FoldersPath                 = Config.FoldersPath;
202        }
203        
204        
205        public virtual void Initialize ()
206        {
207            SparkleLogger.LogInfo ("Environment", "SparkleShare version: " + SparkleLib.SparkleBackend.Version +
208                                   ", Operating system: " + SparkleLib.SparkleBackend.Platform + " (" + Environment.OSVersion + ")");
209            
210            SparklePlugin.PluginsPath = PluginsPath;
211            InstallProtocolHandler ();
212            
213            try {
214                if (CreateSparkleShareFolder ())
215                    AddToBookmarks ();
216                
217            } catch (DirectoryNotFoundException) {
218                this.lost_folders_path = true;
219            }
220            
221            bool keys_imported = false;
222            
223            if (FirstRun) {
224                Config.SetConfigOption ("notifications", bool.TrueString);
225                
226            } else {
227                string keys_path = Path.GetDirectoryName (Config.FullPath);
228                string key_file_path = "";
229                
230                foreach (string file_path in Directory.GetFiles (keys_path)) {
231                    string file_name = Path.GetFileName(file_path);
232                    if (file_name.EndsWith (".key")) {
233                        key_file_path = Path.Combine (keys_path, file_name);
234                        
235                        // Replace spaces with underscores in old keys
236                        if (file_name.Contains (" ")) {
237                            string new_file_name = file_name.Replace (" ", "_");
238                            File.Move (key_file_path, Path.Combine (keys_path, new_file_name));
239                            File.Move (key_file_path + ".pub", Path.Combine (keys_path, new_file_name + ".pub"));
240                            key_file_path = Path.Combine (keys_path, new_file_name);
241                        }
242                        
243                        SparkleKeys.ImportPrivateKey (key_file_path);
244                        keys_imported = true;
245                        
246                        break;
247                    }
248                }
249                
250                if (keys_imported) {
251                    CurrentUser.PublicKey = File.ReadAllText (key_file_path + ".pub");
252                    
253                } else {
254                    string [] key_pair = CreateKeys ();
255                    
256                    SparkleKeys.ImportPrivateKey (key_pair [0]);
257                    CurrentUser.PublicKey = File.ReadAllText (key_pair [1]);    
258                }
259                
260                SparkleKeys.ListPrivateKeys ();
261                FolderListChanged (); // FIXME: Hacky way to update status icon menu to show the key
262            }
263            
264            // Watch the SparkleShare folder
265            this.watcher = new FileSystemWatcher () {
266                Filter                = "*",
267                IncludeSubdirectories = false,
268                Path                  = FoldersPath
269            };
270            
271            watcher.Created += OnFolderActivity;
272            // FIXME watcher.Deleted += OnFolderActivity;
273            // FIXME watcher.Renamed += OnFolderActivity;
274            
275            watcher.EnableRaisingEvents = true;
276        }
277        
278        
279        private int reopen_attempt_counts = 0;
280        
281        public void HandleReopen ()
282        {
283            if (Repositories.Length > 0) {
284                ShowEventLogWindow ();
285                
286            } else if (reopen_attempt_counts > 1) {
287                AlertNotificationRaised ("Hello!", "SparkleShare sits right here, as a status icon.");
288                reopen_attempt_counts = 0;
289                
290            } else {
291                reopen_attempt_counts++;
292            }
293        }
294        
295        
296        public void UIHasLoaded ()
297        {
298            if (this.lost_folders_path) {
299                Program.UI.Bubbles.Controller.ShowBubble ("Where's your SparkleShare folder?",
300                                                          "Did you put it on a detached drive?", null);
301                
302                Environment.Exit (-1);
303            }
304            
305            if (FirstRun) {
306                ShowSetupWindow (PageType.Setup);
307                
308                new Thread (() => { 
309                    string [] key_pair = CreateKeys ();
310                    
311                    SparkleKeys.ImportPrivateKey (key_pair [0]);                    
312                    CurrentUser.PublicKey = File.ReadAllText (key_pair [1]);
313                    
314                    FolderListChanged (); // FIXME: Hacky way to update status icon menu to show the key
315                    
316                }).Start ();
317                
318            } else {
319                new Thread (() => {
320                    StartupInviteScan ();
321                    CheckRepositories ();
322                    RepositoriesLoaded = true;
323                    UpdateState ();
324                    
325                }).Start ();
326            }
327        }
328        
329        
330        public void ShowSetupWindow (PageType page_type)
331        {
332            ShowSetupWindowEvent (page_type);
333        }
334        
335        
336        public void ShowAboutWindow ()
337        {
338            ShowAboutWindowEvent ();
339        }
340
341
342        public void ShowNoteWindow (string project)
343        {
344            ShowNoteWindowEvent (project);
345        }
346        
347        
348        public void ShowEventLogWindow ()
349        {
350            ShowEventLogWindowEvent ();
351        }
352        
353        
354        public void OpenSparkleShareFolder ()
355        {
356            OpenFolder (Config.FoldersPath);
357        }
358        
359        
360        public void OpenSparkleShareFolder (string name)
361        {
362            OpenFolder (new SparkleFolder (name).FullPath);
363        }
364        
365        
366        public void ToggleNotifications ()
367        {
368            bool notifications_enabled = Config.GetConfigOption ("notifications").Equals (bool.TrueString);
369            Config.SetConfigOption ("notifications", (!notifications_enabled).ToString ());
370        }
371        
372        
373        private void CheckRepositories ()
374        {
375            lock (this.check_repos_lock) {
376                string path = Config.FoldersPath;
377                
378                // Detect any renames
379                foreach (string folder_path in Directory.GetDirectories (path)) {
380                    string folder_name = Path.GetFileName (folder_path);
381                    
382                    if (folder_name.Equals (".tmp"))
383                        continue;
384                    
385                    if (Config.GetIdentifierForFolder (folder_name) == null) {
386                        string identifier_file_path = Path.Combine (folder_path, ".sparkleshare");
387                        
388                        if (!File.Exists (identifier_file_path))
389                            continue;
390                        
391                        string identifier = File.ReadAllText (identifier_file_path).Trim ();
392                        
393                        if (Config.IdentifierExists (identifier)) {
394                            RemoveRepository (GetRepoByName (folder_name));
395                            Config.RenameFolder (identifier, folder_name);
396                            
397                            string new_folder_path = Path.Combine (path, folder_name);
398                            AddRepository (new_folder_path);
399                            
400                            SparkleLogger.LogInfo ("Controller",
401                                                   "Renamed folder with identifier " + identifier + " to '" + folder_name + "'");
402                        }
403                    }
404                }
405                
406                // Remove any deleted folders
407                foreach (string folder_name in Config.Folders) {
408                    string folder_path = new SparkleFolder (folder_name).FullPath;
409                    
410                    if (!Directory.Exists (folder_path)) {
411                        Config.RemoveFolder (folder_name);
412                        RemoveRepository (GetRepoByName (folder_name));
413                        
414                        SparkleLogger.LogInfo ("Controller", "Removed folder '" + folder_name + "' from config");
415                        
416                    } else {
417                        AddRepository (folder_path);
418                    }
419                }
420                
421                // Remove any duplicate folders
422                string previous_name = "";
423                foreach (string folder_name in Config.Folders) {
424                    if (!string.IsNullOrEmpty (previous_name) && folder_name.Equals (previous_name))
425                        Config.RemoveFolder (folder_name);
426                    else
427                        previous_name = folder_name;
428                }
429                
430                FolderListChanged ();
431            }
432        }
433        
434        
435        private void AddRepository (string folder_path)
436        {
437            SparkleRepoBase repo = null;
438            string folder_name   = Path.GetFileName (folder_path);
439            string backend       = Config.GetBackendForFolder (folder_name);
440            
441            try {
442                repo = (SparkleRepoBase) Activator.CreateInstance (
443                    Type.GetType ("SparkleLib." + backend + ".SparkleRepo, SparkleLib." + backend),
444                    new object [] { folder_path, Config });
445                
446            } catch (Exception e) {
447                SparkleLogger.LogInfo ("Controller", "Failed to load backend '" + backend + "' for '" + folder_name + "': ", e);
448                return;
449            }
450            
451            repo.ChangesDetected += delegate {
452                UpdateState ();
453            };
454            
455            repo.SyncStatusChanged += delegate (SyncStatus status) {
456                if (status == SyncStatus.Idle) {
457                    ProgressPercentage = 0.0;
458                    ProgressSpeedUp    = 0.0;
459                    ProgressSpeedDown  = 0.0;
460                }
461                
462                UpdateState ();
463            };
464            
465            repo.ProgressChanged += delegate {
466                ProgressPercentage = 0.0;
467                ProgressSpeedUp    = 0.0;
468                ProgressSpeedDown  = 0.0;
469                
470                double percentage = 0.0;
471                int repo_count    = 0;
472                
473                foreach (SparkleRepoBase rep in Repositories) {
474                    if (rep.ProgressPercentage > 0) {
475                        percentage += rep.ProgressPercentage;
476                        repo_count++;
477                    }
478                    
479                    if (rep.Status == SyncStatus.SyncUp)
480                        ProgressSpeedUp += rep.ProgressSpeed;
481                    
482                    if (rep.Status == SyncStatus.SyncDown)
483                        ProgressSpeedDown += rep.ProgressSpeed;
484                }
485                
486                if (repo_count > 0)
487                    ProgressPercentage = percentage / repo_count;
488                
489                UpdateState ();
490            };
491            
492            repo.NewChangeSet += delegate (SparkleChangeSet change_set) {
493                if (AvatarsEnabled)
494                    change_set.User.AvatarFilePath = SparkleAvatars.GetAvatar (change_set.User.Email, 48, Config.FullPath);
495                
496                NotificationRaised (change_set);
497            };
498            
499            repo.ConflictResolved += delegate {
500                AlertNotificationRaised ("Resolved a file collision",
501                                         "Local and server versions were kept.");
502            };
503            
504            AddRepository (repo);
505            repo.Initialize (); 
506        }
507        
508
509        private void OnFolderActivity (object o, FileSystemEventArgs args)
510        {
511            if (args != null && args.FullPath.EndsWith (".xml") &&
512                args.ChangeType == WatcherChangeTypes.Created) {
513                
514                HandleInvite (args);
515                return;
516                
517            }/* else { FIXME: on the fly folder removal doesn't always work. disabling for now
518                Thread.Sleep (1000);
519
520                if (Directory.Exists (args.FullPath) && args.ChangeType == WatcherChangeTypes.Created)
521                    return;
522
523                CheckRepositories ();
524            }*/
525        }
526        
527        
528        private void StartupInviteScan ()
529        {
530            foreach (string invite in Directory.GetFiles (FoldersPath, "*.xml")) {
531                HandleInvite (invite);
532            }
533        }
534        
535        
536        private void HandleInvite (FileSystemEventArgs args)
537        {
538            HandleInvite (args.FullPath);
539        }
540        
541        
542        private void HandleInvite (string path)
543        {
544            if (this.fetcher != null &&
545                this.fetcher.IsActive) {
546                
547                AlertNotificationRaised ("SparkleShare Setup seems busy", "Please wait for it to finish");
548                
549            } else {
550                SparkleInvite invite = new SparkleInvite (path);
551                
552                // It may be that the invite we received a path to isn't
553                // fully downloaded yet, so we try to read it several times
554                int tries = 0;
555                while (!invite.IsValid) {
556                    Thread.Sleep (100);
557                    invite = new SparkleInvite (path);
558                    tries++;
559                    
560                    if (tries > 10) {
561                        AlertNotificationRaised ("Oh noes!", "This invite seems screwed up...");
562                        break;
563                    }
564                }
565                
566                if (invite.IsValid)
567                    InviteReceived (invite);
568                
569                File.Delete (path);
570            }
571        }
572        
573        
574        // Fires events for the current syncing state
575        private void UpdateState ()
576        {
577            bool has_unsynced_repos = false;
578            bool has_syncing_repos  = false;
579            
580            foreach (SparkleRepoBase repo in Repositories) {
581                if (repo.Status == SyncStatus.SyncDown || repo.Status == SyncStatus.SyncUp || repo.IsBuffering) {
582                    has_syncing_repos = true;
583                    break;
584                    
585                } else if (repo.Status == SyncStatus.Idle && repo.HasUnsyncedChanges) {
586                    has_unsynced_repos = true;
587                }
588            }
589            
590            if (has_syncing_repos)
591                OnSyncing ();
592            else if (has_unsynced_repos)
593                OnError ();
594            else
595                OnIdle ();
596        }
597        
598        
599        public void StartFetcher (SparkleFetcherInfo info)
600        {
601            string tmp_path = Config.TmpPath;
602            
603            if (!Directory.Exists (tmp_path)) {
604                Directory.CreateDirectory (tmp_path);
605                File.SetAttributes (tmp_path, File.GetAttributes (tmp_path) | FileAttributes.Hidden);
606            }
607            
608            string canonical_name = Path.GetFileName (info.RemotePath);
609            string backend        = info.Backend; 
610            
611            if (string.IsNullOrEmpty (backend))
612                backend = SparkleFetcherBase.GetBackend (info.Address); 
613            
614            info.TargetDirectory  = Path.Combine (tmp_path, canonical_name);
615            
616            try {
617                this.fetcher = (SparkleFetcherBase) Activator.CreateInstance (
618                    Type.GetType ("SparkleLib." + backend + ".SparkleFetcher, SparkleLib." + backend), info);
619                
620            } catch (Exception e) {
621                SparkleLogger.LogInfo ("Controller",
622                                       "Failed to load '" + backend + "' backend for '" + canonical_name + "' " + e.Message);
623                
624                FolderFetchError (Path.Combine (info.Address, info.RemotePath).Replace (@"\", "/"),
625                                  new string [] {"Failed to load \"" + backend + "\" backend for \"" + canonical_name + "\""});
626                
627                return;
628            }
629            
630            this.fetcher.Finished += delegate (bool repo_is_encrypted, bool repo_is_empty, string [] warnings) {
631                if (repo_is_encrypted && repo_is_empty) {
632                    ShowSetupWindowEvent (PageType.CryptoSetup);
633                    
634                } else if (repo_is_encrypted) {
635                    ShowSetupWindowEvent (PageType.CryptoPassword);
636                    
637                } else {
638                    FinishFetcher ();
639                }
640            };
641            
642            this.fetcher.Failed += delegate {
643                FolderFetchError (this.fetcher.RemoteUrl.ToString (), this.fetcher.Errors);
644                StopFetcher ();
645            };
646            
647            this.fetcher.ProgressChanged += delegate (double percentage, double speed) {
648                FolderFetching (percentage, speed);
649            };
650            
651            this.fetcher.Start ();
652        }
653        
654        
655        public void StopFetcher ()
656        {
657            this.fetcher.Stop ();
658            this.fetcher.Dispose ();
659            
660            this.fetcher = null;
661            this.watcher.EnableRaisingEvents = true;
662        }
663        
664        
665        public bool CheckPassword (string password)
666        {
667            return this.fetcher.IsFetchedRepoPasswordCorrect (password);
668        }
669        
670        
671        public void FinishFetcher (string password)
672        {
673            this.fetcher.EnableFetchedRepoCrypto (password);
674            FinishFetcher ();
675        }
676        
677        
678        public void FinishFetcher ()
679        {
680            this.watcher.EnableRaisingEvents = false;
681            
682            this.fetcher.Complete ();
683            string canonical_name = Path.GetFileName (this.fetcher.RemoteUrl.AbsolutePath);
684            
685            if (canonical_name.EndsWith (".git"))
686                canonical_name = canonical_name.Replace (".git", "");
687            
688            canonical_name = canonical_name.Replace ("-crypto", "");
689            canonical_name = canonical_name.ReplaceUnderscoreWithSpace ();
690            canonical_name = canonical_name.Replace ("%20", " ");
691            
692            bool target_folder_exists = Directory.Exists (
693                Path.Combine (Config.FoldersPath, canonical_name));
694            
695            // Add a numbered suffix to the name if a folder with the same name
696            // already exists. Example: "Folder (2)"
697            int suffix = 1;
698            while (target_folder_exists) {
699                suffix++;
700                target_folder_exists = Directory.Exists (
701                    Path.Combine (Config.FoldersPath, canonical_name + " (" + suffix + ")"));
702            }
703            
704            string target_folder_name = canonical_name;
705            
706            if (suffix > 1)
707                target_folder_name += " (" + suffix + ")";
708            
709            string target_folder_path = Path.Combine (Config.FoldersPath, target_folder_name);
710            
711            try {
712                Directory.Move (this.fetcher.TargetFolder, target_folder_path);
713                
714            } catch (Exception e) {
715                SparkleLogger.LogInfo ("Controller", "Error moving directory, trying again...", e);
716                
717                try {
718                    ClearDirectoryAttributes (this.fetcher.TargetFolder);
719                    Directory.Move (this.fetcher.TargetFolder, target_folder_path);
720                    
721                } catch (Exception x) {
722                    SparkleLogger.LogInfo ("Controller", "Error moving directory", x);
723                    
724                    this.fetcher.Dispose ();
725                    this.fetcher = null;
726                    this.watcher.EnableRaisingEvents = true;
727                    return;
728                }
729            }
730            
731            string backend = SparkleFetcherBase.GetBackend (this.fetcher.RemoteUrl.ToString ());
732            
733            Config.AddFolder (target_folder_name, this.fetcher.Identifier,
734                              this.fetcher.RemoteUrl.ToString (), backend);
735            
736            if (this.fetcher.OriginalFetcherInfo.AnnouncementsUrl != null) {
737                Config.SetFolderOptionalAttribute (target_folder_name, "announcements_url",
738                                                   this.fetcher.OriginalFetcherInfo.AnnouncementsUrl);
739            }
740            
741            RepositoriesLoaded = true;
742            FolderFetched (this.fetcher.RemoteUrl.ToString (), this.fetcher.Warnings.ToArray ());
743            
744            AddRepository (target_folder_path);
745            FolderListChanged ();
746            
747            this.fetcher.Dispose ();
748            this.fetcher = null;
749            
750            this.watcher.EnableRaisingEvents = true;
751        }
752        
753        
754        public virtual void Quit ()
755        {
756            foreach (SparkleRepoBase repo in Repositories)
757                repo.Dispose ();
758            
759            Environment.Exit (0);
760        }
761        
762        
763        private void ClearDirectoryAttributes (string path)
764        {
765            if (!Directory.Exists (path))
766                return;
767            
768            string [] folders = Directory.GetDirectories (path);
769            
770            foreach (string folder in folders)
771                ClearDirectoryAttributes (folder);
772            
773            string [] files = Directory.GetFiles(path);
774            
775            foreach (string file in files)
776                if (!IsSymlink (file))
777                    File.SetAttributes (file, FileAttributes.Normal);
778        }
779        
780        
781        private string [] CreateKeys ()
782        {
783            string keys_path     = Path.GetDirectoryName (SparkleConfig.DefaultConfig.FullPath);
784            string key_file_name = DateTime.Now.ToString ("yyyy-MM-dd_HH\\hmm");
785            
786            return SparkleKeys.GenerateKeyPair (keys_path, key_file_name);
787        }
788        
789        
790        private bool IsSymlink (string file)
791        {
792            FileAttributes attributes = File.GetAttributes (file);
793            return ((attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint);
794        }
795    }
796}