From b6c0cc0b610e8a23d4b8c7353475fce0f4315947 Mon Sep 17 00:00:00 2001 From: Ajpantuso Date: Mon, 31 May 2021 01:51:29 -0400 Subject: [PATCH] archive - Adding exclusion_patterns option (#2616) * Adding exclusion_patterns option * Adding changelog fragment and Python 2.6 compatability * Minor refactoring for readability * Removing unneccessary conditional * Applying initial review suggestions * Adding missed review suggestion --- ...2616-archive-exclusion_patterns-option.yml | 2 + plugins/modules/files/archive.py | 105 ++++++++++++++---- .../targets/archive/tasks/main.yml | 13 +++ 3 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 changelogs/fragments/2616-archive-exclusion_patterns-option.yml diff --git a/changelogs/fragments/2616-archive-exclusion_patterns-option.yml b/changelogs/fragments/2616-archive-exclusion_patterns-option.yml new file mode 100644 index 0000000000..86ef806b63 --- /dev/null +++ b/changelogs/fragments/2616-archive-exclusion_patterns-option.yml @@ -0,0 +1,2 @@ +minor_changes: + - archive - added ``exclusion_patterns`` option to exclude files or subdirectories from archives (https://github.com/ansible-collections/community.general/pull/2616). diff --git a/plugins/modules/files/archive.py b/plugins/modules/files/archive.py index 8b8088dae1..8d4afa58a5 100644 --- a/plugins/modules/files/archive.py +++ b/plugins/modules/files/archive.py @@ -41,8 +41,16 @@ options: exclude_path: description: - Remote absolute path, glob, or list of paths or globs for the file or files to exclude from I(path) list and glob expansion. + - Use I(exclusion_patterns) to instead exclude files or subdirectories below any of the paths from the I(path) list. type: list elements: path + exclusion_patterns: + description: + - Glob style patterns to exclude files or directories from the resulting archive. + - This differs from I(exclude_path) which applies only to the source paths from I(path). + type: list + elements: path + version_added: 3.2.0 force_archive: description: - Allows you to force the module to treat this as an archive even if only a single file is specified. @@ -163,6 +171,8 @@ import re import shutil import tarfile import zipfile +from fnmatch import fnmatch +from sys import version_info from traceback import format_exc from ansible.module_utils.basic import AnsibleModule, missing_required_lib @@ -186,6 +196,8 @@ else: LZMA_IMP_ERR = format_exc() HAS_LZMA = False +PY27 = version_info[0:2] >= (2, 7) + def to_b(s): return to_bytes(s, errors='surrogate_or_strict') @@ -214,6 +226,59 @@ def expand_paths(paths): return expanded_path, is_globby +def matches_exclusion_patterns(path, exclusion_patterns): + return any(fnmatch(path, p) for p in exclusion_patterns) + + +def get_filter(exclusion_patterns, format): + def zip_filter(path): + return matches_exclusion_patterns(path, exclusion_patterns) + + def tar_filter(tarinfo): + return None if matches_exclusion_patterns(tarinfo.name, exclusion_patterns) else tarinfo + + return zip_filter if format == 'zip' or not PY27 else tar_filter + + +def get_archive_contains(format): + def archive_contains(archive, name): + try: + if format == 'zip': + archive.getinfo(name) + else: + archive.getmember(name) + except KeyError: + return False + + return True + + return archive_contains + + +def get_add_to_archive(format, filter): + def add_to_zip_archive(archive_file, path, archive_name): + try: + if not filter(path): + archive_file.write(path, archive_name) + except Exception as e: + return e + + return None + + def add_to_tar_archive(archive_file, path, archive_name): + try: + if PY27: + archive_file.add(path, archive_name, recursive=False, filter=filter) + else: + archive_file.add(path, archive_name, recursive=False, exclude=filter) + except Exception as e: + return e + + return None + + return add_to_zip_archive if format == 'zip' else add_to_tar_archive + + def main(): module = AnsibleModule( argument_spec=dict( @@ -221,6 +286,7 @@ def main(): format=dict(type='str', default='gz', choices=['bz2', 'gz', 'tar', 'xz', 'zip']), dest=dict(type='path'), exclude_path=dict(type='list', elements='path'), + exclusion_patterns=dict(type='list', elements='path'), force_archive=dict(type='bool', default=False), remove=dict(type='bool', default=False), ), @@ -242,6 +308,8 @@ def main(): changed = False state = 'absent' + exclusion_patterns = params['exclusion_patterns'] or [] + # Simple or archive file compression (inapplicable with 'zip' since it's always an archive) b_successes = [] @@ -262,6 +330,10 @@ def main(): # Only attempt to expand the exclude paths if it exists b_expanded_exclude_paths = expand_paths(exclude_paths)[0] if exclude_paths else [] + filter = get_filter(exclusion_patterns, fmt) + archive_contains = get_archive_contains(fmt) + add_to_archive = get_add_to_archive(fmt, filter) + # Only try to determine if we are working with an archive or not if we haven't set archive to true if not force_archive: # If we actually matched multiple files or TRIED to, then @@ -384,38 +456,31 @@ def main(): n_fullpath = to_na(b_fullpath) n_arcname = to_native(b_match_root.sub(b'', b_fullpath), errors='surrogate_or_strict') - try: - if fmt == 'zip': - arcfile.write(n_fullpath, n_arcname) - else: - arcfile.add(n_fullpath, n_arcname, recursive=False) - - except Exception as e: - errors.append('%s: %s' % (n_fullpath, to_native(e))) + err = add_to_archive(arcfile, n_fullpath, n_arcname) + if err: + errors.append('%s: %s' % (n_fullpath, to_native(err))) for b_filename in b_filenames: b_fullpath = b_dirpath + b_filename n_fullpath = to_na(b_fullpath) n_arcname = to_n(b_match_root.sub(b'', b_fullpath)) - try: - if fmt == 'zip': - arcfile.write(n_fullpath, n_arcname) - else: - arcfile.add(n_fullpath, n_arcname, recursive=False) + err = add_to_archive(arcfile, n_fullpath, n_arcname) + if err: + errors.append('Adding %s: %s' % (to_native(b_path), to_native(err))) + if archive_contains(arcfile, n_arcname): b_successes.append(b_fullpath) - except Exception as e: - errors.append('Adding %s: %s' % (to_native(b_path), to_native(e))) else: path = to_na(b_path) arcname = to_n(b_match_root.sub(b'', b_path)) - if fmt == 'zip': - arcfile.write(path, arcname) - else: - arcfile.add(path, arcname, recursive=False) - b_successes.append(b_path) + err = add_to_archive(arcfile, path, arcname) + if err: + errors.append('Adding %s: %s' % (to_native(b_path), to_native(err))) + + if archive_contains(arcfile, arcname): + b_successes.append(b_path) except Exception as e: expanded_fmt = 'zip' if fmt == 'zip' else ('tar.' + fmt) diff --git a/tests/integration/targets/archive/tasks/main.yml b/tests/integration/targets/archive/tasks/main.yml index 2267268715..761f9eb7b8 100644 --- a/tests/integration/targets/archive/tasks/main.yml +++ b/tests/integration/targets/archive/tasks/main.yml @@ -363,6 +363,19 @@ - name: remove nonascii test file: path="{{ output_dir }}/test-archive-nonascii-くらとみ.zip" state=absent +- name: Test exclusion_patterns option + archive: + path: "{{ output_dir }}/*.txt" + dest: "{{ output_dir }}/test-archive-exclustion-patterns.tgz" + exclusion_patterns: b?r.* + register: exclusion_patterns_result + +- name: Assert that exclusion_patterns only archives included files + assert: + that: + - exclusion_patterns_result is changed + - "'bar.txt' not in exclusion_patterns_result.archived" + - name: Remove backports.lzma if previously installed (pip) pip: name=backports.lzma state=absent when: backports_lzma_pip is changed