Skip to content
Merged
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
112 changes: 108 additions & 4 deletions src/SharpSync/Sync/SyncEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -748,13 +748,63 @@ private async Task ResolveConflictAsync(SyncAction action, ThreadSafeSyncResult
break;

case ConflictResolution.RenameLocal:
// TODO: Implement rename logic - create new file with modified name
result.IncrementFilesConflicted();
if (action.LocalItem is not null && action.RemoteItem is not null) {
// Generate unique conflict name for local file using computer name
var conflictPath = await GenerateUniqueConflictNameAsync(action.Path, Environment.MachineName, _localStorage, cancellationToken);

// Move local file to conflict name
await _localStorage.MoveAsync(action.Path, conflictPath, cancellationToken);

// Download remote file to original path
await DownloadFileAsync(action, result, cancellationToken);

// Track the conflict file in database (exists locally, needs to be uploaded)
var conflictItem = await _localStorage.GetItemAsync(conflictPath, cancellationToken);
if (conflictItem is not null) {
var conflictState = new SyncState {
Path = conflictPath,
IsDirectory = conflictItem.IsDirectory,
Status = SyncStatus.LocalNew,
LastSyncTime = DateTime.UtcNow,
LocalHash = conflictItem.ETag ?? await _localStorage.ComputeHashAsync(conflictPath, cancellationToken),
LocalSize = conflictItem.Size,
LocalModified = conflictItem.LastModified
};
await _database.UpdateSyncStateAsync(conflictState, cancellationToken);
}
} else {
result.IncrementFilesConflicted();
}
break;

case ConflictResolution.RenameRemote:
// TODO: Implement rename logic - create new file with modified name
result.IncrementFilesConflicted();
if (action.LocalItem is not null && action.RemoteItem is not null) {
// Generate unique conflict name for remote file using domain name
var conflictPath = await GenerateUniqueConflictNameAsync(action.Path, GetDomainFromUrl(_remoteStorage.RootPath), _remoteStorage, cancellationToken);

// Move remote file to conflict name
await _remoteStorage.MoveAsync(action.Path, conflictPath, cancellationToken);

// Upload local file to original path
await UploadFileAsync(action, result, cancellationToken);

// Track the conflict file in database (exists remotely, needs to be downloaded)
var conflictItem = await _remoteStorage.GetItemAsync(conflictPath, cancellationToken);
if (conflictItem is not null) {
var conflictState = new SyncState {
Path = conflictPath,
IsDirectory = conflictItem.IsDirectory,
Status = SyncStatus.RemoteNew,
LastSyncTime = DateTime.UtcNow,
RemoteHash = conflictItem.ETag ?? await _remoteStorage.ComputeHashAsync(conflictPath, cancellationToken),
RemoteSize = conflictItem.Size,
RemoteModified = conflictItem.LastModified
};
await _database.UpdateSyncStateAsync(conflictState, cancellationToken);
}
} else {
result.IncrementFilesConflicted();
}
break;

default:
Expand All @@ -763,6 +813,60 @@ private async Task ResolveConflictAsync(SyncAction action, ThreadSafeSyncResult
}
}

private static async Task<string> GenerateUniqueConflictNameAsync(string path, string sourceIdentifier, ISyncStorage storage, CancellationToken cancellationToken) {
// Generate a unique conflict filename by inserting the source identifier before the extension
// If a conflict with the same name already exists, append a number
// Examples:
// "document.txt" -> "document (andre-vivobook).txt"
// If exists -> "document (andre-vivobook 2).txt"
// If exists -> "document (andre-vivobook 3).txt"
var directory = Path.GetDirectoryName(path);
var fileName = Path.GetFileNameWithoutExtension(path);
var extension = Path.GetExtension(path);

// Try base name first
var conflictFileName = $"{fileName} ({sourceIdentifier}){extension}";
var conflictPath = string.IsNullOrEmpty(directory)
? conflictFileName
: Path.Combine(directory, conflictFileName);

// Check if this path already exists
if (!await storage.ExistsAsync(conflictPath, cancellationToken)) {
return conflictPath;
}

// If it exists, try numbered versions
for (int i = 2; i <= 100; i++) {
conflictFileName = $"{fileName} ({sourceIdentifier} {i}){extension}";
conflictPath = string.IsNullOrEmpty(directory)
? conflictFileName
: Path.Combine(directory, conflictFileName);

if (!await storage.ExistsAsync(conflictPath, cancellationToken)) {
return conflictPath;
}
}

// Fallback: use timestamp if we somehow have 100+ conflicts
var timestamp = DateTime.Now.ToString("yyyy-MM-dd-HHmmss");
conflictFileName = $"{fileName} ({sourceIdentifier} {timestamp}){extension}";
return string.IsNullOrEmpty(directory)
? conflictFileName
: Path.Combine(directory, conflictFileName);
}

private static string GetDomainFromUrl(string url) {
// Extract domain name from URL
// Example: "https://disk.cx/remote.php/dav/files/user/" -> "disk.cx"
try {
var uri = new Uri(url);
return uri.Host;
} catch {
// Fallback to "remote" if URL parsing fails
return "remote";
}
}

private async Task UpdateDatabaseStateAsync(ChangeSet changes, CancellationToken cancellationToken) {
// Update database with new sync states
var updates = new List<Task>();
Expand Down
Loading