A pure .NET file synchronization library supporting multiple storage backends with bidirectional sync, conflict resolution, and progress reporting. No native dependencies required.
- Multi-Protocol Support: Local filesystem, WebDAV, SFTP, FTP/FTPS, and Amazon S3 (including S3-compatible services)
- Bidirectional Sync: Full two-way synchronization with intelligent change detection
- Conflict Resolution: Pluggable strategies with rich conflict analysis for UI integration
- Selective Sync: Include/exclude patterns, folder-level sync, and on-demand file sync
- Progress Reporting: Real-time progress events for UI binding
- Pause/Resume: Gracefully pause and resume long-running sync operations
- Bandwidth Throttling: Configurable transfer rate limits
- FileSystemWatcher Integration: Built-in support for incremental sync via local change notifications
- Remote Change Detection: Client-fed remote change notifications and storage-level change detection (Nextcloud activity API, S3 date filtering)
- Virtual File Support: Callback hooks for Windows Cloud Files API placeholder integration
- Activity History: Query completed operations for activity feeds
- Cross-Platform: Works on Windows, Linux, and macOS (.NET 8.0+)
dotnet add package Oire.SharpSyncgit clone https://github.com/Oire/sharp-sync.git
cd sharp-sync
dotnet buildusingOire.SharpSync.Core;usingOire.SharpSync.Database;usingOire.SharpSync.Storage;usingOire.SharpSync.Sync;// 1. Create storage backendsvarlocalStorage=newLocalFileStorage("/path/to/local/folder");varremoteStorage=newWebDavStorage("https://cloud.example.com/remote.php/dav/files/user/",username:"user",password:"password");// 2. Create sync state databasevardatabase=newSqliteSyncDatabase("/path/to/sync.db");// 3. Create filter and conflict resolvervarfilter=SyncFilter.CreateDefault();// Excludes .git, node_modules, etc.varconflictResolver=newDefaultConflictResolver(ConflictResolution.UseRemote);// 4. Create sync engineusingvarengine=newSyncEngine(localStorage,remoteStorage,database,conflictResolver,filter);// 5. Run synchronizationvarresult=awaitengine.SynchronizeAsync();if(result.Success){Console.WriteLine($"Synchronized {result.FilesSynchronized} files");}else{Console.WriteLine($"Sync failed: {result.Error?.Message}");}// 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)");};varresult=awaitengine.SynchronizeAsync();// Option 1: Use SmartConflictResolver with a callback for UI integrationvarresolver=newSmartConflictResolver(conflictHandler:async(analysis,ct)=>{// analysis contains: FilePath, LocalSize, RemoteSize, LocalModified,// RemoteModified, NewerVersion, RecommendedResolutionConsole.WriteLine($"Conflict: {analysis.FilePath}");Console.WriteLine($" Local: {analysis.LocalModified}, Remote: {analysis.RemoteModified}");Console.WriteLine($" Recommendation: {analysis.RecommendedResolution}");// Return user's choicereturnanalysis.RecommendedResolution;},defaultResolution:ConflictResolution.Ask);// Option 2: Handle via eventengine.ConflictDetected+=(sender,e)=>{Console.WriteLine($"Conflict detected: {e.Path}");// The resolver will be called to determine resolution};varstorage=newLocalFileStorage("/path/to/folder");// Basic authenticationvarstorage=newWebDavStorage("https://cloud.example.com/remote.php/dav/files/user/",username:"user",password:"password",rootPath:"Documents"// Optional subfolder);// OAuth2 authentication (for desktop apps)varstorage=newWebDavStorage("https://cloud.example.com/remote.php/dav/files/user/",oauth2Provider:myOAuth2Provider,oauth2Config:myOAuth2Config);// Password authenticationvarstorage=newSftpStorage(host:"sftp.example.com",port:22,username:"user",password:"password",rootPath:"/home/user/sync");// SSH key authenticationvarstorage=newSftpStorage(host:"sftp.example.com",port:22,username:"user",privateKeyPath:"/path/to/id_rsa",privateKeyPassphrase:"optional-passphrase",rootPath:"/home/user/sync");// Plain FTPvarstorage=newFtpStorage(host:"ftp.example.com",username:"user",password:"password");// Explicit FTPS (TLS)varstorage=newFtpStorage(host:"ftp.example.com",username:"user",password:"password",useFtps:true);// Implicit FTPSvarstorage=newFtpStorage(host:"ftp.example.com",port:990,username:"user",password:"password",useFtps:true,useImplicitFtps:true);// AWS S3varstorage=newS3Storage(bucketName:"my-bucket",accessKey:"AKIAIOSFODNN7EXAMPLE",secretKey:"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",region:"us-east-1",prefix:"sync-folder/"// Optional key prefix);// S3-compatible (MinIO, LocalStack, Backblaze B2, etc.)varstorage=newS3Storage(bucketName:"my-bucket",accessKey:"minioadmin",secretKey:"minioadmin",serviceUrl:"http://localhost:9000",// Custom endpointprefix:"backups/");varplan=awaitengine.GetSyncPlanAsync();Console.WriteLine($"Downloads: {plan.Downloads.Count}");Console.WriteLine($"Uploads: {plan.Uploads.Count}");Console.WriteLine($"Deletes: {plan.DeleteCount}");Console.WriteLine($"Conflicts: {plan.Conflicts.Count}");foreach(varactioninplan.Downloads){Console.WriteLine($" ↓ {action.Path} ({action.Size} bytes)");}// Sync a specific foldervarresult=awaitengine.SyncFolderAsync("Documents/Projects");// Sync specific filesvarresult=awaitengine.SyncFilesAsync(new[]{"report.docx","data.xlsx"});varwatcher=newFileSystemWatcher(localPath);watcher.Changed+=async(s,e)=>{varrelativePath=Path.GetRelativePath(localPath,e.FullPath);awaitengine.NotifyLocalChangeAsync(relativePath,ChangeType.Changed);};watcher.Created+=async(s,e)=>{varrelativePath=Path.GetRelativePath(localPath,e.FullPath);awaitengine.NotifyLocalChangeAsync(relativePath,ChangeType.Created);};watcher.Deleted+=async(s,e)=>{varrelativePath=Path.GetRelativePath(localPath,e.FullPath);awaitengine.NotifyLocalChangeAsync(relativePath,ChangeType.Deleted);};watcher.Renamed+=async(s,e)=>{varoldPath=Path.GetRelativePath(localPath,e.OldFullPath);varnewPath=Path.GetRelativePath(localPath,e.FullPath);awaitengine.NotifyLocalRenameAsync(oldPath,newPath);};watcher.EnableRaisingEvents=true;// Check pending operationsvarpending=awaitengine.GetPendingOperationsAsync();Console.WriteLine($"{pending.Count} files waiting to sync");Feed remote change events from external sources (e.g., push notifications, polling APIs):
// Notify about remote changes (mirrors local notification API)awaitengine.NotifyRemoteChangeAsync("remote_file.txt",ChangeType.Created);awaitengine.NotifyRemoteChangeAsync("deleted_file.txt",ChangeType.Deleted);awaitengine.NotifyRemoteRenameAsync("old_name.txt","new_name.txt");// Batch remote changesawaitengine.NotifyRemoteChangeBatchAsync(new[]{newChangeInfo("file1.txt",ChangeType.Changed),newChangeInfo("file2.txt",ChangeType.Created)});// GetPendingOperationsAsync returns both local and remote pending operationsvarpending=awaitengine.GetPendingOperationsAsync();varuploads=pending.Where(p =>p.Source==ChangeSource.Local).ToList();vardownloads=pending.Where(p =>p.Source==ChangeSource.Remote).ToList();// Clear local or remote pending changes independentlyengine.ClearPendingLocalChanges();engine.ClearPendingRemoteChanges();Storage backends can also detect changes via ISyncStorage.GetRemoteChangesAsync():
- WebDAV (Nextcloud): Uses the Nextcloud activity API
- S3: Uses
ListObjectsV2with date filtering
These are polled automatically during GetSyncPlanAsync().
// Start sync in backgroundvarsyncTask=engine.SynchronizeAsync();// Pause when neededawaitengine.PauseAsync();Console.WriteLine($"Paused. State: {engine.State}");// Resume laterawaitengine.ResumeAsync();// Wait for completionvarresult=awaitsyncTask;varoptions=newSyncOptions{MaxBytesPerSecond=1_048_576// 1 MB/s limit};varresult=awaitengine.SynchronizeAsync(options);// Get recent operationsvarrecentOps=awaitengine.GetRecentOperationsAsync(limit:50);foreach(varopinrecentOps){varicon=op.ActionTypeswitch{SyncActionType.Upload=>"↑",SyncActionType.Download=>"↓",SyncActionType.DeleteLocal or SyncActionType.DeleteRemote=>"×",
_ =>"?"};varstatus=op.Success?"✓":"✗";Console.WriteLine($"{status}{icon}{op.Path} ({op.Duration.TotalSeconds:F1}s)");}// Cleanup old historyvardeleted=awaitengine.ClearOperationHistoryAsync(DateTime.UtcNow.AddDays(-30));varfilter=newSyncFilter();// Exclude patternsfilter.AddExclusionPattern("*.tmp");filter.AddExclusionPattern("*.log");filter.AddExclusionPattern("node_modules");filter.AddExclusionPattern(".git");filter.AddExclusionPattern("**/*.bak");// Include patterns (if set, only matching files are synced)filter.AddInclusionPattern("Documents/**");filter.AddInclusionPattern("*.docx");varoptions=newSyncOptions{PreservePermissions=true,// Preserve file permissionsPreserveTimestamps=true,// Preserve modification timesFollowSymlinks=false,// Follow symbolic linksDeleteExtraneous=false,// Delete files not in sourceUpdateExisting=true,// Update existing filesChecksumOnly=false,// Use checksums instead of timestampsSizeOnly=false,// Compare by size onlyConflictResolution=ConflictResolution.Ask,TimeoutSeconds=300,// 5 minute timeoutMaxBytesPerSecond=null,// No bandwidth limitExcludePatterns=["*.tmp","~*"]};| Strategy | Description |
|---|---|
Ask | Invoke conflict handler callback (default) |
UseLocal | Always keep the local version |
UseRemote | Always use the remote version |
Skip | Leave conflicted files unchanged |
RenameLocal | Rename local file, download remote |
RenameRemote | Rename remote file, upload local |
SharpSync uses a modular, interface-based architecture:
ISyncEngine- Orchestrates synchronization between storagesISyncStorage- Storage backend abstraction (local, WebDAV, SFTP, FTP, S3)ISyncDatabase- Persists sync state for change detectionIConflictResolver- Pluggable conflict resolution strategiesISyncFilter- File filtering for selective sync
Only one sync operation can run at a time per SyncEngine instance. However, the following members are thread-safe and can be called from any thread (including while a sync runs):
- State properties:
IsSynchronizing,IsPaused,State - Local change notifications:
NotifyLocalChangeAsync(),NotifyLocalChangeBatchAsync(),NotifyLocalRenameAsync()- safe to call from FileSystemWatcher threads - Remote change notifications:
NotifyRemoteChangeAsync(),NotifyRemoteChangeBatchAsync(),NotifyRemoteRenameAsync()- safe to call from any thread - Control methods:
PauseAsync(),ResumeAsync()- safe to call from UI thread - Query methods:
GetPendingOperationsAsync(),GetRecentOperationsAsync(),ClearPendingLocalChanges(),ClearPendingRemoteChanges()
This design supports typical desktop client integration where FileSystemWatcher events arrive on thread pool threads, sync runs on a background thread, and UI controls pause/resume from the main thread.
You can safely run multiple sync operations in parallel using separateSyncEngine instances.
- .NET 8.0 or later
- No native dependencies
Microsoft.Extensions.Logging.Abstractions- Logging abstractionsqlite-net-pcl- SQLite database for sync stateWebDav.Client- WebDAV protocolSSH.NET- SFTP protocolFluentFTP- FTP/FTPS protocolAWSSDK.S3- Amazon S3 and S3-compatible storage
# Build the solution
dotnet build
# Run unit tests
dotnet test# Run integration tests (requires Docker)
./scripts/run-integration-tests.sh # Linux/macOS
.\scripts\run-integration-tests.ps1 # Windows# Create NuGet package
dotnet pack --configuration ReleaseThe samples/ directory contains a buildable console application demonstrating SharpSync features:
cd samples/SharpSync.Samples.Console
dotnet runSee the samples README for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass (
dotnet test) - Ensure code formatting (
dotnet format --verify-no-changes) - Submit a pull request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- WebDav.Client - WebDAV protocol implementation
- SSH.NET - SFTP protocol implementation
- FluentFTP - FTP/FTPS protocol implementation
- AWS SDK for .NET - S3 protocol implementation