|
|
@ -13,7 +13,7 @@ import progressbar |
|
|
|
import logging |
|
|
|
|
|
|
|
progressbar.streams.wrap_stderr() |
|
|
|
coloredlogs.install(level='INFO', fmt='%(levelname)s %(message)s') |
|
|
|
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s') |
|
|
|
log = logging.getLogger() |
|
|
|
|
|
|
|
# 1) Create file list with conflict files |
|
|
@ -34,12 +34,24 @@ class Table(): |
|
|
|
def __init__(self, width, height): |
|
|
|
self.width = width |
|
|
|
self.height = height |
|
|
|
self.data = [['' ** self.height] ** self.width] |
|
|
|
self.data = [['' for _ in range(self.height)] |
|
|
|
for _ in range(self.width)] |
|
|
|
|
|
|
|
def set(x, y, data): |
|
|
|
def set(self, x, y, data): |
|
|
|
self.data[x][y] = str(data) |
|
|
|
|
|
|
|
|
|
|
|
def print(self): |
|
|
|
widths = [max([len(cell) for cell in column]) for column in self.data] |
|
|
|
for y in range(self.height): |
|
|
|
for x in range(self.width): |
|
|
|
cell = self.data[x][y] |
|
|
|
l = len(cell) |
|
|
|
width = widths[x] |
|
|
|
if x > 0: |
|
|
|
cell = ' | ' + cell |
|
|
|
cell = cell + ' ' * (width - l) |
|
|
|
print(cell, end='\t') |
|
|
|
print() |
|
|
|
|
|
|
|
|
|
|
|
class Database(): |
|
|
@ -137,7 +149,8 @@ class Database(): |
|
|
|
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() |
|
|
@ -148,14 +161,20 @@ class Database(): |
|
|
|
pass |
|
|
|
bar.finish() |
|
|
|
|
|
|
|
def act(self): |
|
|
|
pass |
|
|
|
def printDifferences(self): |
|
|
|
for databaseFile in self.data.values(): |
|
|
|
print() |
|
|
|
databaseFile.printInfos(diff=True) |
|
|
|
|
|
|
|
def takeAction(self, execute=False, *args, **kwargs): |
|
|
|
for databaseFile in self.data.values(): |
|
|
|
databaseFile.decideAction(*args, **kwargs) |
|
|
|
databaseFile.takeAction(execute=execute) |
|
|
|
|
|
|
|
|
|
|
|
class DatabaseFile(): |
|
|
|
BLOCK_SIZE = 4096 |
|
|
|
RELEVANT_STATS = ('st_mode', 'st_uid', 'st_gid', |
|
|
|
'st_size', 'st_mtime', 'st_ctime') |
|
|
|
RELEVANT_STATS = ('st_mode', 'st_uid', 'st_gid', 'st_size', 'st_mtime') |
|
|
|
|
|
|
|
def __init__(self, root, filename): |
|
|
|
self.root = root |
|
|
@ -163,6 +182,7 @@ class DatabaseFile(): |
|
|
|
self.stats = [] |
|
|
|
self.conflicts = [] |
|
|
|
self.checksums = [] |
|
|
|
self.action = None |
|
|
|
log.debug(f"{self.root}/{self.filename} - new") |
|
|
|
|
|
|
|
def addConflict(self, conflict): |
|
|
@ -190,16 +210,16 @@ class DatabaseFile(): |
|
|
|
del self.checksums[f] |
|
|
|
log.debug(f"{self.root}/{self.filename} - del: {conflict}") |
|
|
|
|
|
|
|
def getPathFile(self, conflict): |
|
|
|
def getPath(self, conflict): |
|
|
|
return os.path.join(self.root, conflict) |
|
|
|
|
|
|
|
def getPathFiles(self): |
|
|
|
return [self.getPathFile(conflict) for conflict in self.conflicts] |
|
|
|
def getPaths(self): |
|
|
|
return [self.getPath(conflict) for conflict in self.conflicts] |
|
|
|
|
|
|
|
def prune(self): |
|
|
|
toPrune = list() |
|
|
|
for conflict in self.conflicts: |
|
|
|
if not os.path.isfile(self.getPathFile(conflict)): |
|
|
|
if not os.path.isfile(self.getPath(conflict)): |
|
|
|
toPrune.append(conflict) |
|
|
|
|
|
|
|
if len(toPrune): |
|
|
@ -236,7 +256,7 @@ class DatabaseFile(): |
|
|
|
def getStats(self): |
|
|
|
for f, conflict in enumerate(self.conflicts): |
|
|
|
oldStat = self.stats[f] |
|
|
|
newStat = os.stat(self.getPathFile(conflict)) |
|
|
|
newStat = os.stat(self.getPath(conflict)) |
|
|
|
oldChecksum = self.checksums[f] |
|
|
|
|
|
|
|
# If it's been already summed, and we have the same inode and same ctime, don't resum |
|
|
@ -262,7 +282,7 @@ class DatabaseFile(): |
|
|
|
if self.checksums[f] is not None: |
|
|
|
continue |
|
|
|
self.checksums[f] = 1 |
|
|
|
filedescs[f] = open(self.getPathFile(conflict), 'rb') |
|
|
|
filedescs[f] = open(self.getPath(conflict), 'rb') |
|
|
|
|
|
|
|
while len(filedescs): |
|
|
|
toClose = set() |
|
|
@ -285,10 +305,12 @@ class DatabaseFile(): |
|
|
|
|
|
|
|
def getFeatures(self): |
|
|
|
features = dict() |
|
|
|
features['name'] = self.conflicts |
|
|
|
features['sum'] = self.checksums |
|
|
|
for stat in DatabaseFile.RELEVANT_STATS: |
|
|
|
features[stat] = [self.stats[f].__getattribute__( |
|
|
|
stat) for f in enumerate(self.stats)] |
|
|
|
for statName in DatabaseFile.RELEVANT_STATS: |
|
|
|
# Rounding beause I Syncthing also rounds |
|
|
|
features[statName] = [ |
|
|
|
int(stat.__getattribute__(statName)) for stat in self.stats] |
|
|
|
return features |
|
|
|
|
|
|
|
def getDiffFeatures(self): |
|
|
@ -299,18 +321,74 @@ class DatabaseFile(): |
|
|
|
diffFeatures[key] = vals |
|
|
|
return diffFeatures |
|
|
|
|
|
|
|
def printInfos(self): |
|
|
|
print(os.path.join(self.root, self.name)) |
|
|
|
@staticmethod |
|
|
|
def shortConflict(conflict): |
|
|
|
match = Database.CONFLICT_PATTERN.search(conflict) |
|
|
|
if match: |
|
|
|
return match[0][15:] |
|
|
|
else: |
|
|
|
return '-' |
|
|
|
|
|
|
|
# nf = re.sub( '', f) |
|
|
|
# F = os.path.join(root, f) |
|
|
|
# NF = os.path.join(root, nf) |
|
|
|
# if os.path.exists(NF): |
|
|
|
# print(f"'{F}' → '{NF}': file already exists") |
|
|
|
# else: |
|
|
|
# print(f"'{F}' → '{NF}': done") |
|
|
|
# # os.rename(F, NF) |
|
|
|
def printInfos(self, diff=True): |
|
|
|
print(os.path.join(self.root, self.filename)) |
|
|
|
if diff: |
|
|
|
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) |
|
|
|
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.print() |
|
|
|
|
|
|
|
def decideAction(self, mostRecent=False): |
|
|
|
# TODO More arguments for choosing |
|
|
|
reason = "undecided" |
|
|
|
self.action = None |
|
|
|
if len(self.conflicts) == 1: |
|
|
|
self.action = 0 |
|
|
|
reason = "only file" |
|
|
|
else: |
|
|
|
features = self.getDiffFeatures() |
|
|
|
if len(features) == 1: |
|
|
|
reason = "same files" |
|
|
|
self.action = 0 |
|
|
|
elif 'st_mtime' in features and mostRecent: |
|
|
|
recentTime = features['st_mtime'][0] |
|
|
|
recentIndex = 0 |
|
|
|
for index, time in enumerate(features['st_mtime']): |
|
|
|
if time > recentTime: |
|
|
|
recentTime = time |
|
|
|
recentIndex = 0 |
|
|
|
self.action = recentIndex |
|
|
|
reason = "most recent" |
|
|
|
|
|
|
|
if self.action is None: |
|
|
|
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}") |
|
|
|
|
|
|
|
def takeAction(self, execute=False): |
|
|
|
if self.action is None: |
|
|
|
return |
|
|
|
actionName = self.conflicts[self.action] |
|
|
|
if actionName != self.filename: |
|
|
|
log.debug( |
|
|
|
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: |
|
|
|
if conflict is actionName: |
|
|
|
continue |
|
|
|
log.debug(f"Delete {self.getPath(conflict)}") |
|
|
|
if execute: |
|
|
|
os.unlink(self.getPath(conflict)) |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
|
@ -318,11 +396,16 @@ if __name__ == "__main__": |
|
|
|
description="Handle Syncthing's .sync-conflict files ") |
|
|
|
|
|
|
|
# Execution flow |
|
|
|
parser.add_argument( |
|
|
|
'--database', help='Database path for file informations') |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
@ -362,4 +445,7 @@ if __name__ == "__main__": |
|
|
|
database.getChecksums() |
|
|
|
saveDatabase() |
|
|
|
|
|
|
|
database.act() |
|
|
|
if args.print: |
|
|
|
database.printDifferences() |
|
|
|
else: |
|
|
|
database.takeAction(mostRecent=args.most_recent, execute=args.execute) |