SparkleShare/SparkleLib/SparkleRepoBase.cs

574 lines
18 KiB
C#
Raw Normal View History

// SparkleShare, a collaboration and sharing tool.
2011-05-19 16:05:58 +00:00
// Copyright (C) 2010 Hylke Bons <hylkebons@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
2013-10-11 15:13:46 +00:00
// 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.
2011-05-19 16:05:58 +00:00
//
// 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 <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
2012-06-10 10:57:31 +00:00
using Timers = System.Timers;
2011-05-19 16:05:58 +00:00
namespace SparkleLib {
public enum SyncStatus {
Idle,
SyncUp,
SyncDown,
Error
}
public enum ErrorStatus {
None,
HostUnreachable,
HostIdentityChanged,
AuthenticationFailed,
2012-11-23 10:08:50 +00:00
DiskSpaceExceeded,
UnreadableFiles,
NotFound,
IncompatibleClientServer
}
2011-05-19 16:05:58 +00:00
public abstract class SparkleRepoBase {
2012-06-10 10:57:31 +00:00
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; }
2012-06-10 10:57:31 +00:00
public abstract string CurrentRevision { get; }
public abstract double Size { get; }
public abstract double HistorySize { get; }
2012-06-10 10:57:31 +00:00
public abstract List<string> ExcludePaths { get; }
2012-10-15 22:45:03 +00:00
public abstract List<SparkleChangeSet> GetChangeSets ();
public abstract List<SparkleChangeSet> GetChangeSets (string path);
public static bool UseCustomWatcher = false;
public event SyncStatusChangedEventHandler SyncStatusChanged = delegate { };
2011-05-19 16:05:58 +00:00
public delegate void SyncStatusChangedEventHandler (SyncStatus new_status);
public event ProgressChangedEventHandler ProgressChanged = delegate { };
public delegate void ProgressChangedEventHandler ();
public event NewChangeSetEventHandler NewChangeSet = delegate { };
2011-07-23 21:23:14 +00:00
public delegate void NewChangeSetEventHandler (SparkleChangeSet change_set);
2011-05-22 14:46:15 +00:00
public event Action ConflictResolved = delegate { };
public event Action ChangesDetected = delegate { };
2011-05-22 14:46:15 +00:00
2011-05-19 16:05:58 +00:00
2012-02-09 01:46:25 +00:00
public readonly string LocalPath;
public readonly string Name;
2012-04-29 14:19:36 +00:00
public readonly Uri RemoteUrl;
public List<SparkleChangeSet> ChangeSets { get; protected set; }
2012-06-10 10:57:31 +00:00
public SyncStatus Status { get; private set; }
public ErrorStatus Error { get; protected set; }
2012-06-10 10:57:31 +00:00
public bool IsBuffering { get; private set; }
public double ProgressPercentage { get; private set; }
public double ProgressSpeed { get; private set; }
2012-02-09 01:46:25 +00:00
public virtual string Identifier {
2012-05-19 15:17:09 +00:00
get {
if (this.identifier != null)
return this.identifier;
2012-05-19 15:17:09 +00:00
string id_path = Path.Combine (LocalPath, ".sparkleshare");
2012-06-10 10:57:31 +00:00
2012-05-19 15:17:09 +00:00
if (File.Exists (id_path))
this.identifier = File.ReadAllText (id_path).Trim ();
if (!string.IsNullOrEmpty (this.identifier)) {
return this.identifier;
} else {
2012-07-14 10:45:54 +00:00
string config_identifier = this.local_config.GetIdentifierForFolder (Name);
if (!string.IsNullOrEmpty (config_identifier))
this.identifier = config_identifier;
else
this.identifier = SparkleFetcherBase.CreateIdentifier ();
2012-05-19 15:17:09 +00:00
File.WriteAllText (id_path, this.identifier);
File.SetAttributes (id_path, FileAttributes.Hidden);
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Local", Name + " | Assigned identifier: " + this.identifier);
2012-05-19 15:17:09 +00:00
return this.identifier;
}
2012-05-19 15:17:09 +00:00
}
}
2012-02-08 19:42:29 +00:00
2012-06-10 10:57:31 +00:00
protected SparkleConfig local_config;
2012-06-10 10:57:31 +00:00
private string identifier;
2012-06-10 10:57:31 +00:00
private SparkleListenerBase listener;
2012-07-25 09:25:52 +00:00
private SparkleWatcher watcher;
private TimeSpan poll_interval = PollInterval.Short;
private DateTime last_poll = DateTime.Now;
private DateTime progress_last_change = DateTime.Now;
private Timers.Timer remote_timer = new Timers.Timer () { Interval = 5000 };
private DisconnectReason last_disconnect_reason = DisconnectReason.None;
2012-06-10 10:57:31 +00:00
private bool is_syncing {
get { return (Status == SyncStatus.SyncUp || Status == SyncStatus.SyncDown || IsBuffering); }
2012-06-10 10:57:31 +00:00
}
private 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);
}
2012-02-08 19:42:29 +00:00
2012-07-14 10:45:54 +00:00
public SparkleRepoBase (string path, SparkleConfig config)
2011-05-19 16:05:58 +00:00
{
2012-11-28 20:17:39 +00:00
SparkleLogger.LogInfo (path, "Initializing...");
Status = SyncStatus.Idle;
Error = ErrorStatus.None;
2012-07-14 10:45:54 +00:00
this.local_config = config;
2012-07-17 13:09:57 +00:00
LocalPath = path;
Name = Path.GetFileName (LocalPath);
RemoteUrl = new Uri (this.local_config.GetUrlForFolder (Name));
IsBuffering = false;
this.identifier = Identifier;
ChangeSets = GetChangeSets ();
2011-05-19 16:05:58 +00:00
string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare");
File.SetAttributes (identifier_file_path, FileAttributes.Hidden);
if (!UseCustomWatcher)
this.watcher = new SparkleWatcher (LocalPath);
2012-07-15 13:59:04 +00:00
new Thread (() => CreateListener ()).Start ();
this.remote_timer.Elapsed += RemoteTimerElapsedDelegate;
}
2011-05-19 16:05:58 +00:00
private void RemoteTimerElapsedDelegate (object sender, EventArgs args)
{
if (this.is_syncing || IsBuffering)
return;
int time_comparison = DateTime.Compare (this.last_poll, DateTime.Now.Subtract (this.poll_interval));
if (time_comparison < 0) {
if (HasUnsyncedChanges && !this.is_syncing)
2011-05-19 16:05:58 +00:00
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 ()
{
// Sync up everything that changed since we've been offline
new Thread (() => {
if (HasRemoteChanges)
SyncDownBase ();
if (HasUnsyncedChanges || HasLocalChanges) {
2013-01-05 13:15:22 +00:00
do {
SyncUpBase ();
2013-01-05 13:15:22 +00:00
} while (HasLocalChanges);
}
if (!UseCustomWatcher)
this.watcher.ChangeEvent += OnFileActivity;
this.remote_timer.Start ();
}).Start ();
}
private Object buffer_lock = new Object ();
2011-06-14 22:14:03 +00:00
public void OnFileActivity (FileSystemEventArgs args)
2011-05-19 16:05:58 +00:00
{
if (IsBuffering || this.is_syncing)
return;
2012-10-14 18:57:13 +00:00
if (args != null) {
foreach (string exclude_path in ExcludePaths) {
if (args.FullPath.Contains (Path.DirectorySeparatorChar + exclude_path))
2012-10-14 18:57:13 +00:00
return;
}
}
2011-05-19 16:05:58 +00:00
lock (this.buffer_lock) {
if (IsBuffering || this.is_syncing || !HasLocalChanges)
return;
IsBuffering = true;
}
2011-05-19 16:05:58 +00:00
ChangesDetected ();
if (!UseCustomWatcher)
this.watcher.Disable ();
2012-07-25 09:25:52 +00:00
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Local", Name + " | Activity detected, waiting for it to settle...");
List<double> size_buffer = new List<double> ();
DirectoryInfo info = new DirectoryInfo (LocalPath);
2011-05-19 16:05:58 +00:00
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])) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("Local", Name + " | Activity has settled");
IsBuffering = false;
bool first_sync = true;
if (HasLocalChanges && Status == SyncStatus.Idle) {
do {
if (!first_sync)
SparkleLogger.LogInfo ("Local", Name + " | More changes found");
SyncUpBase ();
if (Error == ErrorStatus.UnreadableFiles)
return;
first_sync = false;
} while (HasLocalChanges);
}
2013-06-13 17:17:04 +00:00
if (Status != SyncStatus.Idle && Status != SyncStatus.Error) {
Status = SyncStatus.Idle;
SyncStatusChanged (Status);
}
} else {
Thread.Sleep (500);
2011-05-19 16:05:58 +00:00
}
} while (IsBuffering);
2012-07-25 09:25:52 +00:00
if (!UseCustomWatcher)
this.watcher.Enable ();
2011-05-19 16:05:58 +00:00
}
public void ForceRetry ()
{
if (Error == ErrorStatus.None || this.is_syncing)
return;
SyncUpBase ();
}
2012-06-10 10:57:31 +00:00
protected void OnConflictResolved ()
{
ConflictResolved ();
2012-06-10 10:57:31 +00:00
}
protected void OnProgressChanged (double progress_percentage, double progress_speed)
2012-07-17 13:09:57 +00:00
{
if (progress_percentage < 1)
return;
2012-07-17 13:09:57 +00:00
// 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)
2012-07-17 13:09:57 +00:00
return;
if (progress_percentage == 100.0)
progress_percentage = 99.0;
2012-07-17 13:09:57 +00:00
ProgressPercentage = progress_percentage;
ProgressSpeed = progress_speed;
this.progress_last_change = DateTime.Now;
2012-07-17 13:09:57 +00:00
ProgressChanged ();
2012-07-17 13:09:57 +00:00
}
private void SyncUpBase ()
2011-05-19 16:05:58 +00:00
{
if (!UseCustomWatcher)
this.watcher.Disable ();
2012-07-25 09:25:52 +00:00
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("SyncUp", Name + " | Initiated");
HasUnsyncedChanges = true;
Status = SyncStatus.SyncUp;
SyncStatusChanged (Status);
if (SyncUp ()) {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("SyncUp", Name + " | Done");
ChangeSets = GetChangeSets ();
2012-07-25 09:25:52 +00:00
HasUnsyncedChanges = false;
2012-09-29 10:35:36 +00:00
this.poll_interval = PollInterval.Long;
2012-07-25 09:25:52 +00:00
this.listener.Announce (new SparkleAnnouncement (Identifier, CurrentRevision));
2011-05-19 16:05:58 +00:00
Status = SyncStatus.Idle;
SyncStatusChanged (Status);
} else {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("SyncUp", Name + " | Error");
SyncDownBase ();
2011-05-19 16:05:58 +00:00
if (!UseCustomWatcher)
this.watcher.Disable ();
2012-07-25 09:25:52 +00:00
if (Error == ErrorStatus.None && SyncUp ()) {
2011-05-19 16:05:58 +00:00
HasUnsyncedChanges = false;
2012-02-11 19:23:38 +00:00
this.listener.Announce (new SparkleAnnouncement (Identifier, CurrentRevision));
Status = SyncStatus.Idle;
SyncStatusChanged (Status);
2011-05-19 16:05:58 +00:00
} else {
2012-09-29 10:35:36 +00:00
this.poll_interval = PollInterval.Short;
Status = SyncStatus.Error;
SyncStatusChanged (Status);
2011-05-19 16:05:58 +00:00
}
}
ProgressPercentage = 0.0;
ProgressSpeed = 0.0;
2012-07-25 09:25:52 +00:00
if (!UseCustomWatcher)
this.watcher.Enable ();
2011-05-19 16:05:58 +00:00
}
private void SyncDownBase ()
{
if (!UseCustomWatcher)
this.watcher.Disable ();
2012-07-25 09:25:52 +00:00
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("SyncDown", Name + " | Initiated");
2011-05-19 16:05:58 +00:00
Status = SyncStatus.SyncDown;
SyncStatusChanged (Status);
2011-11-07 15:39:33 +00:00
string pre_sync_revision = CurrentRevision;
2011-05-19 16:05:58 +00:00
if (SyncDown ()) {
Error = ErrorStatus.None;
2011-05-19 16:05:58 +00:00
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 (SparkleChange change in ChangeSets [0].Changes) {
if (change.Path.EndsWith (".sparkleshare")) {
emit_change_event = false;
break;
}
}
if (emit_change_event)
NewChangeSet (ChangeSets [0]);
2011-07-23 21:23:14 +00:00
}
SparkleLogger.LogInfo ("SyncDown", Name + " | Done");
2011-07-24 18:22:17 +00:00
// 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);
2012-05-19 15:17:09 +00:00
if (SyncUp ())
HasUnsyncedChanges = false;
2012-05-19 15:17:09 +00:00
}
Status = SyncStatus.Idle;
SyncStatusChanged (Status);
2011-05-19 16:05:58 +00:00
} else {
2012-07-28 13:58:09 +00:00
SparkleLogger.LogInfo ("SyncDown", Name + " | Error");
2011-05-19 16:05:58 +00:00
ChangeSets = GetChangeSets ();
Status = SyncStatus.Error;
SyncStatusChanged (Status);
2011-05-19 16:05:58 +00:00
}
2012-06-10 10:57:31 +00:00
ProgressPercentage = 0.0;
ProgressSpeed = 0.0;
Status = SyncStatus.Idle;
SyncStatusChanged (Status);
if (!UseCustomWatcher)
this.watcher.Enable ();
2012-06-10 10:57:31 +00:00
}
2012-02-08 19:42:29 +00:00
private void CreateListener ()
2011-05-19 16:05:58 +00:00
{
2012-02-08 19:42:29 +00:00
this.listener = SparkleListenerFactory.CreateListener (Name, Identifier);
if (this.listener.IsConnected)
2012-06-10 10:57:31 +00:00
this.poll_interval = PollInterval.Long;
2012-02-08 19:42:29 +00:00
this.listener.Connected += ListenerConnectedDelegate;
this.listener.Disconnected += ListenerDisconnectedDelegate;
this.listener.AnnouncementReceived += ListenerAnnouncementReceivedDelegate;
2011-05-19 16:05:58 +00:00
if (!this.listener.IsConnected && !this.listener.IsConnecting)
this.listener.Connect ();
}
private 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;
}
2012-02-08 19:42:29 +00:00
private void ListenerDisconnectedDelegate (DisconnectReason reason)
{
SparkleLogger.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 {
SparkleLogger.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 ();
}
}
2012-02-08 19:42:29 +00:00
private void ListenerAnnouncementReceivedDelegate (SparkleAnnouncement announcement)
{
string identifier = Identifier;
2012-02-08 19:42:29 +00:00
if (!announcement.FolderIdentifier.Equals (identifier))
return;
if (!announcement.Message.Equals (CurrentRevision)) {
while (this.is_syncing)
Thread.Sleep (100);
2012-09-08 09:48:01 +00:00
SparkleLogger.LogInfo (Name, "Syncing due to announcement");
SyncDownBase ();
}
}
2012-02-08 19:42:29 +00:00
// Recursively gets a folder's size in bytes
private long CalculateSize (DirectoryInfo parent)
2012-02-08 19:42:29 +00:00
{
if (ExcludePaths.Contains (parent.Name))
return 0;
long size = 0;
2012-02-08 19:42:29 +00:00
try {
2012-02-08 19:42:29 +00:00
foreach (DirectoryInfo directory in parent.GetDirectories ())
size += CalculateSize (directory);
foreach (FileInfo file in parent.GetFiles ())
size += file.Length;
} catch (Exception e) {
SparkleLogger.LogInfo ("Local", "Error calculating directory size", e);
2012-02-08 19:42:29 +00:00
}
return size;
}
2012-06-10 10:57:31 +00:00
public void Dispose ()
{
this.remote_timer.Stop ();
2012-06-10 10:57:31 +00:00
this.remote_timer.Dispose ();
this.listener.Disconnected -= ListenerDisconnectedDelegate;
this.listener.AnnouncementReceived -= ListenerAnnouncementReceivedDelegate;
2012-07-15 10:05:58 +00:00
if (!UseCustomWatcher)
this.watcher.Dispose ();
2012-06-10 10:57:31 +00:00
}
2011-05-19 16:05:58 +00:00
}
}