Skip to content

Commit 989fde9

Browse files
committed
Move to different approach: tar encapsulation respecting extensions
1 parent a024baf commit 989fde9

File tree

5 files changed

+90
-46
lines changed

5 files changed

+90
-46
lines changed

conan/api/subapi/cache.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,13 @@ def restore(self, path):
181181
cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf)
182182
cache_folder = cache.store # Note, this is not the home, but the actual package cache
183183

184-
compression_plugin = self.conan_api.config.compression_plugin
185-
if compression_plugin:
186-
compression_plugin.tar_extract(archive_path=path, dest_dir=cache_folder,
187-
conf=self.conan_api.config.global_conf)
188-
else:
189-
with open(path, mode='rb') as file_handler:
190-
tar_extract(file_handler, cache_folder)
184+
with open(path, mode="rb") as file_handler:
185+
tar_extract(
186+
fileobj=file_handler,
187+
destination_dir=cache_folder,
188+
compression_plugin=self.conan_api.config.compression_plugin,
189+
conf=self.conan_api.config.global_conf,
190+
)
191191

192192
# Retrieve the package list from the already extracted archive
193193
pkglist_path = os.path.join(cache_folder, "pkglist.json")

conan/internal/api/uploader.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import shutil
55
import tarfile
66
import time
7+
from pathlib import Path
78

89
from conan.internal.conan_app import ConanApp
910
from conan.api.output import ConanOutput
@@ -12,7 +13,7 @@
1213
from conan.errors import ConanException
1314
from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME,
1415
EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO)
15-
from conan.internal.util.files import (clean_dirty, is_dirty, gather_files,
16+
from conan.internal.util.files import (COMPRESSED_PLUGIN_TAR_NAME, clean_dirty, is_dirty, gather_files, remove,
1617
set_dirty_context_manager, mkdir, human_size)
1718

1819
UPLOAD_POLICY_FORCE = "force-upload"
@@ -274,13 +275,26 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None):
274275
def compress_files(files, name, dest_dir, conf=None, ref=None, recursive=False, compression_plugin=None):
275276
tgz_path = os.path.join(dest_dir, name)
276277
if compression_plugin:
277-
compression_plugin.tar_compress(
278-
archive_path=tgz_path,
278+
t1 = time.time()
279+
compressed_path = compression_plugin.tar_compress(
280+
archive_path=os.path.join(dest_dir, COMPRESSED_PLUGIN_TAR_NAME),
279281
files=files,
280282
recursive=recursive,
281283
conf=conf,
282284
ref=ref,
283285
)
286+
ConanOutput().debug(f"{name} compressed in {time.time() - t1} time in plugin")
287+
ConanOutput(scope=str(ref or "")).info(f"Compressing {compressed_path}")
288+
t1 = time.time()
289+
ConanOutput().debug(f"Wrapping {compressed_path} in {name}")
290+
with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle:
291+
tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=0)
292+
tgz.add(compressed_path, arcname=os.path.basename(compressed_path), recursive=recursive)
293+
tgz.close()
294+
ConanOutput().debug(f"{name} wrapped in {time.time() - t1} time")
295+
# Only remove wrapped if it is different from the tgz_path
296+
if compressed_path != os.path.basename(tgz_path):
297+
remove(compressed_path)
284298
return tgz_path
285299

286300
t1 = time.time()

conan/internal/rest/remote_manager.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,13 @@ def uncompress_file(src_path, dest_folder, scope="", config_api=None):
304304
hs = human_size(filesize)
305305
ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}")
306306

307-
if config_api and config_api.compression_plugin:
308-
config_api.compression_plugin.tar_extract(archive_path=src_path, dest_dir=dest_folder,
309-
conf=config_api.global_conf)
310-
else:
311-
with open(src_path, mode='rb') as file_handler:
312-
tar_extract(file_handler, dest_folder)
307+
with open(src_path, mode='rb') as file_handler:
308+
tar_extract(
309+
fileobj=file_handler,
310+
destination_dir=dest_folder,
311+
compression_plugin=config_api.compression_plugin if config_api and config_api.compression_plugin else None,
312+
conf=config_api.global_conf if config_api else None
313+
)
313314
except Exception as e:
314315
error_msg = "Error while extracting downloaded file '%s' to %s\n%s\n"\
315316
% (src_path, dest_folder, str(e))

conan/internal/util/files.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import errno
2+
from pathlib import Path
3+
import tempfile
24
import gzip
35
import hashlib
46
import os
@@ -11,10 +13,13 @@
1113

1214
from contextlib import contextmanager
1315

16+
from conan.api.output import ConanOutput
1417
from conan.errors import ConanException
1518

1619
_DIRTY_FOLDER = ".dirty"
1720

21+
# Name (without extension) of the tar file to be created by the compression plugin
22+
COMPRESSED_PLUGIN_TAR_NAME = "__conan_plugin_compressed_contents__"
1823

1924
def set_dirty(folder):
2025
dirty_file = os.path.normpath(folder) + _DIRTY_FOLDER
@@ -256,20 +261,43 @@ def mkdir(path):
256261
os.makedirs(path)
257262

258263

259-
def tar_extract(fileobj, destination_dir):
260-
try:
261-
the_tar = tarfile.open(fileobj=fileobj)
262-
# NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to
263-
# "could not change modification time", with time=0
264-
# the_tar.errorlevel = 2 # raise exception if any error
265-
the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break
266-
the_tar.extractall(path=destination_dir)
267-
the_tar.close()
268-
except tarfile.ReadError:
269-
raise ConanException(f"Error while extracting {os.path.basename(fileobj.name)}. The file compression is not recogniced.\n"
270-
"This file could have been compressed using a `compression` plugin.\n"
264+
def tar_extract(fileobj, destination_dir, compression_plugin=None, conf=None):
265+
if compression_plugin:
266+
_tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf)
267+
return
268+
the_tar = tarfile.open(fileobj=fileobj)
269+
# NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to
270+
# "could not change modification time", with time=0
271+
# the_tar.errorlevel = 2 # raise exception if any error
272+
the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break
273+
the_tar.extractall(path=destination_dir)
274+
the_tar.close()
275+
if Path(destination_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*"):
276+
raise ConanException(f"Error while extracting {os.path.basename(fileobj.name)}.\n"
277+
"This file has been compressed using a `compression` plugin.\n"
271278
"If your organization uses this plugin, ensure it is correctly installed on your environment.")
272279

280+
def _tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf):
281+
"""First remove tar.gz wrapper and then call the plugin to extract"""
282+
with tempfile.TemporaryDirectory() as temp_dir:
283+
t1 = time.time()
284+
the_tar = tarfile.open(fileobj=fileobj)
285+
the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break
286+
the_tar.extractall(path=temp_dir)
287+
# Check if the tar was compressed with the compression plugin by checking the existence of
288+
# our constant COMPRESSED_PLUGIN_TAR_NAME (without extension as extension is added by the plugin)
289+
if list(Path(temp_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*")):
290+
# Get the only extracted file: the plugin tar
291+
plugin_tar_path = os.path.join(temp_dir, the_tar.getnames()[0])
292+
the_tar.close()
293+
ConanOutput().debug(f"Unwrapped in {time.time() - t1} time")
294+
t1 = time.time()
295+
compression_plugin.tar_extract(archive_path=plugin_tar_path, dest_dir=destination_dir, conf=conf)
296+
ConanOutput().debug(f"Extracted in {time.time() - t1} time on plugin")
297+
else:
298+
# The tar was not compressed using the plugin, copy files to destination
299+
from conan.tools.files import copy
300+
copy(None, pattern="*", src=temp_dir, dst=destination_dir)
273301

274302
def merge_directories(src, dst):
275303
from conan.tools.files import copy

test/integration/extensions/test_compression_plugin.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import textwrap
33

4-
from conan.internal.util.files import tar_extract
4+
from conan.internal.util.files import COMPRESSED_PLUGIN_TAR_NAME, tar_extract
55
from conan.test.assets.genconanfile import GenConanfile
66
from conan.test.utils.tools import TestClient
77

@@ -26,7 +26,7 @@ def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs):
2626
}
2727
)
2828
c.run("create .")
29-
c.run("cache save 'pkg/*'", assert_error=True)
29+
c.run("cache save 'pkg/*:*'", assert_error=True)
3030
assert (
3131
"ERROR: The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions"
3232
in c.out
@@ -48,13 +48,15 @@ def test_compression_plugin_correctly_load():
4848
4949
# xz compression
5050
def tar_compress(archive_path, files, recursive, conf=None, ref=None, *args, **kwargs):
51+
archive_path += ".xz"
5152
name = os.path.basename(archive_path)
5253
ConanOutput(scope=ref).info(f"Compressing {name} using compression plugin (xz)")
5354
compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None
5455
kwargs = {"preset": compresslevel} if compresslevel else {}
5556
with tarfile.open(archive_path, f"w:xz", **kwargs) as tgz:
5657
for filename, abs_path in sorted(files.items()):
5758
tgz.add(abs_path, filename, recursive=True)
59+
return archive_path
5860
5961
def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs):
6062
ConanOutput().info(f"Decompressing {os.path.basename(archive_path)} using compression plugin (xz)")
@@ -75,24 +77,25 @@ def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs):
7577
}
7678
)
7779
c.run("create .")
78-
c.run("cache save 'pkg/*'")
79-
assert "Compressing conan_cache_save.tgz using compression plugin (xz)" in c.out
80+
c.run("cache save 'pkg/*:*'")
81+
print(c.out)
82+
assert f"Compressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out
8083
c.run("remove pkg/* -c")
8184
c.run("cache restore conan_cache_save.tgz")
82-
assert "Decompressing conan_cache_save.tgz using compression plugin (xz)" in c.out
85+
assert f"Decompressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out
8386
c.run("list pkg/1.0")
8487
assert "Found 1 pkg/version recipes matching pkg/1.0 in local cache" in c.out
8588

8689
# Remove pre existing tgz to force a recompression
8790
c.run("remove pkg/* -c")
8891
c.run("create .")
8992
# Check the plugin is also used on remote interactions
90-
c.run("upload * -r=default -c")
91-
assert "Compressing conan_package.tgz using compression plugin (xz)" in c.out
93+
c.run("upload *:* -r=default -c")
94+
assert f"Compressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out
9295
assert "pkg/1.0: Uploading recipe" in c.out
9396
c.run("remove pkg/* -c")
9497
c.run("download 'pkg/*' -r=default")
95-
assert "Decompressing conan_package.tgz using compression plugin (xz)" in c.out
98+
assert f"Decompressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out
9699

97100

98101
def test_compression_plugin_tar_not_compatible_with_builtin():
@@ -111,6 +114,7 @@ def test_compression_plugin_tar_not_compatible_with_builtin():
111114
# zip compression
112115
def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs):
113116
# compress files using zipfile library taking into account recursive
117+
archive_path += ".zip"
114118
name = os.path.basename(archive_path)
115119
compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None
116120
ConanOutput().info(f"Compressing {name} using compression plugin (zip)")
@@ -121,6 +125,7 @@ def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs):
121125
zipf.write(abs_path, arcname)
122126
else:
123127
zipf.write(abs_path, filename)
128+
return archive_path
124129
125130
def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs):
126131
# extract tar using zipfile library
@@ -139,13 +144,13 @@ def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs):
139144
}
140145
)
141146
c.run("create .")
142-
c.run("cache save 'pkg/*'")
147+
c.run("cache save 'pkg/*:*'")
143148
c.run("remove pkg/* -c")
144149
os.unlink(os.path.join(c.cache_folder, "extensions", "plugins", "compression.py"))
145150
c.run("cache restore conan_cache_save.tgz", assert_error=True)
146151
assert (
147-
"Error while extracting conan_cache_save.tgz. The file compression is not recogniced.\n"
148-
"This file could have been compressed using a `compression` plugin.\n"
152+
"Error while extracting conan_cache_save.tgz.\n"
153+
"This file has been compressed using a `compression` plugin.\n"
149154
"If your organization uses this plugin, ensure it is correctly installed on your environment."
150155
) in c.out
151156

@@ -160,13 +165,15 @@ def test_compress_in_subdirectory():
160165
from conan.api.output import ConanOutput
161166
def tar_compress(archive_path, files, recursive, *args, **kwargs):
162167
# compress files using tarfile putting all content in a `conan/` subfolder
168+
archive_path += ".tgz"
163169
name = os.path.basename(archive_path)
164170
ConanOutput().info(f"Compressing {os.path.basename(name)} in conan subfolder")
165171
with open(archive_path, "wb") as tgz_handle:
166172
tgz = tarfile.open(name, "w", fileobj=tgz_handle)
167173
for filename, abs_path in sorted(files.items()):
168174
tgz.add(abs_path, os.path.join("conan", filename), recursive=recursive)
169175
tgz.close()
176+
return archive_path
170177
171178
def tar_extract(archive_path, dest_dir, *args, **kwargs):
172179
ConanOutput().info(f"Decompressing {archive_path} in conan subfolder")
@@ -189,12 +196,6 @@ def tar_extract(archive_path, dest_dir, *args, **kwargs):
189196
}
190197
)
191198
c.run("create .")
192-
c.run("cache save 'pkg/*'")
199+
c.run("cache save 'pkg/*:*'")
193200
c.run("remove pkg/* -c")
194201
c.run("cache restore conan_cache_save.tgz")
195-
with open(os.path.join(c.current_folder, "conan_cache_save.tgz"), 'rb') as file_handler:
196-
dest_dir = os.path.join(c.cache_folder, "extracted")
197-
tar_extract(file_handler, dest_dir)
198-
assert os.listdir(dest_dir) == ["conan"]
199-
assert os.path.exists(os.path.join(dest_dir, "conan", "pkglist.json"))
200-

0 commit comments

Comments
 (0)