/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. using System;
  17. using System.Collections.Generic;
  18. using System.IO;
  19. using System.Threading;
  20. using Timers = System.Timers;
  21. namespace SparkleLib {
  22. public enum SyncStatus {
  23. Idle,
  24. Paused,
  25. SyncUp,
  26. SyncDown,
  27. Error
  28. }
  29. public enum ErrorStatus {
  30. None,
  31. HostUnreachable,
  32. HostIdentityChanged,
  33. AuthenticationFailed,
  34. DiskSpaceExceeded,
  35. UnreadableFiles,
  36. NotFound,
  37. IncompatibleClientServer
  38. }
  39. public abstract class SparkleRepoBase {
  40. public abstract bool SyncUp ();
  41. public abstract bool SyncDown ();
  42. public abstract void RestoreFile (string path, string revision, string target_file_path);
  43. public abstract bool HasUnsyncedChanges { get; set; }
  44. public abstract bool HasLocalChanges { get; }
  45. public abstract bool HasRemoteChanges { get; }
  46. public abstract string CurrentRevision { get; }
  47. public abstract double Size { get; }
  48. public abstract double HistorySize { get; }
  49. public abstract List<string> ExcludePaths { get; }
  50. public abstract List<SparkleChange> UnsyncedChanges { get; }
  51. public abstract List<SparkleChangeSet> GetChangeSets ();
  52. public abstract List<SparkleChangeSet> GetChangeSets (string path);
  53. public static bool UseCustomWatcher = false;
  54. public event SyncStatusChangedEventHandler SyncStatusChanged = delegate { };
  55. public delegate void SyncStatusChangedEventHandler (SyncStatus new_status);
  56. public event ProgressChangedEventHandler ProgressChanged = delegate { };
  57. public delegate void ProgressChangedEventHandler ();
  58. public event NewChangeSetEventHandler NewChangeSet = delegate { };
  59. public delegate void NewChangeSetEventHandler (SparkleChangeSet change_set);
  60. public event Action ConflictResolved = delegate { };
  61. public event Action ChangesDetected = delegate { };
  62. public readonly string LocalPath;
  63. public readonly string Name;
  64. public readonly Uri RemoteUrl;
  65. public List<SparkleChangeSet> ChangeSets { get; private set; }
  66. public SyncStatus Status { get; private set; }
  67. public ErrorStatus Error { get; protected set; }
  68. public bool IsBuffering { get; private set; }
  69. public double ProgressPercentage { get; private set; }
  70. public double ProgressSpeed { get; private set; }
  71. public DateTime LastSync {
  72. get {
  73. if (ChangeSets != null && ChangeSets.Count > 0)
  74. return ChangeSets [0].Timestamp;
  75. else
  76. return DateTime.MinValue;
  77. }
  78. }
  79. public virtual string Identifier {
  80. get {
  81. if (this.identifier != null)
  82. return this.identifier;
  83. string id_path = Path.Combine (LocalPath, ".sparkleshare");
  84. if (File.Exists (id_path))
  85. this.identifier = File.ReadAllText (id_path).Trim ();
  86. if (!string.IsNullOrEmpty (this.identifier)) {
  87. return this.identifier;
  88. } else {
  89. string config_identifier = this.local_config.GetIdentifierForFolder (Name);
  90. if (!string.IsNullOrEmpty (config_identifier))
  91. this.identifier = config_identifier;
  92. else
  93. this.identifier = SparkleFetcherBase.CreateIdentifier ();
  94. File.WriteAllText (id_path, this.identifier);
  95. File.SetAttributes (id_path, FileAttributes.Hidden);
  96. SparkleLogger.LogInfo ("Local", Name + " | Assigned identifier: " + this.identifier);
  97. return this.identifier;
  98. }
  99. }
  100. }
  101. protected SparkleConfig local_config;
  102. private string identifier;
  103. private SparkleListenerBase listener;
  104. private SparkleWatcher watcher;
  105. private TimeSpan poll_interval = PollInterval.Short;
  106. private DateTime last_poll = DateTime.Now;
  107. private DateTime progress_last_change = DateTime.Now;
  108. private Timers.Timer remote_timer = new Timers.Timer () { Interval = 5000 };
  109. private DisconnectReason last_disconnect_reason = DisconnectReason.None;
  110. private bool is_syncing {
  111. get { return (Status == SyncStatus.SyncUp || Status == SyncStatus.SyncDown || IsBuffering); }
  112. }
  113. private static class PollInterval {
  114. public static readonly TimeSpan Short = new TimeSpan (0, 0, 5, 0);
  115. public static readonly TimeSpan Long = new TimeSpan (0, 0, 15, 0);
  116. }
  117. public SparkleRepoBase (string path, SparkleConfig config)
  118. {
  119. SparkleLogger.LogInfo (path, "Initializing...");
  120. Status = SyncStatus.Idle;
  121. Error = ErrorStatus.None;
  122. this.local_config = config;
  123. LocalPath = path;
  124. Name = Path.GetFileName (LocalPath);
  125. RemoteUrl = new Uri (this.local_config.GetUrlForFolder (Name));
  126. IsBuffering = false;
  127. this.identifier = Identifier;
  128. ChangeSets = GetChangeSets ();
  129. string is_paused = this.local_config.GetFolderOptionalAttribute (Name, "paused");
  130. if (is_paused != null && is_paused.Equals (bool.TrueString))
  131. Status = SyncStatus.Paused;
  132. string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare");
  133. File.SetAttributes (identifier_file_path, FileAttributes.Hidden);
  134. if (!UseCustomWatcher)
  135. this.watcher = new SparkleWatcher (LocalPath);
  136. new Thread (() => CreateListener ()).Start ();
  137. this.remote_timer.Elapsed += RemoteTimerElapsedDelegate;
  138. }
  139. private void RemoteTimerElapsedDelegate (object sender, EventArgs args)
  140. {
  141. if (this.is_syncing || IsBuffering || Status == SyncStatus.Paused)
  142. return;
  143. int time_comparison = DateTime.Compare (this.last_poll, DateTime.Now.Subtract (this.poll_interval));
  144. if (time_comparison < 0) {
  145. if (HasUnsyncedChanges && !this.is_syncing)
  146. SyncUpBase ();
  147. this.last_poll = DateTime.Now;
  148. if (HasRemoteChanges && !this.is_syncing)
  149. SyncDownBase ();
  150. if (this.listener.IsConnected)
  151. this.poll_interval = PollInterval.Long;
  152. }
  153. // In the unlikely case that we haven't synced up our
  154. // changes or the server was down, sync up again
  155. if (HasUnsyncedChanges && !this.is_syncing && Error == ErrorStatus.None)
  156. SyncUpBase ();
  157. if (Status != SyncStatus.Idle && Status != SyncStatus.Error) {
  158. Status = SyncStatus.Idle;
  159. SyncStatusChanged (Status);
  160. }
  161. }
  162. public void Initialize ()
  163. {
  164. // Sync up everything that changed since we've been offline
  165. new Thread (() => {
  166. if (Status != SyncStatus.Paused) {
  167. if (HasRemoteChanges)
  168. SyncDownBase ();
  169. if (HasUnsyncedChanges || HasLocalChanges) {
  170. do {
  171. SyncUpBase ();
  172. } while (HasLocalChanges);
  173. }
  174. }
  175. if (!UseCustomWatcher)
  176. this.watcher.ChangeEvent += OnFileActivity;
  177. this.remote_timer.Start ();
  178. }).Start ();
  179. }
  180. private Object buffer_lock = new Object ();
  181. public void OnFileActivity (FileSystemEventArgs args)
  182. {
  183. if (IsBuffering || this.is_syncing)
  184. return;
  185. if (args != null) {
  186. foreach (string exclude_path in ExcludePaths) {
  187. if (args.FullPath.Contains (Path.DirectorySeparatorChar + exclude_path))
  188. return;
  189. }
  190. }
  191. if (Status == SyncStatus.Paused) {
  192. ChangesDetected ();
  193. return;
  194. }
  195. lock (this.buffer_lock) {
  196. if (IsBuffering || this.is_syncing || !HasLocalChanges)
  197. return;
  198. IsBuffering = true;
  199. }
  200. ChangesDetected ();
  201. if (!UseCustomWatcher)
  202. this.watcher.Disable ();
  203. SparkleLogger.LogInfo ("Local", Name + " | Activity detected, waiting for it to settle...");
  204. List<double> size_buffer = new List<double> ();
  205. DirectoryInfo info = new DirectoryInfo (LocalPath);
  206. do {
  207. if (size_buffer.Count >= 4)
  208. size_buffer.RemoveAt (0);
  209. size_buffer.Add (CalculateSize (info));
  210. if (size_buffer.Count >= 4 &&
  211. size_buffer [0].Equals (size_buffer [1]) &&
  212. size_buffer [1].Equals (size_buffer [2]) &&
  213. size_buffer [2].Equals (size_buffer [3])) {
  214. SparkleLogger.LogInfo ("Local", Name + " | Activity has settled");
  215. IsBuffering = false;
  216. bool first_sync = true;
  217. if (HasLocalChanges && Status == SyncStatus.Idle) {
  218. do {
  219. if (!first_sync)
  220. SparkleLogger.LogInfo ("Local", Name + " | More changes found");
  221. SyncUpBase ();
  222. if (Error == ErrorStatus.UnreadableFiles)
  223. return;
  224. first_sync = false;
  225. } while (HasLocalChanges);
  226. }
  227. if (Status != SyncStatus.Idle && Status != SyncStatus.Error) {
  228. Status = SyncStatus.Idle;
  229. SyncStatusChanged (Status);
  230. }
  231. } else {
  232. Thread.Sleep (500);
  233. }
  234. } while (IsBuffering);
  235. if (!UseCustomWatcher)
  236. this.watcher.Enable ();
  237. }
  238. public void ForceRetry ()
  239. {
  240. if (Error != ErrorStatus.None && !this.is_syncing)
  241. SyncUpBase ();
  242. }
  243. protected void OnConflictResolved ()
  244. {
  245. ConflictResolved ();
  246. }
  247. protected void OnProgressChanged (double progress_percentage, double progress_speed)
  248. {
  249. if (progress_percentage < 1)
  250. return;
  251. // Only trigger the ProgressChanged event once per second
  252. if (DateTime.Compare (this.progress_last_change, DateTime.Now.Subtract (new TimeSpan (0, 0, 0, 1))) >= 0)
  253. return;
  254. if (progress_percentage == 100.0)
  255. progress_percentage = 99.0;
  256. ProgressPercentage = progress_percentage;
  257. ProgressSpeed = progress_speed;
  258. this.progress_last_change = DateTime.Now;
  259. ProgressChanged ();
  260. }
  261. private void SyncUpBase ()
  262. {
  263. if (!UseCustomWatcher)
  264. this.watcher.Disable ();
  265. SparkleLogger.LogInfo ("SyncUp", Name + " | Initiated");
  266. HasUnsyncedChanges = true;
  267. Status = SyncStatus.SyncUp;
  268. SyncStatusChanged (Status);
  269. if (SyncUp ()) {
  270. SparkleLogger.LogInfo ("SyncUp", Name + " | Done");
  271. ChangeSets = GetChangeSets ();
  272. HasUnsyncedChanges = false;
  273. this.poll_interval = PollInterval.Long;
  274. this.listener.Announce (new SparkleAnnouncement (Identifier, CurrentRevision));
  275. Status = SyncStatus.Idle;
  276. SyncStatusChanged (Status);
  277. } else {
  278. SparkleLogger.LogInfo ("SyncUp", Name + " | Error");
  279. SyncDownBase ();
  280. if (!UseCustomWatcher)
  281. this.watcher.Disable ();
  282. if (Error == ErrorStatus.None && SyncUp ()) {
  283. HasUnsyncedChanges = false;
  284. this.listener.Announce (new SparkleAnnouncement (Identifier, CurrentRevision));
  285. Status = SyncStatus.Idle;
  286. SyncStatusChanged (Status);
  287. } else {
  288. this.poll_interval = PollInterval.Short;
  289. Status = SyncStatus.Error;
  290. SyncStatusChanged (Status);
  291. }
  292. }
  293. ProgressPercentage = 0.0;
  294. ProgressSpeed = 0.0;
  295. if (!UseCustomWatcher)
  296. this.watcher.Enable ();
  297. this.status_message = "";
  298. }
  299. private void SyncDownBase ()
  300. {
  301. if (!UseCustomWatcher)
  302. this.watcher.Disable ();
  303. SparkleLogger.LogInfo ("SyncDown", Name + " | Initiated");
  304. Status = SyncStatus.SyncDown;
  305. SyncStatusChanged (Status);
  306. string pre_sync_revision = CurrentRevision;
  307. if (SyncDown ()) {
  308. Error = ErrorStatus.None;
  309. string identifier_file_path = Path.Combine (LocalPath, ".sparkleshare");
  310. File.SetAttributes (identifier_file_path, FileAttributes.Hidden);
  311. ChangeSets = GetChangeSets ();
  312. if (!pre_sync_revision.Equals (CurrentRevision) &&
  313. ChangeSets != null && ChangeSets.Count > 0 &&
  314. !ChangeSets [0].User.Name.Equals (this.local_config.User.Name)) {
  315. bool emit_change_event = true;
  316. foreach (SparkleChange change in ChangeSets [0].Changes) {
  317. if (change.Path.EndsWith (".sparkleshare")) {
  318. emit_change_event = false;
  319. break;
  320. }
  321. }
  322. if (emit_change_event)
  323. NewChangeSet (ChangeSets [0]);
  324. }
  325. SparkleLogger.LogInfo ("SyncDown", Name + " | Done");
  326. // There could be changes from a resolved
  327. // conflict. Tries only once, then lets
  328. // the timer try again periodically
  329. if (HasUnsyncedChanges) {
  330. Status = SyncStatus.SyncUp;
  331. SyncStatusChanged (Status);
  332. if (SyncUp ())
  333. HasUnsyncedChanges = false;
  334. }
  335. Status = SyncStatus.Idle;
  336. SyncStatusChanged (Status);
  337. } else {
  338. SparkleLogger.LogInfo ("SyncDown", Name + " | Error");
  339. ChangeSets = GetChangeSets ();
  340. Status = SyncStatus.Error;
  341. SyncStatusChanged (Status);
  342. }
  343. ProgressPercentage = 0.0;
  344. ProgressSpeed = 0.0;
  345. Status = SyncStatus.Idle;
  346. SyncStatusChanged (Status);
  347. if (!UseCustomWatcher)
  348. this.watcher.Enable ();
  349. }
  350. private void CreateListener ()
  351. {
  352. this.listener = SparkleListenerFactory.CreateListener (Name, Identifier);
  353. if (this.listener.IsConnected)
  354. this.poll_interval = PollInterval.Long;
  355. this.listener.Connected += ListenerConnectedDelegate;
  356. this.listener.Disconnected += ListenerDisconnectedDelegate;
  357. this.listener.AnnouncementReceived += ListenerAnnouncementReceivedDelegate;
  358. if (!this.listener.IsConnected && !this.listener.IsConnecting)
  359. this.listener.Connect ();
  360. }
  361. private void ListenerConnectedDelegate ()
  362. {
  363. if (this.last_disconnect_reason == DisconnectReason.SystemSleep) {
  364. this.last_disconnect_reason = DisconnectReason.None;
  365. if (HasRemoteChanges && !this.is_syncing)
  366. SyncDownBase ();
  367. }
  368. this.poll_interval = PollInterval.Long;
  369. }
  370. private void ListenerDisconnectedDelegate (DisconnectReason reason)
  371. {
  372. SparkleLogger.LogInfo (Name, "Falling back to regular polling");
  373. this.poll_interval = PollInterval.Short;
  374. this.last_disconnect_reason = reason;
  375. if (reason == DisconnectReason.SystemSleep) {
  376. this.remote_timer.Stop ();
  377. int backoff_time = 2;
  378. do {
  379. SparkleLogger.LogInfo (Name, "Next reconnect attempt in " + backoff_time + " seconds");
  380. Thread.Sleep (backoff_time * 1000);
  381. this.listener.Connect ();
  382. backoff_time *= 2;
  383. } while (backoff_time < 64 && !this.listener.IsConnected);
  384. this.remote_timer.Start ();
  385. }
  386. }
  387. private void ListenerAnnouncementReceivedDelegate (SparkleAnnouncement announcement)
  388. {
  389. string identifier = Identifier;
  390. if (!announcement.FolderIdentifier.Equals (identifier))
  391. return;
  392. if (!announcement.Message.Equals (CurrentRevision)) {
  393. while (this.is_syncing)
  394. Thread.Sleep (100);
  395. SparkleLogger.LogInfo (Name, "Syncing due to announcement");
  396. if (Status == SyncStatus.Paused)
  397. SparkleLogger.LogInfo (Name, "We're paused, skipping sync");
  398. else
  399. SyncDownBase ();
  400. }
  401. }
  402. // Recursively gets a folder's size in bytes
  403. private long CalculateSize (DirectoryInfo parent)
  404. {
  405. if (ExcludePaths.Contains (parent.Name))
  406. return 0;
  407. long size = 0;
  408. try {
  409. foreach (DirectoryInfo directory in parent.GetDirectories ())
  410. size += CalculateSize (directory);
  411. foreach (FileInfo file in parent.GetFiles ())
  412. size += file.Length;
  413. } catch (Exception e) {
  414. SparkleLogger.LogInfo ("Local", "Error calculating directory size", e);
  415. }
  416. return size;
  417. }
  418. public void Pause ()
  419. {
  420. if (Status == SyncStatus.Idle) {
  421. this.local_config.SetFolderOptionalAttribute (Name, "paused", bool.TrueString);
  422. Status = SyncStatus.Paused;
  423. }
  424. }
  425. protected string status_message = "";
  426. public void Resume (string message)
  427. {
  428. this.status_message = message;
  429. if (Status == SyncStatus.Paused) {
  430. this.local_config.SetFolderOptionalAttribute (Name, "paused", bool.FalseString);
  431. Status = SyncStatus.Idle;
  432. if (HasUnsyncedChanges || HasLocalChanges) {
  433. do {
  434. SyncUpBase ();
  435. } while (HasLocalChanges);
  436. }
  437. }
  438. }
  439. public void Dispose ()
  440. {
  441. this.remote_timer.Stop ();
  442. this.remote_timer.Dispose ();
  443. this.listener.Disconnected -= ListenerDisconnectedDelegate;
  444. this.listener.AnnouncementReceived -= ListenerAnnouncementReceivedDelegate;
  445. this.listener.Dispose ();
  446. if (!UseCustomWatcher)
  447. this.watcher.Dispose ();
  448. }
  449. }
  450. }