#!/usr/bin/python3 # Resolve file conflicts introduced by Nextcloud syncs. # Daniel Vedder, 22/7/2021 import os import re import sys # explain to the user what's going on helptext = """ usage: ./resolve_conflicts.py <folder> This script helps you clean up conflicts that sometimes occur when Nextcloud syncs. Within synced folders, Nextcloud keeps track of whether and when you change any files. If you change a file on your local client but not in the cloud, then the new version of the file is uploaded the next time you sync your folder. If you change the version in the cloud (for example with another client, or the web interface) but not the one on your local client, the one from the cloud is downloaded and replaces the local version. However, sometimes both versions, the one on your client and the one in the cloud, are changed between two synchronisations. (This can happen unexpectedly.) In this case, Nextcloud declares the cloud version to be the "correct" one and downloads it. However, so as not to lose any data, it doesn't simply delete the local version, but renames it to "example_file (conflicted copy 2021-07-22 174701).txt" (for example). It is then up to you to choose which file is the one you want to keep, and rename/delete the two conflicting files as appropriate. This script helps you do that. It scans the folder you tell it to (including all subfolders) and finds conflicting files. It then gives you a choice which version you want to keep, and takes care of the renaming or deleting. If you're not sure, you can simply skip a conflict and come back to it later. The script will ask for confirmation before deleting anything, so don't worry if you accidentally press a wrong number. For help, contact Daniel Vedder: daniel@terranostra.one Licensed under the terms of the MIT license. """ # specify a synced folder via the commandline syncdir = "example" if os.path.isdir(sys.argv[-1]): syncdir = sys.argv[-1] # recursively iterate through the folder def deconflict_folder(folder): print("Deconflicting "+folder) for f in os.scandir(folder): # returns `DirEntry` objects (https://docs.python.org/3/library/os.html#os.DirEntry) if f.is_dir(): deconflict_folder(f.path) if "conflicted copy" in f.name: deconflict_file(f) # If a conflict is found, list the conflicting files and ask the user which to keep def deconflict_file(dirent): # identify conflicting files realname = re.sub(" \(conflicted copy [-\s\w]+\)", "", dirent.name) realbasename = os.path.splitext(realname)[0] realfiletype = os.path.splitext(realname)[1] foldercontents = os.listdir(os.path.dirname(dirent.path)) conflicts = [f for f in foldercontents if ((f == realname) or (realbasename in f and "conflicted copy" in f and re.search(realfiletype+"$", f) is not None))] if len(conflicts) < 2: return #perhaps the problem is already fixed # Ask the user what to do answer = user_choice(conflicts, realname) while (not answer.isdigit()) or (int(answer) < 1) or (int(answer) > len(conflicts)+1): print("Please answer with a number between 1 and "+str(len(conflicts)+1)+"!") answer = user_choice(conflicts, realname) while int(answer) <= len(conflicts) and input("Please confirm: delete all versions except '"+conflicts[int(answer)-1]+"'? (y/n) ") != "y": print("No confirmation. Please repeat your choice:") answer = user_choice(conflicts, realname) # Delete unwanted versions if int(answer) == len(conflicts)+1: return #skip this conflict correctversion = conflicts[int(answer)-1] for c in conflicts: deletefile = os.path.join(os.path.dirname(dirent.path), c) if c != correctversion: os.remove(deletefile) if correctversion != realname: oldname = os.path.join(os.path.dirname(dirent.path), correctversion) newname = os.path.join(os.path.dirname(dirent.path), realname) os.rename(oldname, newname) # List all conflicting files and ask the user which to keep (or whether to skip) def user_choice(conflicts, realname): print("Found conflicting copies of "+realname+". What do you want to do?") for c in range(len(conflicts)): print(str(c+1)+") only keep "+conflicts[c]) print(str(len(conflicts)+1)+") skip this conflict, don't change anything.") return input(">>> ") if __name__ == '__main__': if sys.argv[-1] in ("-h", "--help"): print(helptext) elif os.path.exists(syncdir) and os.path.isdir(syncdir): deconflict_folder(syncdir) print("Done.") else: print("Please specify a valid directory path as a commanline argument.")