Skip to content

Buildcodecov

SharpSync

A pure .NET file synchronization library supporting multiple storage backends with bidirectional sync, conflict resolution, and progress reporting. No native dependencies required.

Features

  • 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+)

Installation

From NuGet

dotnet add package Oire.SharpSync

Building from Source

git clone https://github.com/Oire/sharp-sync.git
cd sharp-sync
dotnet build

Quick Start

Basic Local-to-WebDAV Sync

usingOire.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}");}

With Progress Reporting

// 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();

With Conflict Handling

// 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};

Storage Backends

Local File System

varstorage=newLocalFileStorage("/path/to/folder");

WebDAV (Nextcloud, ownCloud, etc.)

// 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);

SFTP

// 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");

FTP/FTPS

// 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);

Amazon S3 (and S3-Compatible Services)

// 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/");

Advanced Usage

Preview Changes Before Sync

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)");}

Selective Sync

// Sync a specific foldervarresult=awaitengine.SyncFolderAsync("Documents/Projects");// Sync specific filesvarresult=awaitengine.SyncFilesAsync(new[]{"report.docx","data.xlsx"});

FileSystemWatcher Integration

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");

Remote Change Detection

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 ListObjectsV2 with date filtering

These are polled automatically during GetSyncPlanAsync().

Pause and Resume

// Start sync in backgroundvarsyncTask=engine.SynchronizeAsync();// Pause when neededawaitengine.PauseAsync();Console.WriteLine($"Paused. State: {engine.State}");// Resume laterawaitengine.ResumeAsync();// Wait for completionvarresult=awaitsyncTask;

Bandwidth Throttling

varoptions=newSyncOptions{MaxBytesPerSecond=1_048_576// 1 MB/s limit};varresult=awaitengine.SynchronizeAsync(options);

Activity History

// 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));

Custom Filtering

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");

Sync Options

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","~*"]};

Conflict Resolution Strategies

StrategyDescription
AskInvoke conflict handler callback (default)
UseLocalAlways keep the local version
UseRemoteAlways use the remote version
SkipLeave conflicted files unchanged
RenameLocalRename local file, download remote
RenameRemoteRename remote file, upload local

Architecture

SharpSync uses a modular, interface-based architecture:

  • ISyncEngine - Orchestrates synchronization between storages
  • ISyncStorage - Storage backend abstraction (local, WebDAV, SFTP, FTP, S3)
  • ISyncDatabase - Persists sync state for change detection
  • IConflictResolver - Pluggable conflict resolution strategies
  • ISyncFilter - File filtering for selective sync

Thread Safety

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.

Requirements

  • .NET 8.0 or later
  • No native dependencies

Dependencies

  • Microsoft.Extensions.Logging.Abstractions - Logging abstraction
  • sqlite-net-pcl - SQLite database for sync state
  • WebDav.Client - WebDAV protocol
  • SSH.NET - SFTP protocol
  • FluentFTP - FTP/FTPS protocol
  • AWSSDK.S3 - Amazon S3 and S3-compatible storage

Building and Testing

# 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 Release

Samples

The samples/ directory contains a buildable console application demonstrating SharpSync features:

cd samples/SharpSync.Samples.Console
dotnet run

See the samples README for details.

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass (dotnet test)
  6. Ensure code formatting (dotnet format --verify-no-changes)
  7. Submit a pull request

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Acknowledgments

About

A pure .NET file synchronization library supporting multiple storage backends with bidirectional sync, conflict resolution, and progress reporting.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Contributors

Languages