Run black on all Python scripts!
This commit is contained in:
parent
fb6cfce656
commit
cd9cbcaa28
30 changed files with 1027 additions and 704 deletions
|
@ -13,7 +13,7 @@ import progressbar
|
|||
import logging
|
||||
|
||||
progressbar.streams.wrap_stderr()
|
||||
coloredlogs.install(level='INFO', fmt='%(levelname)s %(message)s')
|
||||
coloredlogs.install(level="INFO", fmt="%(levelname)s %(message)s")
|
||||
log = logging.getLogger()
|
||||
|
||||
# 1) Create file list with conflict files
|
||||
|
@ -21,21 +21,20 @@ log = logging.getLogger()
|
|||
# 3) Propose what to do
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix='B'):
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
# Stolen from https://stackoverflow.com/a/1094933
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f %s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f %s%s" % (num, 'Yi', suffix)
|
||||
return "%.1f %s%s" % (num, "Yi", suffix)
|
||||
|
||||
|
||||
class Table():
|
||||
class Table:
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.data = [['' for _ in range(self.height)]
|
||||
for _ in range(self.width)]
|
||||
self.data = [["" for _ in range(self.height)] for _ in range(self.width)]
|
||||
|
||||
def set(self, x, y, data):
|
||||
self.data[x][y] = str(data)
|
||||
|
@ -48,15 +47,15 @@ class Table():
|
|||
l = len(cell)
|
||||
width = widths[x]
|
||||
if x > 0:
|
||||
cell = ' | ' + cell
|
||||
cell = cell + ' ' * (width - l)
|
||||
print(cell, end='\t')
|
||||
cell = " | " + cell
|
||||
cell = cell + " " * (width - l)
|
||||
print(cell, end="\t")
|
||||
print()
|
||||
|
||||
|
||||
class Database():
|
||||
class Database:
|
||||
VERSION = 1
|
||||
CONFLICT_PATTERN = re.compile('\.sync-conflict-\d{8}-\d{6}-\w{7}')
|
||||
CONFLICT_PATTERN = re.compile("\.sync-conflict-\d{8}-\d{6}-\w{7}")
|
||||
|
||||
def __init__(self, directory):
|
||||
self.version = Database.VERSION
|
||||
|
@ -83,18 +82,25 @@ class Database():
|
|||
return sum(databaseFile.maxSize() for databaseFile in self.data.values())
|
||||
|
||||
def totalChecksumSize(self):
|
||||
return sum(databaseFile.totalChecksumSize() for databaseFile in self.data.values())
|
||||
return sum(
|
||||
databaseFile.totalChecksumSize() for databaseFile in self.data.values()
|
||||
)
|
||||
|
||||
def getList(self):
|
||||
self.prune()
|
||||
|
||||
log.info("Finding conflict files")
|
||||
widgets = [
|
||||
progressbar.AnimatedMarker(), ' ',
|
||||
progressbar.BouncingBar(), ' ',
|
||||
progressbar.DynamicMessage('conflicts'), ' ',
|
||||
progressbar.DynamicMessage('files'), ' ',
|
||||
progressbar.DynamicMessage('dir', width=20, precision=20), ' ',
|
||||
progressbar.AnimatedMarker(),
|
||||
" ",
|
||||
progressbar.BouncingBar(),
|
||||
" ",
|
||||
progressbar.DynamicMessage("conflicts"),
|
||||
" ",
|
||||
progressbar.DynamicMessage("files"),
|
||||
" ",
|
||||
progressbar.DynamicMessage("dir", width=20, precision=20),
|
||||
" ",
|
||||
progressbar.Timer(),
|
||||
]
|
||||
bar = progressbar.ProgressBar(widgets=widgets).start()
|
||||
|
@ -104,7 +110,7 @@ class Database():
|
|||
f += 1
|
||||
if not Database.CONFLICT_PATTERN.search(conflictFilename):
|
||||
continue
|
||||
filename = Database.CONFLICT_PATTERN.sub('', conflictFilename)
|
||||
filename = Database.CONFLICT_PATTERN.sub("", conflictFilename)
|
||||
key = (root, filename)
|
||||
if key in self.data:
|
||||
dataFile = self.data[key]
|
||||
|
@ -116,11 +122,13 @@ class Database():
|
|||
dataFile.addConflict(filename)
|
||||
dataFile.addConflict(conflictFilename)
|
||||
|
||||
bar.update(conflicts=len(self.data), files=f,
|
||||
dir=root[(len(self.directory)+1):])
|
||||
bar.update(
|
||||
conflicts=len(self.data), files=f, dir=root[(len(self.directory) + 1) :]
|
||||
)
|
||||
bar.finish()
|
||||
log.info(
|
||||
f"Found {len(self.data)} conflicts, totalling {self.nbFiles()} conflict files.")
|
||||
f"Found {len(self.data)} conflicts, totalling {self.nbFiles()} conflict files."
|
||||
)
|
||||
|
||||
def getStats(self):
|
||||
log.info("Getting stats from conflict files")
|
||||
|
@ -132,25 +140,38 @@ class Database():
|
|||
bar.update(f)
|
||||
bar.finish()
|
||||
log.info(
|
||||
f"Total file size: {sizeof_fmt(self.totalSize())}, possible save: {sizeof_fmt(self.totalSize() - self.maxSize())}")
|
||||
f"Total file size: {sizeof_fmt(self.totalSize())}, possible save: {sizeof_fmt(self.totalSize() - self.maxSize())}"
|
||||
)
|
||||
|
||||
def getChecksums(self):
|
||||
log.info("Checksumming conflict files")
|
||||
widgets = [
|
||||
progressbar.DataSize(), ' of ', progressbar.DataSize('max_value'),
|
||||
' (', progressbar.AdaptiveTransferSpeed(), ') ',
|
||||
progressbar.Bar(), ' ',
|
||||
progressbar.DynamicMessage('dir', width=20, precision=20), ' ',
|
||||
progressbar.DynamicMessage('file', width=20, precision=20), ' ',
|
||||
progressbar.Timer(), ' ',
|
||||
progressbar.DataSize(),
|
||||
" of ",
|
||||
progressbar.DataSize("max_value"),
|
||||
" (",
|
||||
progressbar.AdaptiveTransferSpeed(),
|
||||
") ",
|
||||
progressbar.Bar(),
|
||||
" ",
|
||||
progressbar.DynamicMessage("dir", width=20, precision=20),
|
||||
" ",
|
||||
progressbar.DynamicMessage("file", width=20, precision=20),
|
||||
" ",
|
||||
progressbar.Timer(),
|
||||
" ",
|
||||
progressbar.AdaptiveETA(),
|
||||
]
|
||||
bar = progressbar.DataTransferBar(
|
||||
max_value=self.totalChecksumSize(), widgets=widgets).start()
|
||||
max_value=self.totalChecksumSize(), widgets=widgets
|
||||
).start()
|
||||
f = 0
|
||||
for databaseFile in self.data.values():
|
||||
bar.update(f, dir=databaseFile.root[(
|
||||
len(self.directory)+1):], file=databaseFile.filename)
|
||||
bar.update(
|
||||
f,
|
||||
dir=databaseFile.root[(len(self.directory) + 1) :],
|
||||
file=databaseFile.filename,
|
||||
)
|
||||
f += databaseFile.totalChecksumSize()
|
||||
try:
|
||||
databaseFile.getChecksums()
|
||||
|
@ -172,9 +193,9 @@ class Database():
|
|||
databaseFile.takeAction(execute=execute)
|
||||
|
||||
|
||||
class DatabaseFile():
|
||||
class DatabaseFile:
|
||||
BLOCK_SIZE = 4096
|
||||
RELEVANT_STATS = ('st_mode', 'st_uid', 'st_gid', 'st_size', 'st_mtime')
|
||||
RELEVANT_STATS = ("st_mode", "st_uid", "st_gid", "st_size", "st_mtime")
|
||||
|
||||
def __init__(self, root, filename):
|
||||
self.root = root
|
||||
|
@ -260,7 +281,15 @@ class DatabaseFile():
|
|||
oldChecksum = self.checksums[f]
|
||||
|
||||
# If it's been already summed, and we have the same inode and same ctime, don't resum
|
||||
if oldStat is None or not isinstance(oldChecksum, int) or oldStat.st_size != newStat.st_size or oldStat.st_dev != newStat.st_dev or oldStat.st_ino != newStat.st_ino or oldStat.st_ctime != newStat.st_ctime or oldStat.st_dev != newStat.st_dev:
|
||||
if (
|
||||
oldStat is None
|
||||
or not isinstance(oldChecksum, int)
|
||||
or oldStat.st_size != newStat.st_size
|
||||
or oldStat.st_dev != newStat.st_dev
|
||||
or oldStat.st_ino != newStat.st_ino
|
||||
or oldStat.st_ctime != newStat.st_ctime
|
||||
or oldStat.st_dev != newStat.st_dev
|
||||
):
|
||||
self.checksums[f] = None
|
||||
|
||||
self.stats[f] = newStat
|
||||
|
@ -270,7 +299,10 @@ class DatabaseFile():
|
|||
self.checksums = [False] * len(self.conflicts)
|
||||
|
||||
# If all the files are the same inode, set as same files
|
||||
if len(set([s.st_ino for s in self.stats])) == 1 and len(set([s.st_dev for s in self.stats])) == 1:
|
||||
if (
|
||||
len(set([s.st_ino for s in self.stats])) == 1
|
||||
and len(set([s.st_dev for s in self.stats])) == 1
|
||||
):
|
||||
self.checksums = [True] * len(self.conflicts)
|
||||
|
||||
def getChecksums(self):
|
||||
|
@ -282,7 +314,7 @@ class DatabaseFile():
|
|||
if self.checksums[f] is not None:
|
||||
continue
|
||||
self.checksums[f] = 1
|
||||
filedescs[f] = open(self.getPath(conflict), 'rb')
|
||||
filedescs[f] = open(self.getPath(conflict), "rb")
|
||||
|
||||
while len(filedescs):
|
||||
toClose = set()
|
||||
|
@ -305,12 +337,13 @@ class DatabaseFile():
|
|||
|
||||
def getFeatures(self):
|
||||
features = dict()
|
||||
features['name'] = self.conflicts
|
||||
features['sum'] = self.checksums
|
||||
features["name"] = self.conflicts
|
||||
features["sum"] = self.checksums
|
||||
for statName in DatabaseFile.RELEVANT_STATS:
|
||||
# Rounding beause I Syncthing also rounds
|
||||
features[statName] = [
|
||||
int(stat.__getattribute__(statName)) for stat in self.stats]
|
||||
int(stat.__getattribute__(statName)) for stat in self.stats
|
||||
]
|
||||
return features
|
||||
|
||||
def getDiffFeatures(self):
|
||||
|
@ -327,7 +360,7 @@ class DatabaseFile():
|
|||
if match:
|
||||
return match[0][15:]
|
||||
else:
|
||||
return '-'
|
||||
return "-"
|
||||
|
||||
def printInfos(self, diff=True):
|
||||
print(os.path.join(self.root, self.filename))
|
||||
|
@ -335,14 +368,13 @@ class DatabaseFile():
|
|||
features = self.getDiffFeatures()
|
||||
else:
|
||||
features = self.getFeatures()
|
||||
features['name'] = [DatabaseFile.shortConflict(
|
||||
c) for c in self.conflicts]
|
||||
table = Table(len(features), len(self.conflicts)+1)
|
||||
features["name"] = [DatabaseFile.shortConflict(c) for c in self.conflicts]
|
||||
table = Table(len(features), len(self.conflicts) + 1)
|
||||
for x, featureName in enumerate(features.keys()):
|
||||
table.set(x, 0, featureName)
|
||||
for x, featureName in enumerate(features.keys()):
|
||||
for y in range(len(self.conflicts)):
|
||||
table.set(x, y+1, features[featureName][y])
|
||||
table.set(x, y + 1, features[featureName][y])
|
||||
table.print()
|
||||
|
||||
def decideAction(self, mostRecent=False):
|
||||
|
@ -357,10 +389,10 @@ class DatabaseFile():
|
|||
if len(features) == 1:
|
||||
reason = "same files"
|
||||
self.action = 0
|
||||
elif 'st_mtime' in features and mostRecent:
|
||||
recentTime = features['st_mtime'][0]
|
||||
elif "st_mtime" in features and mostRecent:
|
||||
recentTime = features["st_mtime"][0]
|
||||
recentIndex = 0
|
||||
for index, time in enumerate(features['st_mtime']):
|
||||
for index, time in enumerate(features["st_mtime"]):
|
||||
if time > recentTime:
|
||||
recentTime = time
|
||||
recentIndex = 0
|
||||
|
@ -368,11 +400,11 @@ class DatabaseFile():
|
|||
reason = "most recent"
|
||||
|
||||
if self.action is None:
|
||||
log.warning(
|
||||
f"{self.root}/{self.filename}: skip, cause: {reason}")
|
||||
log.warning(f"{self.root}/{self.filename}: skip, cause: {reason}")
|
||||
else:
|
||||
log.info(
|
||||
f"{self.root}/{self.filename}: keep {DatabaseFile.shortConflict(self.conflicts[self.action])}, cause: {reason}")
|
||||
f"{self.root}/{self.filename}: keep {DatabaseFile.shortConflict(self.conflicts[self.action])}, cause: {reason}"
|
||||
)
|
||||
|
||||
def takeAction(self, execute=False):
|
||||
if self.action is None:
|
||||
|
@ -380,7 +412,8 @@ class DatabaseFile():
|
|||
actionName = self.conflicts[self.action]
|
||||
if actionName != self.filename:
|
||||
log.debug(
|
||||
f"Rename {self.getPath(actionName)} → {self.getPath(self.filename)}")
|
||||
f"Rename {self.getPath(actionName)} → {self.getPath(self.filename)}"
|
||||
)
|
||||
if execute:
|
||||
os.rename(self.getPath(actionName), self.getPath(self.filename))
|
||||
for conflict in self.conflicts:
|
||||
|
@ -390,22 +423,33 @@ class DatabaseFile():
|
|||
if execute:
|
||||
os.unlink(self.getPath(conflict))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Handle Syncthing's .sync-conflict files ")
|
||||
description="Handle Syncthing's .sync-conflict files "
|
||||
)
|
||||
|
||||
# Execution flow
|
||||
parser.add_argument('directory', metavar='DIRECTORY',
|
||||
nargs='?', help='Directory to analyse')
|
||||
parser.add_argument('-d', '--database',
|
||||
help='Database path for file informations')
|
||||
parser.add_argument('-r', '--most-recent', action='store_true',
|
||||
help='Always keep the most recent version')
|
||||
parser.add_argument('-e', '--execute', action='store_true',
|
||||
help='Really apply changes')
|
||||
parser.add_argument('-p', '--print', action='store_true',
|
||||
help='Only print differences between files')
|
||||
parser.add_argument(
|
||||
"directory", metavar="DIRECTORY", nargs="?", help="Directory to analyse"
|
||||
)
|
||||
parser.add_argument("-d", "--database", help="Database path for file informations")
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--most-recent",
|
||||
action="store_true",
|
||||
help="Always keep the most recent version",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--execute", action="store_true", help="Really apply changes"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--print",
|
||||
action="store_true",
|
||||
help="Only print differences between files",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
@ -419,13 +463,17 @@ if __name__ == "__main__":
|
|||
if args.database:
|
||||
if os.path.isfile(args.database):
|
||||
try:
|
||||
with open(args.database, 'rb') as databaseFile:
|
||||
with open(args.database, "rb") as databaseFile:
|
||||
database = pickle.load(databaseFile)
|
||||
assert isinstance(database, Database)
|
||||
except BaseException as e:
|
||||
raise ValueError("Not a database file")
|
||||
assert database.version <= Database.VERSION, "Version of the loaded database is too recent"
|
||||
assert database.directory == args.directory, "Directory of the loaded database doesn't match"
|
||||
assert (
|
||||
database.version <= Database.VERSION
|
||||
), "Version of the loaded database is too recent"
|
||||
assert (
|
||||
database.directory == args.directory
|
||||
), "Directory of the loaded database doesn't match"
|
||||
|
||||
if database is None:
|
||||
database = Database(args.directory)
|
||||
|
@ -433,7 +481,7 @@ if __name__ == "__main__":
|
|||
def saveDatabase():
|
||||
if args.database:
|
||||
global database
|
||||
with open(args.database, 'wb') as databaseFile:
|
||||
with open(args.database, "wb") as databaseFile:
|
||||
pickle.dump(database, databaseFile)
|
||||
|
||||
database.getList()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue