- #!/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.")