Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- tiny CLI duplicate deleter v0.01
- #! python3.7
- # |------------------------|
- # | xxhash required! |
- # | "pip install xxhash" |
- # |------------------------|
- import os
- import re
- import xxhash
- class Program:
- def __init__(self):
- self.cw = os.getcwd()
- self.trace_symlinks = False
- self.full_path = os.path.abspath(__file__)
- self.cached_filenames = []
- self.execfilename = ''
- self.prepare_self()
- self.size_map = {}
- self.hash_map = {}
- self.unique_files = []
- self.show_progress = True # UNUSED!
- self.bfile_lim_bytes = 1073741824 # Gigabyte
- # U must prepare self.cw before use it!
- def cache_files(self, as_abs=False):
- files = os.walk(self.cw).__next__()[2]
- if as_abs:
- for I in range(len(files)):
- files[I] = self.cw + "\\" + files[I]
- self.cached_filenames.append([self.full_path] + files)
- def set_cw(self, cw_):
- if os.path.exists(cw_):
- self.cw = cw_
- else:
- raise FileNotFoundError("Invalid path: ", cw_)
- def prepare_self(self):
- self.execfilename = self.full_path[len(self.cw) + 1:]
- def get_files_list(self, dir='', as_abs=False):
- if dir == '':
- self.cache_files(as_abs)
- else:
- if os.path.exists(dir):
- self.set_cw(dir)
- self.cache_files(as_abs)
- else:
- raise FileNotFoundError("Invalid path: ", dir)
- def map_by_size(self, flist):
- self.size_map = {}
- for I in range(1, len(flist)):
- if flist[I] != self.execfilename:
- full_name = flist[I]
- if os.path.exists(full_name):
- size = os.stat(full_name)[6]
- if size not in self.size_map:
- self.size_map[size] = []
- self.size_map[size].append(full_name)
- else:
- print("Strange behavior: file", full_name, 'not exists, but validated successfully')
- print(f'DBG: SIZE_MAP: CREATED {len(self.size_map)} BUCKETS')
- t = sum(len(self.size_map[x]) for x in self.size_map.keys())
- print(f'DBG: SIZE_MAP: PREPARING TO COMPUTE {t} HASHES!')
- def get_prett(self, ffs):
- # Проверка по скобкам
- # Проверка по copy и копия
- # Проверка по цифре в конце
- # Если нет однозначного лидера, то проверка по дате создния (st_ctime -> stat[9])
- prett = 0
- if ffs.find("(") == -1:
- prett += 10
- if ffs.find(")") == -1:
- prett += 2
- else:
- if ffs.find(")") == -1:
- prett += 2
- if ffs.find('копия') == -1:
- prett += 100
- if ffs.find('-') == -1:
- prett += 1
- ptrn = r'\s\(\d+\)\.'
- ps = re.findall(ptrn, ffs)
- if len(ps) == 0:
- prett += 13
- return prett
- @staticmethod
- def hash_ths_file(fname, FREAD_BLOCK=4096 * 8):
- if os.path.exists(fname):
- hFile = open(fname, 'rb', 0)
- if hFile.readable():
- hash_instance = xxhash.xxh32()
- while True:
- block = hFile.read(FREAD_BLOCK)
- hash_instance.update(block)
- if len(block) == 0:
- break
- return hash_instance.intdigest()
- else:
- raise Exception(fname + " can't be accessed")
- else:
- raise FileNotFoundError("Invalid path: ", fname)
- def hash_big_file(self, fnames, zones=8, zone_sizes=4194304):
- # Делаем массив с координатами, откуда читаем файл по zone_sizes
- # Затем считываем оттуда куски этого размера и закидываем эти данные в шехфункцию.
- # Так для всех кандидатов. Сравниваем их хеши. Если всё ещё остлась неопределённость, то считем полный хеш,
- # предварительно спросив у пользователя
- localmap = {}
- if zones > 1:
- zones -= 1
- else:
- raise Exception("Logic error")
- for I in fnames:
- coords = [0]
- fsize = os.stat(I)[6]
- assert fsize >= (zones + 1) * zone_sizes # TODO: Only debug! Rm it after condition in .map_by_hash() -PERF
- hash_inst = xxhash.xxh32()
- hFile = open(I, 'rb', 0)
- cw = 0
- for _ in range(zones - 1):
- cw += (fsize // zones) + (zone_sizes // 2)
- coords.append(cw)
- coords.append(fsize - zone_sizes)
- for K in coords:
- hFile.seek(K)
- hash_inst.update(hFile.read(zone_sizes))
- clc_hash = hash_inst.intdigest()
- if clc_hash not in localmap:
- localmap[clc_hash] = []
- localmap[clc_hash].append(I)
- k = localmap.keys()
- for I in k:
- if len(localmap[I]) > 1:
- # НЕОПРЕДЕЛЁННОСТЬ. Спришиваем юзера и, с его позволения хешируем все файлы в localmap[I]
- ttl_size = 0
- ttl_cnt = 0
- for _ in localmap[I]:
- ttl_size += os.stat(_)[6]
- ttl_cnt += 1
- print('Finded', ttl_cnt, 'files with similar signatures. Total size =', round(ttl_cnt / (1024 ** 2), 3),
- 'MB')
- print('Make a complete comparasion? It will take a long time.')
- if self.answ():
- for _ in localmap[I]: # Считем хеши
- currhash = self.hash_ths_file(_)
- if currhash not in self.hash_map:
- self.hash_map[currhash] = []
- self.hash_map[currhash].append(_)
- # иначе тупо ничего не делаем
- pass
- elif len(localmap[I]) == 1:
- self.unique_files.append(localmap[I][-1])
- else:
- raise Exception("Logic error: .hash_big_file()\n localmap length eq 0")
- def answ(self):
- print("Process? (Y/Other): ", end='')
- if input().lower() in ['y', 'н']:
- return True
- return False
- def map_by_hash(self):
- self.unique_files = []
- self.hash_map = {}
- if not len(self.size_map) < 1:
- # Если в группе содержится лишь 1 файл с таким размером, то можно не брать от него хеш
- k = self.size_map.keys()
- for K in k:
- cg = self.size_map[K]
- if len(cg) == 1:
- self.unique_files.append(cg[0])
- else:
- if K * len(cg) > 268435456:
- self.hash_big_file(cg)
- else:
- for currfilename in cg:
- currhash = self.hash_ths_file(currfilename)
- if currhash not in self.hash_map:
- self.hash_map[currhash] = []
- self.hash_map[currhash].append(currfilename)
- k = self.hash_map.keys()
- for K in k:
- cg = self.hash_map[K]
- if len(cg) == 1:
- self.unique_files.append(cg[0])
- else:
- # Здесь реализуем алгоритм для нахождения наиболее вероятного источника.
- # Все файлы, оказавшиеся в данном cg являются копиями (эту гарантию нам вроде как даёт хеш, но...).
- # Источник - файл с самым "красивым" названием
- # Для определения красивости так же используется время создания и, возможно, сигнатура хедера
- for J in range(len(cg)):
- cg[J] = (self.get_prett(cg[J]), cg[J])
- cg.sort()
- # Мы уже рассмотрели случай с len() == 1. len() == 0 не может быть алгоритмически
- if cg[-1][0] == cg[-2][0]: # Если нет однозначного лидера...
- cptr = len(cg) - 1
- top_element = cg[-1][0]
- while (cptr >= 0) and (top_element == cg[cptr][0]):
- cptr -= 1
- # cptr казывает на первый файл с худшей чем у лучшего красивостью ЛИБО на 0
- best_file = cg[-1][1]
- best_file_time = os.stat(best_file)[9]
- for __ in range(cptr + 1, len(cg)):
- if os.stat(cg[__][1])[9] < best_file_time:
- best_file = cg[__][1]
- best_file_time = os.stat(best_file)[9]
- # Удаляем нахуй всё кроме best_file
- # print(">>>", best_file)
- self.unique_files.append(best_file)
- else:
- self.unique_files.append(cg[-1][1])
- else:
- raise Exception("Logic error: .map_by_hash() -- size_map_length _LE_ 0 -OR- NOT INTEGER")
- def __get_diff_2_ptr(self, access_index):
- del self.cached_filenames[access_index][0]
- # print(self.unique_files)
- self.unique_files.sort() # self.unique
- self.cached_filenames[access_index].sort() # using
- # len(using) always >= len(self.unique)
- ptr_un = 0
- ptr_cf = 0 # В первом элементе путь до закешированных файлов, т.к есть возможность краткого ответа cache_files
- lun = len(self.unique_files)
- ret = []
- while ptr_un != lun:
- # Гарантируется, что unique это подмножество using,
- # так что не проверяем ptr_cf < len(self.cached_filenames[access_index])
- if self.cached_filenames[access_index][ptr_cf] == self.unique_files[ptr_un]:
- ptr_un += 1
- else:
- ret.append(self.cached_filenames[access_index][ptr_cf])
- ptr_cf += 1
- # abcdefghk
- # bdfk
- # ac
- return ret
- def __get_diff_2_ptr_b(self, access_index):
- # print(self.unique_files)
- self.unique_files.sort() # self.unique
- self.cached_filenames.sort() # using
- # len(using) always >= len(self.unique)
- ptr_un = 0
- ptr_cf = 0 # В первом элементе путь до закешированных файлов, т.к есть возможность краткого ответа cache_files
- lun = len(self.unique_files)
- ret = []
- while ptr_un != lun:
- # Гарантируется, что unique это подмножество using,
- # так что не проверяем ptr_cf < len(self.cached_filenames[access_index])
- if self.cached_filenames[ptr_cf] == self.unique_files[ptr_un]:
- ptr_un += 1
- else:
- ret.append(self.cached_filenames[ptr_cf])
- ptr_cf += 1
- # abcdefghk
- # bdfk
- # ac
- print(e)
- return ret
- def process_duplicates(self, dir_=''):
- # Group by filesizes, then in ilesizes by hashes
- # If haven't cached files in dir, caching then
- if not os.path.exists(dir_):
- dir_ = self.cw
- print('Started duplicate scanning in', dir_)
- index = -1
- for I in self.cached_filenames:
- if I[0] == dir_:
- index = self.cached_filenames.index(I)
- if index == -1:
- self.get_files_list(dir_, as_abs=True) # init self.cached_filenames
- # TODO: self.cached_list lenhth may be 0 !!!
- self.map_by_size(self.cached_filenames[index])
- self.map_by_hash()
- print(self.cached_filenames)
- # Теперь у нас соствлена self.unique_files
- # Разница между using[1:] и self.unique_files есть список дял удаления
- to_delete = self.__get_diff_2_ptr(index)
- if len(to_delete) != 0:
- print('Will be deleted:')
- answ = ''
- for I in to_delete:
- answ += '+ ' + I + "\n"
- print(answ)
- if self.answ():
- # for I in to_delete:
- # win32api.DeleteFile(I)
- pass
- else:
- print("Nothing to delete")
- def process_duplicates_from(self, files):
- # Group by filesizes, then in ilesizes by hashes
- # If haven't cached files in dir, caching then
- self.cached_filenames = files
- # TODO: self.cached_list lenhth may be 0 !!!
- print('Size mapping...')
- self.map_by_size(self.cached_filenames)
- print('Hash calculating...')
- self.map_by_hash()
- # Теперь у нас соствлена self.unique_files
- # Разница между using[1:] и self.unique_files есть список дял удаления
- to_delete = self.__get_diff_2_ptr_b(0)
- if len(to_delete) != 0:
- print('Will be deleted:')
- answ = ''
- for I in to_delete:
- answ += '+ ' + I + "\n"
- print(answ)
- if self.answ():
- for I in to_delete:
- win32api.DeleteFile(I)
- pass
- else:
- print("Nothing to delete")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement