Skip to content

Restore persistent directory when a download fails - #5378

Open
frenck wants to merge 2 commits into
mainfrom
frenck/fix-persistent-directory-loss-on-failed-download
Open

Restore persistent directory when a download fails#5378
frenck wants to merge 2 commits into
mainfrom
frenck/fix-persistent-directory-loss-on-failed-download

Conversation

@frenck

@frenck frenck commented Jul 12, 2026

Copy link
Copy Markdown
Member

Proposed change

When downloading an update for an installed repository, the persistent directory (persistent_directory in hacs.json) is moved out of the install directory into /tmp before the backup of the old install is created. That backup therefore never contains the persistent directory.

The restore of the persistent directory only happened on the success path. On a failed download, the error path raised before reaching it: the backup restore brought back the old install without the persistent data, which stayed stranded in /tmp and was gone after a reboot.

This change wraps the download steps in try/finally and moves the persistent directory restore into the finally block, so it is always moved back in place. This also covers download paths that raise directly (like "No content to download") instead of going through the validation error branch.

Added a test that upgrades an installed integration with a persistent directory present, injects a download failure, and asserts the user data is still there afterwards. The test fails on the previous code.

Type of change

  • Bugfix (non-breaking change which fixes an issue)

Checklist

  • The code change is tested and works locally.
  • Local tests pass.
  • There is no commented out code in this PR.

Copilot AI review requested due to automatic review settings July 12, 2026 17:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes update/install rollback behavior so a repository’s persistent_directory is always moved back into place, even when the download step fails, preventing user data from being stranded in /tmp and lost on reboot.

Changes:

  • Wraps repository download steps in try/finally and restores the persistent directory in finally.
  • Adds a regression test covering a failed update download with an existing persistent directory.
  • Adds an API-usage snapshot for the new test.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
custom_components/hacs/repositories/base.py Ensures persistent directory restoration runs on both success and failure paths during install/update.
tests/repositories/test_update_repository.py Adds regression test verifying persistent user data survives a failed download.
tests/snapshots/api-usage/tests/repositories/test_update_repositorytest-update-repository-entity-download-failure-keeps-persistent-directory.json Records expected API usage for the new regression test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread custom_components/hacs/repositories/base.py Outdated
Comment thread custom_components/hacs/repositories/base.py Outdated
"hacs-test-org/integration-basic")

installed_file = Path(repo.localpath) / "__init__.py"
installed_file.parent.mkdir(parents=True, exist_ok=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the repo.localpath inside the configuration directory which we've set to be inside a tmp directory for the tests?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. For an integration localpath is {hacs.core.config_path}/custom_components/{domain}, and hacs.core.config_path is set from hass.config.path(), which the hass fixture points at pytest's tmp dir. Printed during a run it resolves to /tmp/pytest-of-<user>/pytest-<n>/test_update_repository_entity_0/custom_components/example, so the test only writes inside the temporary configuration directory.

The backup and persistent directory that the code moves things through do land in the real system temp dir (tempfile.gettempdir(), so /tmp/hacs_backup/ and /tmp/hacs_persistent_integration/), but that is existing behavior of utils/backup.py that all the other download tests share, not something this test introduces.

"https://raw.githubusercontent.com/hacs-test-org/integration-basic/1.0.0/README.md": 1,
"https://raw.githubusercontent.com/hacs-test-org/integration-basic/2.0.0/hacs.json": 1
}
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These snapshots are generated, not hand written. track_api_usage in tests/conftest.py passes safe_json_dumps(...) to snapshots.assert_match, and that output has no trailing newline, so none of the 96 existing api-usage snapshots have one either. Adding it makes the next run fail:

E  AssertionError: value does not match the expected value in snapshot
E  assert '{\n    "test...: 1\n    }\n}' == '{\n    "test...1\n    }\n}\n'

Leaving these as the fixture writes them.

"https://raw.githubusercontent.com/hacs-test-org/integration-basic/1.0.0/custom_components/example/manifest.json": 1,
"https://raw.githubusercontent.com/hacs-test-org/integration-basic/2.0.0/hacs.json": 1
}
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These snapshots are generated, not hand written. track_api_usage in tests/conftest.py passes safe_json_dumps(...) to snapshots.assert_match, and that output has no trailing newline, so none of the 96 existing api-usage snapshots have one either. Adding it makes the next run fail:

E  AssertionError: value does not match the expected value in snapshot
E  assert '{\n    "test...: 1\n    }\n}' == '{\n    "test...1\n    }\n}\n'

Leaving these as the fixture writes them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants