Repository: materialsproject/custodian

Issue:
[Bug]: The `error.tar.gz` files unpack in a weird way when a custom `directory` is set
### Code snippet

This is a continuation of #378.

When backing up files via an absolute path, the unpacking process is not ideal.


```python
from custodian.utils import backup

backup(["INCAR"], directory="/path/to/test")
```

Doing `tar -xvf error.1.tar.gz` will unpack into `path/to/test/INCAR` in the current working directory. This is due to an oddity of how `tar.add()` works. It adds the file to the archive with the full internal path (but for some reason without the `/` preceding it...). The `arcname` keyword argument has to be supplied to fix this.


### Version

2025.5.12


Discussion hints:
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/materialsproject/custodian/pull/381?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=materialsproject) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 69.71%. Comparing base [(`5d30e88`)](https://app.codecov.io/gh/materialsproject/custodian/commit/5d30e888f522792cd5f4af1e636ae7fbcf381d53?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=materialsproject) to head [(`2b79d8b`)](https://app.codecov.io/gh/materialsproject/custodian/commit/2b79d8b373ddd894637c824b17b0d5a6e438e484?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=materialsproject).
> Report is 8 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master     #381   +/-   ##
=======================================
  Coverage   69.70%   69.71%           
=======================================
  Files          39       39           
  Lines        3492     3493    +1     
=======================================
+ Hits         2434     2435    +1     
  Misses       1058     1058           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/materialsproject/custodian/pull/381?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=materialsproject).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=materialsproject).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
Andrew-S-Rosen: This one is now ready for you, @shyuep. 👍 Thanks again!

Solution patch:
diff --git a/src/custodian/utils.py b/src/custodian/utils.py
index b081a040..19215a7d 100644
--- a/src/custodian/utils.py
+++ b/src/custodian/utils.py
@@ -21,12 +21,13 @@ def backup(filenames, prefix="error", directory="./") -> None:
         directory (str): directory where the files exist
     """
     num = max([0] + [int(file.split(".")[-3]) for file in glob(os.path.join(directory, f"{prefix}.*.tar.gz"))])
-    filename = os.path.join(directory, f"{prefix}.{num + 1}.tar.gz")
+    prefix = f"{prefix}.{num + 1}"
+    filename = os.path.join(directory, f"{prefix}.tar.gz")
     logging.info(f"Backing up run to {filename}.")
     with tarfile.open(filename, "w:gz") as tar:
         for fname in filenames:
             for file in glob(os.path.join(directory, fname)):
-                tar.add(file)
+                tar.add(file, arcname=os.path.join(prefix, os.path.basename(file)))
 
 
 def get_execution_host_info():


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index fb7df302..d8f97d3a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -30,7 +30,37 @@ def test_cache_and_clear() -> None:
     assert n_calls == 3
 
 
-def test_backup(tmp_path) -> None:
+def test_backup(tmp_path, monkeypatch) -> None:
+    monkeypatch.chdir(tmp_path)
+    with open("INCAR", "w") as f:
+        f.write("This is a test file.")
+
+    backup(["INCAR"])
+
+    assert Path("error.1.tar.gz").exists()
+    with tarfile.open("error.1.tar.gz", "r:gz") as tar:
+        assert len(tar.getmembers()) == 1
+        assert tar.getnames() == ["error.1/INCAR"]
+
+
+def test_backup_with_glob(tmp_path, monkeypatch) -> None:
+    monkeypatch.chdir(tmp_path)
+    with open("INCAR", "w") as f:
+        f.write("This is a test file.")
+    with open("POSCAR", "w") as f:
+        f.write("This is a test file.")
+    with open("garbage", "w") as f:
+        f.write("This is a test file.")
+
+    backup(["*CAR"])
+
+    assert Path("error.1.tar.gz").exists()
+    with tarfile.open("error.1.tar.gz", "r:gz") as tar:
+        assert len(tar.getmembers()) == 2
+        assert tar.getnames() == ["error.1/INCAR", "error.1/POSCAR"]
+
+
+def test_backup_with_directory(tmp_path) -> None:
     with open(tmp_path / "INCAR", "w") as f:
         f.write("This is a test file.")
 
@@ -38,4 +68,5 @@ def test_backup(tmp_path) -> None:
 
     assert Path(tmp_path / "error.1.tar.gz").exists()
     with tarfile.open(tmp_path / "error.1.tar.gz", "r:gz") as tar:
-        assert len(tar.getmembers()) > 0
+        assert len(tar.getmembers()) == 1
+        assert tar.getnames() == ["error.1/INCAR"]