Advertisement
FuFsQ

fast_duplicate_deleter

Dec 24th, 2020
1,031
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.47 KB | None | 0 0
  1. tiny CLI duplicate deleter v0.01
  2.  
  3. #! python3.7
  4.  
  5. # |------------------------|
  6. # |   xxhash required!     |
  7. # |  "pip install xxhash"  |
  8. # |------------------------|
  9.  
  10.  
  11. import os
  12. import re
  13.  
  14. import xxhash
  15.  
  16.  
  17. class Program:
  18.     def __init__(self):
  19.         self.cw = os.getcwd()
  20.         self.trace_symlinks = False
  21.         self.full_path = os.path.abspath(__file__)
  22.         self.cached_filenames = []
  23.  
  24.         self.execfilename = ''
  25.         self.prepare_self()
  26.  
  27.         self.size_map = {}
  28.         self.hash_map = {}
  29.  
  30.         self.unique_files = []
  31.  
  32.         self.show_progress = True  # UNUSED!
  33.         self.bfile_lim_bytes = 1073741824  # Gigabyte
  34.  
  35.     # U must prepare self.cw before use it!
  36.     def cache_files(self, as_abs=False):
  37.         files = os.walk(self.cw).__next__()[2]
  38.  
  39.         if as_abs:
  40.             for I in range(len(files)):
  41.                 files[I] = self.cw + "\\" + files[I]
  42.  
  43.         self.cached_filenames.append([self.full_path] + files)
  44.  
  45.     def set_cw(self, cw_):
  46.         if os.path.exists(cw_):
  47.             self.cw = cw_
  48.         else:
  49.             raise FileNotFoundError("Invalid path: ", cw_)
  50.  
  51.     def prepare_self(self):
  52.         self.execfilename = self.full_path[len(self.cw) + 1:]
  53.  
  54.     def get_files_list(self, dir='', as_abs=False):
  55.         if dir == '':
  56.             self.cache_files(as_abs)
  57.         else:
  58.             if os.path.exists(dir):
  59.                 self.set_cw(dir)
  60.                 self.cache_files(as_abs)
  61.             else:
  62.                 raise FileNotFoundError("Invalid path: ", dir)
  63.  
  64.     def map_by_size(self, flist):
  65.         self.size_map = {}
  66.         for I in range(1, len(flist)):
  67.             if flist[I] != self.execfilename:
  68.                 full_name = flist[I]
  69.  
  70.                 if os.path.exists(full_name):
  71.                     size = os.stat(full_name)[6]
  72.  
  73.                     if size not in self.size_map:
  74.                         self.size_map[size] = []
  75.                     self.size_map[size].append(full_name)
  76.                 else:
  77.                     print("Strange behavior: file", full_name, 'not exists, but validated successfully')
  78.         print(f'DBG: SIZE_MAP: CREATED {len(self.size_map)} BUCKETS')
  79.         t = sum(len(self.size_map[x]) for x in self.size_map.keys())
  80.         print(f'DBG: SIZE_MAP: PREPARING TO COMPUTE {t} HASHES!')
  81.  
  82.     def get_prett(self, ffs):
  83.         # Проверка по скобкам
  84.         # Проверка по copy и копия
  85.         # Проверка по цифре в конце
  86.         # Если нет однозначного лидера, то проверка по дате создния (st_ctime -> stat[9])
  87.         prett = 0
  88.         if ffs.find("(") == -1:
  89.             prett += 10
  90.             if ffs.find(")") == -1:
  91.                 prett += 2
  92.         else:
  93.             if ffs.find(")") == -1:
  94.                 prett += 2
  95.  
  96.         if ffs.find('копия') == -1:
  97.             prett += 100
  98.  
  99.         if ffs.find('-') == -1:
  100.             prett += 1
  101.  
  102.         ptrn = r'\s\(\d+\)\.'
  103.  
  104.         ps = re.findall(ptrn, ffs)
  105.         if len(ps) == 0:
  106.             prett += 13
  107.  
  108.         return prett
  109.  
  110.     @staticmethod
  111.     def hash_ths_file(fname, FREAD_BLOCK=4096 * 8):
  112.         if os.path.exists(fname):
  113.             hFile = open(fname, 'rb', 0)
  114.             if hFile.readable():
  115.                 hash_instance = xxhash.xxh32()
  116.                 while True:
  117.                     block = hFile.read(FREAD_BLOCK)
  118.                     hash_instance.update(block)
  119.                     if len(block) == 0:
  120.                         break
  121.  
  122.                 return hash_instance.intdigest()
  123.  
  124.             else:
  125.                 raise Exception(fname + " can't be accessed")
  126.  
  127.         else:
  128.             raise FileNotFoundError("Invalid path: ", fname)
  129.  
  130.     def hash_big_file(self, fnames, zones=8, zone_sizes=4194304):
  131.         # Делаем массив с координатами, откуда читаем файл по zone_sizes
  132.         # Затем считываем оттуда куски этого размера и закидываем эти данные в шехфункцию.
  133.         # Так для всех кандидатов. Сравниваем их хеши. Если всё ещё остлась неопределённость, то считем полный хеш,
  134.         # предварительно спросив у пользователя
  135.  
  136.         localmap = {}
  137.  
  138.         if zones > 1:
  139.             zones -= 1
  140.         else:
  141.             raise Exception("Logic error")
  142.  
  143.         for I in fnames:
  144.             coords = [0]
  145.             fsize = os.stat(I)[6]
  146.  
  147.             assert fsize >= (zones + 1) * zone_sizes  # TODO: Only debug! Rm it after condition in .map_by_hash() -PERF
  148.  
  149.             hash_inst = xxhash.xxh32()
  150.             hFile = open(I, 'rb', 0)
  151.  
  152.             cw = 0
  153.             for _ in range(zones - 1):
  154.                 cw += (fsize // zones) + (zone_sizes // 2)
  155.                 coords.append(cw)
  156.  
  157.             coords.append(fsize - zone_sizes)
  158.  
  159.             for K in coords:
  160.                 hFile.seek(K)
  161.                 hash_inst.update(hFile.read(zone_sizes))
  162.  
  163.             clc_hash = hash_inst.intdigest()
  164.  
  165.             if clc_hash not in localmap:
  166.                 localmap[clc_hash] = []
  167.  
  168.             localmap[clc_hash].append(I)
  169.  
  170.         k = localmap.keys()
  171.         for I in k:
  172.             if len(localmap[I]) > 1:
  173.                 # НЕОПРЕДЕЛЁННОСТЬ. Спришиваем юзера и, с его позволения хешируем все файлы в localmap[I]
  174.                 ttl_size = 0
  175.                 ttl_cnt = 0
  176.                 for _ in localmap[I]:
  177.                     ttl_size += os.stat(_)[6]
  178.                     ttl_cnt += 1
  179.  
  180.                 print('Finded', ttl_cnt, 'files with similar signatures. Total size =', round(ttl_cnt / (1024 ** 2), 3),
  181.                       'MB')
  182.                 print('Make a complete comparasion? It will take a long time.')
  183.  
  184.                 if self.answ():
  185.                     for _ in localmap[I]:  # Считем хеши
  186.                         currhash = self.hash_ths_file(_)
  187.  
  188.                         if currhash not in self.hash_map:
  189.                             self.hash_map[currhash] = []
  190.                         self.hash_map[currhash].append(_)
  191.  
  192.                 # иначе тупо ничего не делаем
  193.                 pass
  194.             elif len(localmap[I]) == 1:
  195.                 self.unique_files.append(localmap[I][-1])
  196.             else:
  197.                 raise Exception("Logic error: .hash_big_file()\n    localmap length eq 0")
  198.  
  199.     def answ(self):
  200.         print("Process? (Y/Other): ", end='')
  201.         if input().lower() in ['y', 'н']:
  202.             return True
  203.         return False
  204.  
  205.     def map_by_hash(self):
  206.         self.unique_files = []
  207.         self.hash_map = {}
  208.         if not len(self.size_map) < 1:
  209.             # Если в группе содержится лишь 1 файл с таким размером, то можно не брать от него хеш
  210.             k = self.size_map.keys()
  211.             for K in k:
  212.                 cg = self.size_map[K]
  213.                 if len(cg) == 1:
  214.                     self.unique_files.append(cg[0])
  215.                 else:
  216.                     if K * len(cg) > 268435456:
  217.                         self.hash_big_file(cg)
  218.                     else:
  219.                         for currfilename in cg:
  220.                             currhash = self.hash_ths_file(currfilename)
  221.  
  222.                             if currhash not in self.hash_map:
  223.                                 self.hash_map[currhash] = []
  224.                             self.hash_map[currhash].append(currfilename)
  225.  
  226.             k = self.hash_map.keys()
  227.             for K in k:
  228.                 cg = self.hash_map[K]
  229.                 if len(cg) == 1:
  230.                     self.unique_files.append(cg[0])
  231.                 else:
  232.                     # Здесь реализуем алгоритм для нахождения наиболее вероятного источника.
  233.                     # Все файлы, оказавшиеся в данном cg являются копиями (эту гарантию нам вроде как даёт хеш, но...).
  234.                     # Источник - файл с самым "красивым" названием
  235.                     # Для определения красивости так же используется время создания и, возможно, сигнатура хедера
  236.  
  237.                     for J in range(len(cg)):
  238.                         cg[J] = (self.get_prett(cg[J]), cg[J])
  239.  
  240.                     cg.sort()
  241.                     # Мы уже рассмотрели случай с len() == 1. len() == 0 не может быть алгоритмически
  242.  
  243.                     if cg[-1][0] == cg[-2][0]:  # Если нет однозначного лидера...
  244.                         cptr = len(cg) - 1
  245.                         top_element = cg[-1][0]
  246.  
  247.                         while (cptr >= 0) and (top_element == cg[cptr][0]):
  248.                             cptr -= 1
  249.                         # cptr казывает на первый файл с худшей чем у лучшего красивостью ЛИБО на 0
  250.  
  251.                         best_file = cg[-1][1]
  252.                         best_file_time = os.stat(best_file)[9]
  253.                         for __ in range(cptr + 1, len(cg)):
  254.                             if os.stat(cg[__][1])[9] < best_file_time:
  255.                                 best_file = cg[__][1]
  256.                                 best_file_time = os.stat(best_file)[9]
  257.  
  258.                         # Удаляем нахуй всё кроме best_file
  259.                         # print(">>>", best_file)
  260.                         self.unique_files.append(best_file)
  261.                     else:
  262.                         self.unique_files.append(cg[-1][1])
  263.  
  264.  
  265.         else:
  266.             raise Exception("Logic error: .map_by_hash() -- size_map_length _LE_ 0 -OR- NOT INTEGER")
  267.  
  268.     def __get_diff_2_ptr(self, access_index):
  269.         del self.cached_filenames[access_index][0]
  270.         # print(self.unique_files)
  271.         self.unique_files.sort()  # self.unique
  272.         self.cached_filenames[access_index].sort()  # using
  273.  
  274.         # len(using) always >= len(self.unique)
  275.  
  276.         ptr_un = 0
  277.         ptr_cf = 0  # В первом элементе путь до закешированных файлов, т.к есть возможность краткого ответа cache_files
  278.         lun = len(self.unique_files)
  279.  
  280.         ret = []
  281.  
  282.         while ptr_un != lun:
  283.             # Гарантируется, что unique это подмножество using,
  284.             # так что не проверяем ptr_cf < len(self.cached_filenames[access_index])
  285.             if self.cached_filenames[access_index][ptr_cf] == self.unique_files[ptr_un]:
  286.                 ptr_un += 1
  287.             else:
  288.                 ret.append(self.cached_filenames[access_index][ptr_cf])
  289.  
  290.             ptr_cf += 1
  291.  
  292.             # abcdefghk
  293.             # bdfk
  294.             # ac
  295.  
  296.         return ret
  297.  
  298.     def __get_diff_2_ptr_b(self, access_index):
  299.         # print(self.unique_files)
  300.         self.unique_files.sort()  # self.unique
  301.         self.cached_filenames.sort()  # using
  302.  
  303.         # len(using) always >= len(self.unique)
  304.  
  305.         ptr_un = 0
  306.         ptr_cf = 0  # В первом элементе путь до закешированных файлов, т.к есть возможность краткого ответа cache_files
  307.         lun = len(self.unique_files)
  308.  
  309.         ret = []
  310.  
  311.         while ptr_un != lun:
  312.             # Гарантируется, что unique это подмножество using,
  313.             # так что не проверяем ptr_cf < len(self.cached_filenames[access_index])
  314.             if self.cached_filenames[ptr_cf] == self.unique_files[ptr_un]:
  315.                 ptr_un += 1
  316.             else:
  317.                 ret.append(self.cached_filenames[ptr_cf])
  318.  
  319.             ptr_cf += 1
  320.  
  321.             # abcdefghk
  322.             # bdfk
  323.             # ac
  324.  
  325.         print(e)
  326.  
  327.         return ret
  328.  
  329.     def process_duplicates(self, dir_=''):
  330.         # Group by filesizes, then in ilesizes by hashes
  331.         # If haven't cached files in dir, caching then
  332.  
  333.         if not os.path.exists(dir_):
  334.             dir_ = self.cw
  335.  
  336.         print('Started duplicate scanning in', dir_)
  337.  
  338.         index = -1
  339.         for I in self.cached_filenames:
  340.             if I[0] == dir_:
  341.                 index = self.cached_filenames.index(I)
  342.  
  343.         if index == -1:
  344.             self.get_files_list(dir_, as_abs=True)  # init self.cached_filenames
  345.  
  346.         # TODO: self.cached_list lenhth may be 0 !!!
  347.         self.map_by_size(self.cached_filenames[index])
  348.  
  349.         self.map_by_hash()
  350.  
  351.         print(self.cached_filenames)
  352.  
  353.         # Теперь у нас соствлена self.unique_files
  354.         # Разница между using[1:] и self.unique_files есть список дял удаления
  355.  
  356.         to_delete = self.__get_diff_2_ptr(index)
  357.  
  358.         if len(to_delete) != 0:
  359.             print('Will be deleted:')
  360.             answ = ''
  361.             for I in to_delete:
  362.                 answ += '+  ' + I + "\n"
  363.  
  364.             print(answ)
  365.  
  366.             if self.answ():
  367.                 # for I in to_delete:
  368.                 #    win32api.DeleteFile(I)
  369.                 pass
  370.         else:
  371.             print("Nothing to delete")
  372.  
  373.     def process_duplicates_from(self, files):
  374.         # Group by filesizes, then in ilesizes by hashes
  375.         # If haven't cached files in dir, caching then
  376.  
  377.         self.cached_filenames = files
  378.  
  379.         # TODO: self.cached_list lenhth may be 0 !!!
  380.         print('Size mapping...')
  381.         self.map_by_size(self.cached_filenames)
  382.  
  383.         print('Hash calculating...')
  384.         self.map_by_hash()
  385.  
  386.         # Теперь у нас соствлена self.unique_files
  387.         # Разница между using[1:] и self.unique_files есть список дял удаления
  388.  
  389.         to_delete = self.__get_diff_2_ptr_b(0)
  390.  
  391.         if len(to_delete) != 0:
  392.             print('Will be deleted:')
  393.             answ = ''
  394.             for I in to_delete:
  395.                 answ += '+  ' + I + "\n"
  396.  
  397.             print(answ)
  398.  
  399.             if self.answ():
  400.                 for I in to_delete:
  401.                    win32api.DeleteFile(I)
  402.                 pass
  403.         else:
  404.             print("Nothing to delete")
  405.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement