Skip to content

Commit b37e656

Browse files
committed
Fix #11: on win/lin 64 a compiled libpar2 is supplied
1 parent 7fc618c commit b37e656

File tree

6 files changed

+84
-37
lines changed

6 files changed

+84
-37
lines changed

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Analogous to the various *deep commands (md5deep, hashdeep...) this tool serves
44

55
This tool will generate one parity file (plus a file for the recovery blocks) per file that you protect. This makes it simple to move files if you change your mind on how your file tree must be organized. Just move the `par2` files along.
66

7+
On Windows and Linux 64bit platforms, a compiled [libpar2](https://github.com/brenthuisman/libpar2) is provided and no external `par2` required.
8+
79
## Motivation
810

911
I chose to use the old but well tested and well known `par2` program to base this tool on, instead of similar tools such as `zfec`, `rsbep` or something like `pyFileFixity`. Some recent forks of `par2` have added recursive scanning abilities, but they're generally not cross-platform. They also do not offer an interactive way of diagnosing (parts of) your file tree, and different problem handling for different areas of your file tree.
@@ -12,22 +14,24 @@ I use `par2deep` to secure my photos and music across drives, machines and opera
1214

1315
## Install
1416

15-
You can now use pip! Make sure to update pip before installation (PyQt5 won't install without a recent pip).
17+
You can now use pip! Make sure to update pip before installation (PyQt5 won't install without a recent pip). On Windows, you make have to run `python` for `python3` and `pip` for `pip3`.
1618

17-
$ pip(3) install -U pip
18-
$ pip(3) install par2deep --user
19+
$ pip3 install -U pip
20+
$ pip3 install par2deep --user
1921

2022
Or clone/download this repo and install manually with:
2123

22-
$ python(3) setup.py install --user
24+
$ python3 setup.py install --user
2325

2426
Or run directly with:
2527

26-
$ python(3) par2deep
28+
$ python3 par2deep
2729

2830
Alternatively, if you have installed the `cx_Freeze` package, you can generate an msi package for Windows. Adapt `setup_cx.py` to suit your needs (include the `par2` executable and, most importantly, the icon of your choice) and then build the `.msi` file in `/dist`:
2931

30-
$ python setup_cx.py bdist_msi
32+
$ python3 setup_cx.py bdist_msi
33+
34+
Note this has not been tested since v1.0.5!
3135

3236
## Usage
3337

@@ -47,10 +51,11 @@ Example `par2deep.ini`:
4751
* configargparse
4852
* Send2Trash
4953
* PyQt5
50-
* `par2` in path or specify a tool with the same interface.
54+
* Optional, if you are not on Windows or Linux 64bit, `par2` in path or specify with `par_cmd`.
5155

5256
### Changelog
5357

58+
* 2020-04-26: Include libpar2 for win64 and lin64 platforms, no external `par2` needed anymore.
5459
* 2020-04-20: recreate verified_repairable creates backup. backups are shows upon init. orphans are shown upon init.
5560
* 2020-04-16: v1.9.3: Packaging still is a pain!
5661
* 2020-04-16: v1.9.0: GUI rewritten in with Qt (PyQt5). Open Issues should be solved for 2.0.0 release.

par2deep/gui_qt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ def textchanged(text):
105105
exex_fld.setToolTip("These extensions will be excluded from the analysis.")
106106
exex_fld.textChanged.connect(lambda fldval : self.p2d.args.update({"extexcludes":fldval.split(',')}))
107107

108-
parpath_lb = QLabel("Path to par2(.exe):")
108+
parpath_lb = QLabel("Fallback path to par2(.exe):")
109109
parpath_fld = QLineEdit(self.p2d.args["par_cmd"])
110-
parpath_fld.setToolTip("Should be set automatically and correctly, but can be overridden.")
110+
parpath_fld.setToolTip("If you don't use Windows or Linux 64bit, and par2 is not in PATH, par2deep will fallback to using this command.")
111111
parpath_fld.textChanged.connect(lambda fldval : self.p2d.args.update({"par_cmd":fldval}))
112112

113113
perc_sldr = BSlider("Percentage of protection",5,100,lambda fldval : self.p2d.args.update({"percentage":fldval}),self.p2d.args["percentage"])

par2deep/libpar2.dll

5.55 MB
Binary file not shown.

par2deep/libpar2.so

1.99 MB
Binary file not shown.

par2deep/par2deep.py

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import sys,os,subprocess,re,glob,shutil
1+
import sys,struct,ctypes,os,subprocess,re,glob,shutil
22
from configargparse import ArgParser
33
from send2trash import send2trash
44

@@ -24,21 +24,20 @@
2424
class par2deep():
2525
def __init__(self,chosen_dir=None):
2626
#CMD arguments and configfile
27+
28+
self.shell=False
29+
self.par_cmd = 'par2'
2730
if sys.platform == 'win32':
28-
self.shell=True
31+
self.shell=True #shell true because otherwise pythonw.exe pops up a cmd.exe for EVERY file.
2932
locs = [os.path.join(sys.path[0],'phpar2.exe'),
3033
'phpar2.exe',
3134
os.path.join(sys.path[0],'par2.exe'),
3235
'par2.exe',
3336
]
34-
par_cmd = 'par2'
3537
for p in locs:
3638
if os.path.isfile(p):
37-
par_cmd = p
39+
self.par_cmd = p
3840
break
39-
else:
40-
self.shell=False
41-
par_cmd = 'par2'
4241

4342
if chosen_dir == None or not os.path.isdir(chosen_dir):
4443
current_data_dir = os.getcwd()
@@ -58,7 +57,7 @@ def __init__(self,chosen_dir=None):
5857
parser.add_argument("-dir", "--directory", type=str, default=current_data_dir, help="Path to protect (default is current directory).")
5958
#parser.add_argument("-pardir", "--parity_directory", type=str, default=os.getcwd(), help="Path to parity data store (default is current directory).")
6059
parser.add_argument("-pc", "--percentage", type=int, default=5, help="Set the parity percentage (default 5%%).")
61-
parser.add_argument("-pcmd", "--par_cmd", type=str, default=par_cmd, help="Set path to alternative par2 command (default \"par2\").")
60+
parser.add_argument("-pcmd", "--par_cmd", type=str, default=self.par_cmd, help="Set path to alternative par2 command (default \"par2\").")
6261

6362
#lets get a nice dict of all o' that.
6463
#FIXME: catch unrecognized arguments
@@ -76,16 +75,25 @@ def __init__(self,chosen_dir=None):
7675
return
7776

7877

79-
def runpar(self,command):
80-
devnull = open(os.devnull, 'wb')
81-
#shell true because otherwise pythonw.exe pops up a cmd.exe for EVERY file.
82-
try:
83-
subprocess.check_call(command,shell=self.shell,stdout=devnull,stderr=devnull)
84-
return 0
85-
except subprocess.CalledProcessError as e:
86-
return e.returncode
87-
except FileNotFoundError:
88-
return 200
78+
def runpar(self,command=""):
79+
if self.fallback:
80+
cmdcommand = [self.par_cmd]
81+
cmdcommand.extend(command)
82+
devnull = open(os.devnull, 'wb')
83+
try:
84+
subprocess.check_call(cmdcommand,shell=self.shell,stdout=devnull,stderr=devnull)
85+
return 0
86+
except subprocess.CalledProcessError as e:
87+
return e.returncode
88+
except FileNotFoundError:
89+
return 200
90+
else:
91+
def strlist2charpp(stringlist):
92+
argc = len(stringlist)
93+
Args = ctypes.c_char_p * (len(stringlist)+1)
94+
argv = Args(*[ctypes.c_char_p(arg.encode("utf-8")) for arg in stringlist])
95+
return argc,argv
96+
return self.libpar2.par2cmdline(*strlist2charpp(command))
8997

9098

9199
def check_state(self):
@@ -94,7 +102,40 @@ def check_state(self):
94102
setattr(self, k, v)
95103
self.percentage = str(self.percentage)
96104

97-
if self.runpar([self.par_cmd]) == 200:
105+
#we provide a win64 and lin64 library, use if on those platforms, otherwise fallback to par_cmd, and check if that is working
106+
self.fallback = True
107+
_void_ptr_size = struct.calcsize('P')
108+
bit64 = _void_ptr_size * 8 == 64
109+
110+
if bit64:
111+
windows = 'win32' in str(sys.platform).lower()
112+
linux = 'linux' in str(sys.platform).lower()
113+
macos = 'darwin' in str(sys.platform).lower()
114+
this_script_dir = os.path.dirname(os.path.abspath(__file__))
115+
if windows:
116+
try:
117+
os.add_dll_directory(this_script_dir) #needed on python3.8 on win
118+
except:
119+
pass #not available or necesary on py37 and before
120+
try:
121+
self.libpar2 = ctypes.CDLL(os.path.join(this_script_dir,"libpar2.dll"))
122+
self.fallback = False
123+
except:
124+
self.fallback = True
125+
elif linux:
126+
try:
127+
self.libpar2 = ctypes.CDLL(os.path.join(this_script_dir,"libpar2.so"))
128+
self.fallback = False
129+
except:
130+
self.fallback = True
131+
elif macos:
132+
pass #TODO if possible
133+
else:
134+
pass
135+
else:
136+
pass
137+
138+
if self.fallback and self.runpar() == 200:
98139
return 200
99140
#if 200, then par2 doesnt exist.
100141

@@ -213,7 +254,7 @@ def execute(self):
213254
for p in pars:
214255
send2trash(p)
215256
#par2 does not delete preexisting parity data, so delete any possible data.
216-
createdfiles.append([ f , self.runpar([self.par_cmd,"c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
257+
createdfiles.append([ f , self.runpar(["c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
217258
createdfiles_err=[ [i,j] for i,j in createdfiles if j != 0 and j != 100 ]
218259

219260
verifiedfiles=[]
@@ -224,7 +265,7 @@ def execute(self):
224265
#print('Verifying ...')
225266
for f in verify:
226267
yield f
227-
verifiedfiles.append([ f , self.runpar([self.par_cmd,"v",f]) ])
268+
verifiedfiles.append([ f , self.runpar(["v",f]) ])
228269
verifiedfiles_err=[ [i,j] for i,j in verifiedfiles if j != 0 and j != 100 and j != 1 ]
229270
verifiedfiles_repairable=[ [i,j] for i,j in verifiedfiles if j == 1 ]
230271
verifiedfiles_succes=[ [i,j] for i,j in verifiedfiles if j == 0 ]
@@ -273,7 +314,7 @@ def execute_repair(self):
273314
if self.len_verified_actions>0:
274315
for f,retcode in self.verifiedfiles_repairable:
275316
yield f
276-
retval = self.runpar([self.par_cmd,"r",f])
317+
retval = self.runpar(["r",f])
277318
if retval == 0:
278319
if self.clean_backup:
279320
#backups should just have been cleaned in the execute phase and therefore a .1 been created.
@@ -287,7 +328,7 @@ def execute_repair(self):
287328
pars = glob.glob(glob.escape(f)+'*.par2')
288329
for p in pars:
289330
send2trash(p)
290-
recreatedfiles.append([ f , self.runpar([self.par_cmd,"c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
331+
recreatedfiles.append([ f , self.runpar(["c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
291332

292333
self.recreate = sorted(recreatedfiles)
293334
self.recreate_err = sorted([f for f,err in recreatedfiles if err !=0])
@@ -311,7 +352,7 @@ def execute_recreate(self):
311352
ftmp = f+".par2deep_tmpfile"
312353
shutil.copyfile(f,ftmp)
313354
# now that we have a backup of the repairable, repair to obtain the actual backup we want.
314-
retval = self.runpar([self.par_cmd,"r",f])
355+
retval = self.runpar(["r",f])
315356
if retval == 0:
316357
# f is now the file we actually want to backup.
317358
shutil.copyfile(f,f+".0") # will overwrite acc. to docs
@@ -333,14 +374,14 @@ def execute_recreate(self):
333374
pars = glob.glob(glob.escape(f)+'*.par2')
334375
for p in pars:
335376
send2trash(p)
336-
recreatedfiles.append([ f , self.runpar([self.par_cmd,"c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
377+
recreatedfiles.append([ f , self.runpar(["c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
337378

338379
for f,retcode in self.verifiedfiles_err:
339380
yield f
340381
pars = glob.glob(glob.escape(f)+'*.par2')
341382
for p in pars:
342383
send2trash(p)
343-
recreatedfiles.append([ f , self.runpar([self.par_cmd,"c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
384+
recreatedfiles.append([ f , self.runpar(["c","-r"+self.percentage,"-n"+self.nr_parfiles,f]) ])
344385

345386
self.recreate = sorted(recreatedfiles)
346387
self.recreate_err = sorted([f for f,err in recreatedfiles if err !=0])

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#! /usr/bin/env python
22
from setuptools import setup
33

4-
VERSION = '1.9.3'
4+
VERSION = '1.9.4'
55

66
def main():
77
setup(name='par2deep',
@@ -22,13 +22,14 @@ def main():
2222
],
2323
keywords='par2 file integrity',
2424
author='Brent Huisman',
25-
author_email='mail@brenthuisman.net',
25+
author_email='brent@huisman.pl',
2626
url='https://github.com/brenthuisman/par2deep',
2727
license='LGPL',
2828
include_package_data=True,
2929
zip_safe=False,
3030
install_requires=['tqdm','configargparse','Send2Trash','PyQt5'],
3131
packages=['par2deep'],
32+
package_data={'': ['libpar2.so','libpar2.dll']},
3233
entry_points={
3334
"console_scripts": ['par2deep = par2deep.gui_qt:main', 'par2deep-tk = par2deep.gui_tk:main', 'par2deep-cli = par2deep.cli:main'],
3435
}

0 commit comments

Comments
 (0)