From 549a5f988e6bbb560f77ae17bb6cb72853c79393 Mon Sep 17 00:00:00 2001 From: Brent Huisman Date: Thu, 3 Feb 2022 18:47:26 +0100 Subject: [PATCH] Fixed `cli.py`, fixed some package name casing, `pardeep` now always starts Qt GUI, removed `gui_tk.py`. --- README.md | 15 +- par2deep/__main__.py | 5 +- par2deep/cli.py | 11 +- par2deep/gui_tk.py | 500 ------------------------------------------- setup.py | 6 +- setup_cx.py | 4 +- 6 files changed, 19 insertions(+), 522 deletions(-) delete mode 100644 par2deep/gui_tk.py diff --git a/README.md b/README.md index 6690d73..f13abe8 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,22 @@ If you have Python installed, you can use pip! Make sure to update pip before in $ pip3 install -U pip $ pip3 install par2deep --user -If you use Windows, and do not have Python installed, you can grab an installer [here](https://github.com/brenthuisman/par2deep/releases). These msi packages are generated created with the `cx_Freeze` package using the `setup_cx.py` script: - - $ python3 setup_cx.py bdist_msi - Alternatively, clone/download this repo and install: - $ python3 setup.py install --user + $ pip3 installl ./par2deep Or run directly: $ python3 par2deep +### Windows + +If you use Windows, and do not have Python installed, you can grab an installer [here](https://github.com/brenthuisman/par2deep/releases). These msi packages are generated created with the `cx_Freeze` package using the `setup_cx.py` script: + + $ python3 setup_cx.py bdist_msi + +Configuration used to produce the release `.msi`: Python 3.8.10 64bit (for Windows 7 compatibility), cx_Freeze 6.4. + ## Usage After installation, run with `par2deep` for the GUI or `par2deep-cli` if you live in the terminal. Command line options may be enumerated by using the --help option. Note that `-q` will update, fix or recreate parity files as it sees fit. If unrepairable damage is found, it will recreate parity data. (The old tkinter GUI is unmaintained but available at `par2deep-tk`.) @@ -53,6 +57,7 @@ Example `par2deep.ini`: ### Changelog + * 2022-02-03: v1.9.4.2: Fixed `cli.py`, fixed some package name casing, `pardeep` now always starts Qt GUI, removed `gui_tk.py`. * 2022-01-31: v1.9.4.1: Moved kinda-broken (no UTF8 filename support) `gopar` changes to gopar branch, release last commit as new version as it fixes some things. * 2020-04-26: v1.9.4: Include libpar2 for win64 and lin64 platforms, no external `par2` needed anymore. * 2020-04-20: recreate verified_repairable creates backup. backups are shows upon init. orphans are shown upon init. diff --git a/par2deep/__main__.py b/par2deep/__main__.py index 09c9328..2798275 100644 --- a/par2deep/__main__.py +++ b/par2deep/__main__.py @@ -1,7 +1,4 @@ import os,gui_qt,cli if __name__ == '__main__': - if 'DISPLAY' in os.environ: - gui_qt.main() - else: - cli.main() + gui_qt.main() diff --git a/par2deep/cli.py b/par2deep/cli.py index 58e1fb8..fd03017 100644 --- a/par2deep/cli.py +++ b/par2deep/cli.py @@ -31,15 +31,12 @@ def disp10(lst,q=False): def main(): p2d = par2deep() - try: - print("Using",p2d.par_cmd,"...") - except: - pass - print("Looking for files in",p2d.directory,"...") - if p2d.check_state() == 200: print("The par2 command you specified is invalid.") return 1 + else: + print("Using",p2d.par_cmd,"...") + print("Looking for files in",p2d.directory,"...") print("==========================================================") print('Will create',len(p2d.create),'new par2 files.') @@ -52,8 +49,6 @@ def main(): disp10(p2d.orphans_delete,p2d.quiet) print('Will remove',len(p2d.backups_delete),'backup files.') disp10(p2d.backups_delete,p2d.quiet) - print('Will remove',len(p2d.par2errcopies),'old repair files.') - disp10(p2d.par2errcopies,p2d.quiet) if not p2d.quiet and p2d.len_all_actions>0 and not ask_yn("Perform actions?", default=None): print('Exiting...') diff --git a/par2deep/gui_tk.py b/par2deep/gui_tk.py deleted file mode 100644 index a3660ef..0000000 --- a/par2deep/gui_tk.py +++ /dev/null @@ -1,500 +0,0 @@ -#!/usr/bin/env python -import os,threading -from tkinter import * -from tkinter.ttk import * -from tkinter import Scale #new one sucks -from tkinter import filedialog #old one dont work -try: - from par2deep import * - from toolbox import startfile -except: - from .par2deep import * - from .toolbox import startfile - - -class CreateToolTip(object): - """ - create a tooltip for a given widget. - Copied from https://stackoverflow.com/a/36221216 - """ - def __init__(self, widget, text='widget info'): - self.waittime = 500 #milliseconds - self.wraplength = 250 #pixels - self.widget = widget - self.text = text - self.widget.bind("", self.enter) - self.widget.bind("", self.leave) - self.widget.bind("", self.leave) - self.id = None - self.tw = None - - def enter(self, event=None): - self.schedule() - - def leave(self, event=None): - self.unschedule() - self.hidetip() - - def schedule(self): - self.unschedule() - self.id = self.widget.after(self.waittime, self.showtip) - - def unschedule(self): - id = self.id - self.id = None - if id: - self.widget.after_cancel(id) - - def showtip(self, event=None): - x = y = 0 - x, y, cx, cy = self.widget.bbox("insert") - x += self.widget.winfo_rootx() + 25 - y += self.widget.winfo_rooty() + 20 - # creates a toplevel window - self.tw = Toplevel(self.widget) - # Leaves only the label and removes the app window - self.tw.wm_overrideredirect(True) - self.tw.wm_geometry("+%d+%d" % (x, y)) - label = Label(self.tw, text=self.text, justify='left', - background="#ffffff", relief='solid', borderwidth=1, - wraplength = self.wraplength) - label.pack(ipadx=1) - - def hidetip(self): - tw = self.tw - self.tw= None - if tw: - tw.destroy() - - -class app_frame(Frame): - def __init__(self, master): - Frame.__init__(self, master) - self.grid(row=0, column=0, sticky=NSEW) - #main window has 1 frame, thats it - Grid.rowconfigure(master, 0, weight=1) - Grid.columnconfigure(master, 0, weight=1) - self.waittime = 500 #milliseconds - - #swithin that, frames, row-wise, which are updated as necesary. - #topbar: displays stage of verification - #middle screen shows options or info - #bottom bar shows actions - self.new_window(self.topbar_frame(0), self.start_options_frame(), self.start_actions_frame()) - return - - - def new_window(self,t,m,b): - #swithin that, frames, row-wise, which are updated as necesary. - #topbar: displays stage of verification - #middle screen shows options or info - #bottom bar shows actions - - for rows in range(3): - Grid.rowconfigure(self, rows, weight=1) - for columns in range(1): - Grid.columnconfigure(self, columns, weight=1) - - Grid.rowconfigure(self, 0, weight=0) #override weight: sets size to minimal size - self.topbar = t - self.topbar.grid(row=0, column=0, sticky=N+S+E+W,padx=20,pady=(20,0)) - - self.mid = m - self.mid.grid(row=1, column=0, sticky=N+S+E+W,padx=20,pady=(20,0)) - - Grid.columnconfigure(self, 2, weight=0) - self.actbar = b - self.actbar.grid(row=2, column=0, sticky=N+S+E+W,padx=20,pady=20) - return - - - def topbar_frame(self,stage): - subframe = Frame(self) - labels = list(range(6)) - labels[0] = Label(subframe, text="Start", pad=10) - labels[1] = Label(subframe, text="Proposed actions", pad=10) - labels[2] = Label(subframe, text="Executing actions", pad=10) - labels[3] = Label(subframe, text="Report", pad=10) - labels[4] = Label(subframe, text="Further actions", pad=10) - labels[5] = Label(subframe, text="Final report", pad=10) - labels[stage].configure(font="-weight bold") - [x.pack(side=LEFT) for x in labels] - - return subframe - - - def start_options_frame(self,chosen_dir=None): - self.p2d = par2deep(chosen_dir) - - self.args = {} - - subframe = Frame(self) - - basicset = LabelFrame(subframe, text="Basic Settings",pad=10) - basicset.pack(fill=X,pady=(0,20)) - advset = LabelFrame(subframe, text="Advanced Settings",pad=10) - advset.pack(fill=X) - - def pickdir(): - # self.args["directory"].delete(0,END) - # self.args["directory"].insert(0,filedialog.askdirectory()) - # self.p2d.__init__(self.args["directory"].get()) - self.new_window(self.topbar_frame(0), self.start_options_frame(filedialog.askdirectory()), self.start_actions_frame()) - Button(basicset, text="Pick directory", command=pickdir).pack(side='left') - self.args["directory"] = Entry(basicset) - self.args["directory"].pack(fill=X) - if chosen_dir == None: - self.args["directory"].insert(0,self.p2d.args["directory"]) - else: - self.args["directory"].insert(0,chosen_dir) - - self.args["overwrite"] = IntVar() - self.args["overwrite"].set(self.p2d.args["overwrite"]) - cb1 = Checkbutton(advset, text="Overwrite all parity data", variable=self.args["overwrite"]) - cb1.pack(fill=X) - CreateToolTip(cb1,"Any existing parity data found (any *.par* files) will be removed and overwritten.") - - self.args["noverify"] = IntVar() - self.args["noverify"].set(self.p2d.args["noverify"]) - cb2 = Checkbutton(advset, text="Skip verification", variable=self.args["noverify"]) - cb2.pack(fill=X) - CreateToolTip(cb2,"Skips verification of files with existing parity data. Use when you only want to create parity data for new files.") - - self.args["keep_orphan"] = IntVar() - self.args["keep_orphan"].set(self.p2d.args["keep_orphan"]) - cb3 = Checkbutton(advset, text="Keep orphaned par2 files.", variable=self.args["keep_orphan"]) - cb3.pack(fill=X) - CreateToolTip(cb3,"Do not remove unused parity files (*.par*).") - - self.args["clean_backup"] = IntVar() - self.args["clean_backup"].set(self.p2d.args["clean_backup"]) - cb3 = Checkbutton(advset, text="Remove backup files", variable=self.args["clean_backup"]) - cb3.pack(fill=X) - CreateToolTip(cb3,"Remove backup files (*.[0-9]).") - - Label(advset, text="Exclude directories (comma separated)").pack(fill=X) - self.args["excludes"] = Entry(advset) - self.args["excludes"].pack(fill=X) - self.args["excludes"].insert(0,','.join(self.p2d.args["excludes"])) - CreateToolTip(self.args["excludes"],"These subdirectories will be excluded from the analysis. Use 'root' for the root of the directory.") - - Label(advset, text="Exclude extensions (comma separated)").pack(fill=X) - self.args["extexcludes"] = Entry(advset) - self.args["extexcludes"].pack(fill=X) - self.args["extexcludes"].insert(0,','.join(self.p2d.args["extexcludes"])) - CreateToolTip(self.args["extexcludes"],"These extensions will be excluded from the analysis.") - - Label(advset, text="Path to par2.exe").pack(fill=X) - self.args["par_cmd"] = Entry(advset) - self.args["par_cmd"].pack(fill=X) - self.args["par_cmd"].insert(0,self.p2d.args["par_cmd"]) - CreateToolTip(self.args["par_cmd"],"Should be set automatically and correctly, but can be overriden.") - - Label(advset, text="Percentage of protection").pack(fill=X) - self.args["percentage"] = IntVar() - self.args["percentage"].set(self.p2d.args["percentage"]) - s1 = Scale(advset,orient=HORIZONTAL,from_=5,to=100,resolution=1,variable=self.args["percentage"]) - s1.pack(fill=X) - CreateToolTip(s1,"The maximum percentage of corrupted data you will be able to recover from. Higher is safer, but uses more data.") - - return subframe - - - def start_actions_frame(self): - subframe = Frame(self) - Button(subframe, text="Check directory contents", command=self.set_start_actions).pack() - return subframe - - - def repair_actions_frame(self): - subframe = Frame(self) - if self.p2d.len_verified_actions > 0: - Button(subframe, text="Fix repairable corrupted files and recreate unrepairable files", command=self.repair_action).pack() - Button(subframe, text="Recreate parity files for the changed and unrepairable files", command=self.recreate_action).pack() - else: - Button(subframe, text="Nothing to do. Exit.", command=self.master.destroy).pack() - return subframe - - - def execute_actions_frame(self): - subframe = Frame(self) - if self.p2d.len_all_actions > 0: - Button(subframe, text="Run actions", command=self.execute_actions).pack() - else: - Button(subframe, text="Nothing to do. Exit.", command=self.master.destroy).pack() - return subframe - - - def exit_actions_frame(self): - subframe = Frame(self) - if hasattr(self.p2d,'len_all_err'): - Label(subframe, text="There were "+str(self.p2d.len_all_err)+" errors.").pack(fill=X) - Button(subframe, text="Exit", command=self.master.destroy).pack() - return subframe - - - def exit_frame(self): - subframe = Frame(self) - Label(subframe, text="The par2 command you specified is invalid.").pack(fill=X) - return subframe - - - def progress_indef_frame(self): - subframe = Frame(self) - self.pb=Progressbar(subframe, mode='indeterminate') - self.pb.start() - self.pb.pack(fill=X,expand=True) - Label(subframe, text="Indexing directory, may take a few moments...").pack(fill=X) - return subframe - - - def progress_frame(self,length): - subframe = Frame(self) - self.pb=Progressbar(subframe, mode='determinate',maximum=length+0.01) - #+.01 to make sure bar is not full when last file processed. - self.pb.pack(fill=X,expand=True) - self.pb_currentfile = StringVar() - self.pb_currentfile.set("Executing actions, may take a few moments...") - Label(subframe, textvariable = self.pb_currentfile).pack(fill=X) - return subframe - - - def blank_frame(self): - subframe = Frame(self) - return subframe - - - def repair_action(self): - self.new_window(self.topbar_frame(4), self.blank_frame(), self.progress_frame(self.p2d.len_verified_actions)) - self.update() - - self.cnt = 0 - self.cnt_stop = False - def run(): - for i in self.p2d.execute_repair(): - self.cnt+=1 - self.currentfile = i - dispdict = { - 'verifiedfiles_succes' : 'Verified and in order', - 'createdfiles' : 'Newly created parity files', - 'removedfiles' : 'Files removed', - 'createdfiles_err' : 'Errors during creating parity files', - 'removedfiles_err' : 'Errors during file removal', - 'fixes' : 'Verified files succesfully fixed', - 'fixes_err' : 'Verified files failed to fix', - 'recreate' : 'Succesfully recreated (overwritten) parity files', - 'recreate_err' : 'Failed (overwritten) new parity files' - } - self.new_window(self.topbar_frame(5), self.scrollable_treeview_frame(dispdict), self.exit_actions_frame()) - #put p2d.len_all_err somewhere in label of final report - self.cnt_stop = True - thread = threading.Thread(target=run) - thread.daemon = True - thread.start() - - def upd(): - if not self.cnt_stop: - self.pb.step(self.cnt) - self.pb_currentfile.set("Processing "+os.path.basename(self.currentfile)) - self.cnt=0 - self.master.after(self.waittime, upd) - else: - return - - upd() - return - - - def recreate_action(self): - self.new_window(self.topbar_frame(4), self.blank_frame(), self.progress_frame(self.p2d.len_verified_actions)) - self.update() - - self.cnt = 0 - self.cnt_stop = False - def run(): - for i in self.p2d.execute_recreate(): - self.cnt+=1 - self.currentfile = i - dispdict = { - 'verifiedfiles_succes' : 'Verified and in order', - 'createdfiles' : 'Newly created parity files', - 'removedfiles' : 'Files removed', - 'createdfiles_err' : 'Errors during creating parity files', - 'removedfiles_err' : 'Errors during file removal', - 'fixes' : 'Verified files succesfully fixed', - 'fixes_err' : 'Verified files failed to fix', - 'recreate' : 'Succesfully recreated (overwritten) parity files', - 'recreate_err' : 'Failed (overwritten) new parity files' - } - self.new_window(self.topbar_frame(5), self.scrollable_treeview_frame(dispdict), self.exit_actions_frame()) - #put p2d.len_all_err somewhere in label of final report - self.cnt_stop = True - thread = threading.Thread(target=run) - thread.daemon = True - thread.start() - - def upd(): - if not self.cnt_stop: - self.pb.step(self.cnt) - self.pb_currentfile.set("Processing "+os.path.basename(self.currentfile)) - self.cnt=0 - self.master.after(self.waittime, upd) - else: - return - - upd() - return - - - def set_start_actions(self): - #update p2d args. - self.p2d.args["quiet"] = False #has no meaning in gui - self.p2d.args["overwrite"] = self.args["overwrite"].get() == 1 - self.p2d.args["noverify"] = self.args["noverify"].get() == 1 - self.p2d.args["keep_orphan"] = self.args["keep_orphan"].get() == 1 - self.p2d.args["clean_backup"] = self.args["clean_backup"].get() == 1 - self.p2d.args["excludes"] = self.args["excludes"].get().split(',') if self.args["excludes"].get().split(',') != [''] else [] - self.p2d.args["extexcludes"] = self.args["extexcludes"].get().split(',') if self.args["extexcludes"].get().split(',') != [''] else [] - self.p2d.args["directory"] = os.path.abspath(self.args["directory"].get()) - self.p2d.args["par_cmd"] = str(self.args["par_cmd"].get()) - self.p2d.args["percentage"] = str(int(self.args["percentage"].get())) - - #go to second frame - self.new_window(self.topbar_frame(0), self.blank_frame(), self.progress_indef_frame()) - self.update() - def run(): - if self.p2d.check_state() == 200: - self.new_window(self.topbar_frame(0), self.exit_frame(), self.exit_actions_frame()) - return - dispdict = { - 'create' : 'Create parity files', - 'incomplete' : 'Replace parity files', - 'verify' : 'Verify files', - 'unused' : 'Remove these unused files', - 'par2errcopies' : 'Remove old repair files' - } - self.new_window(self.topbar_frame(1), self.scrollable_treeview_frame(dispdict), self.execute_actions_frame()) - thread = threading.Thread(target=run) - thread.daemon = True - thread.start() - return - - - def execute_actions(self): - #go to third frame - self.new_window(self.topbar_frame(2), self.blank_frame(), self.progress_frame(self.p2d.len_all_actions)) - self.update() - - self.cnt = 0 - self.cnt_stop = False - def run(): - for i in self.p2d.execute(): - self.cnt+=1 - self.currentfile = i - dispdict = { - 'verifiedfiles_succes' : 'Verified and in order', - 'createdfiles' : 'Newly created parity files', - 'removedfiles' : 'Files removed', - 'createdfiles_err' : 'Errors during creating parity files', - 'verifiedfiles_err' : 'Irrepairable damage found', - 'verifiedfiles_repairable' : 'Repairable damage found', - 'removedfiles_err' : 'Errors during file removal' - } - self.new_window(self.topbar_frame(3), self.scrollable_treeview_frame(dispdict), self.repair_actions_frame()) - self.cnt_stop = True - thread = threading.Thread(target=run) - thread.daemon = True - thread.start() - - def upd(): - if not self.cnt_stop: - self.pb.step(self.cnt) - self.pb_currentfile.set("Processing "+os.path.basename(self.currentfile)) - self.cnt=0 - self.master.after(self.waittime, upd) - else: - return - - upd() - return - - - def scrollable_treeview_frame(self,nodes={}): - subframe = Frame(self) - tree = Treeview(subframe) - tree.pack(side="left",fill=BOTH,expand=True) - - ysb = Scrollbar(subframe, orient='vertical', command=tree.yview) - ysb.pack(side="right", fill=Y, expand=False) - - tree.configure(yscroll=ysb.set) - #tree.heading('#0', text="Category", anchor='w') - tree["columns"]=("fname","action") - tree.column("#0", width=20, stretch=False) - tree.heading("action", text="Action") - tree.column("action", width=60, stretch=False) - tree.column("fname", stretch=True) - tree.heading("fname", text="Filename") - - - def doubleclick_tree(event): - startfile(tree.item(tree.selection()[0],"values")[0]) - return - - def show_contextmenu(event): - print (tree.selection()) - popup = Menu(self.master, tearoff=0) - for node,label in nodes.items(): - popup.add_command(label=node) - try: - popup.tk_popup(event.x_root, event.y_root) - finally: - # make sure to release the grab (Tk 8.0a1 only) - popup.grab_release() - - tree.bind("", doubleclick_tree) - tree.bind("", show_contextmenu) - - for node,label in nodes.items(): - if len(getattr(self.p2d,node))==0: - tree.insert("", 'end', values=(label+": no files.",""), open=False) - else: - thing = tree.insert("", 'end', values=(label+": expand to see "+str(len(getattr(self.p2d,node)))+" files.",""), open=False) - for item in getattr(self.p2d,node): - if not isinstance(item, list): - tree.insert(thing, 'end', values=(item,node), open=False) - else: - tree.insert(thing, 'end', values=(item[0],node), open=False) - - return subframe - - -def main(): - root = Tk() - app = app_frame(root) - - w = 800 # width for the Tk root - h = 650 # height for the Tk root - - # get screen width and height - ws = root.winfo_screenwidth() # width of the screen - hs = root.winfo_screenheight() # height of the screen - - # calculate x and y coordinates for the Tk root window - x = (ws/2) - (w/2) - y = (hs/2) - (h/2) - - # set the dimensions of the screen - # and where it is placed - root.geometry('%dx%d+%d+%d' % (w, h, x, y)) - root.wm_title("par2deep") - - root.mainloop() - - # if app.p2d.len_all_err>0: - # return 1 - # else: - # return 0 - -if __name__ == "__main__": - main() diff --git a/setup.py b/setup.py index 940b278..dc8008c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #! /usr/bin/env python from setuptools import setup -VERSION = '1.9.4.1' +VERSION = '1.9.4.2' def main(): setup(name='par2deep', @@ -27,11 +27,11 @@ def main(): license='LGPL', include_package_data=True, zip_safe=False, - install_requires=['tqdm','configargparse','Send2Trash','PyQt5'], + install_requires=['tqdm','configargparse','send2trash','PyQt5'], packages=['par2deep'], package_data={'': ['libpar2.so','libpar2.dll']}, entry_points={ - "console_scripts": ['par2deep = par2deep.gui_qt:main', 'par2deep-tk = par2deep.gui_tk:main', 'par2deep-cli = par2deep.cli:main'], + "console_scripts": ['par2deep = par2deep.gui_qt:main', 'par2deep-cli = par2deep.cli:main'], } ) diff --git a/setup_cx.py b/setup_cx.py index 34a66b0..f791c96 100644 --- a/setup_cx.py +++ b/setup_cx.py @@ -1,7 +1,7 @@ from cx_Freeze import setup, Executable import sys,os.path,glob -VERSION = '1.9.4.1' +VERSION = '1.9.4.2' NAME = 'par2deep' DESCRIPTION = "Produce, verify and repair par2 files recursively." @@ -9,7 +9,7 @@ 'include_files':[ NAME+'.ico', ] + glob.glob("par2deep/*.py") + glob.glob("par2deep/libpar2.*"), - 'packages': ["configargparse","pyqt5", "Send2Trash", "tqdm"], #adding tqdm causes every installed pip package to be included... + 'packages': ["configargparse","PyQt5", "send2trash", "tqdm"], #adding tqdm causes every installed pip package to be included... } shortcut_table = [