/SparkleLib/SparkleRepoBase.cs
C# | 628 lines | 427 code | 178 blank | 23 comment | 121 complexity | 526f8ea0e829d27ca9c0f569893b79d5 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 Lesser General Public License as 6// published by the Free Software Foundation, either version 3 of the 7// License, or (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.Threading; 22 23using Timers = System.Timers; 24 25namespace SparkleLib { 26 27 public enum SyncStatus { 28 Idle, 29 Paused, 30 SyncUp, 31 SyncDown, 32 Error 33 } 34 35 36 public enum ErrorStatus { 37 None, 38 HostUnreachable, 39 HostIdentityChanged, 40 AuthenticationFailed, 41 DiskSpaceExceeded, 42 UnreadableFiles, 43 NotFound, 44 IncompatibleClientServer 45 } 46 47 48 public abstract class SparkleRepoBase { 49 50 public abstract bool SyncUp (); 51 public abstract bool SyncDown (); 52 public abstract void RestoreFile (string path, string revision, string target_file_path); 53 public abstract bool HasUnsyncedChanges { get; set; } 54 public abstract bool HasLocalChanges { get; } 55 public abstract bool HasRemoteChanges { get; } 56 57 public abstract string CurrentRevision { get; } 58 public abstract double Size { get; } 59 public abstract double HistorySize { get; } 60 61 public abstract List<string> ExcludePaths { get; } 62 public abstract List<SparkleChange> UnsyncedChanges { get; } 63 public abstract List<SparkleChangeSet> GetChangeSets (); 64 public abstract List<SparkleChangeSet> GetChangeSets (string path); 65 66 public static bool UseCustomWatcher = false; 67 68 69 public event SyncStatusChangedEventHandler SyncStatusChanged = delegate { }; 70 public delegate void SyncStatusChangedEventHandler (SyncStatus new_status); 71 72 public event ProgressChangedEventHandler ProgressChanged = delegate { }; 73 public delegate void ProgressChangedEventHandler (); 74 75 public event NewChangeSetEventHandler NewChangeSet = delegate { }; 76 public delegate void NewChangeSetEventHandler (SparkleChangeSet change_set); 77 78 public event Action ConflictResolved = delegate { }; 79 public event Action ChangesDetected = delegate { }; 80 81 82 public readonly string LocalPath; 83 public readonly string Name; 84 public readonly Uri RemoteUrl; 85 public List<SparkleChangeSet> ChangeSets { get; private set; } 86 public SyncStatus Status { get; private set; } 87 public ErrorStatus Error { get; protected set; } 88 public bool IsBuffering { get; private set; } 89 public double ProgressPercentage { get; private set; } 90 public double ProgressSpeed { get; private set; } 91 92 public DateTime LastSync { 93 get { 94 if (ChangeSets != null && ChangeSets.Count > 0) 95 return ChangeSets [0].Timestamp; 96 else 97 return DateTime.MinValue; 98 } 99 } 100 101 public virtual string Identifier { 102 get { 103 if (this.identifier != null) 104 return this.identifier; 105 106 string id_path = Path.Combine (LocalPath, ".sparkleshare"); 107 108 if (File.Exists (id_path)) 109 this.identifier = File.ReadAllText (id_path).Trim (); 110 111 if (!string.IsNullOrEmpty (this.identifier)) { 112 return this.identifier; 113 114 } else { 115 string config_identifier = this.local_config.GetIdentifierForFolder (Name); 116 117 if (!string.IsNullOrEmpty (config_identifier)) 118 this.identifier = config_identifier; 119 else 120 this.identifier = SparkleFetcherBase.CreateIdentifier (); 121 122 File.WriteAllText (id_path, this.identifier); 123 File.SetAttributes (id_path, FileAttributes.Hidden); 124 125 SparkleLogger.LogInfo ("Local", Name + " | Assigned identifier: " + this.identifier); 126 127 return this.identifier; 128 } 129 } 130 } 131 132 133 protected SparkleConfig local_config; 134 135 136 private string identifier; 137 private SparkleListenerBase listener; 138 private SparkleWatcher watcher; 139 private TimeSpan poll_interval = PollInterval.Short; 140 private DateTime last_poll = DateTime.Now; 141 private DateTime progress_last_change = DateTime.Now; 142 private Timers.Timer remote_timer = new Timers.Timer () { Interval = 5000 }; 143 private DisconnectReason last_disconnect_reason = DisconnectReason.None; 144 145 private bool is_syncing { 146 get { return (Status == SyncStatus.SyncUp || Status == SyncStatus.SyncDown || IsBuffering); } 147 } 148 149 private static class PollInterval { 150 public static readonly TimeSpan Short = new TimeSpan (0, 0, 5, 0); 151 public static readonly TimeSpan Long = new TimeSpan (0, 0, 15, 0); 152 } 153 154 155 public SparkleRepoBase (string path, SparkleConfig config) 156 { 157 SparkleLogger.LogInfo (path, "Initializing..."); 158 159 Status = SyncStatus.Idle; 160 Error = ErrorStatus.None; 161 this.local_config = config; 162 LocalPath = path; 163 Name = Path.GetFileName (LocalPath); 164 RemoteUrl = new Uri (this.local_config.GetUrlForFolder (Name)); 165 IsBuffering = false; 166 this.identifier = Identifier; 167 ChangeSets = GetChangeSets (); 168 169 string is_paused = this.local_config.GetFolderOptionalAttribute (Name, "paused"); 170 if (is_paused != null && is_paused.Equals (bool.TrueString)) 171 Status = SyncStatus.Paused; 172 173 string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare"); 174 File.SetAttributes (identifier_file_path, FileAttributes.Hidden); 175 176 if (!UseCustomWatcher) 177 this.watcher = new SparkleWatcher (LocalPath); 178 179 new Thread (() => CreateListener ()).Start (); 180 181 this.remote_timer.Elapsed += RemoteTimerElapsedDelegate; 182 } 183 184 185 private void RemoteTimerElapsedDelegate (object sender, EventArgs args) 186 { 187 if (this.is_syncing || IsBuffering || Status == SyncStatus.Paused) 188 return; 189 190 int time_comparison = DateTime.Compare (this.last_poll, DateTime.Now.Subtract (this.poll_interval)); 191 192 if (time_comparison < 0) { 193 if (HasUnsyncedChanges && !this.is_syncing) 194 SyncUpBase (); 195 196 this.last_poll = DateTime.Now; 197 198 if (HasRemoteChanges && !this.is_syncing) 199 SyncDownBase (); 200 201 if (this.listener.IsConnected) 202 this.poll_interval = PollInterval.Long; 203 } 204 205 // In the unlikely case that we haven't synced up our 206 // changes or the server was down, sync up again 207 if (HasUnsyncedChanges && !this.is_syncing && Error == ErrorStatus.None) 208 SyncUpBase (); 209 210 if (Status != SyncStatus.Idle && Status != SyncStatus.Error) { 211 Status = SyncStatus.Idle; 212 SyncStatusChanged (Status); 213 } 214 } 215 216 217 public void Initialize () 218 { 219 // Sync up everything that changed since we've been offline 220 new Thread (() => { 221 if (Status != SyncStatus.Paused) { 222 if (HasRemoteChanges) 223 SyncDownBase (); 224 225 if (HasUnsyncedChanges || HasLocalChanges) { 226 do { 227 SyncUpBase (); 228 229 } while (HasLocalChanges); 230 } 231 } 232 233 if (!UseCustomWatcher) 234 this.watcher.ChangeEvent += OnFileActivity; 235 236 this.remote_timer.Start (); 237 238 }).Start (); 239 } 240 241 242 private Object buffer_lock = new Object (); 243 244 public void OnFileActivity (FileSystemEventArgs args) 245 { 246 if (IsBuffering || this.is_syncing) 247 return; 248 249 if (args != null) { 250 foreach (string exclude_path in ExcludePaths) { 251 if (args.FullPath.Contains (Path.DirectorySeparatorChar + exclude_path)) 252 return; 253 } 254 } 255 256 if (Status == SyncStatus.Paused) { 257 ChangesDetected (); 258 return; 259 } 260 261 lock (this.buffer_lock) { 262 if (IsBuffering || this.is_syncing || !HasLocalChanges) 263 return; 264 265 IsBuffering = true; 266 } 267 268 ChangesDetected (); 269 270 if (!UseCustomWatcher) 271 this.watcher.Disable (); 272 273 SparkleLogger.LogInfo ("Local", Name + " | Activity detected, waiting for it to settle..."); 274 275 List<double> size_buffer = new List<double> (); 276 DirectoryInfo info = new DirectoryInfo (LocalPath); 277 278 do { 279 if (size_buffer.Count >= 4) 280 size_buffer.RemoveAt (0); 281 282 size_buffer.Add (CalculateSize (info)); 283 284 if (size_buffer.Count >= 4 && 285 size_buffer [0].Equals (size_buffer [1]) && 286 size_buffer [1].Equals (size_buffer [2]) && 287 size_buffer [2].Equals (size_buffer [3])) { 288 289 SparkleLogger.LogInfo ("Local", Name + " | Activity has settled"); 290 IsBuffering = false; 291 292 bool first_sync = true; 293 294 if (HasLocalChanges && Status == SyncStatus.Idle) { 295 do { 296 if (!first_sync) 297 SparkleLogger.LogInfo ("Local", Name + " | More changes found"); 298 299 SyncUpBase (); 300 301 if (Error == ErrorStatus.UnreadableFiles) 302 return; 303 304 first_sync = false; 305 306 } while (HasLocalChanges); 307 } 308 309 if (Status != SyncStatus.Idle && Status != SyncStatus.Error) { 310 Status = SyncStatus.Idle; 311 SyncStatusChanged (Status); 312 } 313 314 } else { 315 Thread.Sleep (500); 316 } 317 318 } while (IsBuffering); 319 320 if (!UseCustomWatcher) 321 this.watcher.Enable (); 322 } 323 324 325 public void ForceRetry () 326 { 327 if (Error != ErrorStatus.None && !this.is_syncing) 328 SyncUpBase (); 329 } 330 331 332 protected void OnConflictResolved () 333 { 334 ConflictResolved (); 335 } 336 337 338 protected void OnProgressChanged (double progress_percentage, double progress_speed) 339 { 340 if (progress_percentage < 1) 341 return; 342 343 // Only trigger the ProgressChanged event once per second 344 if (DateTime.Compare (this.progress_last_change, DateTime.Now.Subtract (new TimeSpan (0, 0, 0, 1))) >= 0) 345 return; 346 347 if (progress_percentage == 100.0) 348 progress_percentage = 99.0; 349 350 ProgressPercentage = progress_percentage; 351 ProgressSpeed = progress_speed; 352 this.progress_last_change = DateTime.Now; 353 354 ProgressChanged (); 355 } 356 357 358 private void SyncUpBase () 359 { 360 if (!UseCustomWatcher) 361 this.watcher.Disable (); 362 363 SparkleLogger.LogInfo ("SyncUp", Name + " | Initiated"); 364 HasUnsyncedChanges = true; 365 366 Status = SyncStatus.SyncUp; 367 SyncStatusChanged (Status); 368 369 if (SyncUp ()) { 370 SparkleLogger.LogInfo ("SyncUp", Name + " | Done"); 371 ChangeSets = GetChangeSets (); 372 373 HasUnsyncedChanges = false; 374 this.poll_interval = PollInterval.Long; 375 376 this.listener.Announce (new SparkleAnnouncement (Identifier, CurrentRevision)); 377 378 Status = SyncStatus.Idle; 379 SyncStatusChanged (Status); 380 381 } else { 382 SparkleLogger.LogInfo ("SyncUp", Name + " | Error"); 383 SyncDownBase (); 384 385 if (!UseCustomWatcher) 386 this.watcher.Disable (); 387 388 if (Error == ErrorStatus.None && SyncUp ()) { 389 HasUnsyncedChanges = false; 390 391 this.listener.Announce (new SparkleAnnouncement (Identifier, CurrentRevision)); 392 393 Status = SyncStatus.Idle; 394 SyncStatusChanged (Status); 395 396 } else { 397 this.poll_interval = PollInterval.Short; 398 399 Status = SyncStatus.Error; 400 SyncStatusChanged (Status); 401 } 402 } 403 404 ProgressPercentage = 0.0; 405 ProgressSpeed = 0.0; 406 407 if (!UseCustomWatcher) 408 this.watcher.Enable (); 409 410 this.status_message = ""; 411 } 412 413 414 private void SyncDownBase () 415 { 416 if (!UseCustomWatcher) 417 this.watcher.Disable (); 418 419 SparkleLogger.LogInfo ("SyncDown", Name + " | Initiated"); 420 421 Status = SyncStatus.SyncDown; 422 SyncStatusChanged (Status); 423 424 string pre_sync_revision = CurrentRevision; 425 426 if (SyncDown ()) { 427 Error = ErrorStatus.None; 428 429 string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare"); 430 File.SetAttributes (identifier_file_path, FileAttributes.Hidden); 431 432 ChangeSets = GetChangeSets (); 433 434 if (!pre_sync_revision.Equals (CurrentRevision) && 435 ChangeSets != null && ChangeSets.Count > 0 && 436 !ChangeSets [0].User.Name.Equals (this.local_config.User.Name)) { 437 438 bool emit_change_event = true; 439 440 foreach (SparkleChange change in ChangeSets [0].Changes) { 441 if (change.Path.EndsWith (".sparkleshare")) { 442 emit_change_event = false; 443 break; 444 } 445 } 446 447 if (emit_change_event) 448 NewChangeSet (ChangeSets [0]); 449 } 450 451 SparkleLogger.LogInfo ("SyncDown", Name + " | Done"); 452 453 // There could be changes from a resolved 454 // conflict. Tries only once, then lets 455 // the timer try again periodically 456 if (HasUnsyncedChanges) { 457 Status = SyncStatus.SyncUp; 458 SyncStatusChanged (Status); 459 460 if (SyncUp ()) 461 HasUnsyncedChanges = false; 462 } 463 464 Status = SyncStatus.Idle; 465 SyncStatusChanged (Status); 466 467 } else { 468 SparkleLogger.LogInfo ("SyncDown", Name + " | Error"); 469 470 ChangeSets = GetChangeSets (); 471 472 Status = SyncStatus.Error; 473 SyncStatusChanged (Status); 474 } 475 476 ProgressPercentage = 0.0; 477 ProgressSpeed = 0.0; 478 479 Status = SyncStatus.Idle; 480 SyncStatusChanged (Status); 481 482 if (!UseCustomWatcher) 483 this.watcher.Enable (); 484 } 485 486 487 private void CreateListener () 488 { 489 this.listener = SparkleListenerFactory.CreateListener (Name, Identifier); 490 491 if (this.listener.IsConnected) 492 this.poll_interval = PollInterval.Long; 493 494 this.listener.Connected += ListenerConnectedDelegate; 495 this.listener.Disconnected += ListenerDisconnectedDelegate; 496 this.listener.AnnouncementReceived += ListenerAnnouncementReceivedDelegate; 497 498 if (!this.listener.IsConnected && !this.listener.IsConnecting) 499 this.listener.Connect (); 500 } 501 502 503 private void ListenerConnectedDelegate () 504 { 505 if (this.last_disconnect_reason == DisconnectReason.SystemSleep) { 506 this.last_disconnect_reason = DisconnectReason.None; 507 508 if (HasRemoteChanges && !this.is_syncing) 509 SyncDownBase (); 510 } 511 512 this.poll_interval = PollInterval.Long; 513 } 514 515 516 private void ListenerDisconnectedDelegate (DisconnectReason reason) 517 { 518 SparkleLogger.LogInfo (Name, "Falling back to regular polling"); 519 this.poll_interval = PollInterval.Short; 520 521 this.last_disconnect_reason = reason; 522 523 if (reason == DisconnectReason.SystemSleep) { 524 this.remote_timer.Stop (); 525 526 int backoff_time = 2; 527 528 do { 529 SparkleLogger.LogInfo (Name, "Next reconnect attempt in " + backoff_time + " seconds"); 530 Thread.Sleep (backoff_time * 1000); 531 this.listener.Connect (); 532 backoff_time *= 2; 533 534 } while (backoff_time < 64 && !this.listener.IsConnected); 535 536 this.remote_timer.Start (); 537 } 538 } 539 540 541 private void ListenerAnnouncementReceivedDelegate (SparkleAnnouncement announcement) 542 { 543 string identifier = Identifier; 544 545 if (!announcement.FolderIdentifier.Equals (identifier)) 546 return; 547 548 if (!announcement.Message.Equals (CurrentRevision)) { 549 while (this.is_syncing) 550 Thread.Sleep (100); 551 552 SparkleLogger.LogInfo (Name, "Syncing due to announcement"); 553 554 if (Status == SyncStatus.Paused) 555 SparkleLogger.LogInfo (Name, "We're paused, skipping sync"); 556 else 557 SyncDownBase (); 558 } 559 } 560 561 562 // Recursively gets a folder's size in bytes 563 private long CalculateSize (DirectoryInfo parent) 564 { 565 if (ExcludePaths.Contains (parent.Name)) 566 return 0; 567 568 long size = 0; 569 570 try { 571 foreach (DirectoryInfo directory in parent.GetDirectories ()) 572 size += CalculateSize (directory); 573 574 foreach (FileInfo file in parent.GetFiles ()) 575 size += file.Length; 576 577 } catch (Exception e) { 578 SparkleLogger.LogInfo ("Local", "Error calculating directory size", e); 579 } 580 581 return size; 582 } 583 584 585 public void Pause () 586 { 587 if (Status == SyncStatus.Idle) { 588 this.local_config.SetFolderOptionalAttribute (Name, "paused", bool.TrueString); 589 Status = SyncStatus.Paused; 590 } 591 } 592 593 594 protected string status_message = ""; 595 596 public void Resume (string message) 597 { 598 this.status_message = message; 599 600 if (Status == SyncStatus.Paused) { 601 this.local_config.SetFolderOptionalAttribute (Name, "paused", bool.FalseString); 602 Status = SyncStatus.Idle; 603 604 if (HasUnsyncedChanges || HasLocalChanges) { 605 do { 606 SyncUpBase (); 607 608 } while (HasLocalChanges); 609 } 610 } 611 } 612 613 614 public void Dispose () 615 { 616 this.remote_timer.Stop (); 617 this.remote_timer.Dispose (); 618 619 this.listener.Disconnected -= ListenerDisconnectedDelegate; 620 this.listener.AnnouncementReceived -= ListenerAnnouncementReceivedDelegate; 621 622 this.listener.Dispose (); 623 624 if (!UseCustomWatcher) 625 this.watcher.Dispose (); 626 } 627 } 628}