Add rarpar.py.
This commit is contained in:
parent
da8d1d6224
commit
83d5993b66
1 changed files with 385 additions and 0 deletions
385
rarpar.py
Normal file
385
rarpar.py
Normal file
|
@ -0,0 +1,385 @@
|
|||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
from voussoirkit import betterhelp
|
||||
from voussoirkit import bytestring
|
||||
from voussoirkit import pathclass
|
||||
from voussoirkit import winglob
|
||||
|
||||
RESERVE_SPACE_ON_DRIVE = 30 * bytestring.GIBIBYTE
|
||||
|
||||
def RARCOMMAND(
|
||||
basename,
|
||||
input_pattern,
|
||||
workdir,
|
||||
password=None,
|
||||
rec=None,
|
||||
rev=None,
|
||||
volume=None,
|
||||
):
|
||||
'''
|
||||
winrar
|
||||
a = make archive
|
||||
-ibck = run in the background
|
||||
-ma = rar5 mode
|
||||
-m0 = compression level: store
|
||||
-mt1 = thread count: 1
|
||||
-v{x}M = split into x megabyte volumes
|
||||
-ri x:y = x priority (lower is less pri) y ms sleep between ops
|
||||
-r = include subdirectories
|
||||
-ep1 = arcnames will start relative to the main folder,
|
||||
instead of having the whole abspath of the input_pattern.
|
||||
-rr{x}% = x% recovery record
|
||||
-rv{x}% = x% recovery volumes
|
||||
-hp{x} = encrypt with password x
|
||||
-y = yes on all prompts
|
||||
-x = exclude certain filenames
|
||||
destination
|
||||
workdir/basename.rar
|
||||
files to include
|
||||
input_pattern
|
||||
'''
|
||||
command = [
|
||||
'winrar',
|
||||
'a -ibck -ma -m0 -mt1 -ri1:30 -r -ep1',
|
||||
'-y -xthumbs.db -xdesktop.ini',
|
||||
]
|
||||
if volume is not None:
|
||||
command.append(f'-v{volume}M')
|
||||
|
||||
if rec is not None:
|
||||
command.append(f'-rr{rec}%')
|
||||
|
||||
if rev is not None:
|
||||
command.append(f'-rv{rev}%')
|
||||
|
||||
if password is not None:
|
||||
command.append(f'-hp{password}')
|
||||
|
||||
command.append(f'"{workdir.absolute_path}{os.sep}{basename}.rar"')
|
||||
command.append(f'"{input_pattern}"')
|
||||
|
||||
command = ' '.join(command)
|
||||
return command
|
||||
|
||||
def PARCOMMAND(workdir, basename, par):
|
||||
'''
|
||||
phpar2
|
||||
c = create pars
|
||||
-t1 = thread count: 1
|
||||
-r{x} = x% recovery
|
||||
destination
|
||||
workdir/basename.par2
|
||||
'''
|
||||
command = [
|
||||
'phpar2',
|
||||
'c -t1',
|
||||
f'-r{par}',
|
||||
f'"{workdir.absolute_path}{os.sep}{basename}"',
|
||||
f'"{workdir.absolute_path}{os.sep}{basename}*.rar"',
|
||||
]
|
||||
command = ' '.join(command)
|
||||
return command
|
||||
|
||||
def assert_enough_space(pathsize, workdir, moveto, rec, rev, par):
|
||||
plus_percent = (rec + rev + par) / 100
|
||||
needed = pathsize * (1 + plus_percent)
|
||||
reserve = RESERVE_SPACE_ON_DRIVE + needed
|
||||
|
||||
workdir_drive = os.path.splitdrive(workdir.absolute_path)[0]
|
||||
free_space = shutil.disk_usage(workdir_drive).free
|
||||
|
||||
if moveto is not None:
|
||||
moveto_drive = os.path.splitdrive(moveto.absolute_path)[0]
|
||||
free_space = min(free_space, shutil.disk_usage(moveto_drive).free)
|
||||
|
||||
message = ' '.join([
|
||||
f'For {bytestring.bytestring(pathsize)},',
|
||||
f'Reserving {bytestring.bytestring(reserve)} /',
|
||||
f'{bytestring.bytestring(free_space)}.',
|
||||
])
|
||||
print(message)
|
||||
|
||||
if reserve > free_space:
|
||||
raise IOError('Please leave more space')
|
||||
|
||||
def move(pattern, directory):
|
||||
files = winglob.glob(pattern)
|
||||
for file in files:
|
||||
print(file)
|
||||
shutil.move(file, directory)
|
||||
|
||||
def _normalize_percentage(rec):
|
||||
if rec is None:
|
||||
return None
|
||||
|
||||
if rec == 0:
|
||||
return None
|
||||
|
||||
if isinstance(rec, str):
|
||||
rec = rec.strip('%')
|
||||
|
||||
rec = int(rec)
|
||||
|
||||
if not (0 <= rec <= 100):
|
||||
raise ValueError(f'rec, rev, par {rec} must be 0-100.')
|
||||
|
||||
return rec
|
||||
|
||||
def normalize_password(password):
|
||||
if password is None:
|
||||
return None
|
||||
|
||||
if not isinstance(password, str):
|
||||
raise TypeError(f'password must be a {str}, not {type(password)}')
|
||||
|
||||
if password == '':
|
||||
return None
|
||||
|
||||
return password
|
||||
|
||||
def _normalize_volume(volume, pathsize):
|
||||
if volume is None:
|
||||
return None
|
||||
|
||||
if isinstance(volume, int):
|
||||
return volume
|
||||
|
||||
if isinstance(volume, float):
|
||||
return int(volume)
|
||||
|
||||
if isinstance(volume, str):
|
||||
volume = volume.strip()
|
||||
|
||||
if volume == '100%':
|
||||
return None
|
||||
|
||||
minmax_parts = re.findall(r'(min|max)\((.+?), (.+?)\)', volume)
|
||||
if minmax_parts:
|
||||
(func, left, right) = minmax_parts[0]
|
||||
func = {'min': min, 'max': max}[func]
|
||||
left = _normalize_volume(left, pathsize=pathsize)
|
||||
right = _normalize_volume(right, pathsize=pathsize)
|
||||
return func(left, right)
|
||||
|
||||
if volume.endswith('%'):
|
||||
volume = volume[:-1]
|
||||
volume = float(volume) / 100
|
||||
volume = (pathsize * volume) / bytestring.MIBIBYTE
|
||||
volume = max(1, volume)
|
||||
return int(volume)
|
||||
|
||||
return int(volume)
|
||||
|
||||
raise TypeError(f'Invalid volume {type(volume)}.')
|
||||
|
||||
def normalize_volume(volume, pathsize):
|
||||
volume = _normalize_volume(volume, pathsize)
|
||||
if volume is not None and volume < 1:
|
||||
raise ValueError('Volume must be >= 1.')
|
||||
return volume
|
||||
|
||||
def run_script(script, dry=False):
|
||||
status = 0
|
||||
|
||||
if dry:
|
||||
for command in script:
|
||||
print(command)
|
||||
return status
|
||||
|
||||
for command in script:
|
||||
print(command)
|
||||
if isinstance(command, str):
|
||||
status = os.system(command)
|
||||
else:
|
||||
status = command()
|
||||
status = status or 0
|
||||
if status != 0:
|
||||
break
|
||||
|
||||
return status
|
||||
|
||||
def rarpar(
|
||||
path,
|
||||
*,
|
||||
basename=None,
|
||||
dry=False,
|
||||
moveto=None,
|
||||
par=None,
|
||||
password=None,
|
||||
rec=None,
|
||||
rev=None,
|
||||
volume=None,
|
||||
workdir='.',
|
||||
):
|
||||
path = pathclass.Path(path)
|
||||
|
||||
path.assert_exists()
|
||||
|
||||
if path.is_dir:
|
||||
input_pattern = path.absolute_path + '\\*'
|
||||
else:
|
||||
input_pattern = path.absolute_path
|
||||
|
||||
workdir = pathclass.Path(workdir)
|
||||
workdir.assert_is_directory()
|
||||
|
||||
if moveto:
|
||||
moveto = pathclass.Path(moveto)
|
||||
moveto.assert_is_directory()
|
||||
|
||||
pathsize = path.size
|
||||
|
||||
volume = normalize_volume(volume, pathsize)
|
||||
rec = _normalize_percentage(rec)
|
||||
rev = _normalize_percentage(rev)
|
||||
par = _normalize_percentage(par)
|
||||
|
||||
if RESERVE_SPACE_ON_DRIVE:
|
||||
assert_enough_space(
|
||||
pathsize,
|
||||
workdir=workdir,
|
||||
moveto=moveto,
|
||||
rec=rec or 0,
|
||||
rev=rev or 0,
|
||||
par=par or 0,
|
||||
)
|
||||
|
||||
timestamp = time.strftime('%Y-%m-%d')
|
||||
if not basename:
|
||||
basename = f'{path.basename} ({timestamp})'
|
||||
else:
|
||||
basename = basename.format(timestamp=timestamp)
|
||||
|
||||
existing = winglob.glob(f'{basename}*.rar')
|
||||
if existing:
|
||||
raise Exception(f'{existing[0]} already exists.')
|
||||
|
||||
#### ####
|
||||
|
||||
script = []
|
||||
|
||||
rarcommand = RARCOMMAND(
|
||||
basename=basename,
|
||||
input_pattern=input_pattern,
|
||||
password=password,
|
||||
rec=rec,
|
||||
rev=rev,
|
||||
volume=volume,
|
||||
workdir=workdir,
|
||||
)
|
||||
script.append(rarcommand)
|
||||
|
||||
if par:
|
||||
parcommand = PARCOMMAND(
|
||||
basename=basename,
|
||||
par=par,
|
||||
workdir=workdir,
|
||||
)
|
||||
script.append(parcommand)
|
||||
|
||||
def move_rars():
|
||||
move(f'{workdir.absolute_path}\\{basename}*.rar', f'{moveto.absolute_path}')
|
||||
def move_revs():
|
||||
move(f'{workdir.absolute_path}\\{basename}*.rev', f'{moveto.absolute_path}')
|
||||
def move_pars():
|
||||
move(f'{workdir.absolute_path}\\{basename}*.par2', f'{moveto.absolute_path}')
|
||||
|
||||
if moveto:
|
||||
if True:
|
||||
script.append(move_rars)
|
||||
if rev:
|
||||
script.append(move_revs)
|
||||
if par:
|
||||
script.append(move_pars)
|
||||
|
||||
#### ####
|
||||
|
||||
status = run_script(script, dry)
|
||||
|
||||
return status
|
||||
|
||||
def rarpar_argparse(args):
|
||||
return rarpar(
|
||||
path=args.path,
|
||||
volume=args.volume,
|
||||
basename=args.basename,
|
||||
dry=args.dry,
|
||||
moveto=args.moveto,
|
||||
par=args.par,
|
||||
password=args.password,
|
||||
rec=args.rec,
|
||||
rev=args.rev,
|
||||
workdir=args.workdir,
|
||||
)
|
||||
|
||||
DOCSTRING = '''
|
||||
path:
|
||||
The input file or directory to rarpar.
|
||||
|
||||
--volume X | X% | min(A, B) | max(A, B)
|
||||
Split rars into volumes of this many megabytes. Should be
|
||||
An integer number of megabytes, or;
|
||||
A percentage "X%" to calculate volumes as X% of the file size, down to
|
||||
a 1 MB minimum, or;
|
||||
A string "min(A, B)" or "max(A, B)" where A and B follow the above rules.
|
||||
|
||||
--rec X
|
||||
A integer to generate X% recovery record in the rars.
|
||||
See winrar documentation for information about recovery records.
|
||||
|
||||
--rev X
|
||||
A integer to generate X% recovery volumes.
|
||||
Note that winrar's behavior is the number of revs will always be less than
|
||||
the number of rars. If you don't split volumes, you will have 1 rar and
|
||||
thus 0 revs even if you ask for 100% rev.
|
||||
See winrar documentation for information about recovery volumes.
|
||||
|
||||
--par X
|
||||
A number to generate X% recovery with par2.
|
||||
|
||||
--basename
|
||||
A basename for the rar and par files. You will end up with
|
||||
basename.partXX.rar and basename.par2.
|
||||
Without this argument, the default basename is "basename ({timestamp})".
|
||||
Your string may include {timestamp} including the braces to get the
|
||||
timestamp there.
|
||||
|
||||
--password
|
||||
A password with which to encrypt the rar files.
|
||||
|
||||
--workdir:
|
||||
The directory in which the rars and pars will be generated while the
|
||||
program is working.
|
||||
|
||||
--moveto:
|
||||
The directory to which the rars and pars will be moved after the program
|
||||
has finished working.
|
||||
|
||||
--dry:
|
||||
Print the commands that will be run, but don't actually run them.
|
||||
'''
|
||||
|
||||
def main(argv):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
|
||||
parser.add_argument('path')
|
||||
parser.add_argument('--volume', dest='volume')
|
||||
parser.add_argument('--rec', dest='rec')
|
||||
parser.add_argument('--rev', dest='rev')
|
||||
parser.add_argument('--par', dest='par')
|
||||
parser.add_argument('--basename', dest='basename')
|
||||
parser.add_argument('--password', dest='password')
|
||||
parser.add_argument('--workdir', dest='workdir', default='.')
|
||||
parser.add_argument('--moveto', dest='moveto')
|
||||
parser.add_argument('--dry', dest='dry', action='store_true')
|
||||
parser.set_defaults(func=rarpar_argparse)
|
||||
|
||||
return betterhelp.single_main(argv, parser, DOCSTRING)
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main(sys.argv[1:]))
|
Loading…
Reference in a new issue