// SparkleShare, a collaboration and sharing tool. // Copyright (C) 2010 Hylke Bons // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . using System; using System.Collections.Generic; using System.IO; using System.Threading; using Timers = System.Timers; namespace Sparkles { public enum StorageType { Unknown, Plain, LargeFiles, Encrypted } public class StorageTypeInfo { public readonly StorageType Type; public readonly string Name; public readonly string Description; public StorageTypeInfo (StorageType storage_type, string name, string description) { Type = storage_type; Name = name; Description = description; } } public enum SyncStatus { Idle, Paused, SyncUp, SyncDown, Error } public enum ErrorStatus { None, HostUnreachable, HostIdentityChanged, AuthenticationFailed, DiskSpaceExceeded, UnreadableFiles, NotFound, IncompatibleClientServer, Unknown } public abstract class BaseRepository { public abstract bool SyncUp (); public abstract bool SyncDown (); public abstract void RestoreFile (string path, string revision, string target_file_path); public abstract bool HasUnsyncedChanges { get; set; } public abstract bool HasLocalChanges { get; } public abstract bool HasRemoteChanges { get; } public abstract string CurrentRevision { get; } public abstract double Size { get; } public abstract double HistorySize { get; } public abstract List ExcludePaths { get; } public abstract List UnsyncedChanges { get; } public abstract List GetChangeSets (); public abstract List GetChangeSets (string path); protected StorageType StorageType = StorageType.Plain; public static bool UseCustomWatcher = false; public event SyncStatusChangedEventHandler SyncStatusChanged = delegate { }; public delegate void SyncStatusChangedEventHandler (SyncStatus new_status); public event ProgressChangedEventHandler ProgressChanged = delegate { }; public delegate void ProgressChangedEventHandler (); public event NewChangeSetEventHandler NewChangeSet = delegate { }; public delegate void NewChangeSetEventHandler (ChangeSet change_set); public event Action ConflictResolved = delegate { }; public event Action ChangesDetected = delegate { }; public readonly string LocalPath; public readonly string Name; public readonly Uri RemoteUrl; public List ChangeSets { get; set; } public SyncStatus Status { get; set; } public ErrorStatus Error { get; protected set; } public bool IsBuffering { get; set; } public double ProgressPercentage { get; private set; } public double ProgressSpeed { get; private set; } public string ProgressInformation { get; private set; } public DateTime LastSync { get { if (ChangeSets != null && ChangeSets.Count > 0) return ChangeSets [0].Timestamp; else return DateTime.MinValue; } } public virtual string Identifier { get { if (this.identifier != null) return this.identifier; string id_path = Path.Combine (LocalPath, ".sparkleshare"); if (File.Exists (id_path)) { File.SetAttributes (id_path, FileAttributes.Hidden); this.identifier = File.ReadAllText (id_path).Trim (); } if (!string.IsNullOrEmpty (this.identifier)) { return this.identifier; } else { string config_identifier = this.local_config.IdentifierByName (Name); if (!string.IsNullOrEmpty (config_identifier)) this.identifier = config_identifier; else this.identifier = Path.GetRandomFileName ().SHA256 (); File.WriteAllText (id_path, this.identifier); File.SetAttributes (id_path, FileAttributes.Hidden); Logger.LogInfo ("Local", Name + " | Assigned identifier: " + this.identifier); return this.identifier; } } } protected Configuration local_config; string identifier; BaseListener listener = null; Watcher watcher; TimeSpan poll_interval = PollInterval.Short; DateTime last_poll = DateTime.Now; Timers.Timer remote_timer = new Timers.Timer () { Interval = 5000 }; DisconnectReason last_disconnect_reason = DisconnectReason.None; bool is_syncing { get { return (Status == SyncStatus.SyncUp || Status == SyncStatus.SyncDown || IsBuffering); } } static class PollInterval { public static readonly TimeSpan Short = new TimeSpan (0, 0, 5, 0); public static readonly TimeSpan Long = new TimeSpan (0, 0, 15, 0); } public BaseRepository (string path, Configuration config) { Logger.LogInfo (path, "Initializing..."); Status = SyncStatus.Idle; Error = ErrorStatus.None; this.local_config = config; LocalPath = path; Name = Path.GetFileName (LocalPath); RemoteUrl = new Uri (this.local_config.UrlByName (Name)); IsBuffering = false; this.identifier = Identifier; string storage_type = this.local_config.GetFolderOptionalAttribute (Name, "storage_type"); if (!string.IsNullOrEmpty (storage_type)) StorageType = (StorageType) Enum.Parse (typeof (StorageType), storage_type); string is_paused = this.local_config.GetFolderOptionalAttribute (Name, "paused"); if (is_paused != null && is_paused.Equals (bool.TrueString)) Status = SyncStatus.Paused; string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare"); File.SetAttributes (identifier_file_path, FileAttributes.Hidden); if (!UseCustomWatcher) this.watcher = new Watcher (LocalPath); new Thread (() => CreateListener ()).Start (); this.remote_timer.Elapsed += RemoteTimerElapsedDelegate; } void RemoteTimerElapsedDelegate (object sender, EventArgs args) { if (this.is_syncing || IsBuffering || Status == SyncStatus.Paused) return; int time_comparison = DateTime.Compare (this.last_poll, DateTime.Now.Subtract (this.poll_interval)); if (time_comparison < 0) { if (HasUnsyncedChanges && !this.is_syncing) SyncUpBase (); this.last_poll = DateTime.Now; if (HasRemoteChanges && !this.is_syncing) SyncDownBase (); // if (this.listener.IsConnected) // this.poll_interval = PollInterval.Long; } // In the unlikely case that we haven't synced up our // changes or the server was down, sync up again if (HasUnsyncedChanges && !this.is_syncing && Error == ErrorStatus.None) SyncUpBase (); if (Status != SyncStatus.Idle && Status != SyncStatus.Error) { Status = SyncStatus.Idle; SyncStatusChanged (Status); } } public void Initialize () { ChangeSets = GetChangeSets (); // Sync up everything that changed since we've been offline new Thread (() => { if (Status != SyncStatus.Paused) { if (HasRemoteChanges) SyncDownBase (); if (HasUnsyncedChanges || HasLocalChanges) { do { SyncUpBase (); } while (HasLocalChanges); } } if (!UseCustomWatcher) this.watcher.ChangeEvent += OnFileActivity; this.remote_timer.Start (); }).Start (); } Object buffer_lock = new Object (); public void OnFileActivity (FileSystemEventArgs args) { if (IsBuffering || this.is_syncing) return; if (args != null) { foreach (string exclude_path in ExcludePaths) { if (args.FullPath.Contains (Path.DirectorySeparatorChar + exclude_path)) return; } } if (Status == SyncStatus.Paused) { ChangesDetected (); return; } lock (this.buffer_lock) { if (IsBuffering || this.is_syncing || !HasLocalChanges) return; IsBuffering = true; } ChangesDetected (); if (!UseCustomWatcher) this.watcher.Disable (); Logger.LogInfo ("Local", Name + " | Activity detected, waiting for it to settle..."); List size_buffer = new List (); DirectoryInfo info = new DirectoryInfo (LocalPath); do { if (size_buffer.Count >= 4) size_buffer.RemoveAt (0); size_buffer.Add (CalculateSize (info)); if (size_buffer.Count >= 4 && size_buffer [0].Equals (size_buffer [1]) && size_buffer [1].Equals (size_buffer [2]) && size_buffer [2].Equals (size_buffer [3])) { Logger.LogInfo ("Local", Name + " | Activity has settled"); IsBuffering = false; bool first_sync = true; if (HasLocalChanges && Status == SyncStatus.Idle) { do { if (!first_sync) Logger.LogInfo ("Local", Name + " | More changes found"); SyncUpBase (); if (Error == ErrorStatus.UnreadableFiles) return; first_sync = false; } while (HasLocalChanges); } if (Status != SyncStatus.Idle && Status != SyncStatus.Error) { Status = SyncStatus.Idle; SyncStatusChanged (Status); } } else { Thread.Sleep (500); } } while (IsBuffering); if (!UseCustomWatcher) this.watcher.Enable (); } public void ForceRetry () { if (Error != ErrorStatus.None && !this.is_syncing) SyncUpBase (); } protected void OnConflictResolved () { ConflictResolved (); } DateTime progress_last_change = DateTime.Now; protected void OnProgressChanged (double percentage, double speed, string information) { if (percentage < 1) return; // Only trigger the ProgressChanged event once per second if (DateTime.Compare (this.progress_last_change, DateTime.Now.Subtract (new TimeSpan (0, 0, 0, 1))) >= 0) return; if (percentage == 100.0) percentage = 99.0; progress_last_change = DateTime.Now; ProgressPercentage = percentage; ProgressSpeed = speed; ProgressInformation = information; ProgressChanged (); } void SyncUpBase () { if (!UseCustomWatcher) this.watcher.Disable (); Logger.LogInfo ("SyncUp", Name + " | Initiated"); HasUnsyncedChanges = true; Status = SyncStatus.SyncUp; SyncStatusChanged (Status); if (SyncUp ()) { Logger.LogInfo ("SyncUp", Name + " | Done"); ChangeSets = GetChangeSets (); HasUnsyncedChanges = false; this.poll_interval = PollInterval.Long; // this.listener.Announce (new Announcement (Identifier, CurrentRevision)); Status = SyncStatus.Idle; SyncStatusChanged (Status); } else { Logger.LogInfo ("SyncUp", Name + " | Error"); SyncDownBase (); if (!UseCustomWatcher) this.watcher.Disable (); if (Error == ErrorStatus.None && SyncUp ()) { HasUnsyncedChanges = false; // this.listener.Announce (new Announcement (Identifier, CurrentRevision)); Status = SyncStatus.Idle; SyncStatusChanged (Status); } else { this.poll_interval = PollInterval.Short; Status = SyncStatus.Error; SyncStatusChanged (Status); } } ProgressPercentage = 0.0; ProgressSpeed = 0.0; if (!UseCustomWatcher) this.watcher.Enable (); this.status_message = ""; } void SyncDownBase () { if (!UseCustomWatcher) this.watcher.Disable (); Logger.LogInfo ("SyncDown", Name + " | Initiated"); Status = SyncStatus.SyncDown; SyncStatusChanged (Status); string pre_sync_revision = CurrentRevision; if (SyncDown ()) { Error = ErrorStatus.None; string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare"); File.SetAttributes (identifier_file_path, FileAttributes.Hidden); ChangeSets = GetChangeSets (); if (!pre_sync_revision.Equals (CurrentRevision) && ChangeSets != null && ChangeSets.Count > 0 && !ChangeSets [0].User.Name.Equals (this.local_config.User.Name)) { bool emit_change_event = true; foreach (Change change in ChangeSets [0].Changes) { if (change.Path.EndsWith (".sparkleshare")) { emit_change_event = false; break; } } if (emit_change_event) NewChangeSet (ChangeSets [0]); } Logger.LogInfo ("SyncDown", Name + " | Done"); // There could be changes from a resolved // conflict. Tries only once, then lets // the timer try again periodically if (HasUnsyncedChanges) { Status = SyncStatus.SyncUp; SyncStatusChanged (Status); if (SyncUp ()) HasUnsyncedChanges = false; } Status = SyncStatus.Idle; SyncStatusChanged (Status); } else { Logger.LogInfo ("SyncDown", Name + " | Error"); ChangeSets = GetChangeSets (); Status = SyncStatus.Error; SyncStatusChanged (Status); } ProgressPercentage = 0.0; ProgressSpeed = 0.0; Status = SyncStatus.Idle; SyncStatusChanged (Status); if (!UseCustomWatcher) this.watcher.Enable (); } void CreateListener () { // this.listener = ListenerFactory.CreateListener (Name, Identifier); // if (this.listener.IsConnected) // this.poll_interval = PollInterval.Long; // this.listener.Connected += ListenerConnectedDelegate; // this.listener.Disconnected += ListenerDisconnectedDelegate; // this.listener.AnnouncementReceived += ListenerAnnouncementReceivedDelegate; // if (!this.listener.IsConnected && !this.listener.IsConnecting) // this.listener.Connect (); } void ListenerConnectedDelegate () { if (this.last_disconnect_reason == DisconnectReason.SystemSleep) { this.last_disconnect_reason = DisconnectReason.None; if (HasRemoteChanges && !this.is_syncing) SyncDownBase (); } this.poll_interval = PollInterval.Long; } void ListenerDisconnectedDelegate (DisconnectReason reason) { Logger.LogInfo (Name, "Falling back to regular polling"); this.poll_interval = PollInterval.Short; this.last_disconnect_reason = reason; if (reason == DisconnectReason.SystemSleep) { this.remote_timer.Stop (); int backoff_time = 2; do { Logger.LogInfo (Name, "Next reconnect attempt in " + backoff_time + " seconds"); Thread.Sleep (backoff_time * 1000); this.listener.Connect (); backoff_time *= 2; } while (backoff_time < 64 && !this.listener.IsConnected); this.remote_timer.Start (); } } void ListenerAnnouncementReceivedDelegate (Announcement announcement) { string identifier = Identifier; if (!announcement.FolderIdentifier.Equals (identifier)) return; if (!announcement.Message.Equals (CurrentRevision)) { while (this.is_syncing) Thread.Sleep (100); Logger.LogInfo (Name, "Syncing due to announcement"); if (Status == SyncStatus.Paused) Logger.LogInfo (Name, "We're paused, skipping sync"); else SyncDownBase (); } } // Recursively gets a folder's size in bytes long CalculateSize (DirectoryInfo parent) { if (ExcludePaths.Contains (parent.Name)) return 0; long size = 0; try { foreach (DirectoryInfo directory in parent.GetDirectories ()) size += CalculateSize (directory); foreach (FileInfo file in parent.GetFiles ()) size += file.Length; } catch (Exception e) { Logger.LogInfo ("Local", "Error calculating directory size", e); } return size; } public void Pause () { if (Status == SyncStatus.Idle) { this.local_config.SetFolderOptionalAttribute (Name, "paused", bool.TrueString); Status = SyncStatus.Paused; } } protected string status_message = ""; public void Resume (string message) { this.status_message = message; if (Status == SyncStatus.Paused) { this.local_config.SetFolderOptionalAttribute (Name, "paused", bool.FalseString); Status = SyncStatus.Idle; if (HasUnsyncedChanges || HasLocalChanges) { do { SyncUpBase (); } while (HasLocalChanges); } } } public void Dispose () { if (remote_timer != null) { this.remote_timer.Elapsed -= RemoteTimerElapsedDelegate; this.remote_timer.Stop (); this.remote_timer.Dispose (); this.remote_timer = null; } // this.listener.Disconnected -= ListenerDisconnectedDelegate; // this.listener.AnnouncementReceived -= ListenerAnnouncementReceivedDelegate; // this.listener.Dispose (); if (!UseCustomWatcher && this.watcher != null) this.watcher.Dispose (); } } }