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
61 changes: 53 additions & 8 deletions src/SharpSync/Database/SqliteSyncDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,78 @@
namespace Oire.SharpSync.Database;

/// <summary>
/// SQLite implementation of the sync database
/// SQLite implementation of the sync database for tracking file synchronization state.
/// </summary>
/// <remarks>
/// This class provides persistent storage for sync state using SQLite. It tracks file metadata,
/// sync status, and conflict information. The database is automatically created if it doesn't exist.
/// </remarks>
public class SqliteSyncDatabase: ISyncDatabase {
private readonly string _databasePath;
private SQLiteAsyncConnection? _connection;
private bool _disposed;

/// <summary>
/// Initializes a new instance of the <see cref="SqliteSyncDatabase"/> class.
/// </summary>
/// <param name="databasePath">The full path to the SQLite database file.</param>
/// <exception cref="ArgumentNullException">Thrown when databasePath is null.</exception>
public SqliteSyncDatabase(string databasePath) {
_databasePath = databasePath ?? throw new ArgumentNullException(nameof(databasePath));
ArgumentNullException.ThrowIfNull(databasePath);
_databasePath = databasePath;
}

/// <summary>
/// Initializes the database connection and creates necessary tables and indexes.
/// </summary>
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
/// <remarks>
/// This method must be called before using any other database operations.
/// It creates the directory if it doesn't exist and sets up the database schema.
/// </remarks>
public async Task InitializeAsync(CancellationToken cancellationToken = default) {
var directory = Path.GetDirectoryName(_databasePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) {
Directory.CreateDirectory(directory);
}

_connection = new SQLiteAsyncConnection(_databasePath);

await _connection.CreateTableAsync<SyncState>();
await _connection.ExecuteAsync(@"
await _connection.ExecuteAsync("""
CREATE INDEX IF NOT EXISTS idx_syncstates_status
ON SyncStates(Status)");
await _connection.ExecuteAsync(@"
ON SyncStates(Status)
""");
await _connection.ExecuteAsync("""
CREATE INDEX IF NOT EXISTS idx_syncstates_lastsync
ON SyncStates(LastSyncTime)");
ON SyncStates(LastSyncTime)
""");
}

/// <summary>
/// Gets the synchronization state for a specific file path.
/// </summary>
/// <param name="path">The file path to look up.</param>
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
/// <returns>The sync state if found; otherwise, null.</returns>
/// <exception cref="InvalidOperationException">Thrown when the database is not initialized.</exception>
public async Task<SyncState?> GetSyncStateAsync(string path, CancellationToken cancellationToken = default) {
EnsureInitialized();
return await _connection!.Table<SyncState>()
.Where(s => s.Path == path)
.FirstOrDefaultAsync();
}

/// <summary>
/// Updates or inserts a synchronization state record.
/// </summary>
/// <param name="state">The sync state to update or insert.</param>
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
/// <remarks>
/// If the state has Id = 0, it will be inserted as a new record.
/// Otherwise, the existing record will be updated.
/// </remarks>
/// <exception cref="InvalidOperationException">Thrown when the database is not initialized.</exception>
public async Task UpdateSyncStateAsync(SyncState state, CancellationToken cancellationToken = default) {
EnsureInitialized();

Expand Down Expand Up @@ -67,6 +105,12 @@ public async Task<IEnumerable<SyncState>> GetPendingSyncStatesAsync(Cancellation
.ToListAsync();
}

/// <summary>
/// Begins a database transaction for atomic operations.
/// </summary>
/// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
/// <returns>A transaction object that can be used to commit or rollback changes.</returns>
/// <exception cref="InvalidOperationException">Thrown when the database is not initialized.</exception>
public async Task<ISyncTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) {
EnsureInitialized();
await Task.CompletedTask; // This method returns immediately but needs to be async for interface consistency
Expand Down Expand Up @@ -115,8 +159,9 @@ public async Task<DatabaseStats> GetStatsAsync(CancellationToken cancellationTok
}

private void EnsureInitialized() {
if (_connection == null)
if (_connection is null) {
throw new InvalidOperationException("Database not initialized. Call InitializeAsync first.");
}
}

public void Dispose() {
Expand Down
2 changes: 2 additions & 0 deletions src/SharpSync/SharpSync.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<PackageTags>file-sync;synchronization;webdav;sftp;nextcloud;owncloud;backup;sync</PackageTags>
<Version>1.0.0</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<!-- Suppress warnings about version-specific RIDs in SQLitePCLRaw v3.x -->
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
9 changes: 6 additions & 3 deletions src/SharpSync/Storage/LocalFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,24 @@ public class LocalFileStorage: ISyncStorage {
public string RootPath => _rootPath;

public LocalFileStorage(string rootPath) {
if (string.IsNullOrWhiteSpace(rootPath))
if (string.IsNullOrWhiteSpace(rootPath)) {
throw new ArgumentException("Root path cannot be empty", nameof(rootPath));
}

_rootPath = Path.GetFullPath(rootPath);

if (!Directory.Exists(_rootPath))
if (!Directory.Exists(_rootPath)) {
throw new DirectoryNotFoundException($"Root path does not exist: {_rootPath}");
}
}

public async Task<IEnumerable<SyncItem>> ListItemsAsync(string path, CancellationToken cancellationToken = default) {
var fullPath = GetFullPath(path);
var items = new List<SyncItem>();

if (!Directory.Exists(fullPath))
if (!Directory.Exists(fullPath)) {
return items;
}

// Get directories
foreach (var dir in Directory.EnumerateDirectories(fullPath)) {
Expand Down
3 changes: 2 additions & 1 deletion src/SharpSync/Storage/WebDavStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@
int chunkSizeBytes = 10 * 1024 * 1024, // 10MB
int maxRetries = 3,
int timeoutSeconds = 300) {
if (string.IsNullOrWhiteSpace(baseUrl))
if (string.IsNullOrWhiteSpace(baseUrl)) {
throw new ArgumentException("Base URL cannot be empty", nameof(baseUrl));
}

_baseUrl = baseUrl.TrimEnd('/');
_rootPath = rootPath.Trim('/');
Expand Down Expand Up @@ -213,7 +214,7 @@
return result.Resources
.Skip(1) // Skip the directory itself
.Select(resource => new SyncItem {
Path = GetRelativePath(resource.Uri),

Check warning on line 217 in src/SharpSync/Storage/WebDavStorage.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'fullUrl' in 'string WebDavStorage.GetRelativePath(string fullUrl)'.

Check warning on line 217 in src/SharpSync/Storage/WebDavStorage.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'fullUrl' in 'string WebDavStorage.GetRelativePath(string fullUrl)'.
IsDirectory = resource.IsCollection,
Size = resource.ContentLength ?? 0,
LastModified = resource.LastModifiedDate?.ToUniversalTime() ?? DateTime.MinValue,
Expand Down
Loading
Loading