Completed
Push — master ( bc42c6...67fcfc )
by John
10:06
created

compress()   B

Complexity

Conditions 2

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 29
ccs 11
cts 11
cp 1
crap 2
rs 8.8571
c 0
b 0
f 0
1
#!/usr/bin/env python3
2 5
"""This module is used to operate with archives."""
3
4 5
import os  # filesystem read
5 5
import subprocess  # invocation of 7z, cap
6 5
import zipfile  # zip compresssion
7 5
import tarfile  # txz/tbz/tgz/tar compression
8 5
from bbarchivist import barutils  # zip tester
9 5
from bbarchivist import utilities  # platform determination
10 5
from bbarchivist import bbconstants  # premade stuff
11 5
from bbarchivist import decorators  # timer
12 5
from bbarchivist import iniconfig  # config parsing
13
14 5
__author__ = "Thurask"
15 5
__license__ = "WTFPL v2"
16 5
__copyright__ = "2015-2017 Thurask"
17
18
19 5
def smart_is_tarfile(filepath):
20
    """
21
    :func:`tarfile.is_tarfile` plus error handling.
22
23
    :param filepath: Filename.
24
    :type filepath: str
25
    """
26 5
    try:
27 5
        istar = tarfile.is_tarfile(filepath)
28 5
    except (OSError, IOError):
29 5
        return False
30
    else:
31 5
        return istar
32
33
34 5
def szcodes():
35
    """
36
    Return dictionary of 7-Zip error codes.
37
    """
38 5
    szc = {
39
        0: "NO ERRORS",
40
        1: "COMPLETED WITH WARNINGS",
41
        2: "FATAL ERROR",
42
        7: "COMMAND LINE ERROR",
43
        8: "OUT OF MEMORY ERROR",
44
        255: "USER STOPPED PROCESS"
45
    }
46 5
    return szc
47
48
49 5
@decorators.timer
50 5
def sz_compress(filepath, filename, szexe=None, strength=5, errors=False):
51
    """
52
    Pack a file into a LZMA2 7z file.
53
54
    :param filepath: Basename of file, no extension.
55
    :type filepath: str
56
57
    :param filename: Name of file to pack.
58
    :type filename: str
59
60
    :param szexe: Path to 7z executable.
61
    :type szexe: str
62
63
    :param strength: Compression strength. 5 is normal, 9 is ultra.
64
    :type strength: int
65
66
    :param errors: Print completion status message. Default is false.
67
    :type errors: bool
68
    """
69 5
    strength = str(strength)
70 5
    szc = szcodes()
71 5
    rawname = os.path.dirname(filepath)
72 5
    thr = str(utilities.get_core_count())
73 5
    fold = os.path.join(rawname, filename)
74 5
    cmd = '{0} a -mx{1} -m0=lzma2 -mmt{2} "{3}.7z" "{4}"'.format(szexe, strength, thr, filepath, fold)
75 5
    excode = sz_subprocess(cmd)
76 5
    if errors:
77 5
        print(szc[excode])
78
79
80 5
def sz_subprocess(cmd):
81
    """
82
    Subprocess wrapper for 7-Zip commands.
83
84
    :param cmd: Command to pass to subprocess.
85
    :type cmd: str
86
    """
87 5
    with open(os.devnull, 'wb') as dnull:
88 5
        output = subprocess.call(cmd, stdout=dnull, stderr=subprocess.STDOUT, shell=True)
89 5
    return output
90
91
92 5
def sz_verify(filepath, szexe=None):
93
    """
94
    Verify that a .7z file is valid and working.
95
96
    :param filepath: Filename.
97
    :type filepath: str
98
99
    :param szexe: Path to 7z executable.
100
    :type szexe: str
101
    """
102 5
    filepath = os.path.abspath(filepath)
103 5
    cmd = '{0} t "{1}"'.format(szexe, filepath)
104 5
    excode = sz_subprocess(cmd)
105 5
    return excode == 0
106
107
108 5
def generic_tarfile_verify(filepath, method):
109
    """
110
    Verify that a tar/tgz/tbz/txz file is valid and working.
111
112
    :param filepath: Filename.
113
    :type filepath: str
114
115
    :param method: Tarfile read method.
116
    :type method: str
117
    """
118 5
    if smart_is_tarfile(filepath):
119 5
        with tarfile.open(filepath, method) as thefile:
120 5
            mems = thefile.getmembers()
121 5
        return False if not mems else True
122
    else:
123 5
        return False
124
125
126 5
def generic_tarfile_compress(archivename, filename, method, strength=5):
127
    """
128
    Pack a file into an uncompressed/gzip/bzip2/LZMA tarfile.
129
130
    :param archivename: Archive name.
131
    :type archivename: str
132
133
    :param filename: Name of file to pack into archive.
134
    :type filename: str
135
136
    :param method: Tarfile compress method.
137
    :type method: str
138
139
    :param strength: Compression strength. 5 is normal, 9 is ultra.
140
    :type strength: int
141
    """
142 5
    nocomp = ["w:", "w:xz"]  # methods w/o compression: tar, tar.xz
143 5
    if method in nocomp:
144 5
        generic_nocompresslevel(archivename, filename, method)
145
    else:
146 5
        generic_compresslevel(archivename, filename, method, strength)
147
148
149 5
def generic_compresslevel(archivename, filename, method, strength=5):
150
    """
151
    Pack a file into a gzip/bzip2 tarfile.
152
153
    :param archivename: Archive name.
154
    :type archivename: str
155
156
    :param filename: Name of file to pack into archive.
157
    :type filename: str
158
159
    :param method: Tarfile compress method.
160
    :type method: str
161
162
    :param strength: Compression strength. 5 is normal, 9 is ultra.
163
    :type strength: int
164
    """
165 5
    with tarfile.open(archivename, method, compresslevel=strength) as afile:
166 5
        afile.add(filename, filter=None, arcname=os.path.basename(filename))
167
168
169 5
def generic_nocompresslevel(archivename, filename, method):
170
    """
171
    Pack a file into an uncompressed/LZMA tarfile.
172
173
    :param archivename: Archive name.
174
    :type archivename: str
175
176
    :param filename: Name of file to pack into archive.
177
    :type filename: str
178
179
    :param method: Tarfile compress method.
180
    :type method: str
181
    """
182 5
    with tarfile.open(archivename, method) as afile:
183 5
        afile.add(filename, filter=None, arcname=os.path.basename(filename))
184
185
186 5
@decorators.timer
187
def tar_compress(filepath, filename):
188
    """
189
    Pack a file into an uncompressed tarfile.
190
191
    :param filepath: Basename of file, no extension.
192
    :type filepath: str
193
194
    :param filename: Name of file to pack.
195
    :type filename: str
196
    """
197 5
    generic_tarfile_compress("{0}.tar".format(filepath), filename, "w:")
198
199
200 5
def tar_verify(filepath):
201
    """
202
    Verify that a tar file is valid and working.
203
204
    :param filepath: Filename.
205
    :type filepath: str
206
    """
207 5
    return generic_tarfile_verify(filepath, "r:")
208
209
210 5
@decorators.timer
211 5
def tgz_compress(filepath, filename, strength=5):
212
    """
213
    Pack a file into a gzip tarfile.
214
215
    :param filepath: Basename of file, no extension.
216
    :type filepath: str
217
218
    :param filename: Name of file to pack.
219
    :type filename: str
220
221
    :param strength: Compression strength. 5 is normal, 9 is ultra.
222
    :type strength: int
223
    """
224 5
    generic_tarfile_compress("{0}.tar.gz".format(filepath), filename, "w:gz", strength)
225
226
227 5
def tgz_verify(filepath):
228
    """
229
    Verify that a tar.gz file is valid and working.
230
231
    :param filepath: Filename.
232
    :type filepath: str
233
    """
234 5
    return generic_tarfile_verify(filepath, "r:gz")
235
236
237 5
@decorators.timer
238 5
def tbz_compress(filepath, filename, strength=5):
239
    """
240
    Pack a file into a bzip2 tarfile.
241
242
    :param filepath: Basename of file, no extension.
243
    :type filepath: str
244
245
    :param filename: Name of file to pack.
246
    :type filename: str
247
248
    :param strength: Compression strength. 5 is normal, 9 is ultra.
249
    :type strength: int
250
    """
251 5
    generic_tarfile_compress("{0}.tar.bz2".format(filepath), filename, "w:bz2", strength)
252
253
254 5
def tbz_verify(filepath):
255
    """
256
    Verify that a tar.bz2 file is valid and working.
257
258
    :param filepath: Filename.
259
    :type filepath: str
260
    """
261 5
    return generic_tarfile_verify(filepath, "r:bz2")
262
263
264 5
@decorators.timer
265
def txz_compress(filepath, filename):
266
    """
267
    Pack a file into a LZMA tarfile.
268
269
    :param filepath: Basename of file, no extension.
270
    :type filepath: str
271
272
    :param filename: Name of file to pack.
273
    :type filename: str
274
    """
275 5
    if not utilities.new_enough(3):
276 5
        pass
277
    else:
278 4
        generic_tarfile_compress("{0}.tar.xz".format(filepath), filename, "w:xz")
279
280
281 5
def txz_verify(filepath):
282
    """
283
    Verify that a tar.xz file is valid and working.
284
285
    :param filepath: Filename.
286
    :type filepath: str
287
    """
288 5
    if not utilities.new_enough(3):
289 5
        return None
290
    else:
291 4
        return generic_tarfile_verify(filepath, "r:xz")
292
293
294 5
@decorators.timer
295
def zip_compress(filepath, filename):
296
    """
297
    Pack a file into a DEFLATE zipfile.
298
299
    :param filepath: Basename of file, no extension.
300
    :type filepath: str
301
302
    :param filename: Name of file to pack.
303
    :type filename: str
304
    """
305 5
    with zipfile.ZipFile("{0}.zip".format(filepath), 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as zfile:
306 5
        zfile.write(filename, arcname=os.path.basename(filename))
307
308
309 5
def zip_verify(filepath):
310
    """
311
    Verify that a .zip file is valid and working.
312
313
    :param filepath: Filename.
314
    :type filepath: str
315
    """
316 5
    if zipfile.is_zipfile(filepath):
317 5
        brokens = barutils.bar_tester(filepath)
318 5
        return brokens != filepath
319
    else:
320 5
        return False
321
322
323 5
def filter_method(method, szexe=None):
324
    """
325
    Make sure methods are OK.
326
327
    :param method: Compression method to use.
328
    :type method: str
329
330
    :param szexe: Path to 7z executable, if needed.
331
    :type szexe: str
332
    """
333 5
    if not utilities.new_enough(3) and method == "txz":
334 5
        method = "zip"  # fallback
335 5
    method = filter_method_nosz(method, szexe)
336 5
    return method
337
338
339 5
def filter_method_nosz(method, szexe=None):
340
    """
341
    Make sure 7-Zip is OK.
342
343
    :param method: Compression method to use.
344
    :type method: str
345
346
    :param szexe: Path to 7z executable, if needed.
347
    :type szexe: str
348
    """
349 5
    if method == "7z" and szexe is None:
350 5
        ifexists = utilities.prep_seven_zip()  # see if 7z exists
351 5
        if not ifexists:
352 5
            method = "zip"  # fallback
353
        else:
354 5
            szexe = utilities.get_seven_zip(False)
355 5
    return method
356
357
358 5
def calculate_strength():
359
    """
360
    Determine zip/gzip/bzip2 strength by OS bit setting.
361
    """
362 5
    strength = 9 if utilities.is_amd64() else 5
363 5
    return strength
364
365
366 5
def filter_with_boolfilt(files, criterion, critargs):
367
    """
368
    Return everything that matches criterion.
369
370
    :param files: Files to work on.
371
    :type files: list(str)
372
373
    :param criterion: Function to use for evaluation.
374
    :type criterion: func
375
376
    :param critargs: Arguments for function, other than file.
377
    :type critargs: list
378
    """
379 5
    return [file for file in files if criterion(file, *critargs)]
380
381
382 5
def filter_without_boolfilt(files, criterion, critargs):
383
    """
384
    Return everything that doesn't match criterion.
385
386
    :param files: Files to work on.
387
    :type files: list(str)
388
389
    :param criterion: Function to use for evaluation.
390
    :type criterion: func
391
392
    :param critargs: Arguments for function, other than file.
393
    :type critargs: list
394
    """
395 5
    return [file for file in files if not criterion(file, *critargs)]
396
397
398 5
def filtercomp(files, criterion, critargs, boolfilt=True):
399
    """
400
    :param files: Files to work on.
401
    :type files: list(str)
402
403
    :param criterion: Function to use for evaluation.
404
    :type criterion: func
405
406
    :param critargs: Arguments for function, other than file.
407
    :type critargs: list
408
409
    :param boolfilt: True if comparing criterion, False if comparing not criterion.
410
    :type boolfilt: bool
411
    """
412 5
    if boolfilt:
413 5
        fx2 = filter_with_boolfilt(files, criterion, critargs)
414
    else:
415 5
        fx2 = filter_without_boolfilt(files, criterion, critargs)
416 5
    return fx2
417
418
419 5
def compressfilter_select(filepath, files, selective=False):
420
    """
421
    :param filepath: Working directory. Required.
422
    :type filepath: str
423
424
    :param files: List of files in filepath.
425
    :type files: list(str)
426
427
    :param selective: Only compress autoloaders. Default is false.
428
    :type selective: bool/str
429
    """
430 5
    arx = bbconstants.ARCS
431 5
    pfx = bbconstants.PREFIXES
432 5
    if selective is None:
433 5
        filt2 = os.listdir(filepath)
434 5
    elif selective == "arcsonly":
435 5
        filt2 = filtercomp(files, utilities.prepends, ("", arx))
436 5
    elif selective:
437 5
        filt0 = filtercomp(files, utilities.prepends, (pfx, ""))
438 5
        filt1 = filtercomp(filt0, utilities.prepends, ("", arx), False)  # pop archives
439 5
        filt2 = filtercomp(filt1, utilities.prepends, ("", ".exe"))  # include exes
440
    else:
441 5
        filt2 = filtercomp(files, utilities.prepends, ("", arx), False)  # pop archives
442 5
    return filt2
443
444
445 5
def compressfilter(filepath, selective=False):
446
    """
447
    Filter directory listing of working directory.
448
449
    :param filepath: Working directory. Required.
450
    :type filepath: str
451
452
    :param selective: Only compress autoloaders. Default is false.
453
    :type selective: bool/str
454
    """
455
456 5
    files = [file for file in os.listdir(filepath) if not os.path.isdir(file)]
457 5
    filt2 = compressfilter_select(filepath, files, selective)
458 5
    filt3 = [os.path.join(filepath, file) for file in filt2]
459 5
    return filt3
460
461
462 5
def prep_compress_function(method="7z", szexe=None, errors=False):
463
    """
464
    Prepare compression function and partial arguments.
465
466
    :param method: Compression type. Default is "7z".
467
    :type method: str
468
469
    :param szexe: Path to 7z executable, if needed.
470
    :type szexe: str
471
472
    :param errors: Print completion status message. Default is false.
473
    :type errors: bool
474
    """
475 5
    methods = {"7z": sz_compress, "tgz": tgz_compress, "txz": txz_compress, "tbz": tbz_compress,
476
               "tar": tar_compress, "zip": zip_compress}
477 5
    args = [szexe] if method == "7z" else []
478 5
    if method in ("7z", "tbz", "tgz"):
479 5
        args.append(calculate_strength())
480 5
    if method == "7z":
481 5
        args.append(errors)
482 5
    return methods[method], args
483
484
485 5
def compress(filepath, method="7z", szexe=None, selective=False, errors=False):
486
    """
487
    Compress all autoloader files in a given folder, with a given method.
488
489
    :param filepath: Working directory. Required.
490
    :type filepath: str
491
492
    :param method: Compression type. Default is "7z".
493
    :type method: str
494
495
    :param szexe: Path to 7z executable, if needed.
496
    :type szexe: str
497
498
    :param selective: Only compress autoloaders. Default is false.
499
    :type selective: bool
500
501
    :param errors: Print completion status message. Default is false.
502
    :type errors: bool
503
    """
504 5
    method = filter_method(method, szexe)
505 5
    files = compressfilter(filepath, selective)
506 5
    for file in files:
507 5
        fname = os.path.basename(file)
508 5
        filename = os.path.splitext(fname)[0]
509 5
        fileloc = os.path.join(filepath, filename)
510 5
        print("COMPRESSING: {0}".format(fname))
511 5
        compfunc, extargs = prep_compress_function(method, szexe, errors)
512 5
        compfunc(fileloc, file, *extargs)
513 5
    return True
514
515
516 5
def tarzip_verifier(file):
517
    """
518
    Assign .tar.xxx, .tar and .zip verifiers.
519
520
    :param file: Filename.
521
    :type file: str
522
    """
523 5
    maps = {".tar.gz": tgz_verify, ".tar.xz": txz_verify,
524
            ".tar.bz2": tbz_verify, ".tar": tar_verify,
525
            ".zip": zip_verify, ".bar": zip_verify}
526 5
    for key, value in maps.items():
527 5
        if file.endswith(key):
528 5
            return value(file)
529
530
531 5
def decide_verifier(file, szexe=None):
532
    """
533
    Decide which verifier function to use.
534
535
    :param file: Filename.
536
    :type file: str
537
538
    :param szexe: Path to 7z executable, if needed.
539
    :type szexe: str
540
    """
541 5
    print("VERIFYING: {0}".format(file))
542 5
    if file.endswith(".7z") and szexe is not None:
543 5
        verif = sz_verify(os.path.abspath(file), szexe)
544
    else:
545 5
        verif = tarzip_verifier(file)
546 5
    decide_verifier_printer(file, verif)
547 5
    return verif
548
549
550 5
def decide_verifier_printer(file, verif):
551
    """
552
    Print status of verifier function.
553
554
    :param file: Filename.
555
    :type file: str
556
557
    :param verif: If the file is OK or not.
558
    :type verif: bool
559
    """
560 5
    if not verif:
561 5
        print("{0} IS BROKEN!".format(os.path.basename(file)))
562
    else:
563 5
        print("{0} OK".format(os.path.basename(file)))
564
565
566 5
def verify(filepath, method="7z", szexe=None, selective=False):
567
    """
568
    Verify specific archive files in a given folder.
569
570
    :param filepath: Working directory. Required.
571
    :type filepath: str
572
573
    :param method: Compression type. Default is "7z". Defined in source.
574
    :type method: str
575
576
    :param szexe: Path to 7z executable, if needed.
577
    :type szexe: str
578
579
    :param selective: Only verify autoloaders. Default is false.
580
    :type selective: bool
581
    """
582 5
    method = filter_method(method, szexe)
583 5
    files = compressfilter(filepath, selective)
584 5
    for file in files:
585 5
        decide_verifier(file, szexe)
586
587
588 5
def compress_config_loader(homepath=None):
589
    """
590
    Read a ConfigParser file to get compression preferences.
591
592
    :param homepath: Folder containing ini file. Default is user directory.
593
    :type homepath: str
594
    """
595 5
    compini = iniconfig.generic_loader('compression', homepath)
596 5
    method = compini.get('method', fallback="7z")
597 5
    if not utilities.new_enough(3) and method == "txz":
598 5
        method = "zip"  # for 3.2 compatibility
599 5
    return method
600
601
602 5
def compress_config_writer(method=None, homepath=None):
603
    """
604
    Write a ConfigParser file to store compression preferences.
605
606
    :param method: Method to use.
607
    :type method: str
608
609
    :param homepath: Folder containing ini file. Default is user directory.
610
    :type homepath: str
611
    """
612 5
    method = compress_config_loader() if method is None else method
613 5
    results = {"method": method}
614 5
    iniconfig.generic_writer("compression", results, homepath)
615
616
617 5
def verify_android_tools(tooldir):
618
    """
619
    Verify Android SDK platform tools archives.
620
621
    :param tooldir: Directory containing platform tools zips.
622
    :type tooldir: str
623
    """
624 5
    zipver = [zip_verify(os.path.join(os.path.abspath(tooldir), x)) for x in os.listdir(os.path.abspath(tooldir)) if x.endswith(".zip")]
625 5
    if zipver = [True, True, True]:  # all OK
626 5
        return True
627
    else:
628 5
        return False
629
630
631 5
def extract_zip(infile, outpath):
632
    """
633
    Extract a zip file to a given output path.
634
635
    :param infile: Path to input zip file.
636
    :type infile: str
637
638 5
    :param outpath: Path to extract input zip to.
639 5
    :type outpath: str
640 5
    """
641 5
    zipf = zipfile.ZipFile(infile)
642 5
    zipf.extractall(outpath)
643 5
644 5
645
def extract_android_tools(tooldir):
646
    """
647 5
    Extract Android SDK platform tools archives.
648
649
    :param tooldir: Directory containing platform tools zips.
650
    :type tooldir: str
651
    """
652
    plats = [x for x in os.listdir(os.path.abspath(tooldir)) if x.endswith(".zip")]
653
    for zipx in plats:
654
        platform = os.path.basename(zipx).replace("platform-tools-latest-", "").replace(".zip", "")
655
        platdir = os.path.join(os.path.abspath(tooldir), platform)
656
        os.makedirs(platdir)
657
        extract_zip(os.path.join(os.path.abspath(tooldir), zipx), platdir)
658 5
659 5
660
@decorators.timer
661 5
def pack_tclloader(dirname, filename):
662
    """
663
    Compress Android autoloader folder.
664 5
665
    :param dirname: Target folder.
666
    :type dirname: str
667
668
    :param filename: File title, without extension.
669
    :type filename: str
670
    """
671
    if utilities.prep_seven_zip():
672
        pack_tclloader_sz(dirname, filename)
673
    else:
674 5
        pack_tclloader_zip(dirname, filename)
675 5
676 5
677 5
def pack_tclloader_sz(dirname, filename):
678 5
    """
679
    Compress Android autoloader folder into a 7z file.
680
681 5
    :param dirname: Target folder.
682
    :type dirname: str
683
684
    :param filename: File title, without extension.
685
    :type filename: str
686
    """
687
    szexe = utilities.get_seven_zip()
688
    strength = calculate_strength()
689
    thr = str(utilities.get_core_count())
690
    cmd = '{0} a -mx{1} -m0=lzma2 -mmt{2} "{3}.7z" "./{4}/*"'.format(szexe, strength, thr, filename, dirname)
691 5
    sz_subprocess(cmd)
692 5
693 5
694 5
def pack_tclloader_zip(dirname, filename):
695 5
    """
696 5
    Compress Android autoloader folder into a zip file.
697 5
698 5
    :param dirname: Target folder.
699
    :type dirname: str
700
701
    :param filename: File title, without extension.
702
    :type filename: str
703
    """
704
    with zipfile.ZipFile("{0}.zip".format(filename), 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as zfile:
705
        for root, dirs, files in os.walk(dirname):
706
            del dirs
707
            for file in files:
708
                print("ZIPPING: {0}".format(utilities.stripper(file)))
709
                abs_filename = os.path.join(root, file)
710
                abs_arcname = abs_filename.replace("{0}{1}".format(dirname, os.sep), "")
711
                zfile.write(abs_filename, abs_arcname)
712