-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add decompress module #9175
base: main
Are you sure you want to change the base?
Add decompress module #9175
Changes from 24 commits
29705f0
ecac887
0473131
f9ff4cd
b17d344
a2c85c6
a578fba
9580084
8db0256
21f301d
a5bfe90
36bd421
c7b6e48
c5a2214
4816b8f
203312b
89cd527
3e4a174
ee9f58c
c6e7961
eba3564
7774900
9e34fb1
ef683c1
85a0765
52b3f57
51b31ee
cbcd66c
3fbcbd4
8210a7c
e5917cc
e2c94ab
d2d9159
28752e3
ae8db2b
e68936b
893b92a
bab6620
6e7fed0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
#!/usr/bin/python | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright (c) 2024, Stanislav Shamilov <[email protected]> | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
from __future__ import (absolute_import, division, print_function) | ||
|
||
__metaclass__ = type | ||
|
||
DOCUMENTATION = r''' | ||
--- | ||
module: decompress | ||
short_description: Decompresses compressed files | ||
version_added: 10.1.0 | ||
description: | ||
- Decompresses compressed files. | ||
- The source (compressed) file and destination (decompressed) files are on the remote host. | ||
- Source file can be deleted after decompression. | ||
extends_documentation_fragment: | ||
- ansible.builtin.files | ||
- community.general.attributes | ||
attributes: | ||
check_mode: | ||
support: full | ||
diff_mode: | ||
support: none | ||
options: | ||
src: | ||
description: | ||
- Remote absolute path for the file to decompress. | ||
type: path | ||
required: true | ||
dest: | ||
description: | ||
- The file name of the destination file where the compressed file will be decompressed. | ||
- If the destination file exists, it will be truncated and overwritten. | ||
- If not specified, the destination filename will be derived from O(src) by removing the compression format | ||
extension. For example, if O(src) is V(/path/to/file.txt.gz) and O(format) is V(gz), O(dest) will be | ||
V(/path/to/file.txt). If the O(src) file does not have an extension for the current O(format), the O(dest) | ||
filename will be made by appending C(_decompressed) to the O(src) filename. For instance, if O(src) is | ||
V(/path/to/file.myextension), the (dest) filename will be V(/path/to/file.myextension_decompressed). | ||
type: path | ||
format: | ||
description: | ||
- The type of compression to use to decompress. | ||
type: str | ||
choices: [ gz, bz2, xz ] | ||
default: gz | ||
remove: | ||
description: | ||
- Remove original compressed file after decompression. | ||
type: bool | ||
default: false | ||
requirements: | ||
- Requires C(lzma) (standard library of Python 3) or L(backports.lzma, https://pypi.org/project/backports.lzma/) (Python 2) if using C(xz) format. | ||
author: | ||
- Stanislav Shamilov (@shamilovstas) | ||
''' | ||
|
||
EXAMPLES = r''' | ||
- name: Decompress file /path/to/file.txt.gz into /path/to/file.txt (gz compression is used by default) | ||
community.general.decompress: | ||
src: /path/to/file.txt.gz | ||
dest: /path/to/file.txt | ||
|
||
- name: Decompress file /path/to/file.txt.gz into /path/to/file.txt | ||
community.general.decompress: | ||
src: /path/to/file.txt.gz | ||
|
||
- name: Decompress file compressed with bzip2 | ||
community.general.decompress: | ||
src: /path/to/file.txt.bz2 | ||
dest: /path/to/file.bz2 | ||
format: bz2 | ||
|
||
- name: Decompress file and delete the compressed file afterwards | ||
community.general.decompress: | ||
src: /path/to/file.txt.gz | ||
dest: /path/to/file.txt | ||
remove: true | ||
''' | ||
|
||
RETURN = r''' | ||
dest: | ||
description: Path to decompressed file | ||
type: str | ||
returned: success | ||
sample: /path/to/file.txt | ||
''' | ||
|
||
import bz2 | ||
import filecmp | ||
import gzip | ||
import os | ||
import shutil | ||
import tempfile | ||
|
||
from traceback import format_exc | ||
from ansible.module_utils import six | ||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib | ||
from ansible.module_utils.common.text.converters import to_native, to_bytes | ||
|
||
LZMA_IMP_ERR = None | ||
if six.PY3: | ||
try: | ||
import lzma | ||
|
||
HAS_LZMA = True | ||
except ImportError: | ||
LZMA_IMP_ERR = format_exc() | ||
HAS_LZMA = False | ||
else: | ||
try: | ||
from backports import lzma | ||
|
||
HAS_LZMA = True | ||
except ImportError: | ||
LZMA_IMP_ERR = format_exc() | ||
HAS_LZMA = False | ||
|
||
|
||
def lzma_decompress(src): | ||
return lzma.open(src, "rb") | ||
|
||
|
||
def bz2_decompress(src): | ||
return bz2.open(src, "rb") | ||
|
||
|
||
def gzip_decompress(src): | ||
return gzip.open(src, "rb") | ||
|
||
|
||
def decompress(b_src, b_dest, handler): | ||
with handler(b_src) as src_file: | ||
with open(b_dest, "wb") as dest_file: | ||
shutil.copyfileobj(src_file, dest_file) | ||
|
||
|
||
class Decompress(object): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might want to consider using |
||
destination_filename_template = "%s_decompressed" | ||
|
||
def __init__(self, module): | ||
self.src = module.params['src'] | ||
self.fmt = module.params['format'] | ||
self.remove = module.params['remove'] | ||
self.check_mode = module.check_mode | ||
self.module = module | ||
self.changed = False | ||
self.handlers = {"gz": gzip_decompress, "bz2": bz2_decompress, "xz": lzma_decompress} | ||
|
||
dest = module.params['dest'] | ||
if dest is None: | ||
self.dest = self.get_destination_filename() | ||
else: | ||
self.dest = dest | ||
self.b_dest = to_bytes(self.dest, errors='surrogate_or_strict') | ||
self.b_src = to_bytes(self.src, errors='surrogate_or_strict') | ||
|
||
def configure(self): | ||
if not os.path.exists(self.b_src): | ||
msg = "Path does not exist: '%s'" % self.b_src | ||
if self.remove and os.path.exists(self.b_dest): | ||
self.module.warn(msg) | ||
self.module.exit_json(changed=False) | ||
else: | ||
self.module.fail_json(msg=msg) | ||
if os.path.isdir(self.b_src): | ||
self.module.fail_json(msg="Cannot decompress directory '%s'" % self.b_src) | ||
if os.path.isdir(self.b_dest): | ||
self.module.fail_json(msg="Destination is a directory, cannot decompress: '%s'" % self.b_src) | ||
self.fmt = self.module.params['format'] | ||
if self.fmt not in self.handlers: | ||
self.module.fail_json(msg="Could not decompress '%s' format" % self.fmt) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The format is controlled by the module parameter, so it is always supposed to have a handler. This code is redundant. |
||
|
||
def run(self): | ||
self.configure() | ||
file_args = self.module.load_file_common_arguments(self.module.params, path=self.dest) | ||
handler = self.handlers[self.fmt] | ||
try: | ||
tempfd, temppath = tempfile.mkstemp(dir=self.module.tmpdir) | ||
russoz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.module.add_cleanup_file(temppath) | ||
b_temppath = to_bytes(temppath, errors='surrogate_or_strict') | ||
decompress(self.b_src, b_temppath, handler) | ||
except OSError as e: | ||
self.module.fail_json(msg="Unable to create temporary file '%s'" % to_native(e)) | ||
|
||
if os.path.exists(self.b_dest): | ||
self.changed = not filecmp.cmp(b_temppath, self.b_dest, shallow=False) | ||
else: | ||
self.changed = True | ||
|
||
if self.changed and not self.module.check_mode: | ||
try: | ||
self.module.atomic_move(b_temppath, self.b_dest) | ||
except OSError: | ||
self.module.fail_json(msg="Unable to move temporary file '%s' to '%s'" % (b_temppath, self.dest)) | ||
|
||
if self.remove and not self.check_mode: | ||
os.remove(self.b_src) | ||
self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed) | ||
|
||
def get_destination_filename(self): | ||
src = self.src | ||
fmt_extension = ".%s" % self.fmt | ||
if src.endswith(fmt_extension) and len(src) > len(fmt_extension): | ||
filename = src[:-len(fmt_extension)] | ||
else: | ||
filename = Decompress.destination_filename_template % src | ||
return filename | ||
|
||
|
||
def main(): | ||
module = AnsibleModule( | ||
argument_spec=dict( | ||
src=dict(type='path', required=True), | ||
dest=dict(type='path'), | ||
format=dict(type='str', default='gz', choices=['gz', 'bz2', 'xz']), | ||
remove=dict(type='bool', default=False) | ||
), | ||
add_file_common_args=True, | ||
supports_check_mode=True | ||
) | ||
if not HAS_LZMA and module.params['format'] == 'xz': | ||
module.fail_json( | ||
msg=missing_required_lib("lzma or backports.lzma", reason="when using xz format"), exception=LZMA_IMP_ERR | ||
) | ||
|
||
d = Decompress(module) | ||
d.run() | ||
|
||
module.exit_json(changed=d.changed, dest=d.dest) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
felixfontein marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
This is sample file |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
Content of this file must differ from the 'file.txt' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
dependencies: | ||
- setup_remote_tmp_dir |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
- name: Delete decompressed files | ||
file: | ||
path: "{{ remote_tmp_dir }}/file_from_{{ format }}.txt" | ||
state: absent | ||
loop: "{{ formats }}" | ||
loop_control: | ||
loop_var: format |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
- name: Set mode for decompressed file ({{ format }} test) | ||
set_fact: | ||
decompressed_mode: "0640" | ||
|
||
- name: Simple decompress ({{ format }} test) | ||
community.general.decompress: | ||
felixfontein marked this conversation as resolved.
Show resolved
Hide resolved
|
||
src: "{{ remote_tmp_dir }}/file.txt.{{ format }}" | ||
dest: "{{ remote_tmp_dir }}/file_from_{{ format }}.txt" | ||
format: "{{ format }}" | ||
mode: "{{ decompressed_mode }}" | ||
register: first_decompression | ||
|
||
- name: Stat decompressed file ({{ format }} test) | ||
stat: | ||
path: "{{ remote_tmp_dir }}/file_from_{{ format }}.txt" | ||
register: decompressed_file_stat | ||
|
||
- name: Check that file was decompressed correctly ({{ format }} test) | ||
assert: | ||
that: | ||
- first_decompression.changed | ||
- decompressed_file_stat.stat.exists | ||
- decompressed_file_stat.stat.mode == decompressed_mode | ||
- orig_file_stat.stat.checksum == decompressed_file_stat.stat.checksum |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
- name: Copy a compressed file | ||
copy: | ||
src: "{{ item.orig }}" | ||
dest: "{{ item.new }}" | ||
remote_src: true | ||
loop: | ||
- { orig: "{{ remote_tmp_dir }}/file.txt.gz", new: "{{ remote_tmp_dir }}/dest.txt.gz" } | ||
- { orig: "{{ remote_tmp_dir }}/file.txt.gz", new: "{{ remote_tmp_dir }}/dest" } | ||
|
||
- name: Decompress a file without specifying destination | ||
community.general.decompress: | ||
src: "{{ remote_tmp_dir }}/dest.txt.gz" | ||
remove: true | ||
|
||
- name: Decompress a file which lacks extension without specifying destination | ||
community.general.decompress: | ||
src: "{{ remote_tmp_dir }}/dest" | ||
remove: true | ||
|
||
- name: Stat result files | ||
stat: | ||
path: "{{ remote_tmp_dir }}/{{ filename }}" | ||
loop: | ||
- dest.txt | ||
- dest_decompressed | ||
loop_control: | ||
loop_var: filename | ||
register: result_files_stat | ||
|
||
- name: Test that file exists | ||
assert: | ||
that: "{{ item.stat.exists }}" | ||
quiet: true | ||
loop: "{{ result_files_stat.results }}" | ||
loop_control: | ||
label: "{{ item.stat.path }}" | ||
|
||
- name: Delete test files | ||
file: | ||
path: "{{ filename }}" | ||
state: absent | ||
loop: | ||
- "dest.txt" | ||
- "dest_decompressed" | ||
loop_control: | ||
loop_var: filename |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to consider using
module_utils.deps
, see https://docs.ansible.com/ansible/latest/collections/community/general/docsite/guide_deps.html for guidance on how to use it.