diff --git a/src/core.py b/src/core.py old mode 100644 new mode 100755 index dfcfcb2..048e6e3 --- a/src/core.py +++ b/src/core.py @@ -40,11 +40,18 @@ from os.path import dirname,relpath,normpath,join as path_join,abspath as abspath,exists as path_exists,isdir as path_isdir +from subprocess import run as subprocess_run + from pickle import dumps,loads from zstandard import ZstdCompressor,ZstdDecompressor from send2trash import send2trash +DELETE=0 +SOFTLINK=1 +HARDLINK=2 +WIN_LNK=3 + def localtime_catched(t): try: #mtime sometimes happens to be negative (Virtual box ?) @@ -814,6 +821,21 @@ def do_soft_link(self,src,dest,relative,l_info): self.log.error(e) return 'Error on soft linking:%s' % e + def do_win_lnk_link(self,src,dest,l_info): + l_info('win-lnk-linking %s<-%s',src,dest) + try: + powershell_cmd = f'$ol=(New-Object -ComObject WScript.Shell).CreateShortcut("{dest}")\n\r$ol.TargetPath="{src}"\n\r$ol.Save()' + l_info(f'{powershell_cmd=}') + + res = subprocess_run(["powershell", "-Command", powershell_cmd], capture_output=True) + + if res.returncode != 0: + return f"Error on win lnk code: {res.returncode} error: {res.stderr}" + + except Exception as e: + self.log.error(e) + return 'Error on win lnk linking:%s' % e + def do_hard_link(self,src,dest,l_info): l_info('hard-linking %s<-%s',src,dest) try: @@ -874,12 +896,31 @@ def delete_file_wrapper(self,size,crc,index_tuple_set,to_trash=False,file_callba return messages + def win_lnk_wrapper (self,\ + size,crc,\ + index_tuple_ref,index_tuple_list,file_callback=None,crc_callback=None): + + l_info = self.log.info + + l_info(f'win_lnk_wrapper:{size},{crc},{index_tuple_ref},{index_tuple_list}') + + (path_nr_keep,path_keep,file_keep,ctime_keep,dev_keep,inode_keep)=index_tuple_ref + + self_get_full_path_scanned = self.get_full_path_scanned + self_files_of_size_of_crc_size_crc = self.files_of_size_of_crc[size][crc] + + self_rename_file = self.rename_file + self_delete_file = self.delete_file + + full_file_path_keep=self_get_full_path_scanned(path_nr_keep,path_keep,file_keep) + def link_wrapper(self,\ - soft,relative,size,crc,\ + kind,relative,size,crc,\ index_tuple_ref,index_tuple_list,file_callback=None,crc_callback=None): + l_info = self.log.info - l_info('link_wrapper:%s,%s,%s,%s,%s,%s',soft,relative,size,crc,index_tuple_ref,index_tuple_list) + l_info('link_wrapper:%s,%s,%s,%s,%s,%s',kind,relative,size,crc,index_tuple_ref,index_tuple_list) (path_nr_keep,path_keep,file_keep,ctime_keep,dev_keep,inode_keep)=index_tuple_ref @@ -891,7 +932,8 @@ def link_wrapper(self,\ full_file_path_keep=self_get_full_path_scanned(path_nr_keep,path_keep,file_keep) - link_command = (lambda p : self.do_soft_link(full_file_path_keep,p,relative,l_info)) if soft else (lambda p : self.do_hard_link(full_file_path_keep,p,l_info)) + #link_command = (lambda p : self.do_soft_link(full_file_path_keep,p,relative,l_info)) if soft else (lambda p : self.do_hard_link(full_file_path_keep,p,l_info)) + link_command = (lambda p : self.do_soft_link(full_file_path_keep,p,relative,l_info)) if kind==SOFTLINK else (lambda p : self.do_win_lnk_link(full_file_path_keep,str(p) + ".lnk",l_info)) if kind==WIN_LNK else (lambda p : self.do_hard_link(full_file_path_keep,p,l_info)) if index_tuple_ref not in self_files_of_size_of_crc_size_crc: return 'link_wrapper - Internal Data Inconsistency:%s / %s' % (full_file_path_keep,index_tuple_ref) @@ -926,7 +968,7 @@ def link_wrapper(self,\ tuples_to_remove.add(index_tuple) - if not soft: + if kind==HARDLINK: tuples_to_remove.add(index_tuple_ref) self.remove_from_data_pool(size,crc,tuples_to_remove,file_callback,crc_callback) diff --git a/src/dude.py b/src/dude.py index 3ba95e3..679aaf4 100755 --- a/src/dude.py +++ b/src/dude.py @@ -122,10 +122,6 @@ CFG_KEY_EXCLUDE:'' } -DELETE=0 -SOFTLINK=1 -HARDLINK=2 - NAME={DELETE:'Delete',SOFTLINK:'Softlink',HARDLINK:'Hardlink'} HOMEPAGE='https://github.com/PJDude/dude' @@ -1162,7 +1158,7 @@ def get_settings_dialog(self): bfr.grid(row=row,column=0) ; row+=1 Button(bfr, text='Set defaults',width=14, command=self.settings_reset).pack(side='left', anchor='n',padx=5,pady=5) - Button(bfr, text='OK', width=14, command=self.settings_ok ).pack(side='left', anchor='n',padx=5,pady=5) + Button(bfr, text='OK', width=14, command=self.settings_ok ).pack(side='left', anchor='n',padx=5,pady=5,fill='both') self.cancel_button=Button(bfr, text='Cancel', width=14 ,command=self.settings_dialog.hide ) self.cancel_button.pack(side='right', anchor='n',padx=5,pady=5) @@ -2378,13 +2374,16 @@ def context_menu_show(self,event): #marks_state=('disabled','normal')[len(tree.tag_has(self.MARK))!=0] marks_state=('disabled','normal')[bool(self.tagged)] + marks_state_win=('disabled','normal')[bool(self.tagged) and windows ] c_local_add_command(label = 'Remove Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(DELETE,0),accelerator="Delete",state=marks_state, image = self.ico_empty,compound='left') c_local_entryconfig(19,foreground='red',activeforeground='red') c_local_add_command(label = 'Softlink Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(SOFTLINK,0),accelerator="Insert",state=marks_state, image = self.ico_empty,compound='left') c_local_entryconfig(20,foreground='red',activeforeground='red') - c_local_add_command(label = 'Hardlink Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(HARDLINK,0),accelerator="Shift+Insert",state=marks_state, image = self.ico_empty,compound='left') + c_local_add_command(label = 'Create *.lnk for Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(WIN_LNK,0),state=marks_state_win, image = self.ico_empty,compound='left') c_local_entryconfig(21,foreground='red',activeforeground='red') + c_local_add_command(label = 'Hardlink Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(HARDLINK,0),accelerator="Shift+Insert",state=marks_state, image = self.ico_empty,compound='left') + c_local_entryconfig(22,foreground='red',activeforeground='red') pop_add_cascade(label = 'Local (this CRC group)',menu = c_local,state=item_actions_state, image = self.ico_empty,compound='left') pop_add_separator() @@ -2427,8 +2426,10 @@ def context_menu_show(self,event): c_all.entryconfig(21,foreground='red',activeforeground='red') c_all.add_command(label = 'Softlink Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(SOFTLINK,1),accelerator="Ctrl+Insert",state=marks_state, image = self.ico_empty,compound='left') c_all.entryconfig(22,foreground='red',activeforeground='red') - c_all.add_command(label = 'Hardlink Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(HARDLINK,1),accelerator="Ctrl+Shift+Insert",state=marks_state, image = self.ico_empty,compound='left') + c_all.add_command(label = 'Create *.lnk for Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(WIN_LNK,1),state=marks_state_win, image = self.ico_empty,compound='left') c_all.entryconfig(23,foreground='red',activeforeground='red') + c_all.add_command(label = 'Hardlink Marked Files ...',command=lambda : self.process_files_in_groups_wrapper(HARDLINK,1),accelerator="Ctrl+Shift+Insert",state=marks_state, image = self.ico_empty,compound='left') + c_all.entryconfig(24,foreground='red',activeforeground='red') pop_add_cascade(label = 'All Files',menu = c_all,state=item_actions_state, image = self.ico_empty,compound='left') @@ -2455,6 +2456,7 @@ def context_menu_show(self,event): else: dir_actions_state=('disabled','normal')[self.sel_kind in (self.DIR,self.DIRLINK)] + dir_actions_state_win=('disabled','normal')[(self.sel_kind in (self.DIR,self.DIRLINK)) and windows] c_local = Menu(pop,tearoff=0,bg=self.bg_color) c_local_add_command = c_local.add_command @@ -2472,13 +2474,15 @@ def context_menu_show(self,event): #marks_state=('disabled','normal')[len(tree.tag_has(self.MARK))!=0] marks_state=('disabled','normal')[bool(self.current_folder_items_tagged)] + marks_state_win=('disabled','normal')[bool(self.current_folder_items_tagged) and windows] c_local_add_command(label = 'Remove Marked Files ...',command=lambda : self.process_files_in_folder_wrapper(DELETE,0),accelerator="Delete",state=marks_state, image = self.ico_empty,compound='left') c_local_add_command(label = 'Softlink Marked Files ...',command=lambda : self.process_files_in_folder_wrapper(SOFTLINK,0),accelerator="Insert",state=marks_state, image = self.ico_empty,compound='left') + c_local_add_command(label = 'Create *.lnk for Marked Files ...',command=lambda : self.process_files_in_folder_wrapper(WIN_LNK,0),state=marks_state_win, image = self.ico_empty,compound='left') c_local_entryconfig(8,foreground='red',activeforeground='red') c_local_entryconfig(9,foreground='red',activeforeground='red') - #c_local_entryconfig(10,foreground='red',activeforeground='red') + c_local_entryconfig(10,foreground='red',activeforeground='red') pop_add_cascade(label = 'Local (this folder)',menu = c_local,state=item_actions_state, image = self.ico_empty,compound='left') pop_add_separator() @@ -2491,9 +2495,11 @@ def context_menu_show(self,event): c_sel_sub_add_command(label = 'Remove Marked Files in Subdirectory Tree ...',command=lambda : self.process_files_in_folder_wrapper(DELETE,True),accelerator="Delete",state=dir_actions_state, image = self.ico_empty,compound='left') c_sel_sub_add_command(label = 'Softlink Marked Files in Subdirectory Tree ...',command=lambda : self.process_files_in_folder_wrapper(SOFTLINK,True),accelerator="Insert",state=dir_actions_state, image = self.ico_empty,compound='left') + c_sel_sub_add_command(label = 'Create *.lnk for Marked Files in Subdirectory Tree ...',command=lambda : self.process_files_in_folder_wrapper(WIN_LNK,True),state=dir_actions_state_win, image = self.ico_empty,compound='left') c_sel_sub.entryconfig(3,foreground='red',activeforeground='red') c_sel_sub.entryconfig(4,foreground='red',activeforeground='red') + c_sel_sub.entryconfig(5,foreground='red',activeforeground='red') pop_add_cascade(label = 'Selected Subdirectory',menu = c_sel_sub,state=dir_actions_state, image = self.ico_empty,compound='left') @@ -4134,7 +4140,7 @@ def process_files_check_correctness(self,action,processed_items,remaining_items) incorrect_groups_append(crc) problem_header = 'All files marked' - if action==SOFTLINK: + if action==SOFTLINK or action==WIN_LNK: problem_message = "Keep at least one file unmarked\nor enable option:\n\"Skip groups with invalid selection\"" else: problem_message = "Keep at least one file unmarked\nor enable option:\n\"Skip groups with invalid selection\"\nor enable option:\n\"Allow deletion of all copies\"" @@ -4211,6 +4217,11 @@ def process_files_confirm(self,action,processed_items,remaining_items,scope_titl message=[] message_append = message.append + if action==WIN_LNK: + message_append('Link files will be created with the names of the listed files with the ".lnk" suffix.') + message_append('Original files will be removed.') + message_append('') + self_item_full_path = self.item_full_path self_groups_tree_item_to_data = self.groups_tree_item_to_data @@ -4230,7 +4241,7 @@ def process_files_confirm(self,action,processed_items,remaining_items,scope_titl message_append(' ' + (self_item_full_path(item) if show_full_path else file) + '|RED' ) - if action==SOFTLINK: + if action==SOFTLINK or action==WIN_LNK: if remaining_items[crc]: item = remaining_items[crc][0] if cfg_show_links_targets: @@ -4244,7 +4255,11 @@ def process_files_confirm(self,action,processed_items,remaining_items,scope_titl if not self.text_ask_dialog.res_bool: return True elif action==SOFTLINK: - self.get_text_ask_dialog().show('Soft-Link marked files to first unmarked file in group ?','Scope: ' + scope_title + '\n\n' + size_info + '\n'+'\n'.join(message)) + self.get_text_ask_dialog().show('Soft-Link marked files to the first unmarked file in the group ?','Scope: ' + scope_title + '\n\n' + size_info + '\n'+'\n'.join(message)) + if not self.text_ask_dialog.res_bool: + return True + elif action==WIN_LNK: + self.get_text_ask_dialog().show('replace marked files with .lnk files pointing to the first unmarked file in the group ?','Scope: ' + scope_title + '\n\n' + size_info + '\n'+'\n'.join(message)) if not self.text_ask_dialog.res_bool: return True elif action==HARDLINK: @@ -4333,6 +4348,7 @@ def process_files_core(self,action,processed_items,remaining_items): dude_core_delete_file_wrapper = dude_core.delete_file_wrapper dude_core_link_wrapper = dude_core.link_wrapper + dude_core_win_lnk_wrapper = dude_core.win_lnk_wrapper final_info=[] @@ -4402,7 +4418,28 @@ def process_files_core(self,action,processed_items,remaining_items): index_tuple_ref=self_groups_tree_item_to_data[to_keep_item][3] size=self_groups_tree_item_to_data[to_keep_item][1] - if resmsg:=dude_core_link_wrapper(True, do_rel_symlink, size,crc, index_tuple_ref, [self_groups_tree_item_to_data[item][3] for item in items_dict.values() ],self.file_remove_callback,self.crc_remove_callback ): + if resmsg:=dude_core_link_wrapper(SOFTLINK, do_rel_symlink, size,crc, index_tuple_ref, [self_groups_tree_item_to_data[item][3] for item in items_dict.values() ],self.file_remove_callback,self.crc_remove_callback ): + l_error(resmsg) + + end_message_list_append(resmsg) + + if abort_on_error: + break + + if counter%16==0: + self_status('processing crc groups %s ...' % counter) + + + elif action==WIN_LNK: + + for crc,items_dict in processed_items.items(): + counter+=1 + to_keep_item=remaining_items[crc][0] + + index_tuple_ref=self_groups_tree_item_to_data[to_keep_item][3] + size=self_groups_tree_item_to_data[to_keep_item][1] + + if resmsg:=dude_core_link_wrapper(WIN_LNK, False, size,crc, index_tuple_ref, [self_groups_tree_item_to_data[item][3] for item in items_dict.values() ],self.file_remove_callback,self.crc_remove_callback ): l_error(resmsg) end_message_list_append(resmsg) @@ -4420,7 +4457,7 @@ def process_files_core(self,action,processed_items,remaining_items): index_tuple_ref=self_groups_tree_item_to_data[ref_item][3] size=self_groups_tree_item_to_data[ref_item][1] - if resmsg:=dude_core_link_wrapper(False, False, size,crc, index_tuple_ref, [self_groups_tree_item_to_data[item][3] for index,item in items_dict.items() if index!=0 ],self.file_remove_callback,self.crc_remove_callback ): + if resmsg:=dude_core_link_wrapper(HARDLINK, False, size,crc, index_tuple_ref, [self_groups_tree_item_to_data[item][3] for index,item in items_dict.items() if index!=0 ],self.file_remove_callback,self.crc_remove_callback ): l_error(resmsg) end_message_list_append(resmsg)