/SparkleShare/SparkleControllerBase.cs
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}