From 52325fcab508bacaeb34bb43ac86be6f12eeb4c3 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Tue, 15 Jul 2025 13:54:51 +0200 Subject: [PATCH 01/14] wip --- src/aiida/tools/archive/create.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index e5c88cde49..2f4b846502 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -283,7 +283,10 @@ def querybuilder(): # Create and open the archive for writing. # We create in a temp dir then move to final place at end, # so that the user cannot end up with a half written archive on errors - with tempfile.TemporaryDirectory() as tmpdir: + import ipdb; ipdb.set_trace() + base_temp_dir = '/mount' # or whatever directory you want to use + with tempfile.TemporaryDirectory(dir=base_temp_dir) as tmpdir: + # with tempfile.TemporaryDirectory() as tmpdir: tmp_filename = Path(tmpdir) / 'export.zip' with archive_format.open(tmp_filename, mode='x', compression=compression) as writer: # add metadata From 3331f6d73d7083fe4c08e397b6e15821994fe49a Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Wed, 16 Jul 2025 19:06:00 +0200 Subject: [PATCH 02/14] wip --- src/aiida/cmdline/commands/cmd_archive.py | 6 ++++ src/aiida/tools/archive/create.py | 34 ++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_archive.py b/src/aiida/cmdline/commands/cmd_archive.py index 94746536a0..c221725b65 100644 --- a/src/aiida/cmdline/commands/cmd_archive.py +++ b/src/aiida/cmdline/commands/cmd_archive.py @@ -138,6 +138,10 @@ def inspect(ctx, archive, version, meta_data, database): help='Determine entities to export, but do not create the archive. Deprecated, please use `--dry-run` instead.', ) @options.DRY_RUN(help='Determine entities to export, but do not create the archive.') +@click.option( + '--base-tmp-dir', + help='Determine entities to export, but do not create the archive. Deprecated, please use `--dry-run` instead.', +) @decorators.with_dbenv() def create( output_file, @@ -160,6 +164,7 @@ def create( batch_size, test_run, dry_run, + temp_dir ): """Create an archive from all or part of a profiles's data. @@ -211,6 +216,7 @@ def create( 'compression': compress, 'batch_size': batch_size, 'test_run': dry_run, + 'temp_dir': temp_dir } if AIIDA_LOGGER.level <= logging.REPORT: # type: ignore[attr-defined] diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index 2f4b846502..3e89225f43 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -59,6 +59,7 @@ def create_archive( compression: int = 6, test_run: bool = False, backend: Optional[StorageBackend] = None, + temp_dir: Optional[Union[str, Path]] = None, **traversal_rules: bool, ) -> Path: """Export AiiDA data to an archive file. @@ -139,6 +140,12 @@ def create_archive( :param backend: the backend to export from. If not specified, the default backend is used. + :param temp_dir: Directory to use for temporary files during archive creation. + If not specified, a temporary directory will be created in the same directory as the output file + with a '.aiida-export-' prefix. This parameter is useful when the output directory has limited + space or when you want to use a specific filesystem (e.g., faster storage) for temporary operations. + The directory must exist and be writable. + :param traversal_rules: graph traversal rules. See :const:`aiida.common.links.GraphTraversalRules` what rule names are toggleable and what the defaults are. @@ -179,6 +186,25 @@ def querybuilder(): name: traversal_rules.get(name, rule.default) for name, rule in GraphTraversalRules.EXPORT.value.items() } + # Handle temporary directory configuration + if temp_dir is not None: + temp_dir = Path(temp_dir) + if not temp_dir.exists(): + msg = f"Specified temporary directory '{temp_dir}' does not exist" + raise ArchiveExportError(msg) + if not temp_dir.is_dir(): + msg = f"Specified temporary directory '{temp_dir}' is not a directory" + raise ArchiveExportError(msg) + # Check if directory is writable + if not temp_dir.is_writable(): + msg = f"Specified temporary directory '{temp_dir}' is not writable" + raise ArchiveExportError() + tmp_prefix = None # Use default tempfile prefix + else: + # Create temporary directory in the same folder as the output file + temp_dir = filename.parent + tmp_prefix = '.aiida-export-' + initial_summary = get_init_summary( archive_version=archive_format.latest_version, outfile=filename, @@ -283,10 +309,10 @@ def querybuilder(): # Create and open the archive for writing. # We create in a temp dir then move to final place at end, # so that the user cannot end up with a half written archive on errors - import ipdb; ipdb.set_trace() - base_temp_dir = '/mount' # or whatever directory you want to use - with tempfile.TemporaryDirectory(dir=base_temp_dir) as tmpdir: - # with tempfile.TemporaryDirectory() as tmpdir: + # import ipdb; ipdb.set_trace() + temp_dir = Path('/mount') # or whatever directory you want to use + with tempfile.TemporaryDirectory(dir=temp_dir, prefix=tmp_prefix) as tmpdir: + # NOTE: Add the `tmp_prefix` to the directory or file? tmp_filename = Path(tmpdir) / 'export.zip' with archive_format.open(tmp_filename, mode='x', compression=compression) as writer: # add metadata From 0e3d38a3e04cf6ef28ca26564c0e96c1d70fe9c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:08:18 +0000 Subject: [PATCH 03/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/aiida/cmdline/commands/cmd_archive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_archive.py b/src/aiida/cmdline/commands/cmd_archive.py index c221725b65..1eb421202f 100644 --- a/src/aiida/cmdline/commands/cmd_archive.py +++ b/src/aiida/cmdline/commands/cmd_archive.py @@ -164,7 +164,7 @@ def create( batch_size, test_run, dry_run, - temp_dir + temp_dir, ): """Create an archive from all or part of a profiles's data. @@ -216,7 +216,7 @@ def create( 'compression': compress, 'batch_size': batch_size, 'test_run': dry_run, - 'temp_dir': temp_dir + 'temp_dir': temp_dir, } if AIIDA_LOGGER.level <= logging.REPORT: # type: ignore[attr-defined] From 58b8d916036f6ab3843902ef2baa9d855bbb9189 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Fri, 18 Jul 2025 15:08:04 +0200 Subject: [PATCH 04/14] writable check --- src/aiida/cmdline/commands/cmd_archive.py | 9 +- src/aiida/tools/archive/create.py | 202 ++++++++++++---------- 2 files changed, 113 insertions(+), 98 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_archive.py b/src/aiida/cmdline/commands/cmd_archive.py index 1eb421202f..f87da27efd 100644 --- a/src/aiida/cmdline/commands/cmd_archive.py +++ b/src/aiida/cmdline/commands/cmd_archive.py @@ -139,8 +139,13 @@ def inspect(ctx, archive, version, meta_data, database): ) @options.DRY_RUN(help='Determine entities to export, but do not create the archive.') @click.option( - '--base-tmp-dir', - help='Determine entities to export, but do not create the archive. Deprecated, please use `--dry-run` instead.', + '--tmp-dir', + type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, path_type=Path), + help='Directory to use for temporary files during archive creation. ' + 'If not specified, a temporary directory will be created in the same directory as the output file ' + 'with a \'.aiida-export-\' prefix. This parameter is useful when the output directory has limited ' + 'space or when you want to use a specific filesystem (e.g., faster storage) for temporary operations. ' + 'The directory must exist and be writable.', ) @decorators.with_dbenv() def create( diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index 3e89225f43..e04bdac1b5 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -12,6 +12,7 @@ stored in a single file. """ +import os import shutil import tempfile from datetime import datetime @@ -59,7 +60,7 @@ def create_archive( compression: int = 6, test_run: bool = False, backend: Optional[StorageBackend] = None, - temp_dir: Optional[Union[str, Path]] = None, + tmp_dir: Optional[Union[str, Path]] = None, **traversal_rules: bool, ) -> Path: """Export AiiDA data to an archive file. @@ -187,22 +188,23 @@ def querybuilder(): } # Handle temporary directory configuration - if temp_dir is not None: - temp_dir = Path(temp_dir) - if not temp_dir.exists(): - msg = f"Specified temporary directory '{temp_dir}' does not exist" + if tmp_dir is not None: + tmp_dir = Path(tmp_dir) + if not tmp_dir.exists(): + msg = f"Specified temporary directory '{tmp_dir}' does not exist" raise ArchiveExportError(msg) - if not temp_dir.is_dir(): - msg = f"Specified temporary directory '{temp_dir}' is not a directory" + if not tmp_dir.is_dir(): + msg = f"Specified temporary directory '{tmp_dir}' is not a directory" raise ArchiveExportError(msg) # Check if directory is writable - if not temp_dir.is_writable(): - msg = f"Specified temporary directory '{temp_dir}' is not writable" + # Taken from: https://stackoverflow.com/a/2113511 + if not os.access(tmp_dir, os.W_OK | os.X_OK): + msg = f"Specified temporary directory '{tmp_dir}' is not writable" raise ArchiveExportError() tmp_prefix = None # Use default tempfile prefix else: # Create temporary directory in the same folder as the output file - temp_dir = filename.parent + tmp_dir = filename.parent tmp_prefix = '.aiida-export-' initial_summary = get_init_summary( @@ -310,93 +312,101 @@ def querybuilder(): # We create in a temp dir then move to final place at end, # so that the user cannot end up with a half written archive on errors # import ipdb; ipdb.set_trace() - temp_dir = Path('/mount') # or whatever directory you want to use - with tempfile.TemporaryDirectory(dir=temp_dir, prefix=tmp_prefix) as tmpdir: - # NOTE: Add the `tmp_prefix` to the directory or file? - tmp_filename = Path(tmpdir) / 'export.zip' - with archive_format.open(tmp_filename, mode='x', compression=compression) as writer: - # add metadata - writer.update_metadata( - { - 'ctime': datetime.now().isoformat(), - 'creation_parameters': { - 'entities_starting_set': None - if entities is None - else {etype.value: list(unique) for etype, unique in starting_uuids.items() if unique}, - 'include_authinfos': include_authinfos, - 'include_comments': include_comments, - 'include_logs': include_logs, - 'graph_traversal_rules': full_traversal_rules, - }, - } - ) - # stream entity data to the archive - with get_progress_reporter()(desc='Archiving database: ', total=sum(entity_counts.values())) as progress: - for etype, ids in entity_ids.items(): - if etype == EntityTypes.NODE and strip_checkpoints: - - def transform(row): - data = row['entity'] - if data.get('node_type', '').startswith('process.'): - data['attributes'].pop(orm.ProcessNode.CHECKPOINT_KEY, None) - return data - else: - - def transform(row): - return row['entity'] - - progress.set_description_str(f'Archiving database: {etype.value}s') - if ids: - for nrows, rows in batch_iter( - querybuilder() - .append( - entity_type_to_orm[etype], filters={'id': {'in': ids}}, tag='entity', project=['**'] - ) - .iterdict(batch_size=batch_size), - batch_size, - transform, - ): - writer.bulk_insert(etype, rows) - progress.update(nrows) - - # stream links - progress.set_description_str(f'Archiving database: {EntityTypes.LINK.value}s') - - def transform(d): - return { - 'input_id': d.source_id, - 'output_id': d.target_id, - 'label': d.link_label, - 'type': d.link_type, + tmp_dir = Path('/mount') # or whatever directory you want to use + + try: + with tempfile.TemporaryDirectory(dir=tmp_dir, prefix=tmp_prefix) as tmpdir: + tmp_filename = Path(tmpdir) / 'export.zip' + with archive_format.open(tmp_filename, mode='x', compression=compression) as writer: + # add metadata + writer.update_metadata( + { + 'ctime': datetime.now().isoformat(), + 'creation_parameters': { + 'entities_starting_set': None + if entities is None + else {etype.value: list(unique) for etype, unique in starting_uuids.items() if unique}, + 'include_authinfos': include_authinfos, + 'include_comments': include_comments, + 'include_logs': include_logs, + 'graph_traversal_rules': full_traversal_rules, + }, } - - for nrows, rows in batch_iter(link_data, batch_size, transform): - writer.bulk_insert(EntityTypes.LINK, rows, allow_defaults=True) - progress.update(nrows) - del link_data # release memory - - # stream group_nodes - progress.set_description_str(f'Archiving database: {EntityTypes.GROUP_NODE.value}s') - - def transform(d): - return {'dbgroup_id': d[0], 'dbnode_id': d[1]} - - for nrows, rows in batch_iter(group_nodes, batch_size, transform): - writer.bulk_insert(EntityTypes.GROUP_NODE, rows, allow_defaults=True) - progress.update(nrows) - del group_nodes # release memory - - # stream node repository files to the archive - if entity_ids[EntityTypes.NODE]: - _stream_repo_files(archive_format.key_format, writer, entity_ids[EntityTypes.NODE], backend, batch_size) - - EXPORT_LOGGER.report('Finalizing archive creation...') - - if filename.exists(): - filename.unlink() - - filename.parent.mkdir(parents=True, exist_ok=True) - shutil.move(tmp_filename, filename) + ) + # stream entity data to the archive + with get_progress_reporter()(desc='Archiving database: ', total=sum(entity_counts.values())) as progress: + for etype, ids in entity_ids.items(): + if etype == EntityTypes.NODE and strip_checkpoints: + + def transform(row): + data = row['entity'] + if data.get('node_type', '').startswith('process.'): + data['attributes'].pop(orm.ProcessNode.CHECKPOINT_KEY, None) + return data + else: + + def transform(row): + return row['entity'] + + progress.set_description_str(f'Archiving database: {etype.value}s') + if ids: + for nrows, rows in batch_iter( + querybuilder() + .append( + entity_type_to_orm[etype], filters={'id': {'in': ids}}, tag='entity', project=['**'] + ) + .iterdict(batch_size=batch_size), + batch_size, + transform, + ): + writer.bulk_insert(etype, rows) + progress.update(nrows) + + # stream links + progress.set_description_str(f'Archiving database: {EntityTypes.LINK.value}s') + + def transform(d): + return { + 'input_id': d.source_id, + 'output_id': d.target_id, + 'label': d.link_label, + 'type': d.link_type, + } + + for nrows, rows in batch_iter(link_data, batch_size, transform): + writer.bulk_insert(EntityTypes.LINK, rows, allow_defaults=True) + progress.update(nrows) + del link_data # release memory + + # stream group_nodes + progress.set_description_str(f'Archiving database: {EntityTypes.GROUP_NODE.value}s') + + def transform(d): + return {'dbgroup_id': d[0], 'dbnode_id': d[1]} + + for nrows, rows in batch_iter(group_nodes, batch_size, transform): + writer.bulk_insert(EntityTypes.GROUP_NODE, rows, allow_defaults=True) + progress.update(nrows) + del group_nodes # release memory + + # stream node repository files to the archive + if entity_ids[EntityTypes.NODE]: + _stream_repo_files(archive_format.key_format, writer, entity_ids[EntityTypes.NODE], backend, batch_size) + + EXPORT_LOGGER.report('Finalizing archive creation...') + + if filename.exists(): + filename.unlink() + + filename.parent.mkdir(parents=True, exist_ok=True) + shutil.move(tmp_filename, filename) + except OSError as e: + if e.errno == 28: # No space left on device + raise ArchiveExportError( + f"Insufficient disk space in temporary directory '{tmp_dir}'. " + f"Consider using --tmp-dir to specify a location with more available space." + ) from e + raise ArchiveExportError(f"Failed to create temporary directory: {e}") from e EXPORT_LOGGER.report('Archive created successfully') From d3737aaf61b7e272b60446e46d2f5ecd6071f54b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:08:30 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/aiida/cmdline/commands/cmd_archive.py | 8 ++++---- src/aiida/tools/archive/create.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_archive.py b/src/aiida/cmdline/commands/cmd_archive.py index f87da27efd..81365f3239 100644 --- a/src/aiida/cmdline/commands/cmd_archive.py +++ b/src/aiida/cmdline/commands/cmd_archive.py @@ -142,10 +142,10 @@ def inspect(ctx, archive, version, meta_data, database): '--tmp-dir', type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, path_type=Path), help='Directory to use for temporary files during archive creation. ' - 'If not specified, a temporary directory will be created in the same directory as the output file ' - 'with a \'.aiida-export-\' prefix. This parameter is useful when the output directory has limited ' - 'space or when you want to use a specific filesystem (e.g., faster storage) for temporary operations. ' - 'The directory must exist and be writable.', + 'If not specified, a temporary directory will be created in the same directory as the output file ' + "with a '.aiida-export-' prefix. This parameter is useful when the output directory has limited " + 'space or when you want to use a specific filesystem (e.g., faster storage) for temporary operations. ' + 'The directory must exist and be writable.', ) @decorators.with_dbenv() def create( diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index e04bdac1b5..d4f5c04bf5 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -334,7 +334,9 @@ def querybuilder(): } ) # stream entity data to the archive - with get_progress_reporter()(desc='Archiving database: ', total=sum(entity_counts.values())) as progress: + with get_progress_reporter()( + desc='Archiving database: ', total=sum(entity_counts.values()) + ) as progress: for etype, ids in entity_ids.items(): if etype == EntityTypes.NODE and strip_checkpoints: @@ -391,7 +393,9 @@ def transform(d): # stream node repository files to the archive if entity_ids[EntityTypes.NODE]: - _stream_repo_files(archive_format.key_format, writer, entity_ids[EntityTypes.NODE], backend, batch_size) + _stream_repo_files( + archive_format.key_format, writer, entity_ids[EntityTypes.NODE], backend, batch_size + ) EXPORT_LOGGER.report('Finalizing archive creation...') @@ -404,9 +408,9 @@ def transform(d): if e.errno == 28: # No space left on device raise ArchiveExportError( f"Insufficient disk space in temporary directory '{tmp_dir}'. " - f"Consider using --tmp-dir to specify a location with more available space." + f'Consider using --tmp-dir to specify a location with more available space.' ) from e - raise ArchiveExportError(f"Failed to create temporary directory: {e}") from e + raise ArchiveExportError(f'Failed to create temporary directory: {e}') from e EXPORT_LOGGER.report('Archive created successfully') From f7a711198f928acfba802ef371f745bc6eb14a64 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Fri, 18 Jul 2025 15:11:15 +0200 Subject: [PATCH 06/14] temp_dir rename --- src/aiida/cmdline/commands/cmd_archive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_archive.py b/src/aiida/cmdline/commands/cmd_archive.py index 81365f3239..491e72aa6d 100644 --- a/src/aiida/cmdline/commands/cmd_archive.py +++ b/src/aiida/cmdline/commands/cmd_archive.py @@ -169,7 +169,7 @@ def create( batch_size, test_run, dry_run, - temp_dir, + tmp_dir, ): """Create an archive from all or part of a profiles's data. @@ -221,7 +221,7 @@ def create( 'compression': compress, 'batch_size': batch_size, 'test_run': dry_run, - 'temp_dir': temp_dir, + 'tmp_dir': tmp_dir, } if AIIDA_LOGGER.level <= logging.REPORT: # type: ignore[attr-defined] From 74dc0c698f7b885a37f2a13dc81e7e33321b2704 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Fri, 18 Jul 2025 16:20:55 +0200 Subject: [PATCH 07/14] add simple tests --- src/aiida/tools/archive/create.py | 12 +++--- tests/tools/archive/test_simple.py | 61 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index d4f5c04bf5..94426b8548 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -141,7 +141,7 @@ def create_archive( :param backend: the backend to export from. If not specified, the default backend is used. - :param temp_dir: Directory to use for temporary files during archive creation. + :param tmp_dir: Directory to use for temporary files during archive creation. If not specified, a temporary directory will be created in the same directory as the output file with a '.aiida-export-' prefix. This parameter is useful when the output directory has limited space or when you want to use a specific filesystem (e.g., faster storage) for temporary operations. @@ -200,7 +200,7 @@ def querybuilder(): # Taken from: https://stackoverflow.com/a/2113511 if not os.access(tmp_dir, os.W_OK | os.X_OK): msg = f"Specified temporary directory '{tmp_dir}' is not writable" - raise ArchiveExportError() + raise ArchiveExportError(msg) tmp_prefix = None # Use default tempfile prefix else: # Create temporary directory in the same folder as the output file @@ -311,9 +311,6 @@ def querybuilder(): # Create and open the archive for writing. # We create in a temp dir then move to final place at end, # so that the user cannot end up with a half written archive on errors - # import ipdb; ipdb.set_trace() - tmp_dir = Path('/mount') # or whatever directory you want to use - try: with tempfile.TemporaryDirectory(dir=tmp_dir, prefix=tmp_prefix) as tmpdir: tmp_filename = Path(tmpdir) / 'export.zip' @@ -406,10 +403,11 @@ def transform(d): shutil.move(tmp_filename, filename) except OSError as e: if e.errno == 28: # No space left on device - raise ArchiveExportError( + msg = ( f"Insufficient disk space in temporary directory '{tmp_dir}'. " f'Consider using --tmp-dir to specify a location with more available space.' - ) from e + ) + raise ArchiveExportError(msg) from e raise ArchiveExportError(f'Failed to create temporary directory: {e}') from e EXPORT_LOGGER.report('Archive created successfully') diff --git a/tests/tools/archive/test_simple.py b/tests/tools/archive/test_simple.py index ac6209ab95..6b256b2b0d 100644 --- a/tests/tools/archive/test_simple.py +++ b/tests/tools/archive/test_simple.py @@ -18,6 +18,7 @@ from aiida.common.exceptions import IncompatibleStorageSchema, LicensingException from aiida.common.links import LinkType from aiida.tools.archive import create_archive, import_archive +from aiida.tools.archive.exceptions import ArchiveExportError @pytest.mark.parametrize('entities', ['all', 'specific']) @@ -154,3 +155,63 @@ def crashing_filter(_): with pytest.raises(LicensingException): create_archive([struct], test_run=True, forbidden_licenses=crashing_filter) + + +@pytest.mark.usefixtures("aiida_profile_clean") +def test_tmp_dir_custom_valid(tmp_path): + """Test using a custom valid temporary directory.""" + from unittest.mock import patch + + node = orm.Int(42).store() + custom_tmp = tmp_path / 'custom_tmp' + custom_tmp.mkdir() + filename = tmp_path / 'export.aiida' # Put output file outside custom_tmp + + with patch('tempfile.TemporaryDirectory') as mock_temp_dir: + # Create the actual temp directory that the mock returns + actual_temp_dir = custom_tmp / 'temp_dir' + actual_temp_dir.mkdir() + + mock_temp_dir.return_value.__enter__.return_value = str(actual_temp_dir) + mock_temp_dir.return_value.__exit__.return_value = None + + create_archive([node], filename=filename, tmp_dir=custom_tmp) + + # Check that TemporaryDirectory was called with custom directory + mock_temp_dir.assert_called_once_with(dir=custom_tmp, prefix=None) + +@pytest.mark.usefixtures("aiida_profile_clean") +def test_tmp_dir_validation_errors(tmp_path): + """Test tmp_dir validation errors.""" + + node = orm.Int(42).store() + filename = tmp_path / 'export.aiida' + + # Non-existent directory + with pytest.raises(ArchiveExportError, match="does not exist"): + create_archive([node], filename=filename, tmp_dir=tmp_path / 'nonexistent') + + # File instead of directory + not_a_dir = tmp_path / 'file.txt' + not_a_dir.write_text('content') + with pytest.raises(ArchiveExportError, match="is not a directory"): + create_archive([node], filename=filename, tmp_dir=not_a_dir) + +@pytest.mark.usefixtures("aiida_profile_clean") +def test_tmp_dir_disk_space_error(tmp_path): + """Test disk space error handling.""" + from unittest.mock import patch + + node = orm.Int(42).store() + custom_tmp = tmp_path / 'custom_tmp' + custom_tmp.mkdir() + filename = tmp_path / 'export.aiida' + + def mock_temp_dir_error(*args, **kwargs): + error = OSError("No space left on device") + error.errno = 28 + raise error + + with patch('tempfile.TemporaryDirectory', side_effect=mock_temp_dir_error): + with pytest.raises(ArchiveExportError, match="Insufficient disk space.*--tmp-dir"): + create_archive([node], filename=filename, tmp_dir=custom_tmp) From f38ecd02e5f02b2aa264b1ffa58ac7d54ed1d761 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:21:52 +0000 Subject: [PATCH 08/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/tools/archive/test_simple.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/tools/archive/test_simple.py b/tests/tools/archive/test_simple.py index 6b256b2b0d..729b6f41ac 100644 --- a/tests/tools/archive/test_simple.py +++ b/tests/tools/archive/test_simple.py @@ -157,7 +157,7 @@ def crashing_filter(_): create_archive([struct], test_run=True, forbidden_licenses=crashing_filter) -@pytest.mark.usefixtures("aiida_profile_clean") +@pytest.mark.usefixtures('aiida_profile_clean') def test_tmp_dir_custom_valid(tmp_path): """Test using a custom valid temporary directory.""" from unittest.mock import patch @@ -180,7 +180,8 @@ def test_tmp_dir_custom_valid(tmp_path): # Check that TemporaryDirectory was called with custom directory mock_temp_dir.assert_called_once_with(dir=custom_tmp, prefix=None) -@pytest.mark.usefixtures("aiida_profile_clean") + +@pytest.mark.usefixtures('aiida_profile_clean') def test_tmp_dir_validation_errors(tmp_path): """Test tmp_dir validation errors.""" @@ -188,16 +189,17 @@ def test_tmp_dir_validation_errors(tmp_path): filename = tmp_path / 'export.aiida' # Non-existent directory - with pytest.raises(ArchiveExportError, match="does not exist"): + with pytest.raises(ArchiveExportError, match='does not exist'): create_archive([node], filename=filename, tmp_dir=tmp_path / 'nonexistent') # File instead of directory not_a_dir = tmp_path / 'file.txt' not_a_dir.write_text('content') - with pytest.raises(ArchiveExportError, match="is not a directory"): + with pytest.raises(ArchiveExportError, match='is not a directory'): create_archive([node], filename=filename, tmp_dir=not_a_dir) -@pytest.mark.usefixtures("aiida_profile_clean") + +@pytest.mark.usefixtures('aiida_profile_clean') def test_tmp_dir_disk_space_error(tmp_path): """Test disk space error handling.""" from unittest.mock import patch @@ -208,10 +210,10 @@ def test_tmp_dir_disk_space_error(tmp_path): filename = tmp_path / 'export.aiida' def mock_temp_dir_error(*args, **kwargs): - error = OSError("No space left on device") + error = OSError('No space left on device') error.errno = 28 raise error with patch('tempfile.TemporaryDirectory', side_effect=mock_temp_dir_error): - with pytest.raises(ArchiveExportError, match="Insufficient disk space.*--tmp-dir"): + with pytest.raises(ArchiveExportError, match='Insufficient disk space.*--tmp-dir'): create_archive([node], filename=filename, tmp_dir=custom_tmp) From f0ae6f8ced88c27e2046794157feeab35789e70b Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Mon, 21 Jul 2025 09:32:19 +0200 Subject: [PATCH 09/14] fix nested directory creation --- src/aiida/tools/archive/create.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index 94426b8548..55da4060f6 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -188,11 +188,12 @@ def querybuilder(): } # Handle temporary directory configuration + tmp_prefix = '.aiida-export-' if tmp_dir is not None: tmp_dir = Path(tmp_dir) if not tmp_dir.exists(): - msg = f"Specified temporary directory '{tmp_dir}' does not exist" - raise ArchiveExportError(msg) + EXPORT_LOGGER.warning(f"Specified temporary directory '{tmp_dir}' doesn't exist. Creating it.") + tmp_dir.mkdir(parents=False) if not tmp_dir.is_dir(): msg = f"Specified temporary directory '{tmp_dir}' is not a directory" raise ArchiveExportError(msg) @@ -201,11 +202,10 @@ def querybuilder(): if not os.access(tmp_dir, os.W_OK | os.X_OK): msg = f"Specified temporary directory '{tmp_dir}' is not writable" raise ArchiveExportError(msg) - tmp_prefix = None # Use default tempfile prefix + else: # Create temporary directory in the same folder as the output file tmp_dir = filename.parent - tmp_prefix = '.aiida-export-' initial_summary = get_init_summary( archive_version=archive_format.latest_version, @@ -311,7 +311,9 @@ def querybuilder(): # Create and open the archive for writing. # We create in a temp dir then move to final place at end, # so that the user cannot end up with a half written archive on errors + try: + tmp_dir.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=tmp_dir, prefix=tmp_prefix) as tmpdir: tmp_filename = Path(tmpdir) / 'export.zip' with archive_format.open(tmp_filename, mode='x', compression=compression) as writer: @@ -408,7 +410,9 @@ def transform(d): f'Consider using --tmp-dir to specify a location with more available space.' ) raise ArchiveExportError(msg) from e - raise ArchiveExportError(f'Failed to create temporary directory: {e}') from e + + msg = f'Failed to create temporary directory: {e}' + raise ArchiveExportError(msg) from e EXPORT_LOGGER.report('Archive created successfully') From c71d1ea1f199ca2354207f591e9b0d6126b670a5 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Thu, 24 Jul 2025 13:44:23 +0200 Subject: [PATCH 10/14] Move path validation, and fix failing tests --- src/aiida/tools/archive/create.py | 41 +++++++++++++++--------------- tests/tools/archive/test_simple.py | 6 +---- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index 55da4060f6..c18af15dda 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -187,26 +187,6 @@ def querybuilder(): name: traversal_rules.get(name, rule.default) for name, rule in GraphTraversalRules.EXPORT.value.items() } - # Handle temporary directory configuration - tmp_prefix = '.aiida-export-' - if tmp_dir is not None: - tmp_dir = Path(tmp_dir) - if not tmp_dir.exists(): - EXPORT_LOGGER.warning(f"Specified temporary directory '{tmp_dir}' doesn't exist. Creating it.") - tmp_dir.mkdir(parents=False) - if not tmp_dir.is_dir(): - msg = f"Specified temporary directory '{tmp_dir}' is not a directory" - raise ArchiveExportError(msg) - # Check if directory is writable - # Taken from: https://stackoverflow.com/a/2113511 - if not os.access(tmp_dir, os.W_OK | os.X_OK): - msg = f"Specified temporary directory '{tmp_dir}' is not writable" - raise ArchiveExportError(msg) - - else: - # Create temporary directory in the same folder as the output file - tmp_dir = filename.parent - initial_summary = get_init_summary( archive_version=archive_format.latest_version, outfile=filename, @@ -308,10 +288,29 @@ def querybuilder(): EXPORT_LOGGER.report(f'Creating archive with:\n{tabulate(count_summary)}') + # Handle temporary directory configuration + tmp_prefix = '.aiida-export-' + if tmp_dir is not None: + tmp_dir = Path(tmp_dir) + if not tmp_dir.exists(): + EXPORT_LOGGER.warning(f"Specified temporary directory '{tmp_dir}' doesn't exist. Creating it.") + tmp_dir.mkdir(parents=False) + if not tmp_dir.is_dir(): + msg = f"Specified temporary directory '{tmp_dir}' is not a directory" + raise ArchiveExportError(msg) + # Check if directory is writable + # Taken from: https://stackoverflow.com/a/2113511 + if not os.access(tmp_dir, os.W_OK | os.X_OK): + msg = f"Specified temporary directory '{tmp_dir}' is not writable" + raise ArchiveExportError(msg) + + else: + # Create temporary directory in the same folder as the output file + tmp_dir = filename.parent + # Create and open the archive for writing. # We create in a temp dir then move to final place at end, # so that the user cannot end up with a half written archive on errors - try: tmp_dir.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=tmp_dir, prefix=tmp_prefix) as tmpdir: diff --git a/tests/tools/archive/test_simple.py b/tests/tools/archive/test_simple.py index 729b6f41ac..36ee15ae73 100644 --- a/tests/tools/archive/test_simple.py +++ b/tests/tools/archive/test_simple.py @@ -178,7 +178,7 @@ def test_tmp_dir_custom_valid(tmp_path): create_archive([node], filename=filename, tmp_dir=custom_tmp) # Check that TemporaryDirectory was called with custom directory - mock_temp_dir.assert_called_once_with(dir=custom_tmp, prefix=None) + mock_temp_dir.assert_called_once_with(dir=custom_tmp, prefix='.aiida-export-') @pytest.mark.usefixtures('aiida_profile_clean') @@ -188,10 +188,6 @@ def test_tmp_dir_validation_errors(tmp_path): node = orm.Int(42).store() filename = tmp_path / 'export.aiida' - # Non-existent directory - with pytest.raises(ArchiveExportError, match='does not exist'): - create_archive([node], filename=filename, tmp_dir=tmp_path / 'nonexistent') - # File instead of directory not_a_dir = tmp_path / 'file.txt' not_a_dir.write_text('content') From 2379d2126bc90ce690e64653809986b39d5904b5 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Thu, 24 Jul 2025 16:50:58 +0200 Subject: [PATCH 11/14] mkdir parents true --- src/aiida/tools/archive/create.py | 2 +- tests/tools/archive/test_simple.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index c18af15dda..7d97c756b0 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -294,7 +294,7 @@ def querybuilder(): tmp_dir = Path(tmp_dir) if not tmp_dir.exists(): EXPORT_LOGGER.warning(f"Specified temporary directory '{tmp_dir}' doesn't exist. Creating it.") - tmp_dir.mkdir(parents=False) + tmp_dir.mkdir(parents=True) if not tmp_dir.is_dir(): msg = f"Specified temporary directory '{tmp_dir}' is not a directory" raise ArchiveExportError(msg) diff --git a/tests/tools/archive/test_simple.py b/tests/tools/archive/test_simple.py index 36ee15ae73..ab6a7a1a57 100644 --- a/tests/tools/archive/test_simple.py +++ b/tests/tools/archive/test_simple.py @@ -182,7 +182,7 @@ def test_tmp_dir_custom_valid(tmp_path): @pytest.mark.usefixtures('aiida_profile_clean') -def test_tmp_dir_validation_errors(tmp_path): +def test_tmp_dir_file_error(tmp_path): """Test tmp_dir validation errors.""" node = orm.Int(42).store() From 4756f5ed5420d05673932f0f8e50317e0f9ae5eb Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Wed, 13 Aug 2025 15:14:30 +0200 Subject: [PATCH 12/14] tmp_dir test without patching --- tests/tools/archive/test_simple.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/tests/tools/archive/test_simple.py b/tests/tools/archive/test_simple.py index ab6a7a1a57..c861c97ddd 100644 --- a/tests/tools/archive/test_simple.py +++ b/tests/tools/archive/test_simple.py @@ -158,27 +158,15 @@ def crashing_filter(_): @pytest.mark.usefixtures('aiida_profile_clean') -def test_tmp_dir_custom_valid(tmp_path): - """Test using a custom valid temporary directory.""" - from unittest.mock import patch - +def test_tmp_dir(tmp_path, aiida_profile_clean): + """Test that tmp_dir parameter is used correctly.""" node = orm.Int(42).store() custom_tmp = tmp_path / 'custom_tmp' custom_tmp.mkdir() - filename = tmp_path / 'export.aiida' # Put output file outside custom_tmp - - with patch('tempfile.TemporaryDirectory') as mock_temp_dir: - # Create the actual temp directory that the mock returns - actual_temp_dir = custom_tmp / 'temp_dir' - actual_temp_dir.mkdir() - - mock_temp_dir.return_value.__enter__.return_value = str(actual_temp_dir) - mock_temp_dir.return_value.__exit__.return_value = None - - create_archive([node], filename=filename, tmp_dir=custom_tmp) + filename = tmp_path / 'export.aiida' - # Check that TemporaryDirectory was called with custom directory - mock_temp_dir.assert_called_once_with(dir=custom_tmp, prefix='.aiida-export-') + create_archive([node], filename=filename, tmp_dir=custom_tmp) + assert filename.exists() @pytest.mark.usefixtures('aiida_profile_clean') From ea4007f8502d24e8f135670b305fb513dec20e7a Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Wed, 13 Aug 2025 15:57:16 +0200 Subject: [PATCH 13/14] expand tests --- src/aiida/cmdline/commands/cmd_archive.py | 2 +- src/aiida/tools/archive/create.py | 12 ++-- tests/cmdline/commands/test_archive_create.py | 14 ++++ tests/tools/archive/test_simple.py | 67 ++++++++++++++++++- 4 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/aiida/cmdline/commands/cmd_archive.py b/src/aiida/cmdline/commands/cmd_archive.py index 491e72aa6d..63c7ba9718 100644 --- a/src/aiida/cmdline/commands/cmd_archive.py +++ b/src/aiida/cmdline/commands/cmd_archive.py @@ -338,7 +338,7 @@ class ExtrasImportCode(Enum): '--extras-mode-new', type=click.Choice(EXTRAS_MODE_NEW), default='import', - help='Specify whether to import extras of new nodes: ' 'import: import extras. ' 'none: do not import extras.', + help='Specify whether to import extras of new nodes: import: import extras. none: do not import extras.', ) @click.option( '--comment-mode', diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index 7d97c756b0..34e64aaac9 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -247,7 +247,7 @@ def querybuilder(): entity_ids[EntityTypes.USER].add(entry.pk) else: raise ArchiveExportError( - f'I was given {entry} ({type(entry)}),' ' which is not a User, Node, Computer, or Group instance' + f'I was given {entry} ({type(entry)}), which is not a User, Node, Computer, or Group instance' ) group_nodes, link_data = _collect_required_entities( querybuilder, @@ -712,7 +712,7 @@ def _check_unsealed_nodes(querybuilder: QbType, node_ids: set[int], batch_size: if unsealed_node_pks: raise ExportValidationError( 'All ProcessNodes must be sealed before they can be exported. ' - f"Node(s) with PK(s): {', '.join(str(pk) for pk in unsealed_node_pks)} is/are not sealed." + f'Node(s) with PK(s): {", ".join(str(pk) for pk in unsealed_node_pks)} is/are not sealed.' ) @@ -803,7 +803,7 @@ def get_init_summary( """Get summary for archive initialisation""" parameters = [['Path', str(outfile)], ['Version', archive_version], ['Compression', compression]] - result = f"\n{tabulate(parameters, headers=['Archive Parameters', ''])}" + result = f'\n{tabulate(parameters, headers=["Archive Parameters", ""])}' inclusions = [ ['Computers/Nodes/Groups/Users', 'All' if collect_all else 'Selected'], @@ -811,10 +811,10 @@ def get_init_summary( ['Node Comments', include_comments], ['Node Logs', include_logs], ] - result += f"\n\n{tabulate(inclusions, headers=['Inclusion rules', ''])}" + result += f'\n\n{tabulate(inclusions, headers=["Inclusion rules", ""])}' if not collect_all: - rules_table = [[f"Follow links {' '.join(name.split('_'))}s", value] for name, value in traversal_rules.items()] - result += f"\n\n{tabulate(rules_table, headers=['Traversal rules', ''])}" + rules_table = [[f'Follow links {" ".join(name.split("_"))}s', value] for name, value in traversal_rules.items()] + result += f'\n\n{tabulate(rules_table, headers=["Traversal rules", ""])}' return result + '\n' diff --git a/tests/cmdline/commands/test_archive_create.py b/tests/cmdline/commands/test_archive_create.py index 5fea646714..37d7f53f31 100644 --- a/tests/cmdline/commands/test_archive_create.py +++ b/tests/cmdline/commands/test_archive_create.py @@ -208,3 +208,17 @@ def test_info_empty_archive(run_cli_command): filename_input = get_archive_file('empty.aiida', filepath='export/migrate') result = run_cli_command(cmd_archive.archive_info, [filename_input], raises=True) assert 'archive file unreadable' in result.output + + +def test_create_tmp_dir_option(run_cli_command, tmp_path): + """Test that the --tmp-dir CLI option passes through correctly.""" + node = Dict().store() + + custom_tmp = tmp_path / 'custom_tmp' + custom_tmp.mkdir() + filename_output = tmp_path / 'archive.aiida' + + options = ['--tmp-dir', str(custom_tmp), '-N', node.pk, '--', filename_output] + + run_cli_command(cmd_archive.create, options) + assert filename_output.is_file() diff --git a/tests/tools/archive/test_simple.py b/tests/tools/archive/test_simple.py index c861c97ddd..6c1df441a3 100644 --- a/tests/tools/archive/test_simple.py +++ b/tests/tools/archive/test_simple.py @@ -158,7 +158,7 @@ def crashing_filter(_): @pytest.mark.usefixtures('aiida_profile_clean') -def test_tmp_dir(tmp_path, aiida_profile_clean): +def test_tmp_dir_basic(tmp_path): """Test that tmp_dir parameter is used correctly.""" node = orm.Int(42).store() custom_tmp = tmp_path / 'custom_tmp' @@ -201,3 +201,68 @@ def mock_temp_dir_error(*args, **kwargs): with patch('tempfile.TemporaryDirectory', side_effect=mock_temp_dir_error): with pytest.raises(ArchiveExportError, match='Insufficient disk space.*--tmp-dir'): create_archive([node], filename=filename, tmp_dir=custom_tmp) + + +@pytest.mark.usefixtures('aiida_profile_clean') +def test_tmp_dir_auto_create(tmp_path): + """Test automatic creation of non-existent tmp_dir.""" + node = orm.Int(42).store() + filename = tmp_path / 'export.aiida' + custom_tmp = tmp_path / 'nonexistent_tmp' # Don't create it! + + create_archive([node], filename=filename, tmp_dir=custom_tmp) + assert filename.exists() + # Verify the directory was created + assert custom_tmp.exists() + + +@pytest.mark.usefixtures('aiida_profile_clean') +def test_tmp_dir_permission_error(tmp_path): + """Test tmp_dir permission validation.""" + import stat + + node = orm.Int(42).store() + filename = tmp_path / 'export.aiida' + readonly_tmp = tmp_path / 'readonly_tmp' + readonly_tmp.mkdir() + + # Make directory read-only + readonly_tmp.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) + + try: + with pytest.raises(ArchiveExportError, match='is not writable'): + create_archive([node], filename=filename, tmp_dir=readonly_tmp) + finally: + # Restore permissions for cleanup + readonly_tmp.chmod(stat.S_IRWXU) + + +@pytest.mark.usefixtures('aiida_profile_clean') +def test_tmp_dir_default_behavior(tmp_path): + """Test default tmp_dir behavior (no tmp_dir specified).""" + node = orm.Int(42).store() + filename = tmp_path / 'export.aiida' + + # Don't specify tmp_dir - test the default path + create_archive([node], filename=filename) + assert filename.exists() + + +@pytest.mark.usefixtures('aiida_profile_clean') +def test_tmp_dir_general_os_error(tmp_path): + """Test general OS error handling.""" + from unittest.mock import patch + + node = orm.Int(42).store() + custom_tmp = tmp_path / 'custom_tmp' + custom_tmp.mkdir() + filename = tmp_path / 'export.aiida' + + def mock_temp_dir_error(*args, **kwargs): + error = OSError('Permission denied') + error.errno = 13 # Different from 28 + raise error + + with patch('aiida.tools.archive.create.tempfile.TemporaryDirectory', side_effect=mock_temp_dir_error): + with pytest.raises(ArchiveExportError, match='Failed to create temporary directory'): + create_archive([node], filename=filename, tmp_dir=custom_tmp) From a5157851499f55cf9b423d6a51c9685513a47d12 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Fri, 22 Aug 2025 09:08:22 +0200 Subject: [PATCH 14/14] start review changes --- docs/source/reference/command_line.rst | 2 +- src/aiida/cmdline/commands/cmd_archive.py | 11 ++++++----- src/aiida/tools/archive/create.py | 12 +++++------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/source/reference/command_line.rst b/docs/source/reference/command_line.rst index 6bc2272694..2a85592330 100644 --- a/docs/source/reference/command_line.rst +++ b/docs/source/reference/command_line.rst @@ -453,7 +453,7 @@ Below is a list with all available subcommands. --broker-host HOSTNAME Hostname for the message broker. [default: 127.0.0.1] --broker-port INTEGER Port for the message broker. [default: 5672] --broker-virtual-host TEXT Name of the virtual host for the message broker without - leading forward slash. [default: ""] + leading forward slash. --repository DIRECTORY Absolute path to the file repository. --test-profile Designate the profile to be used for running the test suite only. diff --git a/src/aiida/cmdline/commands/cmd_archive.py b/src/aiida/cmdline/commands/cmd_archive.py index 63c7ba9718..71b947fe88 100644 --- a/src/aiida/cmdline/commands/cmd_archive.py +++ b/src/aiida/cmdline/commands/cmd_archive.py @@ -141,11 +141,12 @@ def inspect(ctx, archive, version, meta_data, database): @click.option( '--tmp-dir', type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, path_type=Path), - help='Directory to use for temporary files during archive creation. ' - 'If not specified, a temporary directory will be created in the same directory as the output file ' - "with a '.aiida-export-' prefix. This parameter is useful when the output directory has limited " - 'space or when you want to use a specific filesystem (e.g., faster storage) for temporary operations. ' - 'The directory must exist and be writable.', + help=( + 'Location where the temporary directory will be written during archive creation.' + 'The directory must exist and be writable, and defaults to the parent directory of the output file.' + 'This parameter is useful when the output directory has limited space or when you want to use a specific' + 'filesystem (e.g., faster storage) for temporary operations.' + ), ) @decorators.with_dbenv() def create( diff --git a/src/aiida/tools/archive/create.py b/src/aiida/tools/archive/create.py index 34e64aaac9..c4239cbd93 100644 --- a/src/aiida/tools/archive/create.py +++ b/src/aiida/tools/archive/create.py @@ -141,11 +141,10 @@ def create_archive( :param backend: the backend to export from. If not specified, the default backend is used. - :param tmp_dir: Directory to use for temporary files during archive creation. - If not specified, a temporary directory will be created in the same directory as the output file - with a '.aiida-export-' prefix. This parameter is useful when the output directory has limited - space or when you want to use a specific filesystem (e.g., faster storage) for temporary operations. - The directory must exist and be writable. + :param tmp_dir: Location where the temporary directory will be written during archive creation. + The directory must exist and be writable, and defaults to the parent directory of the output file. + This parameter is useful when the output directory has limited space or when you want to use a specific + filesystem (e.g., faster storage) for temporary operations. :param traversal_rules: graph traversal rules. See :const:`aiida.common.links.GraphTraversalRules` what rule names are toggleable and what the defaults are. @@ -289,7 +288,6 @@ def querybuilder(): EXPORT_LOGGER.report(f'Creating archive with:\n{tabulate(count_summary)}') # Handle temporary directory configuration - tmp_prefix = '.aiida-export-' if tmp_dir is not None: tmp_dir = Path(tmp_dir) if not tmp_dir.exists(): @@ -313,7 +311,7 @@ def querybuilder(): # so that the user cannot end up with a half written archive on errors try: tmp_dir.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=tmp_dir, prefix=tmp_prefix) as tmpdir: + with tempfile.TemporaryDirectory(dir=tmp_dir, prefix='.aiida-export-') as tmpdir: tmp_filename = Path(tmpdir) / 'export.zip' with archive_format.open(tmp_filename, mode='x', compression=compression) as writer: # add metadata