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
28 changes: 24 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,22 @@ await engine.SyncFolderAsync("Documents/Important");

// 9. Or sync specific files
await engine.SyncFilesAsync(new[] { "notes.txt", "config.json" });

// 10. Display activity history in UI
var recentOps = await engine.GetRecentOperationsAsync(limit: 50);
foreach (var op in recentOps) {
var icon = op.ActionType switch {
SyncActionType.Upload => "↑",
SyncActionType.Download => "↓",
SyncActionType.DeleteLocal or SyncActionType.DeleteRemote => "×",
_ => "?"
};
var status = op.Success ? "✓" : "✗";
ActivityList.Items.Add($"{status} {icon} {op.Path} ({op.Duration.TotalSeconds:F1}s)");
}

// 11. Periodic cleanup of old history (e.g., on app startup)
var deleted = await engine.ClearOperationHistoryAsync(DateTime.UtcNow.AddDays(-30));
```

### Current API Gaps (To Be Resolved in v1.0)
Expand All @@ -331,6 +347,8 @@ await engine.SyncFilesAsync(new[] { "notes.txt", "config.json" });
| Pending operations query | `GetPendingOperationsAsync()` - inspect sync queue for UI display |
| Clear pending changes | `ClearPendingChanges()` - discard pending notifications without syncing |
| 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 |

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

Expand All @@ -341,9 +359,11 @@ These APIs are required for v1.0 release to support Nimbus desktop client:

**Progress & History:**
2. Per-file progress events (currently only per-sync-operation)
3. `GetRecentOperationsAsync()` - Operation history for activity feed

**✅ Completed:**
- `GetRecentOperationsAsync()` - Operation history for activity feed with time filtering
- `ClearOperationHistoryAsync()` - Cleanup old operation history entries
- `CompletedOperation` model - Rich operation details with timing, success/failure, rename tracking
- `SyncOptions.MaxBytesPerSecond` - Built-in bandwidth throttling
- `SyncOptions.VirtualFileCallback` - Hook for virtual file systems (Windows Cloud Files API)
- `SyncOptions.CreateVirtualFilePlaceholders` - Enable/disable virtual file placeholder creation
Expand Down Expand Up @@ -515,7 +535,7 @@ The core library is production-ready, but several critical items must be address
- ⚠️ All storage implementations tested (LocalFileStorage ✅, SftpStorage ✅, FtpStorage ✅, S3Storage ✅, WebDavStorage ❌)
- ❌ README matches actual API (completely wrong)
- ✅ No TODOs/FIXMEs in code (achieved)
- Examples directory exists (missing)
- Examples directory exists (created)
- ✅ Package metadata accurate (SFTP, FTP, and S3 now implemented!)
- ✅ Integration test infrastructure (Docker-based CI testing for SFTP, FTP, and S3)

Expand Down Expand Up @@ -550,12 +570,12 @@ Documentation & Testing:
- [ ] WebDavStorage integration tests
- [ ] Multi-platform CI testing (Windows, macOS)
- [ ] Code coverage reporting
- [ ] Examples directory with working samples
- [x] Examples directory with working samples

Desktop Client APIs (for Nimbus):
- [ ] OCIS TUS protocol implementation (currently falls back to generic upload at `WebDavStorage.cs:547`)
- [ ] Per-file progress events (currently only per-sync-operation)
- [ ] `GetRecentOperationsAsync()` - Operation history for activity feed
- [x] `GetRecentOperationsAsync()` - Operation history for activity feed

Performance & Polish:
- [ ] Performance benchmarks with BenchmarkDotNet
Expand Down
238 changes: 238 additions & 0 deletions examples/BasicSyncExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// =============================================================================
// SharpSync Basic Usage Example
// =============================================================================
// This file demonstrates how to use SharpSync for file synchronization.
// Copy this code into your own project that references the SharpSync NuGet package.
//
// Required NuGet packages:
// - Oire.SharpSync
// - Microsoft.Extensions.Logging.Console (optional, for logging)
// =============================================================================

using Microsoft.Extensions.Logging;
using Oire.SharpSync.Core;
using Oire.SharpSync.Database;
using Oire.SharpSync.Storage;
using Oire.SharpSync.Sync;

namespace YourApp;

public class SyncExample {
/// <summary>
/// Basic example: Sync local folder with a remote storage.
/// </summary>
public static async Task BasicSyncAsync() {
// 1. Create storage instances
var localStorage = new LocalFileStorage("/path/to/local/folder");

// For remote storage, choose one:
// - WebDavStorage for Nextcloud/ownCloud/WebDAV servers
// - SftpStorage for SFTP servers
// - FtpStorage for FTP/FTPS servers
// - S3Storage for AWS S3 or S3-compatible storage (MinIO, etc.)
var remoteStorage = new LocalFileStorage("/path/to/remote/folder"); // Demo only

// 2. Create and initialize sync database
var database = new SqliteSyncDatabase("/path/to/sync.db");
await database.InitializeAsync();

// 3. Create filter for selective sync (optional)
var filter = new SyncFilter();
filter.AddExcludePattern("*.tmp");
filter.AddExcludePattern("*.log");
filter.AddExcludePattern(".git/**");
filter.AddExcludePattern("node_modules/**");

// 4. Create conflict resolver
var conflictResolver = new DefaultConflictResolver(ConflictResolution.UseNewer);

// 5. Create sync engine
using var syncEngine = new SyncEngine(
localStorage,
remoteStorage,
database,
filter,
conflictResolver);

// 6. Wire up events for UI updates
syncEngine.ProgressChanged += (sender, e) => {
Console.WriteLine($"[{e.Progress.Percentage:F0}%] {e.Operation}: {e.Progress.CurrentItem}");
};

syncEngine.ConflictDetected += (sender, e) => {
Console.WriteLine($"Conflict: {e.Path}");
};

// 7. Run synchronization
var result = await syncEngine.SynchronizeAsync();

Console.WriteLine($"Sync completed: {result.FilesSynchronized} files synchronized");
}

/// <summary>
/// Preview changes before syncing.
/// </summary>
public static async Task PreviewSyncAsync(ISyncEngine syncEngine) {
var plan = await syncEngine.GetSyncPlanAsync();

Console.WriteLine($"Uploads planned: {plan.Uploads.Count}");
foreach (var upload in plan.Uploads) {
Console.WriteLine($" + {upload.Path} ({upload.Size} bytes)");
}

Console.WriteLine($"Downloads planned: {plan.Downloads.Count}");
foreach (var download in plan.Downloads) {
Console.WriteLine($" - {download.Path} ({download.Size} bytes)");
}

Console.WriteLine($"Conflicts: {plan.Conflicts.Count}");
}

/// <summary>
/// Display activity history (recent operations).
/// </summary>
public static async Task ShowActivityHistoryAsync(ISyncEngine syncEngine) {
// Get last 50 operations
var recentOps = await syncEngine.GetRecentOperationsAsync(limit: 50);

Console.WriteLine("=== Recent Sync Activity ===");
foreach (var op in recentOps) {
var icon = op.ActionType switch {
SyncActionType.Upload => "↑",
SyncActionType.Download => "↓",
SyncActionType.DeleteLocal or SyncActionType.DeleteRemote => "×",
SyncActionType.Conflict => "!",
_ => "?"
};
var status = op.Success ? "✓" : "✗";
Console.WriteLine($"{status} {icon} {op.Path} ({op.Duration.TotalSeconds:F1}s)");
}

// Get operations from last hour only
var lastHour = await syncEngine.GetRecentOperationsAsync(
limit: 100,
since: DateTime.UtcNow.AddHours(-1));
Console.WriteLine($"\nOperations in last hour: {lastHour.Count}");

// Cleanup old history (e.g., on app startup)
var deleted = await syncEngine.ClearOperationHistoryAsync(DateTime.UtcNow.AddDays(-30));
Console.WriteLine($"Cleaned up {deleted} old operation records");
}

/// <summary>
/// Integrate with FileSystemWatcher for real-time sync.
/// </summary>
public static void SetupFileSystemWatcher(ISyncEngine syncEngine, string localPath) {
var watcher = new FileSystemWatcher(localPath) {
IncludeSubdirectories = true,
EnableRaisingEvents = true
};

watcher.Created += async (s, e) => {
var relativePath = Path.GetRelativePath(localPath, e.FullPath);
await syncEngine.NotifyLocalChangeAsync(relativePath, ChangeType.Created);
};

watcher.Changed += async (s, e) => {
var relativePath = Path.GetRelativePath(localPath, e.FullPath);
await syncEngine.NotifyLocalChangeAsync(relativePath, ChangeType.Changed);
};

watcher.Deleted += async (s, e) => {
var relativePath = Path.GetRelativePath(localPath, e.FullPath);
await syncEngine.NotifyLocalChangeAsync(relativePath, ChangeType.Deleted);
};

watcher.Renamed += async (s, e) => {
var oldRelativePath = Path.GetRelativePath(localPath, e.OldFullPath);
var newRelativePath = Path.GetRelativePath(localPath, e.FullPath);
await syncEngine.NotifyLocalRenameAsync(oldRelativePath, newRelativePath);
};

// Check pending operations
Task.Run(async () => {
var pending = await syncEngine.GetPendingOperationsAsync();
Console.WriteLine($"Pending operations: {pending.Count}");
});
}

/// <summary>
/// Sync specific files on demand.
/// </summary>
public static async Task SyncSpecificFilesAsync(ISyncEngine syncEngine) {
// Sync a specific folder
var folderResult = await syncEngine.SyncFolderAsync("Documents/Important");
Console.WriteLine($"Folder sync: {folderResult.FilesSynchronized} files");

// Sync specific files
var fileResult = await syncEngine.SyncFilesAsync(new[] {
"config.json",
"data/settings.xml"
});
Console.WriteLine($"File sync: {fileResult.FilesSynchronized} files");
}

/// <summary>
/// Pause and resume sync operations.
/// </summary>
public static async Task PauseResumeDemoAsync(ISyncEngine syncEngine, CancellationToken ct) {
// Start sync in background
var syncTask = syncEngine.SynchronizeAsync(cancellationToken: ct);

// Pause after some time
await Task.Delay(1000);
if (syncEngine.State == SyncEngineState.Running) {
await syncEngine.PauseAsync();
Console.WriteLine($"Sync paused. State: {syncEngine.State}");

// Do something while paused...
await Task.Delay(2000);

// Resume
await syncEngine.ResumeAsync();
Console.WriteLine($"Sync resumed. State: {syncEngine.State}");
}

await syncTask;
}

/// <summary>
/// Configure bandwidth throttling.
/// </summary>
public static async Task ThrottledSyncAsync(ISyncEngine syncEngine) {
var options = new SyncOptions {
// Limit to 1 MB/s
MaxBytesPerSecond = 1024 * 1024
};

var result = await syncEngine.SynchronizeAsync(options);
Console.WriteLine($"Throttled sync completed: {result.FilesSynchronized} files");
}

/// <summary>
/// Smart conflict resolution with UI callback.
/// </summary>
public static ISyncEngine CreateEngineWithSmartConflictResolver(
ISyncStorage localStorage,
ISyncStorage remoteStorage,
ISyncDatabase database,
ISyncFilter filter) {
// SmartConflictResolver analyzes conflicts and can prompt the user
var resolver = new SmartConflictResolver(
conflictHandler: async (analysis, ct) => {
// This callback is invoked for each conflict
Console.WriteLine($"Conflict: {analysis.Path}");
Console.WriteLine($" Local: {analysis.LocalSize} bytes, modified {analysis.LocalModified}");
Console.WriteLine($" Remote: {analysis.RemoteSize} bytes, modified {analysis.RemoteModified}");
Console.WriteLine($" Recommendation: {analysis.Recommendation}");
Console.WriteLine($" Reason: {analysis.ReasonForRecommendation}");

// In a real app, show a dialog and return user's choice
// For this example, accept the recommendation
return analysis.Recommendation;
},
defaultResolution: ConflictResolution.Ask);

return new SyncEngine(localStorage, remoteStorage, database, filter, resolver);
}
}
76 changes: 76 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# SharpSync Examples

This directory contains example code demonstrating how to use the SharpSync library.

## BasicSyncExample.cs

A comprehensive example showing:

- **Basic sync setup** - Creating storage, database, filter, and sync engine
- **Progress events** - Wiring up UI updates during sync
- **Sync preview** - Previewing changes before executing
- **Activity history** - Using `GetRecentOperationsAsync()` to display sync history
- **FileSystemWatcher integration** - Real-time change detection
- **Selective sync** - Syncing specific files or folders on demand
- **Pause/Resume** - Controlling long-running sync operations
- **Bandwidth throttling** - Limiting transfer speeds
- **Smart conflict resolution** - Handling conflicts with UI prompts

## Usage

This is a standalone example file, not a buildable project. To use it:

1. Create a new .NET 8.0+ project
2. Add the SharpSync NuGet package:
```bash
dotnet add package Oire.SharpSync
```
3. Optionally add logging:
```bash
dotnet add package Microsoft.Extensions.Logging.Console
```
4. Copy the relevant code from `BasicSyncExample.cs` into your project

## Storage Options

SharpSync supports multiple storage backends:

| Storage | Class | Use Case |
|---------|-------|----------|
| Local filesystem | `LocalFileStorage` | Local folders, testing |
| WebDAV | `WebDavStorage` | Nextcloud, ownCloud, any WebDAV server |
| SFTP | `SftpStorage` | SSH/SFTP servers |
| FTP/FTPS | `FtpStorage` | FTP servers with optional TLS |
| S3 | `S3Storage` | AWS S3, MinIO, LocalStack, S3-compatible |

## Quick Start

```csharp
using Oire.SharpSync.Core;
using Oire.SharpSync.Database;
using Oire.SharpSync.Storage;
using Oire.SharpSync.Sync;

// Create storage instances
var localStorage = new LocalFileStorage("/local/path");
var remoteStorage = new SftpStorage("sftp.example.com", "user", "password", "/remote/path");

// Create database
var database = new SqliteSyncDatabase("/path/to/sync.db");
await database.InitializeAsync();

// Create sync engine
var filter = new SyncFilter();
var resolver = new DefaultConflictResolver(ConflictResolution.UseNewer);
using var engine = new SyncEngine(localStorage, remoteStorage, database, filter, resolver);

// Run sync
var result = await engine.SynchronizeAsync();
Console.WriteLine($"Synced {result.FilesSynchronized} files");

// View activity history
var history = await engine.GetRecentOperationsAsync(limit: 20);
foreach (var op in history) {
Console.WriteLine($"{op.ActionType}: {op.Path} ({op.Duration.TotalSeconds:F1}s)");
}
```
Loading
Loading