PageRenderTime 45ms CodeModel.GetById 19ms app.highlight 20ms RepoModel.GetById 2ms app.codeStats 0ms

/SparkleLib/SparkleRepoBase.cs

http://github.com/hbons/SparkleShare
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}