Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ SharpSync serves as the core sync library for **Nimbus**, an accessible Nextclou

| Feature | Status | Notes |
|---------|--------|-------|
| Progress events | Excellent | `ProgressChanged` event with percentage, item counts, current file |
| Progress events | Excellent | `ProgressChanged` (item-level) and `FileProgressChanged` (per-file byte-level) |
| Conflict events | Excellent | `ConflictDetected` with rich `ConflictAnalysis` data |
| Sync preview | Excellent | `GetSyncPlanAsync()` returns detailed plan before execution |
| OAuth2 abstraction | Good | `IOAuth2Provider` interface for app-specific implementation |
Expand Down Expand Up @@ -275,12 +275,21 @@ var engine = new SyncEngine(localStorage, remoteStorage, database);
// 3. Wire up UI binding via events
engine.ProgressChanged += (s, e) => {
Dispatcher.Invoke(() => {
ProgressBar.Value = e.Progress.Percentage;
OverallProgressBar.Value = e.Progress.Percentage;
StatusLabel.Text = $"Syncing: {e.Progress.CurrentItem}";
ItemCountLabel.Text = $"{e.Progress.ProcessedItems}/{e.Progress.TotalItems}";
});
};

// 3b. Wire up per-file transfer progress for detailed UI
engine.FileProgressChanged += (s, e) => {
Dispatcher.Invoke(() => {
FileProgressBar.Value = e.PercentComplete;
FileProgressLabel.Text = $"{e.Path}: {e.BytesTransferred / 1024}KB / {e.TotalBytes / 1024}KB";
TransferTypeLabel.Text = e.Operation == FileTransferOperation.Upload ? "Uploading" : "Downloading";
});
};

// 4. Implement conflict resolution with UI dialogs
var resolver = new SmartConflictResolver(
conflictHandler: async (analysis, ct) => {
Expand Down Expand Up @@ -361,15 +370,17 @@ var deleted = await engine.ClearOperationHistoryAsync(DateTime.UtcNow.AddDays(-3
| GetSyncPlanAsync integration | `GetSyncPlanAsync()` now incorporates pending changes from notifications |
| Activity history | `GetRecentOperationsAsync()` - query completed operations for activity feed |
| History cleanup | `ClearOperationHistoryAsync()` - purge old operation records |
| Per-file progress | `FileProgressChanged` event on `ISyncEngine` - byte-level progress for individual file transfers |

### Required SharpSync API Additions (v1.0)

These APIs are required for v1.0 release to support Nimbus desktop client:

**Progress & History:**
1. Per-file progress events (currently only per-sync-operation)

**✅ Completed:**
- `FileProgressChanged` event on `ISyncEngine` - Per-file byte-level progress during uploads/downloads
- `FileProgressEventArgs` - Per-file progress data with path, bytes transferred, total bytes, and operation type
- `FileTransferOperation` enum - Upload/Download operation type for per-file progress
- `ISyncStorage.ProgressChanged` - Standardized progress event on the storage interface
- OCIS TUS 1.0.0 protocol - Resumable uploads for OCIS servers with chunked transfer and fallback
- `GetRecentOperationsAsync()` - Operation history for activity feed with time filtering
- `ClearOperationHistoryAsync()` - Cleanup old operation history entries
Expand Down Expand Up @@ -401,15 +412,13 @@ These APIs are required for v1.0 release to support Nimbus desktop client:
| Core sync engine | 9/10 | Production-ready, well-tested |
| Nextcloud WebDAV | 9/10 | Full support including OCIS TUS protocol |
| OAuth2 abstraction | 9/10 | Clean interface, Nimbus implements |
| UI binding (events) | 9/10 | Excellent progress/conflict events |
| UI binding (events) | 10/10 | Per-file byte-level progress, item-level progress, conflict events |
| Conflict resolution | 9/10 | Rich analysis, extensible callbacks |
| Selective sync | 10/10 | Complete: folder/file/incremental sync, batch notifications, rename tracking |
| Pause/Resume | 10/10 | Fully implemented with graceful pause points |
| Desktop integration hooks | 10/10 | Virtual file callback, bandwidth throttling, pause/resume, pending operations |

**Current Overall: 9.5/10** - Production-ready with comprehensive desktop client APIs

**Target for v1.0: 9.7/10** - Per-file progress remaining
**Current Overall: 10/10** - Production-ready with comprehensive desktop client APIs including per-file progress

## Version 1.0 Release Readiness

Expand Down Expand Up @@ -461,11 +470,7 @@ All critical items have been resolved.
- BenchmarkDotNet suite for sync operations
- Helps track performance regressions

5. **Per-file Progress Events**
- Currently only per-sync-operation progress
- Would improve UI granularity for large file transfers

6. **Advanced Filtering (Regex Support)**
5. **Advanced Filtering (Regex Support)**
- Current glob patterns are sufficient for most use cases

### 📊 Quality Metrics for v1.0
Expand Down Expand Up @@ -499,4 +504,5 @@ All critical items have been resolved.
- ✅ FileSystemWatcher integration (`NotifyLocalChangeAsync()`, `NotifyLocalChangesAsync()`, `NotifyLocalRenameAsync()`)
- ✅ Pending operations query (`GetPendingOperationsAsync()`)
- ✅ Activity history (`GetRecentOperationsAsync()`, `ClearOperationHistoryAsync()`)
- ✅ Per-file progress events (`FileProgressChanged` on `ISyncEngine`, `FileProgressEventArgs`, `FileTransferOperation`)
- ✅ Examples directory with working samples
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,19 @@ else
### With Progress Reporting

```csharp
// Item-level progress (overall sync progress)
engine.ProgressChanged += (sender, e) =>
{
Console.WriteLine($"[{e.Progress.Percentage:F1}%] {e.Progress.CurrentItem}");
Console.WriteLine($" {e.Progress.ProcessedItems}/{e.Progress.TotalItems} items");
};

// Per-file byte-level progress (individual file transfer progress)
engine.FileProgressChanged += (sender, e) =>
{
Console.WriteLine($" {e.Operation}: {e.Path} - {e.PercentComplete}% ({e.BytesTransferred}/{e.TotalBytes} bytes)");
};

var result = await engine.SynchronizeAsync();
```

Expand Down
5 changes: 5 additions & 0 deletions examples/BasicSyncExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public static async Task BasicSyncAsync() {
Console.WriteLine($"[{e.Progress.Percentage:F0}%] {e.Operation}: {e.Progress.CurrentItem}");
};

// Per-file byte-level progress for large file transfers
syncEngine.FileProgressChanged += (sender, e) => {
Console.WriteLine($" {e.Operation}: {e.Path} - {e.PercentComplete}% ({e.BytesTransferred}/{e.TotalBytes} bytes)");
};

syncEngine.ConflictDetected += (sender, e) => {
Console.WriteLine($"Conflict: {e.Path}");
};
Expand Down
69 changes: 69 additions & 0 deletions src/SharpSync/Core/FileProgressEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Oire.SharpSync.Core;

/// <summary>
/// Event arguments for per-file transfer progress during sync operations.
/// </summary>
/// <remarks>
/// <para>
/// This event provides byte-level progress for individual file transfers,
/// allowing UI applications to display detailed progress bars for large files.
/// </para>
/// <para>
/// This complements <see cref="SyncProgressEventArgs"/> which reports item-level progress
/// (number of files processed). Use both events together for comprehensive progress reporting:
/// <list type="bullet">
/// <item><description><see cref="SyncProgressEventArgs"/>: Overall sync progress (X of Y files)</description></item>
/// <item><description><see cref="FileProgressEventArgs"/>: Current file progress (X of Y bytes)</description></item>
/// </list>
/// </para>
/// </remarks>
/// <example>
/// <code>
/// engine.FileProgressChanged += (sender, e) => {
/// // Update per-file progress bar
/// FileProgressBar.Value = e.PercentComplete;
/// FileProgressLabel.Text = $"{e.Path}: {e.BytesTransferred / 1024}KB / {e.TotalBytes / 1024}KB";
/// OperationLabel.Text = e.Operation == FileTransferOperation.Upload ? "Uploading" : "Downloading";
/// };
/// </code>
/// </example>
public class FileProgressEventArgs: EventArgs {
/// <summary>
/// Gets the relative path of the file being transferred.
/// </summary>
public string Path { get; }

/// <summary>
/// Gets the number of bytes transferred so far.
/// </summary>
public long BytesTransferred { get; }

/// <summary>
/// Gets the total number of bytes to transfer.
/// </summary>
public long TotalBytes { get; }

/// <summary>
/// Gets the type of transfer operation (upload or download).
/// </summary>
public FileTransferOperation Operation { get; }

/// <summary>
/// Gets the percentage complete (0-100).
/// </summary>
public int PercentComplete => TotalBytes > 0 ? (int)(BytesTransferred * 100 / TotalBytes) : 0;

/// <summary>
/// Initializes a new instance of the <see cref="FileProgressEventArgs"/> class.
/// </summary>
/// <param name="path">The relative path of the file being transferred</param>
/// <param name="bytesTransferred">The number of bytes transferred so far</param>
/// <param name="totalBytes">The total number of bytes to transfer</param>
/// <param name="operation">The type of transfer operation</param>
public FileProgressEventArgs(string path, long bytesTransferred, long totalBytes, FileTransferOperation operation) {
Path = path;
BytesTransferred = bytesTransferred;
TotalBytes = totalBytes;
Operation = operation;
}
}
16 changes: 16 additions & 0 deletions src/SharpSync/Core/FileTransferOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Oire.SharpSync.Core;

/// <summary>
/// Type of file transfer operation for per-file progress reporting.
/// </summary>
public enum FileTransferOperation {
/// <summary>
/// Uploading a file to remote storage.
/// </summary>
Upload,

/// <summary>
/// Downloading a file from remote storage.
/// </summary>
Download
}
34 changes: 33 additions & 1 deletion src/SharpSync/Core/ISyncEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,42 @@ namespace Oire.SharpSync.Core;
/// </remarks>
public interface ISyncEngine: IDisposable {
/// <summary>
/// Event raised to report synchronization progress
/// Event raised to report synchronization progress at the item level.
/// </summary>
/// <remarks>
/// This event reports overall sync progress (number of files processed out of total).
/// For per-file byte-level progress during large file transfers, subscribe to
/// <see cref="FileProgressChanged"/> instead.
/// </remarks>
event EventHandler<SyncProgressEventArgs>? ProgressChanged;

/// <summary>
/// Event raised to report per-file transfer progress during uploads and downloads.
/// </summary>
/// <remarks>
/// <para>
/// This event provides byte-level progress for individual file transfers,
/// allowing UI applications to display detailed progress for large files.
/// </para>
/// <para>
/// This complements <see cref="ProgressChanged"/> which reports item-level progress
/// (X of Y files). Use both events for comprehensive progress reporting:
/// <list type="bullet">
/// <item><description><see cref="ProgressChanged"/>: Overall sync progress (X of Y files)</description></item>
/// <item><description><see cref="FileProgressChanged"/>: Current file progress (X of Y bytes)</description></item>
/// </list>
/// </para>
/// </remarks>
/// <example>
/// <code>
/// engine.FileProgressChanged += (sender, e) => {
/// FileProgressBar.Value = e.PercentComplete;
/// FileProgressLabel.Text = $"{e.Path}: {e.BytesTransferred / 1024}KB / {e.TotalBytes / 1024}KB";
/// };
/// </code>
/// </example>
event EventHandler<FileProgressEventArgs>? FileProgressChanged;

/// <summary>
/// Event raised when a file conflict is detected
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/SharpSync/Core/ISyncStorage.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
using Oire.SharpSync.Storage;

namespace Oire.SharpSync.Core;

/// <summary>
/// Represents a storage backend for synchronization (local filesystem, WebDAV, etc.)
/// </summary>
public interface ISyncStorage {
/// <summary>
/// Event raised to report transfer progress for large files.
/// </summary>
/// <remarks>
/// Storage implementations should raise this event during file uploads and downloads
/// to report byte-level progress. This enables UIs to display per-file progress bars.
/// </remarks>
event EventHandler<StorageProgressEventArgs>? ProgressChanged;

/// <summary>
/// Gets the storage type
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/SharpSync/Storage/LocalFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ namespace Oire.SharpSync.Storage;
/// Local filesystem storage implementation
/// </summary>
public class LocalFileStorage: ISyncStorage {
/// <summary>
/// Event raised to report transfer progress for file operations.
/// </summary>
/// <remarks>
/// <para>
/// This event is not typically raised by local file storage because local
/// filesystem operations are very fast compared to network transfers.
/// </para>
/// <para>
/// The event is implemented to satisfy the <see cref="ISyncStorage"/> interface,
/// allowing consistent handling across all storage types.
/// </para>
/// </remarks>
#pragma warning disable CS0067 // Event is never used (intentional - local storage doesn't report progress)
public event EventHandler<StorageProgressEventArgs>? ProgressChanged;
#pragma warning restore CS0067

/// <summary>
/// Gets the storage type (always returns <see cref="Core.StorageType.Local"/>)
/// </summary>
Expand Down
38 changes: 38 additions & 0 deletions src/SharpSync/Sync/SyncEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ public class SyncEngine: ISyncEngine {
/// </summary>
public event EventHandler<SyncProgressEventArgs>? ProgressChanged;

/// <summary>
/// Event raised to report per-file transfer progress during uploads and downloads.
/// </summary>
/// <remarks>
/// <para>
/// This event provides byte-level progress for individual file transfers,
/// allowing UI applications to display detailed progress for large files.
/// </para>
/// <para>
/// This complements <see cref="ProgressChanged"/> which reports item-level progress
/// (X of Y files). Use both events for comprehensive progress reporting.
/// </para>
/// </remarks>
public event EventHandler<FileProgressEventArgs>? FileProgressChanged;

/// <summary>
/// Event raised when a conflict is detected
/// </summary>
Expand Down Expand Up @@ -142,6 +157,10 @@ public SyncEngine(
_changeDetectionWindow = changeDetectionWindow ?? TimeSpan.FromSeconds(2);

_syncSemaphore = new SemaphoreSlim(1, 1);

// Subscribe to storage progress events for per-file progress reporting
_localStorage.ProgressChanged += OnStorageProgressChanged;
_remoteStorage.ProgressChanged += OnStorageProgressChanged;
}


Expand Down Expand Up @@ -1375,6 +1394,21 @@ private void RaiseProgress(SyncProgress progress, SyncOperation operation) {
ProgressChanged?.Invoke(this, new SyncProgressEventArgs(progress, progress.CurrentItem, operation));
}

private void RaiseFileProgress(string path, long bytesTransferred, long totalBytes, FileTransferOperation operation) {
FileProgressChanged?.Invoke(this, new FileProgressEventArgs(path, bytesTransferred, totalBytes, operation));
}

/// <summary>
/// Handles storage progress events and forwards them to the FileProgressChanged event.
/// </summary>
private void OnStorageProgressChanged(object? sender, StorageProgressEventArgs e) {
var operation = e.Operation == StorageOperation.Upload
? FileTransferOperation.Upload
: FileTransferOperation.Download;

RaiseFileProgress(e.Path, e.BytesTransferred, e.TotalBytes, operation);
}

/// <summary>
/// Gets synchronization database statistics including file counts and sizes.
/// </summary>
Expand Down Expand Up @@ -2169,6 +2203,10 @@ public async Task<int> ClearOperationHistoryAsync(DateTime olderThan, Cancellati
/// </remarks>
public void Dispose() {
if (!_disposed) {
// Unsubscribe from storage progress events
_localStorage.ProgressChanged -= OnStorageProgressChanged;
_remoteStorage.ProgressChanged -= OnStorageProgressChanged;

// Resume any paused operation so it can exit cleanly
_pauseEvent.Set();
_currentSyncCts?.Cancel();
Expand Down
Loading
Loading