210 Commits
Author SHA1 Message Date
xixu-me 1efdd4c091 fix(actions): avoid status event fan-out
Publish Documentation / build-and-deploy-docs (push) Waiting to run
Publish Documentation / docs-quality-check (push) Waiting to run
2026-08-18 11:29:40 +08:00
xixu-me 713530da75 fix(actions): retry Dependabot merge after checks 2026-08-18 11:25:22 +08:00
xixu-me eb89bb8f3b fix(ci): evaluate all Dependabot pull requests 2026-08-14 18:15:02 +08:00
github-actions[bot] d2fa7bcc14 Merge pull request #43 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.13.2
chore(deps): update sphinx-autodoc-typehints requirement from >=3.13.0 to >=3.13.2
2026-08-10 04:55:51 +00:00
dependabot[bot] 5f9f3f3e16 chore(deps): update sphinx-autodoc-typehints requirement
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/3.13.0...3.13.2)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.13.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 04:55:02 +00:00
github-actions[bot] a75e865b45 Merge pull request #42 from xixu-me/dependabot/github_actions/actions/setup-python-7
chore(deps): bump actions/setup-python from 6 to 7
2026-08-01 04:54:39 +00:00
dependabot[bot] 2e34172f0c chore(deps): bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 04:52:54 +00:00
github-actions[bot] d07cd60c3a Merge pull request #41 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.13.0
chore(deps): update sphinx-autodoc-typehints requirement from >=3.12.1 to >=3.13.0
2026-07-27 04:55:03 +00:00
dependabot[bot] 4a5b423e7c chore(deps): update sphinx-autodoc-typehints requirement
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/3.12.1...3.13.0)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.13.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 04:54:18 +00:00
github-actions[bot] 5165a71659 Merge pull request #40 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.12.1
chore(deps): update sphinx-autodoc-typehints requirement from >=3.12.0 to >=3.12.1
2026-07-06 04:56:21 +00:00
dependabot[bot] afa4e0e01b chore(deps): update sphinx-autodoc-typehints requirement
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/3.12.0...3.12.1)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.12.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 04:54:41 +00:00
xixu-me e59431b610 Merge pull request #39 from xixu-me/dependabot/github_actions/actions/checkout-7
Bump actions/checkout from 6 to 7
2026-07-04 15:15:16 +08:00
xixu-me f5262c92c6 ci: trigger Dependabot auto-merge from workflow runs 2026-07-03 23:34:17 +08:00
xixu-me df46fe8a48 ci: tolerate protected Dependabot workflow updates 2026-07-03 23:14:27 +08:00
xixu-me 5f44b21e6a ci: add Dependabot auto merge workflow 2026-07-03 23:03:44 +08:00
github-actions[bot] 734a7817ff Merge pull request #38 from xixu-me/dependabot/github_actions/actions/cache-6
Bump actions/cache from 5 to 6
2026-07-01 05:16:03 +00:00
github-actions[bot] b43e152a75 Merge pull request #37 from xixu-me/dependabot/github_actions/codecov/codecov-action-7
Bump codecov/codecov-action from 6 to 7
2026-07-01 05:15:59 +00:00
dependabot[bot] 427e422a54 Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 04:53:30 +00:00
dependabot[bot] 1ec12195fb Bump actions/cache from 5 to 6
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 04:53:26 +00:00
dependabot[bot] 1e8ff36a9b Bump codecov/codecov-action from 6 to 7
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6 to 7.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 04:53:22 +00:00
github-actions[bot] b9f0382e08 Merge pull request #36 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.12.0
Update sphinx-autodoc-typehints requirement from >=3.11.0 to >=3.12.0
2026-06-29 05:51:52 +00:00
dependabot[bot] feaafc4192 Update sphinx-autodoc-typehints requirement from >=3.11.0 to >=3.12.0
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/3.11.0...3.12.0)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 04:54:12 +00:00
github-actions[bot] f033846a0f Merge pull request #35 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.11.0
Update sphinx-autodoc-typehints requirement from >=3.10.5 to >=3.11.0
2026-06-15 05:45:24 +00:00
dependabot[bot] 62cb41bb0c Update sphinx-autodoc-typehints requirement from >=3.10.5 to >=3.11.0
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/3.10.5...3.11.0)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.11.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 04:54:17 +00:00
github-actions[bot] 4d66fccd59 Merge pull request #34 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.10.5
Update sphinx-autodoc-typehints requirement from >=3.10.4 to >=3.10.5
2026-06-08 05:33:16 +00:00
dependabot[bot] a0e7c08f95 Update sphinx-autodoc-typehints requirement from >=3.10.4 to >=3.10.5
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/3.10.4...3.10.5)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.10.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 04:53:58 +00:00
github-actions[bot] 0f9d0e7b89 Merge pull request #33 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.10.4
Update sphinx-autodoc-typehints requirement from >=3.10.2 to >=3.10.4
2026-06-01 13:19:58 +00:00
dependabot[bot] db3f4e356c Update sphinx-autodoc-typehints requirement from >=3.10.2 to >=3.10.4
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/3.10.2...3.10.4)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.10.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-01 11:45:15 +00:00
github-actions[bot] 78cf17be4b Merge pull request #32 from xixu-me/dependabot/pip/myst-parser-gte-5.1.0
Update myst-parser requirement from >=5.0.0 to >=5.1.0
2026-05-18 06:47:02 +00:00
dependabot[bot] 065c566b5d Update myst-parser requirement from >=5.0.0 to >=5.1.0
Updates the requirements on [myst-parser](https://github.com/executablebooks/MyST-Parser) to permit the latest version.
- [Release notes](https://github.com/executablebooks/MyST-Parser/releases)
- [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/MyST-Parser/compare/v5.0.0...v5.1.0)

---
updated-dependencies:
- dependency-name: myst-parser
  dependency-version: 5.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 06:24:18 +00:00
xixu-me 903a748ab8 Merge pull request #27 from xixu-me/dependabot/pip/sphinx-autodoc-typehints-gte-3.10.2
Update sphinx-autodoc-typehints requirement from >=1.25.0 to >=3.10.2
2026-04-27 15:06:38 +08:00
github-actions[bot] 59401b4f88 Merge pull request #30 from xixu-me/dependabot/pip/sphinx-gte-9.1.0
Update sphinx requirement from >=7.1.0 to >=9.1.0
2026-04-27 06:30:05 +00:00
github-actions[bot] e4bb2e3edb Merge pull request #28 from xixu-me/dependabot/pip/myst-parser-gte-5.0.0
Update myst-parser requirement from >=3.0.0 to >=5.0.0
2026-04-27 06:30:01 +00:00
dependabot[bot] a6e1021a1b Update myst-parser requirement from >=3.0.0 to >=5.0.0
Updates the requirements on [myst-parser](https://github.com/executablebooks/MyST-Parser) to permit the latest version.
- [Release notes](https://github.com/executablebooks/MyST-Parser/releases)
- [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/MyST-Parser/compare/v3.0.0...v5.0.0)

---
updated-dependencies:
- dependency-name: myst-parser
  dependency-version: 5.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 06:28:14 +00:00
dependabot[bot] ce07d5cff0 Update sphinx requirement from >=7.1.0 to >=9.1.0
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.1.0...v9.1.0)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-version: 9.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 06:28:02 +00:00
dependabot[bot] 87cbd15bc9 Update sphinx-autodoc-typehints requirement from >=1.25.0 to >=3.10.2
Updates the requirements on [sphinx-autodoc-typehints](https://github.com/tox-dev/sphinx-autodoc-typehints) to permit the latest version.
- [Release notes](https://github.com/tox-dev/sphinx-autodoc-typehints/releases)
- [Commits](https://github.com/tox-dev/sphinx-autodoc-typehints/compare/1.25.0...3.10.2)

---
updated-dependencies:
- dependency-name: sphinx-autodoc-typehints
  dependency-version: 3.10.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 06:27:57 +00:00
github-actions[bot] 8d39b46a2c Merge pull request #31 from xixu-me/dependabot/pip/furo-gte-2025.12.19
Update furo requirement from >=2024.1.29 to >=2025.12.19
2026-04-27 06:27:15 +00:00
github-actions[bot] 71ac857984 Merge pull request #29 from xixu-me/dependabot/pip/sphinx-sitemap-gte-2.9.0
Update sphinx-sitemap requirement from >=2.6.0 to >=2.9.0
2026-04-27 06:27:11 +00:00
github-actions[bot] 2e8e99a200 Merge pull request #26 from xixu-me/dependabot/pip/sphinx-rtd-theme-gte-3.1.0
Update sphinx-rtd-theme requirement from >=2.0.0 to >=3.1.0
2026-04-27 06:27:05 +00:00
github-actions[bot] 7d7e170fdc Merge pull request #25 from xixu-me/dependabot/pip/sphinx-autobuild-gte-2025.8.25
Update sphinx-autobuild requirement from >=2021.3.14 to >=2025.8.25
2026-04-27 06:27:01 +00:00
github-actions[bot] 81ac67bf86 Merge pull request #24 from xixu-me/dependabot/pip/sphinxext-opengraph-gte-0.13.0
Update sphinxext-opengraph requirement from >=0.9.0 to >=0.13.0
2026-04-27 06:26:58 +00:00
github-actions[bot] 927226b839 Merge pull request #23 from xixu-me/dependabot/pip/linkify-it-py-gte-2.1.0
Update linkify-it-py requirement from >=2.0.0 to >=2.1.0
2026-04-27 06:26:55 +00:00
xixu-me a1a9c45ad8 ci: retry dependabot merge when state is pending 2026-04-27 14:26:35 +08:00
xixu-me 68ec8bfbcf ci: set gh repo for auto merge workflow 2026-04-27 14:25:40 +08:00
xixu-me f0caba5c66 ci: auto merge green dependabot prs 2026-04-27 14:24:31 +08:00
dependabot[bot] 264578b866 Update furo requirement from >=2024.1.29 to >=2025.12.19
Updates the requirements on [furo](https://github.com/pradyunsg/furo) to permit the latest version.
- [Release notes](https://github.com/pradyunsg/furo/releases)
- [Changelog](https://github.com/pradyunsg/furo/blob/main/docs/changelog.md)
- [Commits](https://github.com/pradyunsg/furo/compare/2024.01.29...2025.12.19)

---
updated-dependencies:
- dependency-name: furo
  dependency-version: 2025.12.19
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 05:13:23 +00:00
dependabot[bot] 324e227399 Update sphinx-sitemap requirement from >=2.6.0 to >=2.9.0
Updates the requirements on [sphinx-sitemap](https://github.com/jdillard/sphinx-sitemap) to permit the latest version.
- [Release notes](https://github.com/jdillard/sphinx-sitemap/releases)
- [Changelog](https://github.com/jdillard/sphinx-sitemap/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jdillard/sphinx-sitemap/compare/v2.6.0...v2.9.0)

---
updated-dependencies:
- dependency-name: sphinx-sitemap
  dependency-version: 2.9.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 05:13:13 +00:00
dependabot[bot] c6234e12af Update sphinx-rtd-theme requirement from >=2.0.0 to >=3.1.0
Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/2.0.0...3.1.0)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-version: 3.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 05:13:01 +00:00
dependabot[bot] 69f0dd3c06 Update sphinx-autobuild requirement from >=2021.3.14 to >=2025.8.25
Updates the requirements on [sphinx-autobuild](https://github.com/sphinx-doc/sphinx-autobuild) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx-autobuild/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx-autobuild/blob/main/NEWS.rst)
- [Commits](https://github.com/sphinx-doc/sphinx-autobuild/compare/2021.03.14...2025.08.25)

---
updated-dependencies:
- dependency-name: sphinx-autobuild
  dependency-version: 2025.8.25
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 05:12:56 +00:00
dependabot[bot] 535c48cb22 Update sphinxext-opengraph requirement from >=0.9.0 to >=0.13.0
Updates the requirements on [sphinxext-opengraph](https://github.com/sphinx-doc/sphinxext-opengraph) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinxext-opengraph/releases)
- [Commits](https://github.com/sphinx-doc/sphinxext-opengraph/compare/v0.9.0...v0.13.0)

---
updated-dependencies:
- dependency-name: sphinxext-opengraph
  dependency-version: 0.13.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 05:12:52 +00:00
dependabot[bot] a2035596a5 Update linkify-it-py requirement from >=2.0.0 to >=2.1.0
Updates the requirements on [linkify-it-py](https://github.com/tsutsu3/linkify-it-py) to permit the latest version.
- [Release notes](https://github.com/tsutsu3/linkify-it-py/releases)
- [Changelog](https://github.com/tsutsu3/linkify-it-py/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tsutsu3/linkify-it-py/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: linkify-it-py
  dependency-version: 2.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 05:12:48 +00:00
xixu-me 5774eddbbb docs(readme): align multilingual readmes 2026-04-11 21:59:25 +08:00
xixu-me 33aae43b40 chore(release): 1.3.3
CI/CD / test (macos-latest, 3.12) (push) Waiting to run
CI/CD / test (macos-latest, 3.13) (push) Waiting to run
CI/CD / test (macos-latest, 3.14) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.12) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.13) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.14) (push) Waiting to run
CI/CD / test (windows-latest, 3.12) (push) Waiting to run
CI/CD / test (windows-latest, 3.13) (push) Waiting to run
CI/CD / test (windows-latest, 3.14) (push) Waiting to run
2026-04-11 21:23:26 +08:00
xixu-me 148b3b3780 ci: add Python 3.14 support 2026-04-11 21:20:53 +08:00
xixu-me 8a15c2d7db Update README.zh.md 2026-04-07 23:45:40 +08:00
xixu-me 75447562cd Add welcome message for Xget community
Added a welcome message for the Xget open source and AI group.
2026-04-07 23:45:20 +08:00
xixu-me 502b3a6b78 Merge pull request #22 from xixu-me/dependabot/github_actions/codecov/codecov-action-6
Bump codecov/codecov-action from 5 to 6
2026-04-01 14:37:17 +08:00
dependabot[bot] e552fca0b2 Bump codecov/codecov-action from 5 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 05:07:24 +00:00
xixu-me 698eb9cf09 Adjust Codecov upload conditions and fix actionlint warnings 2026-03-24 19:10:30 +08:00
xixu-me 8e5e1e3271 Format coverage gap tests for CI 2026-03-20 12:52:53 +08:00
xixu-me 15e27f6aa6 Fix CI workflow triggers for test changes 2026-03-20 12:51:32 +08:00
xixu-me a2ef8dd115 Add targeted coverage gap tests 2026-03-20 12:47:14 +08:00
xixu-me e0cf05dea0 Fix release workflow runners and Node 24 readiness
CI/CD / test (macos-latest, 3.12) (push) Waiting to run
CI/CD / test (macos-latest, 3.13) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.12) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.13) (push) Waiting to run
CI/CD / test (windows-latest, 3.12) (push) Waiting to run
CI/CD / test (windows-latest, 3.13) (push) Waiting to run
2026-03-17 17:10:36 +08:00
xixu-me a3350b1a27 Fix macOS CI path assertions for JSON output 2026-03-17 17:00:56 +08:00
xixu-me 0c52a51a15 Add machine-readable CLI output for desktop clients 2026-03-17 16:29:09 +08:00
xixu-me 96a13fd29a Merge pull request #20 from xixu-me/dependabot/github_actions/actions/upload-artifact-7
Bump actions/upload-artifact from 6 to 7
2026-03-01 13:54:11 +08:00
xixu-me 6ae689d945 Merge pull request #21 from xixu-me/dependabot/github_actions/actions/download-artifact-8
Bump actions/download-artifact from 7 to 8
2026-03-01 13:53:27 +08:00
dependabot[bot] 7ac5b57496 Bump actions/download-artifact from 7 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 04:56:40 +00:00
dependabot[bot] 4e0f04e10a Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 04:56:35 +00:00
xixu-me cc98287944 Merge pull request #19 from xixu-me/dependabot/github_actions/actions/cache-5
Bump actions/cache from 4 to 5
2026-01-01 12:42:16 +08:00
xixu-me fa82ed95bb Merge pull request #18 from xixu-me/dependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2026-01-01 12:41:27 +08:00
xixu-me 1878584aa0 Merge pull request #17 from xixu-me/dependabot/github_actions/actions/download-artifact-7
Bump actions/download-artifact from 6 to 7
2026-01-01 12:40:34 +08:00
dependabot[bot] 8048393440 Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 04:16:16 +00:00
dependabot[bot] 750fb36643 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 04:16:13 +00:00
dependabot[bot] 879c1166b4 Bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 04:16:07 +00:00
xixu-me aa1b218d4a Remove copyright year in cli.py 2026-01-01 11:44:30 +08:00
xixu-me 96fc060aeb Merge pull request #16 from xixu-me/docs-remove-copyright-year-5183375226831100009
Remove year from copyright info in READMEs and docs
2026-01-01 01:25:19 +08:00
google-labs-jules[bot] e2fca92d4b docs: remove year from copyright info in READMEs and docs
Removes the year "2025" from the copyright section in all README files (all languages) and docs/index.md, as requested. The copyright line now reads "Copyright (c) [Author]..." without a specific year.
2025-12-31 17:24:55 +00:00
xixu-me 08e3bdc849 Rename publish_docs.yml to publish-docs.yml 2026-01-01 01:10:33 +08:00
xixu-me a48631159f Merge pull request #15 from xixu-me/update-docs-and-copyright-11831253178374946146
Update docs copyright and sync installation instructions
2026-01-01 01:06:11 +08:00
google-labs-jules[bot] 1f4b77ffc7 Update copyright year to be dynamic and sync docs with README
- Update `docs/conf.py` to use `datetime` for dynamic copyright year.
- Update `docs/quickstart.md` to match installation instructions from `README.md`.
- Remove redundant installation details from `docs/index.md` and link to `quickstart`.
2025-12-31 17:05:26 +00:00
xixu-me 66fbe5852b Merge pull request #14 from xixu-me/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-12-01 12:58:23 +08:00
dependabot[bot] 0eb219965a Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 04:54:33 +00:00
xixu-me 13f14c847a Merge pull request #13 from xixu-me/claude/enhance-doc-seo-011CV4B1p2pxajncAxBbuMDb
Improve Documentation Search Engine Optimization
2025-11-12 23:23:22 +08:00
Claude 40c7724bcb Remove SEO_ENHANCEMENTS.md documentation file
- Delete SEO_ENHANCEMENTS.md to keep docs directory clean
- Remove from exclude_patterns in conf.py
- SEO enhancements remain in place and functional
2025-11-12 15:18:52 +00:00
Claude a316a8ac2e Fix documentation build: exclude SEO_ENHANCEMENTS.md and clean template formatting
- Add SEO_ENHANCEMENTS.md to exclude_patterns to prevent Sphinx warnings
- Clean up layout.html template formatting for better compatibility
- Split template tags across multiple lines for readability
2025-11-12 15:16:43 +00:00
Claude 9e741b6f8f Enhance documentation SEO with comprehensive optimizations
This commit implements extensive SEO improvements for the tzst documentation:

## Key Enhancements:

### 1. Sitemap & Crawling
- Add sphinx-sitemap extension for automatic XML sitemap generation
- Create robots.txt with crawler guidelines and sitemap reference
- Configure html_baseurl for proper sitemap generation

### 2. Structured Data (Schema.org)
- Add SoftwareApplication schema with detailed metadata
- Implement BreadcrumbList schema for all pages
- Add TechArticle schema for documentation pages
- Create FAQPage schema for quickstart with 5 common Q&As
- Add HowTo schema for examples page with step-by-step guide

### 3. Enhanced Meta Tags
- Add comprehensive Open Graph tags (og:site_name, og:locale)
- Enhance Twitter Card tags (twitter:site, twitter:creator)
- Add article meta tags for proper attribution
- Include rating and revisit-after tags

### 4. Page-Specific Optimization
- Add SEO meta tags to development.md
- Enhance all existing page meta tags
- Implement page-specific canonical URLs
- Add unique descriptions and keywords per page

### 5. Performance Optimization
- Add preconnect hints for external domains
- Add dns-prefetch for frequently accessed domains
- Configure html_copy_source=False to reduce duplicate content
- Hide unnecessary Sphinx links and branding

### 6. Documentation
- Create comprehensive SEO_ENHANCEMENTS.md documenting all changes
- Include best practices and monitoring recommendations
- Provide verification steps and future enhancement ideas

## Benefits:
- Improved search engine visibility and indexing
- Rich snippets in search results
- Better social media sharing previews
- Enhanced click-through rates
- Faster page discovery
- Professional appearance

## Files Modified:
- docs/conf.py - Sitemap config, meta tags, SEO settings
- docs/requirements.txt - Add sphinx-sitemap dependency
- docs/_templates/layout.html - Structured data, schemas
- docs/development.md - Add page-specific meta tags
- docs/_static/robots.txt - New file for crawler guidance
- docs/SEO_ENHANCEMENTS.md - New comprehensive documentation
2025-11-12 14:48:58 +00:00
xixu-me 988cdc0306 Update PyPI downloads badge links to PyPIStats
Changed the PyPI downloads badge links in all README translations and documentation from pypi.org to pypistats.org for more accurate download statistics.
2025-11-12 17:59:36 +08:00
xixu-me c872ca623a Add link to in-depth technical article in all READMEs
A link to the technical analysis article 'Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard' has been added to the introduction section of all language README files to provide readers with more detailed information about the project.
2025-11-09 20:00:02 +08:00
xixu-me d82f82bd5d Update install instructions and remove CLI notes
Added uv as a recommended installation method alongside pip in all README translations. Removed the note about downloading standalone binaries and using uvx for command line usage for brevity and consistency.
2025-11-05 12:24:19 +08:00
xixu-me 20cfb855c8 Merge pull request #10 from xixu-me/dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-11-01 12:37:47 +08:00
xixu-me 662d84da27 Merge pull request #11 from xixu-me/dependabot/github_actions/actions/download-artifact-6
Bump actions/download-artifact from 5 to 6
2025-11-01 12:37:31 +08:00
xixu-me 81c92e3a33 Merge pull request #12 from xixu-me/dependabot/github_actions/stefanzweifel/git-auto-commit-action-7
Bump stefanzweifel/git-auto-commit-action from 6 to 7
2025-11-01 12:37:14 +08:00
dependabot[bot] 37103bf869 Bump stefanzweifel/git-auto-commit-action from 6 to 7
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 6 to 7.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 04:15:14 +00:00
dependabot[bot] 7c4459dc5a Bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 04:15:12 +00:00
dependabot[bot] 1f3e9ef76e Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 04:15:09 +00:00
xixu-me 5ecb10411f Update compression level error message in test
Adjusted the expected error message in the test for invalid compression level to match the new format, specifying that the value must be an integer between 1 and 22.
2025-10-04 20:36:23 +08:00
xixu-me 2cb2b9eaa7 Fix regex in compression level validation test
Updated the regex pattern in the compression level validation test to escape the dot character, ensuring correct matching of invalid input '1.5'.
2025-10-03 22:05:22 +08:00
xixu-me e5ad0336f3 Add CLAUDE.md with contributor guidance
Introduces CLAUDE.md to provide architecture overview, development commands, code quality checks, build instructions, key features, and testing patterns for the tzst Python library. This file is intended to guide contributors and AI assistants working with the repository.
2025-10-03 21:37:44 +08:00
xixu-me 6442ba91f3 Merge pull request #9 from xixu-me/dependabot/github_actions/actions/setup-python-6
Bump actions/setup-python from 5 to 6
2025-10-03 12:26:45 +08:00
dependabot[bot] e5f6fd2bc4 Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 04:19:18 +00:00
xixu-me ff6c321f0d Merge pull request #8 from xixu-me/dependabot/github_actions/actions/download-artifact-5
Bump actions/download-artifact from 4 to 5
2025-09-01 18:51:25 +08:00
xixu-me 1fc8a85c64 Merge pull request #7 from xixu-me/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-09-01 18:50:40 +08:00
dependabot[bot] 0aae913613 Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 09:55:01 +00:00
dependabot[bot] 4987bcd468 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 09:32:46 +00:00
xixu-me fd11edd877 Add funding options to FUNDING.yml 2025-08-24 17:44:18 +08:00
xixu-me aca7b5f42c Merge pull request #6 from xixu-me/dependabot/github_actions/stefanzweifel/git-auto-commit-action-6
Bump stefanzweifel/git-auto-commit-action from 5 to 6
2025-07-04 17:17:06 +08:00
dependabot[bot] e454f79e18 Bump stefanzweifel/git-auto-commit-action from 5 to 6
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 5 to 6.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 07:25:41 +00:00
xixu-me bcfb299bfb Add SECURITY.md with security policy and best practices
Introduces a SECURITY.md file detailing supported versions, security features, best practices for safe archive extraction, vulnerability reporting procedures, and development security practices for the tzst project.
2025-06-17 21:16:31 +08:00
xixu-me 171c00c8ce Create CODE_OF_CONDUCT.md 2025-06-17 21:04:08 +08:00
xixu-me e6f81ab68b Bump version to 1.2.8
CI/CD / test (macos-latest, 3.12) (push) Waiting to run
CI/CD / test (macos-latest, 3.13) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.12) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.13) (push) Waiting to run
CI/CD / test (windows-latest, 3.12) (push) Waiting to run
CI/CD / test (windows-latest, 3.13) (push) Waiting to run
Update the package version from 1.2.7 to 1.2.8 in preparation for a new release.
2025-06-15 16:22:19 +08:00
xixu-me 0a827ec260 Normalize archive path in CLI output
Introduces a helper to ensure the archive path shown in CLI output matches the final file created, mirroring the extension logic from core.py. This improves user clarity by displaying the correct archive filename before and after creation.
2025-06-15 15:17:10 +08:00
xixu-me 3645979315 Bump version to 1.2.7
CI/CD / test (macos-latest, 3.12) (push) Waiting to run
CI/CD / test (macos-latest, 3.13) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.12) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.13) (push) Waiting to run
CI/CD / test (windows-latest, 3.12) (push) Waiting to run
CI/CD / test (windows-latest, 3.13) (push) Waiting to run
Update the __version__ variable in __init__.py to 1.2.7 to reflect the new release.
2025-06-10 21:34:16 +08:00
xixu-me 3d85da8267 Add cross-platform file move utility for extraction
Introduced _move_file_cross_platform to handle file moves across drives, especially on Windows where rename may fail. Updated archive extraction logic to use this utility, ensuring robust file operations during extraction and conflict resolution.
2025-06-10 20:04:58 +08:00
xixu-me 4e87976fde Bump version to 1.2.6
Updated the __version__ variable in __init__.py to reflect the new release version 1.2.6.
2025-06-10 18:56:30 +08:00
xixu-me 372ad5d653 Update binary filenames in docs and READMEs
Standardized the naming convention for downloadable binaries across all documentation and translated README files. The new format uses 'tzst-{version}-{platform}-{arch}.zip' (e.g., 'tzst-{version}-linux-amd64.zip') for all platforms and architectures, replacing the previous inconsistent patterns.
2025-06-10 18:27:37 +08:00
xixu-me ffa12db17b Update CI workflow for architecture and naming consistency
Replaced 'x86_64' and 'aarch64' with 'amd64' and 'arm64' for consistency in architecture naming. Updated macOS 'os_name' to 'darwin' and removed 'v' prefix from artifact filenames to standardize naming conventions. Adjusted related conditions and paths in the workflow.
2025-06-10 15:18:19 +08:00
xixu-me 4ba2749e82 Update logo URLs in README files
CI/CD / test (macos-latest, 3.12) (push) Waiting to run
CI/CD / test (macos-latest, 3.13) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.12) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.13) (push) Waiting to run
CI/CD / test (windows-latest, 3.12) (push) Waiting to run
CI/CD / test (windows-latest, 3.13) (push) Waiting to run
Replaced relative paths for the logo image with absolute URLs pointing to the GitHub repository. This ensures the logo is correctly displayed across all localized README files.
2025-06-09 13:48:06 +08:00
xixu-me 3fc59f248a Bump version to 1.2.5
Updated the `__version__` in `__init__.py` from 1.2.4 to 1.2.5 to reflect the latest changes or release.
2025-06-09 13:44:27 +08:00
xixu-me 308e7e3c12 Add custom icon to PyInstaller builds
Updated the PyInstaller commands in the CI workflow to include a custom icon (`docs/_static/favicon.ico`) for all binary builds. This ensures the binaries have a consistent branding across platforms.
2025-06-09 13:43:58 +08:00
xixu-me 68800be7a9 Bump version to 1.2.4
Updated the __version__ attribute in the __init__.py file to reflect the new version 1.2.4.
2025-06-09 12:20:49 +08:00
xixu-me af3e657aa4 Update README files with logo and documentation link
Added a centered logo and a link to the documentation in all README files across multiple languages. This enhances the visual presentation and provides direct access to the project's documentation.
2025-06-09 12:18:44 +08:00
xixu-me 305c13119f Improve robustness and clarity in TzstArchive
Refactored error handling, added default behaviors for unknown resolutions, and improved documentation for unsupported modes. Enhanced memory efficiency and compatibility in archive operations, streamlined extraction logic, and validated compression levels more effectively.
2025-06-09 11:14:01 +08:00
xixu-me efbcbbce99 Improve error handling and path validation
Updated compression level validation to use a fallback max level if the zstandard constant is unavailable. Added checks to ensure paths are not None before file operations in the extract_archive function, improving robustness and preventing potential errors.
2025-06-09 10:51:20 +08:00
xixu-me 320ed0a27a Improve robustness in TzstArchive handling
Enhanced error handling and validation in TzstArchive methods, including better checks for file object initialization and compressed stream creation. Updated compression level validation to use dynamic max level from zstd library. Simplified and clarified comments and error messages for unsupported modes and extraction operations.
2025-06-09 10:41:09 +08:00
xixu-me 36ffd4fd2b Update index.md 2025-06-08 11:42:28 +08:00
xixu-me 8abad3616a Update publish_docs.yml 2025-06-08 11:35:26 +08:00
xixu-me ec8705a7cf Update README.pt.md 2025-06-08 11:31:48 +08:00
xixu-me 89efc8e3e8 Update README.ko.md 2025-06-08 11:31:34 +08:00
xixu-me 64f2dd6757 Update README.fr.md 2025-06-08 11:31:18 +08:00
xixu-me 7b306554f7 Update README.de.md 2025-06-08 11:31:01 +08:00
xixu-me c78d74768b Update README.ru.md 2025-06-08 11:30:43 +08:00
xixu-me 37af38e8bd Update README.ar.md 2025-06-08 11:30:16 +08:00
xixu-me 6476e2445a Update README.ja.md 2025-06-08 11:29:59 +08:00
xixu-me d9ca6d0d4e Update README.es.md 2025-06-08 11:29:38 +08:00
xixu-me 641256a133 Update README.zh.md 2025-06-08 11:29:19 +08:00
xixu-me 0b37b707b2 Update README.md 2025-06-08 11:27:22 +08:00
xixu-me 069ac67eea Update examples.md 2025-06-07 00:30:24 +08:00
xixu-me 7e3bb1cf6a Update quickstart.md 2025-06-07 00:12:44 +08:00
xixu-me 57c5fd0e6c Update development.md 2025-06-07 00:00:49 +08:00
xixu-me 7dc51f18c1 Add docs/_static/tzst-square-logo.png 2025-06-06 22:55:37 +08:00
xixu-me 2e40a77f29 Delete docs/_static/tzst-square-logo.png 2025-06-06 22:53:58 +08:00
xixu-me e60f252abc Update development.md 2025-06-06 22:41:45 +08:00
xixu-me 56b39fea44 Update index.md 2025-06-06 22:31:44 +08:00
xixu-me ce061aa396 Update index.md 2025-06-06 22:19:41 +08:00
xixu-me 8de8ec0356 Add social media metadata to documentation
Updated multiple documentation files to include Open Graph and Twitter metadata for better social media sharing. Added a new logo image to the static assets folder and referenced it in the metadata.
2025-06-06 22:15:54 +08:00
xixu-me 6c48d80d9a Update badges in documentation
Added new badges for PyPI downloads and GitHub stars to the documentation. Updated the GitHub license badge link to point to the correct URL.
2025-06-06 22:02:50 +08:00
xixu-me 44843e0035 Update documentation and add new guides
This commit updates multiple documentation files to improve clarity, remove emojis, and add new sections. Key changes include the addition of 'development.md' and 'performance.md', updates to the quickstart guide, and enhancements to the API and CLI documentation. These changes aim to provide better guidance for users and contributors.
2025-06-06 22:00:11 +08:00
xixu-me 42180498e0 Fix broken link in documentation index
Replaced `{doc}` with `{ref}` for the 'genindex' link to ensure proper rendering and functionality in the documentation.
2025-06-06 20:34:57 +08:00
xixu-me 9b9ad446c1 Update docs and remove CLI interactive option
Updated the Sphinx configuration to set 'includehidden' to False and revised the documentation index to streamline content. Removed the '--interactive' option from CLI commands 'extract' and 'extract_flat' as it is no longer supported.
2025-06-06 20:27:47 +08:00
xixu-me af7bef5371 Update documentation index with hidden toctree
Added a hidden toctree section in docs/index.md to include 404 and README entries. This improves the structure and navigation of the documentation.
2025-06-06 20:05:49 +08:00
xixu-me 27e1809bf9 Update documentation URLs to new domain
Replaced all occurrences of the old domain 'xixu-me.github.io/tzst' with the new domain 'tzst.xi-xu.me' in layout.html and conf.py for improved SEO and consistency.
2025-06-06 19:59:46 +08:00
xixu-me 4d3d34051f Update Sphinx config and documentation index
Added 'sphinx.ext.coverage' to the Sphinx extensions in conf.py to enable coverage reporting. Removed 'README' and '404' entries from the documentation index in index.md for cleanup.
2025-06-06 19:50:17 +08:00
xixu-me 526ddb76ea Update logo image in documentation
Replaced the existing logo image in the documentation static files with an updated version.
2025-06-06 19:35:32 +08:00
xixu-me 199fe41293 Update documentation references and add anchors
Updated internal references in the documentation to use simplified anchor names. Added missing anchors for sections in 'examples.md' and 'quickstart.md'. Minor formatting adjustments were also made for consistency.
2025-06-06 19:07:31 +08:00
xixu-me 6a9a783ad3 Update documentation workflow and 404 page
Added a new 404.md file for handling 'Page Not Found' errors in the documentation. Updated the publish_docs.yml workflow to include a 'cname' parameter. Removed the CNAME file from the docs directory as it is now managed in the workflow.
2025-06-06 18:55:00 +08:00
xixu-me 571809c36f Add CNAME file for custom domain
Added a CNAME file to configure the custom domain tzst.xi-xu.me for the documentation site.
2025-06-06 18:40:40 +08:00
xixu-me 8b39da60da Update documentation structure and metadata
Added favicon and logo to the static assets. Updated layout.html for improved SEO and performance. Migrated metadata in documentation files to use 'myst' format for consistency. Adjusted configuration in conf.py and fixed formatting issues.
2025-06-06 18:34:17 +08:00
xixu-me b5f7fa8dca Enhance documentation with SEO metadata and layout
Added a new custom layout template for SEO and social media meta tags. Updated multiple documentation files with metadata for improved search engine optimization and social sharing. Enhanced Sphinx configuration with additional HTML options and meta tags.
2025-06-06 17:50:43 +08:00
xixu-me abbeab83f1 Update documentation with indexing and references
Added the ':no-index:' directive to CLI, core, and exceptions API documentation to exclude them from indexing. Updated examples documentation to use Sphinx references for better navigation and added section anchors for improved structure.
2025-06-06 17:39:15 +08:00
xixu-me ef5d06cb0e Enhance documentation for tzst library
- Updated the introduction to provide a clearer overview of tzst's capabilities and features.
- Expanded the Key Features section with detailed descriptions and icons for better readability.
- Improved the Quick Example section to include both command line and Python API usage.
- Added installation options with detailed steps for PyPI, standalone binaries, and source installation.
- Enhanced the Quick Start Guide with structured installation methods and basic usage examples.
- Introduced advanced features like security filters, conflict resolution, and performance optimization.
- Provided comprehensive error handling and common patterns for backup scripts and archive verification.
- Updated the requirements and reference links for better navigation.
2025-06-06 17:31:08 +08:00
xixu-me d0e09347eb Update documentation URL in CLI script
Replaced the outdated GitHub README URL with the new documentation URL (https://tzst.xi-xu.me) in the CLI script for better reference.
2025-06-06 16:23:25 +08:00
xixu-me 76e56c984f Update Sphinx build ignore rules
Removed '_static/' from .gitignore to allow tracking of the '_static' directory. Added a .gitkeep file in '_static' to ensure the directory is included in the repository.
2025-06-06 16:05:45 +08:00
xixu-me d3e5b4e064 Remove duplicate CLI function documentation
Eliminated redundant sections for `format_size` and `validate_compression_level` in the CLI documentation to improve clarity and avoid repetition.
2025-06-06 16:04:34 +08:00
xixu-me 23dcdab305 Update documentation structure and configuration
Revised Sphinx directives in CLI and exceptions API docs to use ':no-index' instead of ':members', ':undoc-members', and ':show-inheritance'. Removed 'display_version' from Sphinx configuration. Added 'README' to the index page for better navigation.
2025-06-06 15:47:38 +08:00
xixu-me 2777fea4fc Add linkify-it-py to documentation requirements
Added the linkify-it-py package (version 2.0.0 or higher) to the docs/requirements.txt file to support automatic linkification in the documentation.
2025-06-06 15:26:27 +08:00
xixu-me 2ca81e0918 Update docs workflow and improve documentation scripts
Modified the GitHub Actions workflow to trigger on push and pull requests to the main branch. Updated `.gitignore` to include additional Sphinx build outputs. Refactored `build_docs.py` for better formatting and error handling. Removed 'changelog' from the documentation index.
2025-06-06 15:15:52 +08:00
xixu-me eb3f66f85e Update README.zh.md 2025-06-06 00:23:20 +08:00
xixu-me 028c7e3650 Update README.ru.md 2025-06-06 00:23:06 +08:00
xixu-me 9e5a677155 Update README.pt.md 2025-06-06 00:22:55 +08:00
xixu-me 19356b819f Update README.md 2025-06-06 00:22:43 +08:00
xixu-me bac2f44072 Update README.ko.md 2025-06-06 00:22:30 +08:00
xixu-me ffb6981965 Update README.ja.md 2025-06-06 00:22:19 +08:00
xixu-me 15d0828fae Update README.fr.md 2025-06-06 00:22:08 +08:00
xixu-me 710c22f605 Update README.es.md 2025-06-06 00:21:55 +08:00
xixu-me 7c268be215 Update README.de.md 2025-06-06 00:21:32 +08:00
xixu-me b92b5c8652 Update README.ar.md 2025-06-06 00:21:18 +08:00
xixu-me af0159a9fe Update language links in README files
Replaced '🇬🇧 English' with 'us English' in all README files and standardized placeholder text for version numbers in the Arabic README. These changes improve consistency across documentation.
2025-06-05 15:29:12 +08:00
xixu-me f6d1ff631d Add Portuguese, Russian, etc. translations for README files 2025-06-05 12:19:39 +08:00
xixu-me b5a654c187 Update README.zh.md 2025-06-05 10:34:33 +08:00
xixu-me 9b30a8657c Update README.ko.md 2025-06-05 02:05:03 +08:00
xixu-me 7f2e53c9d0 Create README.ko.md 2025-06-05 02:03:47 +08:00
xixu-me 58400c8f42 Update README.ja.md 2025-06-05 01:51:28 +08:00
xixu-me cd56764f84 Update README.ja.md 2025-06-05 00:55:16 +08:00
xixu-me 8b3e4d2429 Create README.ja.md 2025-06-05 00:54:04 +08:00
xixu-me 22a879a002 Update README.es.md 2025-06-05 00:51:21 +08:00
xixu-me ac14a4bca7 Create README.es.md 2025-06-05 00:42:39 +08:00
xixu-me f89a10ec50 Update README.zh.md 2025-06-05 00:31:31 +08:00
xixu-me 518e90f0ee Create README.zh.md 2025-06-05 00:03:31 +08:00
xixu-me 194037b87c Update README.md 2025-06-05 00:01:14 +08:00
xixu-me 47d6504f39 Update README.md 2025-06-04 22:33:38 +08:00
xixu-me a15a497c52 Update PyPI badge and README formatting
Modified the PyPI version badge URL in the CI workflow and README to simplify the URL. Enhanced README with emojis for better visual appeal and added detailed installation instructions for standalone binaries.
2025-06-04 22:16:41 +08:00
xixu-me f866f30b7e Bump version to 1.2.3
CI/CD / test (macos-latest, 3.12) (push) Waiting to run
CI/CD / test (macos-latest, 3.13) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.12) (push) Waiting to run
CI/CD / test (ubuntu-latest, 3.13) (push) Waiting to run
CI/CD / test (windows-latest, 3.12) (push) Waiting to run
CI/CD / test (windows-latest, 3.13) (push) Waiting to run
Updated the `__version__` in `__init__.py` from 1.2.2 to 1.2.3 to reflect the latest changes or release.
2025-06-04 21:39:18 +08:00
xixu-me b2eebdedb2 Simplify GitHub release notes in CI workflow
Updated the GitHub release creation step to use a simplified title format and removed detailed release notes. This change streamlines the release process by omitting installation instructions and commit history links.
2025-06-04 21:39:02 +08:00
xixu-me 1414138341 Update badges [skip ci] 2025-06-04 13:29:39 +00:00
xixu-me eb4d29e503 Bump version to 1.2.2
Updated the `__version__` in `__init__.py` from 1.2.1 to 1.2.2 to reflect the latest changes or release.
2025-06-04 21:27:09 +08:00
xixu-me 76963aaf9f Fix indentation in CI workflow script
Corrected the indentation in the Python script within the CI workflow to ensure proper execution of the version assignment and file writing logic.
2025-06-04 21:26:38 +08:00
xixu-me 8035a77c10 Bump version to 1.2.1
Updated the `__version__` in `__init__.py` from 1.2.0 to 1.2.1 to reflect the latest changes or release.
2025-06-04 21:18:57 +08:00
xixu-me d95f173308 Update PyInstaller command in CI workflow
Modified the PyInstaller command in the CI configuration to specify 'src/main.py' as the entry point for building binaries across macOS, Windows, and Linux ARM64 platforms.
2025-06-04 21:12:43 +08:00
xixu-me 637f1d2562 Update CI workflow and add main entry point
Modified the CI workflow to correctly specify the path to the main script for PyInstaller builds. Added a new standalone entry point in src/main.py to serve as the executable script for tzst.
2025-06-04 21:08:33 +08:00
xixu-me d2f11636be Revert "Add standalone entry point for tzst"
This reverts commit 69918b4be5.
2025-06-04 21:05:15 +08:00
xixu-me 69918b4be5 Add standalone entry point for tzst
Created a new main.py file as the standalone entry point for tzst, intended for PyInstaller builds. This script sets up the Python path and invokes the CLI main function.
2025-06-04 21:04:24 +08:00
xixu-me 7d688d420b Bump version to 1.2.0
Updated the `__version__` in `__init__.py` to reflect the new release version 1.2.0.
2025-06-04 20:54:49 +08:00
xixu-me 56a10bc038 Update CI workflow for macOS and binary verification
This commit updates the CI workflow to use macOS-14 for ARM-based runners, adds binary verification steps for Linux, macOS, and Windows, and improves release asset upload handling with success checks. Additionally, it refines the README badge update process for PyPI version.
2025-06-04 20:54:31 +08:00
xixu-me 1dd3cde654 Enhance CI/CD workflow for versioned tags
Updated the CI/CD pipeline to support versioned tags for releases. Added new jobs for building binaries across multiple platforms and architectures, creating release assets, and uploading them to GitHub. Improved conditional logic for triggering workflows based on tags and streamlined badge updates.
2025-06-04 20:42:48 +08:00
xixu-me a1de2e1855 Remove unused report configuration in Codecov
The 'report.exclude_labels' section was removed from the Codecov configuration as it is no longer needed. This simplifies the configuration file.
2025-06-04 20:06:18 +08:00
xixu-me 6397968a4d Improve conflict resolution in archive extraction
Updated the `extract_archive` function to handle string-based conflict resolution by converting it to an enum. Added fallback to `ConflictResolution.REPLACE` for invalid values, ensuring safer and more predictable behavior.
2025-06-04 19:27:41 +08:00
xixu-me cb5c198d16 Refactor tests and enhance coverage for conflict resolution and edge cases
- Removed outdated tests for missing lines in core.py.
- Added comprehensive tests for conflict resolution functionality, including various resolution strategies and edge cases.
- Improved error handling tests for archive creation and extraction processes.
- Enhanced unit tests for basic functionality and convenience functions with pytest markers.
- Introduced tests for unique filename generation and conflict handling scenarios.
- Added edge case tests for archive extraction and error conditions to improve overall test coverage.
2025-06-04 18:55:53 +08:00
xixu-me afe5d738a6 Merge pull request #5 from xixu-me/alert-autofix-5
Potential fix for code scanning alert no. 5: Workflow does not contain permissions
2025-06-02 22:02:55 +08:00
xixu-meandCopilot Autofix powered by AI 06eac113fa Potential fix for code scanning alert no. 5: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-06-02 21:46:34 +08:00
57 changed files with 11553 additions and 2287 deletions

No files matched your search

-5
View File
@@ -11,11 +11,6 @@ coverage:
threshold: 1%
informational: true
# Report coverage for all files, even if not touched in PR
report:
exclude_labels:
- "skip-coverage"
comment:
layout: "reach,diff,flags,tree"
behavior: default
+1
View File
@@ -1 +1,2 @@
custom: https://xi-xu.me/#sponsorships
buy_me_a_coffee: xixu
+235 -30
View File
@@ -1,22 +1,30 @@
name: CI/CD
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
on:
push:
branches: [main, develop]
paths-ignore:
- "*.md"
- "LICENSE"
- "docs/**"
- ".gitignore"
tags:
- "v*.*.*"
paths:
- "src/**"
- "tests/**"
- ".github/workflows/ci.yml"
- "pyproject.toml"
- "src/main.py"
pull_request:
branches: [main]
paths-ignore:
- "*.md"
- "LICENSE"
- "docs/**"
- ".gitignore"
paths:
- "src/**"
- "tests/**"
- ".github/workflows/ci.yml"
- "pyproject.toml"
- "src/main.py"
release:
types: [published]
workflow_dispatch:
jobs:
test:
@@ -26,13 +34,13 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.12", "3.13"]
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
@@ -54,8 +62,8 @@ jobs:
pytest --cov=tzst --cov-branch --cov-report=xml
- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
uses: codecov/codecov-action@v5
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' && github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: xixu-me/tzst
@@ -66,10 +74,10 @@ jobs:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.12"
@@ -85,7 +93,7 @@ jobs:
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
@@ -93,7 +101,7 @@ jobs:
publish:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'release' && github.event.action == 'published'
if: startsWith(github.ref, 'refs/tags/v')
environment:
name: pypi
url: https://pypi.org/p/tzst
@@ -102,7 +110,7 @@ jobs:
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: dist
path: dist/
@@ -110,24 +118,214 @@ jobs:
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
build-binaries:
needs: test
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
strategy:
matrix:
include:
# Linux architectures
- os: ubuntu-latest
os_name: linux
arch: amd64
python-version: "3.12"
- os: ubuntu-latest
os_name: linux
arch: arm64
python-version: "3.12"
cross_compile: true
# Windows architectures
- os: windows-latest
os_name: windows
arch: amd64
python-version: "3.12"
- os: windows-latest
os_name: windows
arch: arm64
python-version: "3.12"
cross_compile: true
# macOS architectures
- os: macos-15-intel # Intel-based runner
os_name: darwin
arch: amd64
python-version: "3.12"
- os: macos-15 # ARM-based runner (M1/M2)
os_name: darwin
arch: arm64
python-version: "3.12"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Extract version from tag
id: version
shell: bash
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pyinstaller
- name: Build binary
run: |
python -m PyInstaller --onefile --name tzst --console --icon docs/_static/favicon.ico src/main.py
- name: Verify binary (Linux/macOS)
if: matrix.os != 'windows-latest'
run: |
if [ ! -f "dist/tzst" ]; then
echo "Binary build failed!"
exit 1
fi
- name: Verify binary (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
if (-not (Test-Path "dist/tzst.exe")) {
Write-Host "Binary build failed!"
exit 1
}
- name: Build binary for ARM64 on macOS
if: matrix.os_name == 'darwin' && matrix.arch == 'arm64'
run: |
python -m PyInstaller --onefile --name tzst --console --target-arch arm64 --icon docs/_static/favicon.ico src/main.py
- name: Build binary for ARM64 on Windows
if: matrix.os == 'windows-latest' && matrix.arch == 'arm64'
run: |
python -m PyInstaller --onefile --name tzst --console --target-arch arm64 --icon docs/_static/favicon.ico src/main.py
- name: Setup cross-compilation for Linux ARM64
if: matrix.os == 'ubuntu-latest' && matrix.arch == 'arm64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
- name: Build binary for ARM64 on Linux
if: matrix.os == 'ubuntu-latest' && matrix.arch == 'arm64'
env:
CC: aarch64-linux-gnu-gcc
run: |
python -m PyInstaller --onefile --name tzst --console --target-arch aarch64 --icon docs/_static/favicon.ico src/main.py
- name: Prepare archive contents (Linux/macOS)
if: matrix.os != 'windows-latest'
run: |
mkdir -p archive
cp dist/tzst archive/
cp README.md archive/
cp LICENSE archive/
- name: Prepare archive contents (Windows)
if: matrix.os == 'windows-latest'
run: |
New-Item -ItemType Directory -Path archive -Force
Copy-Item dist/tzst.exe archive/
Copy-Item README.md archive/
Copy-Item LICENSE archive/
- name: Create zip archive (Linux/macOS)
if: matrix.os != 'windows-latest'
run: |
cd archive
zip -r ../tzst-${{ steps.version.outputs.version }}-${{ matrix.os_name }}-${{ matrix.arch }}.zip .
- name: Create zip archive (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
cd archive
Compress-Archive -Path * -DestinationPath ../tzst-${{ steps.version.outputs.version }}-${{ matrix.os_name }}-${{ matrix.arch }}.zip
- name: Upload binary artifacts
uses: actions/upload-artifact@v7
with:
name: binary-${{ matrix.os_name }}-${{ matrix.arch }}
path: tzst-${{ steps.version.outputs.version }}-${{ matrix.os_name }}-${{ matrix.arch }}.zip
create-release:
needs: [publish, build-binaries]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- name: Extract version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download all binary artifacts
uses: actions/download-artifact@v8
with:
pattern: binary-*
merge-multiple: true
- name: Create GitHub Release
run: |
gh release create ${{ github.ref_name }} \
--title "tzst ${{ steps.version.outputs.version }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Release Assets
run: |
echo "Available files:"
ls -la tzst-${{ steps.version.outputs.version }}-*.zip 2>/dev/null || echo "No zip files found"
success=0
for file in tzst-${{ steps.version.outputs.version }}-*.zip; do
if [ -f "$file" ]; then
echo "Uploading $file"
if gh release upload ${{ github.ref_name }} "$file" --clobber; then
echo "Successfully uploaded $file"
success=$((success + 1))
else
echo "Failed to upload $file"
fi
fi
done
if [ $success -eq 0 ]; then
echo "Warning: No files were successfully uploaded"
else
echo "Successfully uploaded $success file(s)"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update_badges:
needs: [build, publish]
if: always() && needs.build.result == 'success'
if: always() && needs.build.result == 'success' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
- name: Wait if this is after a publish
if: github.event_name == 'release' && github.event.action == 'published'
run: sleep 30
ref: main
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.12"
@@ -151,12 +349,19 @@ jobs:
- name: Update README badge
run: |
echo "Updating PyPI badge with version: ${{ steps.pypi_version.outputs.version }}"
# Update PyPI version badge in README.md
if [ -f "README.md" ]; then
# Replace PyPI version badge
sed -i 's|https://img.shields.io/pypi/v/tzst[^)]*|https://img.shields.io/pypi/v/tzst|g' README.md || true
# Update version in badge alt text if exists
sed -i 's|PyPI - Version[^]]*|PyPI - Version|g' README.md || true
fi
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Update badges [skip ci]"
file_pattern: README.md
branch: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
branch: main
+205
View File
@@ -0,0 +1,205 @@
name: Dependabot Auto Merge
on:
check_suite:
types: [completed]
workflow_run:
workflows:
- "CI/CD"
- "CodeQL"
- "Dependabot Updates"
- "Dependency Graph"
- "pages-build-deployment"
- "Publish Documentation"
types:
- completed
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
checks: read
statuses: read
jobs:
merge:
name: Auto-merge Dependabot PRs
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Merge Dependabot PRs when checks pass
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const successfulCheckConclusions = new Set(["success", "skipped", "neutral"]);
const successfulStatusStates = new Set(["success"]);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const latestBy = (items, keyOf, timeOf) => {
const latest = new Map();
for (const item of items) {
const key = keyOf(item);
const itemTime = new Date(timeOf(item) || 0).getTime();
const existing = latest.get(key);
const existingTime = existing ? new Date(timeOf(existing) || 0).getTime() : -1;
if (!existing || itemTime >= existingTime) {
latest.set(key, item);
}
}
return [...latest.values()];
};
const findCandidatePulls = async () => {
const pulls = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
return pulls.filter((pr) => pr.user?.login === "dependabot[bot]");
};
const getMergeablePullRequest = async (pull_number) => {
for (let attempt = 1; attempt <= 6; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
if (pr.mergeable !== null) {
return pr;
}
core.info("PR #" + pull_number + " mergeability is still being computed; retry " + attempt + "/6.");
await wait(5000);
}
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
return pr;
};
const getSignalState = async (sha) => {
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const latestChecks = latestBy(
checkRuns.filter((run) => run.name !== "Dependabot Auto Merge"),
(run) => (run.app?.slug || "unknown") + ":" + run.name,
(run) => run.completed_at || run.started_at || run.created_at,
);
const latestStatuses = latestBy(statuses, (status) => status.context, (status) => status.updated_at || status.created_at);
const pendingChecks = latestChecks.filter((run) => run.status !== "completed");
const failedChecks = latestChecks.filter(
(run) => run.status === "completed" && !successfulCheckConclusions.has(String(run.conclusion || "").toLowerCase()),
);
const failedStatuses = latestStatuses.filter((status) => !successfulStatusStates.has(String(status.state || "").toLowerCase()));
return {
totalSignals: latestChecks.length + latestStatuses.length,
pendingChecks,
failedChecks,
failedStatuses,
};
};
const { data: repository } = await github.rest.repos.get({ owner, repo });
const mergeMethods = [];
if (repository.allow_merge_commit) mergeMethods.push("merge");
if (repository.allow_squash_merge) mergeMethods.push("squash");
if (repository.allow_rebase_merge) mergeMethods.push("rebase");
if (mergeMethods.length === 0) {
core.info("This repository has no enabled pull request merge methods.");
return;
}
const pulls = await findCandidatePulls();
if (pulls.length === 0) {
core.info("No open Dependabot PRs to evaluate.");
return;
}
for (const candidate of pulls) {
const pull_number = candidate.number;
const pr = await getMergeablePullRequest(pull_number);
if (pr.user?.login !== "dependabot[bot]") {
core.info("PR #" + pull_number + " is no longer a Dependabot PR.");
continue;
}
if (pr.state !== "open" || pr.draft) {
core.info("PR #" + pull_number + " is not an open, ready PR.");
continue;
}
if (pr.mergeable !== true) {
core.info("PR #" + pull_number + " is not currently mergeable.");
continue;
}
const signalState = await getSignalState(pr.head.sha);
if (signalState.totalSignals === 0) {
core.info("PR #" + pull_number + " has no checks or statuses yet; skipping.");
continue;
}
if (signalState.pendingChecks.length > 0) {
core.info("PR #" + pull_number + " still has pending checks: " + signalState.pendingChecks.map((run) => run.name).join(", ") + ".");
continue;
}
if (signalState.failedChecks.length > 0 || signalState.failedStatuses.length > 0) {
const failedChecks = signalState.failedChecks.map((run) => run.name + "=" + run.conclusion).join(", ");
const failedStatuses = signalState.failedStatuses.map((status) => status.context + "=" + status.state).join(", ");
core.info("PR #" + pull_number + " is not green. Checks: " + (failedChecks || "none") + ". Statuses: " + (failedStatuses || "none") + ".");
continue;
}
let merged = false;
let lastError = null;
for (const merge_method of mergeMethods) {
try {
await github.rest.pulls.merge({ owner, repo, pull_number, merge_method });
core.info("Merged Dependabot PR #" + pull_number + " with " + merge_method + ".");
merged = true;
break;
} catch (error) {
lastError = error;
if (error.status === 403 || error.status === 405 || error.status === 409) {
core.info("Cannot merge PR #" + pull_number + " with " + merge_method + ": " + error.message);
continue;
}
throw error;
}
}
if (!merged) {
core.info("PR #" + pull_number + " could not be merged: " + (lastError?.message || "unknown error") + ".");
continue;
}
if (pr.head.repo?.full_name === owner + "/" + repo) {
try {
await github.rest.git.deleteRef({ owner, repo, ref: "heads/" + pr.head.ref });
core.info("Deleted branch " + pr.head.ref + ".");
} catch (error) {
core.info("Could not delete branch " + pr.head.ref + ": " + error.message);
}
}
}
@@ -4,9 +4,17 @@ on:
push:
branches:
- main
paths-ignore:
- "*.md"
- "LICENSE"
- ".gitignore"
pull_request:
branches:
- main
paths-ignore:
- "*.md"
- "LICENSE"
- ".gitignore"
jobs:
build-and-deploy-docs:
@@ -18,17 +26,17 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Cache pip dependencies
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-docs-${{ hashFiles('docs/requirements.txt') }}
@@ -48,7 +56,7 @@ jobs:
python -m sphinx -b html . _build -W --keep-going
- name: Upload documentation artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: documentation
path: docs/_build/
@@ -64,15 +72,18 @@ jobs:
user_name: "github-actions[bot]"
user_email: "github-actions[bot]@users.noreply.github.com"
commit_message: "Deploy documentation from ${{ github.sha }}"
cname: tzst.xi-xu.me
docs-quality-check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: "3.12"
+75
View File
@@ -0,0 +1,75 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
tzst is a next-generation Python library for modern archive management using Zstandard compression. It provides both a Python API and a command-line interface for creating, extracting, listing, and testing .tzst/.tar.zst archives. The library focuses on performance, security, and reliability with features like atomic operations, streaming mode for large archives, and security filtering for extraction.
## Architecture
The codebase is structured as follows:
- `src/tzst/core.py` - Core implementation containing the `TzstArchive` class and convenience functions
- `src/tzst/cli.py` - Command-line interface built with argparse
- `src/tzst/exceptions.py` - Custom exception classes
- `src/tzst/__init__.py` - Public API exports
The library wraps Python's tarfile module with Zstandard compression/decompression streams. It supports both buffered and streaming modes for memory efficiency with large archives.
## Commands
### Development Environment
```bash
# Development installation with dev dependencies
pip install -e .[dev]
# Run tests
pytest
pytest --cov=tzst --cov-report=html # with coverage
```
### Code Quality
```bash
# Check code quality
ruff check src tests
# Format code
ruff format src tests
```
### Building and Distribution
```bash
# Build package
python -m build
# Upload to PyPI (requires credentials)
python -m twine upload dist/*
```
## Key Features to Consider
1. **Streaming Mode**: For archives >100MB, always recommend using streaming mode to reduce memory usage
2. **Security Filters**: Use 'data' filter by default for extraction to prevent path traversal attacks
3. **Atomic Operations**: Archive creation uses temporary files by default for safety
4. **Conflict Resolution**: During extraction, implement proper file conflict resolution strategies
5. **Compression Levels**: Default to level 3 unless the user has specific performance/size requirements
## Testing Patterns
The project uses pytest with comprehensive test coverage including:
- Unit tests in `tests/unit/`
- Integration tests in `tests/integration/`
- CLI-specific tests in `tests/cli/`
- Edge case handling in `tests/test_core_edge_cases.py`
## Common Tasks
- To add functionality: work in `src/tzst/core.py` following existing patterns
- To modify CLI behavior: update `src/tzst/cli.py` and adjust argument parsing as needed
- To add new command: extend `create_parser()` in cli.py and add appropriate handler function
- For error handling: use appropriate TzstError subclasses from exceptions.py
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
i@xi-xu.me.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+3 -3
View File
@@ -23,7 +23,7 @@ This project follows the principles of respectful collaboration. Please be kind,
### Prerequisites
- Python 3.12 or higher
- Python 3.12 or higher (tested on 3.12-3.14)
- Git
- Basic knowledge of tar archives and compression
@@ -198,7 +198,7 @@ This project uses [Ruff](https://docs.astral.sh/ruff/) for linting and formattin
Settings are defined in `pyproject.toml`:
- Line length: 88 characters
- Target Python version: 3.12+
- Target Python version: 3.12+ (tested on 3.12-3.14)
- Import sorting with isort
- Quote style: double quotes
@@ -356,7 +356,7 @@ When suggesting features:
### Compatibility
- Support Python 3.12+
- Support Python 3.12+ with CI coverage for 3.12-3.14
- Test on multiple platforms (Windows, macOS, Linux)
- Consider different filesystem behaviors
- Maintain backwards compatibility when possible
+530
View File
@@ -0,0 +1,530 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | **🇦🇪 العربية** | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
<div dir="rtl" lang="ar">
**tzst** هي مكتبة وأداة سطر أوامر لـ Python 3.12+ لإنشاء أرشيفات `.tzst` و`.tar.zst` واستخراجها وعرض محتوياتها والتحقق منها. تجمع بين توافق tar وضغط Zstandard ووضع التدفق والكتابة الذرية والاستخراج الآمن افتراضياً ضمن واجهة موجزة جاهزة للاستخدام الإنتاجي.
> [!NOTE]
> مقال تقني مفصل: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## الميزات
- **ضغط عالي**: ضغط Zstandard لنسب ضغط وسرعة ممتازة
- **توافق Tar**: ينشئ أرشيف tar قياسي مضغوط بـ Zstandard
- **واجهة سطر الأوامر**: واجهة CLI بديهية مع دعم التدفق وخيارات شاملة
- **Python API**: واجهة برمجة تطبيقات نظيفة وpythonic للاستخدام البرمجي
- **متعدد المنصات**: يعمل على Windows وmacOS وLinux
- **امتدادات متعددة**: يدعم كلاً من امتدادات `.tzst` و `.tar.zst`
- **فعال في الذاكرة**: وضع التدفق للتعامل مع الأرشيف الكبير باستخدام أقل للذاكرة
- **عمليات ذرية**: عمليات ملف آمنة مع تنظيف تلقائي عند المقاطعة
- **آمن افتراضياً**: يستخدم مرشح 'data' للحد الأقصى من الأمان أثناء الاستخراج
- **معالجة أخطاء محسنة**: رسائل خطأ واضحة مع بدائل مفيدة
## التثبيت
### من إصدارات GitHub
تحميل ملفات تنفيذية مستقلة لا تتطلب تثبيت Python:
#### المنصات المدعومة
| المنصة | المعمارية | الملف |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{version}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{version}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{version}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{version}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{version}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{version}-darwin-arm64.zip` |
#### خطوات التثبيت
1. **تحميل** الأرشيف المناسب لمنصتك من [صفحة الإصدارات الأحدث](https://github.com/xixu-me/tzst/releases/latest)
2. **استخراج** الأرشيف للحصول على الملف التنفيذي `tzst` (أو `tzst.exe` على Windows)
3. **نقل** الملف التنفيذي إلى مجلد في PATH الخاص بك:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: أضف المجلد الذي يحتوي على `tzst.exe` إلى متغير البيئة PATH
4. **تحقق** من التثبيت: `tzst --help`
#### فوائد التثبيت الثنائي
- **لا يتطلب Python** - ملف تنفيذي مستقل
- **بدء تشغيل أسرع** - بدون إضافة مفسر Python
- **نشر سهل** - توزيع ملف واحد
- **سلوك متسق** - تبعيات مجمعة
### من PyPI
استخدام pip:
```bash
pip install tzst
```
أو استخدام uv (موصى به):
```bash
uv tool install tzst
```
### من المصدر
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### تثبيت التطوير
يستخدم هذا المشروع معايير تعبئة Python الحديثة:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## البداية السريعة
### استخدام سطر الأوامر
```bash
# إنشاء أرشيف
tzst a archive.tzst file1.txt file2.txt directory/
# استخراج أرشيف
tzst x archive.tzst
# قائمة محتويات الأرشيف
tzst l archive.tzst
# اختبار سلامة الأرشيف
tzst t archive.tzst
```
### استخدام Python API
```python
from tzst import create_archive, extract_archive, list_archive
# إنشاء أرشيف
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# استخراج أرشيف
extract_archive("archive.tzst", "output_directory/")
# قائمة محتويات الأرشيف
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## واجهة سطر الأوامر
### عمليات الأرشيف
#### إنشاء أرشيف
```bash
# الاستخدام الأساسي
tzst a archive.tzst file1.txt file2.txt
# مع مستوى الضغط (1-22، افتراضي: 3)
tzst a archive.tzst files/ -l 15
# أوامر بديلة
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### استخراج أرشيف
```bash
# استخراج مع هيكل المجلد الكامل
tzst x archive.tzst
# استخراج إلى مجلد محدد
tzst x archive.tzst -o output/
# استخراج ملفات محددة
tzst x archive.tzst file1.txt dir/file2.txt
# استخراج بدون هيكل المجلد (مسطح)
tzst e archive.tzst -o output/
# استخدام وضع التدفق للأرشيف الكبير
tzst x archive.tzst --streaming -o output/
```
#### قائمة المحتويات
```bash
# قائمة بسيطة
tzst l archive.tzst
# قائمة مفصلة مع التفاصيل
tzst l archive.tzst -v
# استخدام وضع التدفق للأرشيف الكبير
tzst l archive.tzst --streaming -v
```
#### اختبار السلامة
```bash
# اختبار سلامة الأرشيف
tzst t archive.tzst
# اختبار مع وضع التدفق
tzst t archive.tzst --streaming
```
### مرجع الأوامر
| الأمر | البدائل | الوصف | دعم التدفق |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | إنشاء أو إضافة إلى أرشيف | N/A |
| `x` | `extract` | استخراج مع المسارات الكاملة | ✓ `--streaming` |
| `e` | `extract-flat` | استخراج بدون هيكل المجلد | ✓ `--streaming` |
| `l` | `list` | قائمة محتويات الأرشيف | ✓ `--streaming` |
| `t` | `test` | اختبار سلامة الأرشيف | ✓ `--streaming` |
### خيارات CLI
- `-v, --verbose`: تمكين الإخراج المفصل
- `-o, --output DIR`: تحديد مجلد الإخراج (أوامر الاستخراج)
- `-l, --level LEVEL`: تحديد مستوى الضغط 1-22 (أمر الإنشاء)
- `--streaming`: تمكين وضع التدفق للمعالجة الفعالة في الذاكرة
- `--filter FILTER`: مرشح الأمان للاستخراج (data/tar/fully_trusted)
- `--no-atomic`: تعطيل العمليات الذرية للملفات (غير مستحسن)
### مرشحات الأمان
```bash
# استخراج مع أقصى أمان (افتراضي)
tzst x archive.tzst --filter data
# استخراج مع توافق tar قياسي
tzst x archive.tzst --filter tar
# استخراج مع ثقة كاملة (خطر - فقط للأرشيف الموثوق)
tzst x archive.tzst --filter fully_trusted
```
**خيارات مرشح الأمان:**
- `data` (افتراضي): الأكثر أماناً. يحجب الملفات الخطيرة والمسارات المطلقة والمسارات خارج مجلد الاستخراج
- `tar`: توافق tar قياسي. يحجب المسارات المطلقة واجتياز المجلد
- `fully_trusted`: لا قيود أمان. استخدم فقط مع الأرشيف الموثوق تماماً
## Python API
### فئة TzstArchive
```python
from tzst import TzstArchive
# إنشاء أرشيف جديد
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# قراءة أرشيف موجود
with TzstArchive("archive.tzst", "r") as archive:
# قائمة المحتويات
contents = archive.list(verbose=True)
# استخراج مع مرشح الأمان
archive.extract("file.txt", "output/", filter="data")
# اختبار السلامة
is_valid = archive.test()
# للأرشيف الكبير، استخدم وضع التدفق
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**قيود مهمة:**
- **وضع الإلحاق غير مدعوم**: أنشئ أرشيف متعدد أو أعد إنشاء الأرشيف بالكامل بدلاً من ذلك
### دوال الراحة
#### create_archive()
```python
from tzst import create_archive
# إنشاء مع عمليات ذرية (افتراضي)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# استخراج مع الأمان (افتراضي: مرشح 'data')
extract_archive("backup.tzst", "restore/")
# استخراج ملفات محددة
extract_archive("backup.tzst", "restore/", members=["config.txt"])
# تسطيح هيكل المجلد
extract_archive("backup.tzst", "restore/", flatten=True)
# استخدام التدفق للأرشيف الكبير
extract_archive("large_backup.tzst", "restore/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# قائمة بسيطة
files = list_archive("backup.tzst")
# قائمة مفصلة
files = list_archive("backup.tzst", verbose=True)
# تدفق للأرشيف الكبير
files = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# اختبار سلامة أساسي
if test_archive("backup.tzst"):
print("الأرشيف صالح")
# اختبار مع التدفق
if test_archive("large_backup.tzst", streaming=True):
print("الأرشيف الكبير صالح")
```
## الميزات المتقدمة
### امتدادات الملفات
تتعامل المكتبة تلقائياً مع امتدادات الملفات مع التطبيع الذكي:
- `.tzst` - الامتداد الأساسي لأرشيف tar+zstandard
- `.tar.zst` - امتداد قياسي بديل
- الكشف التلقائي عند فتح الأرشيف الموجود
- إضافة الامتداد التلقائي عند إنشاء الأرشيف
```python
# هذه كلها تنشئ أرشيف صالح
create_archive("backup.tzst", files) # ينشئ backup.tzst
create_archive("backup.tar.zst", files) # ينشئ backup.tar.zst
create_archive("backup", files) # ينشئ backup.tzst
create_archive("backup.txt", files) # ينشئ backup.tzst (مُطبع)
```
### مستويات الضغط
تتراوح مستويات ضغط Zstandard من 1 (الأسرع) إلى 22 (أفضل ضغط):
- **المستوى 1-3**: ضغط سريع، ملفات أكبر
- **المستوى 3** (افتراضي): توازن جيد بين السرعة والضغط
- **المستوى 10-15**: ضغط أفضل، أبطأ
- **المستوى 20-22**: أقصى ضغط، أبطأ بكثير
### وضع التدفق
استخدم وضع التدفق للمعالجة الفعالة في الذاكرة للأرشيف الكبير:
**الفوائد:**
- انخفاض كبير في استخدام الذاكرة
- أداء أفضل للأرشيف الذي لا يناسب الذاكرة
- تنظيف تلقائي للموارد
**متى تستخدم:**
- أرشيف أكبر من 100 ميجابايت
- بيئات ذاكرة محدودة
- معالجة أرشيف بملفات كبيرة كثيرة
```python
# مثال: معالجة أرشيف نسخ احتياطي كبير
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# عمليات فعالة في الذاكرة
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### العمليات الذرية
جميع عمليات إنشاء الملفات تستخدم عمليات ملف ذرية افتراضياً:
- الأرشيف منشأ في ملفات مؤقتة أولاً، ثم نُقل ذرياً
- تنظيف تلقائي إذا تمت مقاطعة العملية
- لا خطر من أرشيف تالف أو غير مكتمل
- توافق متعدد المنصات
```python
# العمليات الذرية ممكنة افتراضياً
create_archive("important.tzst", files) # آمن من المقاطعة
# يمكن تعطيلها إذا لزم الأمر (غير مستحسن)
create_archive("test.tzst", files, use_temp_file=False)
```
### معالجة الأخطاء
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("فشل في إلغاء ضغط الأرشيف")
except TzstFileNotFoundError:
print("ملف الأرشيف غير موجود")
except KeyboardInterrupt:
print("العملية مقاطعة من قبل المستخدم")
# التنظيف يتم تلقائياً
```
## الأداء والمقارنة
### نصائح الأداء
1. **مستويات الضغط**: المستوى 3 هو الأمثل لمعظم حالات الاستخدام
2. **التدفق**: استخدم للأرشيف أكبر من 100 ميجابايت
3. **عمليات الدفعات**: أضف ملفات متعددة في جلسة واحدة
4. **أنواع الملفات**: الملفات المضغوطة مسبقاً لن تنضغط كثيراً أكثر
### مقابل أدوات أخرى
**مقابل tar + gzip:**
- نسب ضغط أفضل
- إلغاء ضغط أسرع
- خوارزمية حديثة
**مقابل tar + xz:**
- ضغط أسرع بشكل كبير
- نسب ضغط مماثلة
- توازن سرعة/ضغط أفضل
**مقابل zip:**
- ضغط أفضل
- يحافظ على أذونات Unix والبيانات الوصفية
- دعم تدفق أفضل
## المتطلبات
- Python 3.12 أو أعلى
- zstandard >= 0.19.0
## التطوير
### إعداد بيئة التطوير
يستخدم هذا المشروع معايير تعبئة Python الحديثة:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### تشغيل الاختبارات
```bash
# تشغيل الاختبارات مع التغطية
pytest --cov=tzst --cov-report=html
# أو استخدم الأمر الأبسط (إعدادات التغطية في pyproject.toml)
pytest
```
### جودة الكود
```bash
# فحص جودة الكود
ruff check src tests
# تنسيق الكود
ruff format src tests
```
## المساهمة
نرحب بالمساهمات! يرجى قراءة [دليل المساهمة](CONTRIBUTING.md) لـ:
- إعداد التطوير وهيكل المشروع
- إرشادات أسلوب الكود وأفضل الممارسات
- متطلبات الاختبار وكتابة الاختبارات
- عملية طلب السحب وسير عمل المراجعة
### البداية السريعة للمساهمين
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### أنواع المساهمات المرحب بها
- **إصلاح الأخطاء** - إصلاح مشاكل في الوظائف الموجودة
- **الميزات** - إضافة قدرات جديدة للمكتبة
- **التوثيق** - تحسين أو إضافة التوثيق
- **الاختبارات** - إضافة أو تحسين تغطية الاختبار
- **الأداء** - تحسين الكود الموجود
- **الأمان** - معالجة الثغرات الأمنية
## الشكر والتقدير
- [Meta Zstandard](https://github.com/facebook/zstd) لخوارزمية الضغط الممتازة
- [python-zstandard](https://github.com/indygreg/python-zstandard) لروابط Python
- مجتمع Python للإلهام والملاحظات
## الترخيص
حقوق النشر &copy; [شي شو](https://xi-xu.me). جميع الحقوق محفوظة.
مرخص تحت ترخيص [BSD 3-Clause](LICENSE).
</div>
+526
View File
@@ -0,0 +1,526 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | **🇩🇪 Deutsch** | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
**tzst** ist eine Python-3.12+-Bibliothek mit CLI zum Erstellen, Extrahieren, Auflisten und Prüfen von `.tzst`- und `.tar.zst`-Archiven. Sie bündelt tar-Kompatibilität, Zstandard-Komprimierung, Streaming, atomare Schreibvorgänge und standardmäßig sicheres Extrahieren in einer kompakten, produktionsreifen Oberfläche.
> [!NOTE]
> Ausführlicher technischer Artikel: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## Funktionen
- **Hohe Komprimierung**: Zstandard-Komprimierung für ausgezeichnete Komprimierungsraten und Geschwindigkeit
- **Tar-Kompatibilität**: Erstellt Standard-Tar-Archive, komprimiert mit Zstandard
- **Kommandozeilenschnittstelle**: Intuitive CLI mit Streaming-Unterstützung und umfassenden Optionen
- **Python API**: Saubere, pythonische API für programmatische Nutzung
- **Plattformübergreifend**: Funktioniert auf Windows, macOS und Linux
- **Mehrere Erweiterungen**: Unterstützt sowohl `.tzst` als auch `.tar.zst` Erweiterungen
- **Speichereffizient**: Streaming-Modus für die Behandlung großer Archive mit minimalem Speicherverbrauch
- **Atomare Operationen**: Sichere Dateioperationen mit automatischer Bereinigung bei Unterbrechung
- **Standardmäßig sicher**: Verwendet den 'data' Filter für maximale Sicherheit beim Extrahieren
- **Verbesserte Fehlerbehandlung**: Klare Fehlermeldungen mit hilfreichen Alternativen
## Installation
### Von GitHub Releases
Lade eigenständige ausführbare Dateien herunter, die keine Python-Installation erfordern:
#### Unterstützte Plattformen
| Plattform | Architektur | Datei |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{version}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{version}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{version}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{version}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{version}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{version}-darwin-arm64.zip` |
#### Installationsschritte
1. **Lade** das entsprechende Archiv für deine Plattform von der [Seite der neuesten Releases](https://github.com/xixu-me/tzst/releases/latest) herunter
2. **Extrahiere** das Archiv, um die ausführbare Datei `tzst` (oder `tzst.exe` unter Windows) zu erhalten
3. **Verschiebe** die ausführbare Datei in ein Verzeichnis in deinem PATH:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: Füge das Verzeichnis mit `tzst.exe` zu deiner PATH-Umgebungsvariable hinzu
4. **Überprüfe** die Installation: `tzst --help`
#### Vorteile der Binärinstallation
- **Kein Python erforderlich** - Eigenständige ausführbare Datei
- **Schnellerer Start** - Kein Python-Interpreter-Overhead
- **Einfache Bereitstellung** - Einzeldatei-Distribution
- **Konsistentes Verhalten** - Gebündelte Abhängigkeiten
### Von PyPI
Mit pip:
```bash
pip install tzst
```
Oder mit uv (empfohlen):
```bash
uv tool install tzst
```
### Aus dem Quellcode
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### Entwicklungsinstallation
Dieses Projekt verwendet moderne Python-Packaging-Standards:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## Schnellstart
### Kommandozeilennutzung
```bash
# Archiv erstellen
tzst a archive.tzst file1.txt file2.txt directory/
# Archiv extrahieren
tzst x archive.tzst
# Archivinhalt auflisten
tzst l archive.tzst
# Archivintegrität testen
tzst t archive.tzst
```
### Python API Nutzung
```python
from tzst import create_archive, extract_archive, list_archive
# Archiv erstellen
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# Archiv extrahieren
extract_archive("archive.tzst", "output_directory/")
# Archivinhalt auflisten
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## Kommandozeilenschnittstelle
### Archivoperationen
#### Archiv erstellen
```bash
# Grundlegende Nutzung
tzst a archive.tzst file1.txt file2.txt
# Mit Komprimierungsstufe (1-22, Standard: 3)
tzst a archive.tzst files/ -l 15
# Alternative Befehle
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### Archiv extrahieren
```bash
# Mit vollständiger Verzeichnisstruktur extrahieren
tzst x archive.tzst
# In spezifisches Verzeichnis extrahieren
tzst x archive.tzst -o output/
# Spezifische Dateien extrahieren
tzst x archive.tzst file1.txt dir/file2.txt
# Ohne Verzeichnisstruktur extrahieren (flach)
tzst e archive.tzst -o output/
# Streaming-Modus für große Archive verwenden
tzst x archive.tzst --streaming -o output/
```
#### Inhalt auflisten
```bash
# Einfache Auflistung
tzst l archive.tzst
# Ausführliche Auflistung mit Details
tzst l archive.tzst -v
# Streaming-Modus für große Archive verwenden
tzst l archive.tzst --streaming -v
```
#### Integrität testen
```bash
# Archivintegrität testen
tzst t archive.tzst
# Mit Streaming-Modus testen
tzst t archive.tzst --streaming
```
### Befehlsreferenz
| Befehl | Aliase | Beschreibung | Streaming-Unterstützung |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | Archiv erstellen oder hinzufügen | N/A |
| `x` | `extract` | Mit vollständigen Pfaden extrahieren | ✓ `--streaming` |
| `e` | `extract-flat` | Ohne Verzeichnisstruktur extrahieren | ✓ `--streaming` |
| `l` | `list` | Archivinhalt auflisten | ✓ `--streaming` |
| `t` | `test` | Archivintegrität testen | ✓ `--streaming` |
### CLI-Optionen
- `-v, --verbose`: Ausführliche Ausgabe aktivieren
- `-o, --output DIR`: Ausgabeverzeichnis spezifizieren (Extraktionsbefehle)
- `-l, --level LEVEL`: Komprimierungsstufe 1-22 setzen (Erstellungsbefehl)
- `--streaming`: Streaming-Modus für speichereffiziente Verarbeitung aktivieren
- `--filter FILTER`: Sicherheitsfilter für Extraktion (data/tar/fully_trusted)
- `--no-atomic`: Atomare Dateioperationen deaktivieren (nicht empfohlen)
### Sicherheitsfilter
```bash
# Mit maximaler Sicherheit extrahieren (Standard)
tzst x archive.tzst --filter data
# Mit Standard-Tar-Kompatibilität extrahieren
tzst x archive.tzst --filter tar
# Mit vollem Vertrauen extrahieren (gefährlich - nur für vertrauenswürdige Archive)
tzst x archive.tzst --filter fully_trusted
```
**Sicherheitsfilter-Optionen:**
- `data` (Standard): Am sichersten. Blockiert gefährliche Dateien, absolute Pfade und Pfade außerhalb des Extraktionsverzeichnisses
- `tar`: Standard-Tar-Kompatibilität. Blockiert absolute Pfade und Verzeichnisdurchquerung
- `fully_trusted`: Keine Sicherheitsbeschränkungen. Nur bei vollständig vertrauenswürdigen Archiven verwenden
## Python API
### TzstArchive Klasse
```python
from tzst import TzstArchive
# Neues Archiv erstellen
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# Vorhandenes Archiv lesen
with TzstArchive("archive.tzst", "r") as archive:
# Inhalt auflisten
contents = archive.list(verbose=True)
# Mit Sicherheitsfilter extrahieren
archive.extract("file.txt", "output/", filter="data")
# Integrität testen
is_valid = archive.test()
# Für große Archive, Streaming-Modus verwenden
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**Wichtige Einschränkungen:**
- **Anhängemodus nicht unterstützt**: Erstelle mehrere Archive oder erstelle das gesamte Archiv neu
### Convenience-Funktionen
#### create_archive()
```python
from tzst import create_archive
# Mit atomaren Operationen erstellen (Standard)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# Mit Sicherheit extrahieren (Standard: 'data' Filter)
extract_archive("backup.tzst", "restore/")
# Spezifische Dateien extrahieren
extract_archive("backup.tzst", "restore/", members=["config.txt"])
# Verzeichnisstruktur abflachen
extract_archive("backup.tzst", "restore/", flatten=True)
# Streaming für große Archive verwenden
extract_archive("large_backup.tzst", "restore/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# Einfache Auflistung
files = list_archive("backup.tzst")
# Detaillierte Auflistung
files = list_archive("backup.tzst", verbose=True)
# Streaming für große Archive
files = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# Grundlegende Integritätsprüfung
if test_archive("backup.tzst"):
print("Archiv ist gültig")
# Mit Streaming testen
if test_archive("large_backup.tzst", streaming=True):
print("Großes Archiv ist gültig")
```
## Erweiterte Funktionen
### Dateierweiterungen
Die Bibliothek behandelt Dateierweiterungen automatisch mit intelligenter Normalisierung:
- `.tzst` - Primäre Erweiterung für tar+zstandard Archive
- `.tar.zst` - Alternative Standarderweiterung
- Automatische Erkennung beim Öffnen vorhandener Archive
- Automatisches Hinzufügen von Erweiterungen beim Erstellen von Archiven
```python
# Diese erstellen alle gültige Archive
create_archive("backup.tzst", files) # Erstellt backup.tzst
create_archive("backup.tar.zst", files) # Erstellt backup.tar.zst
create_archive("backup", files) # Erstellt backup.tzst
create_archive("backup.txt", files) # Erstellt backup.tzst (normalisiert)
```
### Komprimierungsstufen
Zstandard-Komprimierungsstufen reichen von 1 (schnellste) bis 22 (beste Komprimierung):
- **Stufe 1-3**: Schnelle Komprimierung, größere Dateien
- **Stufe 3** (Standard): Guter Kompromiss zwischen Geschwindigkeit und Komprimierung
- **Stufe 10-15**: Bessere Komprimierung, langsamer
- **Stufe 20-22**: Maximale Komprimierung, viel langsamer
### Streaming-Modus
Verwende den Streaming-Modus für speichereffiziente Verarbeitung großer Archive:
**Vorteile:**
- Deutlich reduzierter Speicherverbrauch
- Bessere Leistung für Archive, die nicht in den Speicher passen
- Automatische Bereinigung von Ressourcen
**Wann verwenden:**
- Archive größer als 100MB
- Umgebungen mit begrenztem Speicher
- Verarbeitung von Archiven mit vielen großen Dateien
```python
# Beispiel: Verarbeitung eines großen Backup-Archivs
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# Speichereffiziente Operationen
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### Atomare Operationen
Alle Dateierstellungsoperationen verwenden standardmäßig atomare Dateioperationen:
- Archive werden zuerst in temporären Dateien erstellt, dann atomisch verschoben
- Automatische Bereinigung bei Prozessunterbrechung
- Kein Risiko von beschädigten oder unvollständigen Archiven
- Plattformübergreifende Kompatibilität
```python
# Atomare Operationen standardmäßig aktiviert
create_archive("important.tzst", files) # Sicher vor Unterbrechung
# Kann bei Bedarf deaktiviert werden (nicht empfohlen)
create_archive("test.tzst", files, use_temp_file=False)
```
### Fehlerbehandlung
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("Fehler beim Dekomprimieren des Archivs")
except TzstFileNotFoundError:
print("Archivdatei nicht gefunden")
except KeyboardInterrupt:
print("Operation vom Benutzer unterbrochen")
# Bereinigung wird automatisch durchgeführt
```
## Leistung und Vergleich
### Leistungstipps
1. **Komprimierungsstufen**: Stufe 3 ist optimal für die meisten Anwendungsfälle
2. **Streaming**: Verwende für Archive größer als 100MB
3. **Batch-Operationen**: Füge mehrere Dateien in einer Sitzung hinzu
4. **Dateitypen**: Bereits komprimierte Dateien werden nicht viel weiter komprimiert
### vs Andere Tools
**vs tar + gzip:**
- Bessere Komprimierungsraten
- Schnellere Dekomprimierung
- Moderner Algorithmus
**vs tar + xz:**
- Deutlich schnellere Komprimierung
- Ähnliche Komprimierungsraten
- Besserer Geschwindigkeit/Komprimierung-Kompromiss
**vs zip:**
- Bessere Komprimierung
- Bewahrt Unix-Berechtigungen und Metadaten
- Bessere Streaming-Unterstützung
## Anforderungen
- Python 3.12 oder höher
- zstandard >= 0.19.0
## Entwicklung
### Entwicklungsumgebung einrichten
Dieses Projekt verwendet moderne Python-Packaging-Standards:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### Tests ausführen
```bash
# Tests mit Coverage ausführen
pytest --cov=tzst --cov-report=html
# Oder den einfacheren Befehl verwenden (Coverage-Einstellungen sind in pyproject.toml)
pytest
```
### Code-Qualität
```bash
# Code-Qualität prüfen
ruff check src tests
# Code formatieren
ruff format src tests
```
## Beitragen
Wir begrüßen Beiträge! Bitte lies unseren [Beitragsleitfaden](CONTRIBUTING.md) für:
- Entwicklungssetup und Projektstruktur
- Code-Stil-Richtlinien und bewährte Praktiken
- Testanforderungen und Schreibtests
- Pull-Request-Prozess und Review-Workflow
### Schnellstart für Mitwirkende
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### Arten willkommener Beiträge
- **Fehlerbehebungen** - Probleme in vorhandener Funktionalität beheben
- **Funktionen** - Neue Fähigkeiten zur Bibliothek hinzufügen
- **Dokumentation** - Dokumentation verbessern oder hinzufügen
- **Tests** - Testabdeckung hinzufügen oder verbessern
- **Leistung** - Vorhandenen Code optimieren
- **Sicherheit** - Sicherheitsschwachstellen beheben
## Danksagungen
- [Meta Zstandard](https://github.com/facebook/zstd) für den exzellenten Komprimierungsalgorithmus
- [python-zstandard](https://github.com/indygreg/python-zstandard) für Python-Bindings
- Der Python-Community für Inspiration und Feedback
## Lizenz
Urheberrecht &copy; [Xi Xu](https://xi-xu.me). Alle Rechte vorbehalten.
Lizenziert unter der [BSD 3-Clause](LICENSE) Lizenz.
+526
View File
@@ -0,0 +1,526 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | **🇪🇸 español** | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
**tzst** es una biblioteca y CLI para Python 3.12+ orientada a crear, extraer, listar y validar archivos `.tzst` y `.tar.zst`. Reúne compatibilidad con tar, compresión Zstandard, modo streaming, escrituras atómicas y extracción segura por defecto en una interfaz compacta lista para producción.
> [!NOTE]
> Artículo técnico detallado: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## Características
- **Alta Compresión**: Compresión Zstandard para excelentes ratios de compresión y velocidad.
- **Compatibilidad con Tar**: Crea archivos tar estándar comprimidos con Zstandard.
- **Interfaz de Línea de Comandos**: CLI intuitiva con soporte para transmisión y opciones completas.
- **API de Python**: API limpia y pitónica para uso programático.
- **Multiplataforma**: Funciona en Windows, macOS y Linux.
- **Múltiples Extensiones**: Soporta las extensiones `.tzst` y `.tar.zst`.
- **Eficiente en Memoria**: Modo de transmisión para manejar archivos grandes con un uso mínimo de memoria.
- **Operaciones Atómicas**: Operaciones de archivo seguras con limpieza automática en caso de interrupción.
- **Seguro por Defecto**: Utiliza el filtro 'data' para máxima seguridad durante la extracción.
- **Manejo de Errores Mejorado**: Mensajes de error claros con alternativas útiles.
## Instalación
### Desde los Lanzamientos de GitHub
Descarga ejecutables independientes que no requieren instalación de Python:
#### Plataformas Soportadas
| Plataforma | Arquitectura | Archivo |
|--------------|---------------|---------------------------------------|
| **Linux** | x86_64 | `tzst-{versión}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{versión}-linux-arm64.zip` |
| **Windows**| x64 | `tzst-{versión}-windows-amd64.zip` |
| **Windows**| ARM64 | `tzst-{versión}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{versión}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{versión}-darwin-arm64.zip` |
#### Pasos de Instalación
1. **Descarga** el archivo apropiado para tu plataforma desde la [página de lanzamientos más recientes](https://github.com/xixu-me/tzst/releases/latest).
2. **Extrae** el archivo para obtener el ejecutable `tzst` (o `tzst.exe` en Windows).
3. **Mueve** el ejecutable a un directorio en tu PATH:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: Añade el directorio que contiene `tzst.exe` a tu variable de entorno PATH.
4. **Verifica** la instalación: `tzst --help`
#### Beneficios de la Instalación Binaria
- **No requiere Python** - Ejecutable independiente.
- **Inicio más rápido** - Sin la sobrecarga del intérprete de Python.
- **Despliegue fácil** - Distribución en un solo archivo.
- **Comportamiento consistente** - Dependencias incluidas.
### Desde PyPI
Usando pip:
```
pip install tzst
```
O usando uv (recomendado):
```
uv tool install tzst
```
### Desde el Código Fuente
```
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### Instalación para Desarrollo
Este proyecto utiliza estándares modernos de empaquetado de Python:
```
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## Inicio Rápido
### Uso desde la Línea de Comandos
```
# Crear un archivo
tzst a archivo.tzst archivo1.txt archivo2.txt directorio/
# Extraer un archivo
tzst x archivo.tzst
# Listar el contenido del archivo
tzst l archivo.tzst
# Probar la integridad del archivo
tzst t archivo.tzst
```
### Uso de la API de Python
```
from tzst import create_archive, extract_archive, list_archive
# Crear un archivo
create_archive("archivo.tzst", ["archivo1.txt", "archivo2.txt", "directorio/"])
# Extraer un archivo
extract_archive("archivo.tzst", "directorio_salida/")
# Listar el contenido del archivo
contents = list_archive("archivo.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## Interfaz de Línea de Comandos
### Operaciones con Archivos
#### Crear Archivo
```
# Uso básico
tzst a archivo.tzst archivo1.txt archivo2.txt
# Con nivel de compresión (1-22, por defecto: 3)
tzst a archivo.tzst archivos/ -l 15
# Comandos alternativos
tzst add archivo.tzst archivos/
tzst create archivo.tzst archivos/
```
#### Extraer Archivo
```
# Extraer con la estructura de directorios completa
tzst x archivo.tzst
# Extraer a un directorio específico
tzst x archivo.tzst -o salida/
# Extraer archivos específicos
tzst x archivo.tzst archivo1.txt dir/archivo2.txt
# Extraer sin estructura de directorios (plano)
tzst e archivo.tzst -o salida/
# Usar modo de transmisión para archivos grandes
tzst x archivo.tzst --streaming -o salida/
```
#### Listar Contenido
```
# Listado simple
tzst l archivo.tzst
# Listado detallado con detalles
tzst l archivo.tzst -v
# Usar modo de transmisión para archivos grandes
tzst l archivo.tzst --streaming -v
```
#### Probar Integridad
```
# Probar la integridad del archivo
tzst t archivo.tzst
# Probar con modo de transmisión
tzst t archivo.tzst --streaming
```
### Referencia de Comandos
| Comando | Alias | Descripción | Soporte de Transmisión |
|---------|--------------------|-------------------------------------------|------------------------|
| `a` | `add`, `create` | Crear o añadir a un archivo | N/A |
| `x` | `extract` | Extraer con rutas completas | ✓ `--streaming` |
| `e` | `extract-flat` | Extraer sin estructura de directorios | ✓ `--streaming` |
| `l` | `list` | Listar el contenido del archivo | ✓ `--streaming` |
| `t` | `test` | Probar la integridad del archivo | ✓ `--streaming` |
### Opciones de CLI
- `-v, --verbose`: Habilitar salida detallada.
- `-o, --output DIR`: Especificar directorio de salida (comandos de extracción).
- `-l, --level NIVEL`: Establecer nivel de compresión 1-22 (comando de creación).
- `--streaming`: Habilitar modo de transmisión para procesamiento eficiente en memoria.
- `--filter FILTRO`: Filtro de seguridad para extracción (data/tar/fully_trusted).
- `--no-atomic`: Deshabilitar operaciones de archivo atómicas (no recomendado).
### Filtros de Seguridad
```
# Extraer con máxima seguridad (por defecto)
tzst x archivo.tzst --filter data
# Extraer con compatibilidad estándar de tar
tzst x archivo.tzst --filter tar
# Extraer con confianza total (peligroso - solo para archivos de confianza)
tzst x archivo.tzst --filter fully_trusted
```
**Opciones de Filtro de Seguridad:**
- `data` (por defecto): El más seguro. Bloquea archivos peligrosos, rutas absolutas y rutas fuera del directorio de extracción.
- `tar`: Compatibilidad estándar con tar. Bloquea rutas absolutas y recorrido de directorios (directory traversal).
- `fully_trusted`: Sin restricciones de seguridad. Usar solo con archivos completamente confiables.
## API de Python
### Clase TzstArchive
```
from tzst import TzstArchive
# Crear un nuevo archivo
with TzstArchive("archivo.tzst", "w", compression_level=5) as archive:
archive.add("archivo.txt")
archive.add("directorio/", recursive=True)
# Leer un archivo existente
with TzstArchive("archivo.tzst", "r") as archive:
# Listar contenido
contents = archive.list(verbose=True)
# Extraer con filtro de seguridad
archive.extract("archivo.txt", "salida/", filter="data")
# Probar integridad
is_valid = archive.test()
# Para archivos grandes, usar modo de transmisión
with TzstArchive("archivo_grande.tzst", "r", streaming=True) as archive:
archive.extract(path="salida/")
```
**Limitaciones Importantes:**
- **Modo de Añadir No Soportado**: Crea múltiples archivos o recrea el archivo completo en su lugar.
### Funciones de Conveniencia
#### create_archive()
```
from tzst import create_archive
# Crear con operaciones atómicas (por defecto)
create_archive(
archive_path="backup.tzst",
files=["documentos/", "fotos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```
from tzst import extract_archive
# Extraer con seguridad (por defecto: filtro 'data')
extract_archive("backup.tzst", "restaurar/")
# Extraer archivos específicos
extract_archive("backup.tzst", "restaurar/", members=["config.txt"])
# Aplanar estructura de directorios
extract_archive("backup.tzst", "restaurar/", flatten=True)
# Usar transmisión para archivos grandes
extract_archive("backup_grande.tzst", "restaurar/", streaming=True)
```
#### list_archive()
```
from tzst import list_archive
# Listado simple
files = list_archive("backup.tzst")
# Listado detallado
files = list_archive("backup.tzst", verbose=True)
# Transmisión para archivos grandes
files = list_archive("backup_grande.tzst", streaming=True)
```
#### test_archive()
```
from tzst import test_archive
# Prueba de integridad básica
if test_archive("backup.tzst"):
print("El archivo es válido")
# Prueba con transmisión
if test_archive("backup_grande.tzst", streaming=True):
print("El archivo grande es válido")
```
## Características Avanzadas
### Extensiones de Archivo
La biblioteca maneja automáticamente las extensiones de archivo con normalización inteligente:
- `.tzst` - Extensión principal para archivos tar+zstandard.
- `.tar.zst` - Extensión estándar alternativa.
- Autodetección al abrir archivos existentes.
- Adición automática de extensión al crear archivos.
```
# Todos estos crean archivos válidos
create_archive("backup.tzst", files) # Crea backup.tzst
create_archive("backup.tar.zst", files) # Crea backup.tar.zst
create_archive("backup", files) # Crea backup.tzst
create_archive("backup.txt", files) # Crea backup.tzst (normalizado)
```
### Niveles de Compresión
Los niveles de compresión de Zstandard van de 1 (más rápido) a 22 (mejor compresión):
- **Nivel 1-3**: Compresión rápida, archivos más grandes.
- **Nivel 3** (por defecto): Buen equilibrio entre velocidad y compresión.
- **Nivel 10-15**: Mejor compresión, más lento.
- **Nivel 20-22**: Máxima compresión, mucho más lento.
### Modo de Transmisión (Streaming)
Usa el modo de transmisión para el procesamiento eficiente en memoria de archivos grandes:
**Beneficios:**
- Uso de memoria significativamente reducido.
- Mejor rendimiento para archivos que no caben en memoria.
- Limpieza automática de recursos.
**Cuándo Usar:**
- Archivos mayores de 100MB.
- Entornos con memoria limitada.
- Procesamiento de archivos con muchos archivos grandes.
```
# Ejemplo: Procesando un archivo de copia de seguridad grande
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# Operaciones eficientes en memoria
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### Operaciones Atómicas
Todas las operaciones de creación de archivos utilizan operaciones de archivo atómicas por defecto:
- Los archivos se crean primero en archivos temporales y luego se mueven atómicamente.
- Limpieza automática si el proceso se interrumpe.
- Sin riesgo de archivos corruptos o incompletos.
- Compatibilidad multiplataforma.
```
# Operaciones atómicas habilitadas por defecto
create_archive("importante.tzst", files) # Seguro contra interrupciones
# Se pueden deshabilitar si es necesario (no recomendado)
create_archive("test.tzst", files, use_temp_file=False)
```
### Manejo de Errores
```
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archivo.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("Falló la descompresión del archivo")
except TzstFileNotFoundError:
print("Archivo no encontrado")
except KeyboardInterrupt:
print("Operación interrumpida por el usuario")
# La limpieza se maneja automáticamente
```
## Rendimiento y Comparación
### Consejos de Rendimiento
1. **Niveles de compresión**: El nivel 3 es óptimo para la mayoría de los casos de uso.
2. **Transmisión**: Usar para archivos mayores de 100MB.
3. **Operaciones por lotes**: Añadir múltiples archivos en una sola sesión.
4. **Tipos de archivo**: Los archivos ya comprimidos no se comprimirán mucho más.
### vs Otras Herramientas
**vs tar + gzip:**
- Mejores ratios de compresión.
- Descompresión más rápida.
- Algoritmo moderno.
**vs tar + xz:**
- Compresión significativamente más rápida.
- Ratios de compresión similares.
- Mejor equilibrio velocidad/compresión.
**vs zip:**
- Mejor compresión.
- Preserva permisos y metadatos de Unix.
- Mejor soporte para transmisión.
## Requisitos
- Python 3.12 o superior
- zstandard >= 0.19.0
## Desarrollo
### Configuración del Entorno de Desarrollo
Este proyecto utiliza estándares modernos de empaquetado de Python:
```
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### Ejecución de Pruebas
```
# Ejecutar pruebas con cobertura
pytest --cov=tzst --cov-report=html
# O usar el comando más simple (la configuración de cobertura está en pyproject.toml)
pytest
```
### Calidad del Código
```
# Comprobar la calidad del código
ruff check src tests
# Formatear el código
ruff format src tests
```
## Contribuir
¡Aceptamos contribuciones! Por favor, lee nuestra [Guía de Contribución](CONTRIBUTING.md) para:
- Configuración del desarrollo y estructura del proyecto.
- Directrices de estilo de código y mejores prácticas.
- Requisitos de prueba y escritura de pruebas.
- Proceso de pull request y flujo de trabajo de revisión.
### Inicio Rápido para Colaboradores
```
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### Tipos de Contribuciones Bienvenidas
- **Corrección de errores** - Soluciona problemas en la funcionalidad existente.
- **Características** - Añade nuevas capacidades a la biblioteca.
- **Documentación** - Mejora o añade documentación.
- **Pruebas** - Añade o mejora la cobertura de pruebas.
- **Rendimiento** - Optimiza el código existente.
- **Seguridad** - Aborda vulnerabilidades de seguridad.
## Agradecimientos
- [Meta Zstandard](https://github.com/facebook/zstd) por el excelente algoritmo de compresión.
- [python-zstandard](https://github.com/indygreg/python-zstandard) por los bindings de Python.
- La comunidad de Python por la inspiración y los comentarios.
## Licencia
Copyright &copy; [Xi Xu](https://xi-xu.me). Todos los derechos reservados.
Licenciado bajo la licencia [BSD 3-Clause](LICENSE).
+526
View File
@@ -0,0 +1,526 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | **🇫🇷 français** | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
**tzst** est une bibliothèque et une CLI pour Python 3.12+ destinées à créer, extraire, lister et vérifier des archives `.tzst` et `.tar.zst`. Elle réunit la compatibilité tar, la compression Zstandard, le mode streaming, les écritures atomiques et une extraction sécurisée par défaut dans une interface compacte prête pour la production.
> [!NOTE]
> Article technique détaillé : **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## Fonctionnalités
- **Compression élevée** : Compression Zstandard pour d'excellents taux de compression et une vitesse remarquable
- **Compatibilité Tar** : Crée des archives tar standard compressées avec Zstandard
- **Interface en ligne de commande** : CLI intuitive avec support de streaming et options complètes
- **API Python** : API propre et pythonique pour un usage programmatique
- **Multi-plateforme** : Fonctionne sur Windows, macOS et Linux
- **Extensions multiples** : Supporte les extensions `.tzst` et `.tar.zst`
- **Efficace en mémoire** : Mode streaming pour gérer de grandes archives avec une utilisation mémoire minimale
- **Opérations atomiques** : Opérations de fichiers sécurisées avec nettoyage automatique en cas d'interruption
- **Sécurisé par défaut** : Utilise le filtre 'data' pour une sécurité maximale lors de l'extraction
- **Gestion d'erreurs améliorée** : Messages d'erreur clairs avec des alternatives utiles
## Installation
### Depuis les Releases GitHub
Téléchargez des exécutables autonomes qui ne nécessitent pas d'installation Python :
#### Plateformes supportées
| Plateforme | Architecture | Fichier |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{version}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{version}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{version}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{version}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{version}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{version}-darwin-arm64.zip` |
#### Étapes d'installation
1. **Téléchargez** l'archive appropriée pour votre plateforme depuis la [page des dernières versions](https://github.com/xixu-me/tzst/releases/latest)
2. **Extrayez** l'archive pour obtenir l'exécutable `tzst` (ou `tzst.exe` sous Windows)
3. **Déplacez** l'exécutable vers un répertoire dans votre PATH :
- **Linux/macOS** : `sudo mv tzst /usr/local/bin/`
- **Windows** : Ajoutez le répertoire contenant `tzst.exe` à votre variable d'environnement PATH
4. **Vérifiez** l'installation : `tzst --help`
#### Avantages de l'installation binaire
- **Aucun Python requis** - Exécutable autonome
- **Démarrage plus rapide** - Aucune surcharge d'interpréteur Python
- **Déploiement facile** - Distribution en fichier unique
- **Comportement cohérent** - Dépendances intégrées
### Depuis PyPI
Avec pip :
```bash
pip install tzst
```
Ou avec uv (recommandé) :
```bash
uv tool install tzst
```
### Depuis le code source
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### Installation de développement
Ce projet utilise les standards modernes d'empaquetage Python :
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## Démarrage rapide
### Utilisation en ligne de commande
```bash
# Créer une archive
tzst a archive.tzst file1.txt file2.txt directory/
# Extraire une archive
tzst x archive.tzst
# Lister le contenu d'une archive
tzst l archive.tzst
# Tester l'intégrité d'une archive
tzst t archive.tzst
```
### Utilisation de l'API Python
```python
from tzst import create_archive, extract_archive, list_archive
# Créer une archive
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# Extraire une archive
extract_archive("archive.tzst", "output_directory/")
# Lister le contenu d'une archive
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## Interface en ligne de commande
### Opérations d'archives
#### Créer une archive
```bash
# Utilisation de base
tzst a archive.tzst file1.txt file2.txt
# Avec niveau de compression (1-22, défaut : 3)
tzst a archive.tzst files/ -l 15
# Commandes alternatives
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### Extraire une archive
```bash
# Extraire avec structure complète des répertoires
tzst x archive.tzst
# Extraire vers un répertoire spécifique
tzst x archive.tzst -o output/
# Extraire des fichiers spécifiques
tzst x archive.tzst file1.txt dir/file2.txt
# Extraire sans structure de répertoires (à plat)
tzst e archive.tzst -o output/
# Utiliser le mode streaming pour de grandes archives
tzst x archive.tzst --streaming -o output/
```
#### Lister le contenu
```bash
# Liste simple
tzst l archive.tzst
# Liste détaillée avec informations
tzst l archive.tzst -v
# Utiliser le mode streaming pour de grandes archives
tzst l archive.tzst --streaming -v
```
#### Tester l'intégrité
```bash
# Tester l'intégrité de l'archive
tzst t archive.tzst
# Tester avec le mode streaming
tzst t archive.tzst --streaming
```
### Référence des commandes
| Commande | Alias | Description | Support streaming |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | Créer ou ajouter à une archive | N/A |
| `x` | `extract` | Extraire avec chemins complets | ✓ `--streaming` |
| `e` | `extract-flat` | Extraire sans structure de répertoires | ✓ `--streaming` |
| `l` | `list` | Lister le contenu de l'archive | ✓ `--streaming` |
| `t` | `test` | Tester l'intégrité de l'archive | ✓ `--streaming` |
### Options CLI
- `-v, --verbose` : Activer la sortie détaillée
- `-o, --output DIR` : Spécifier le répertoire de sortie (commandes d'extraction)
- `-l, --level LEVEL` : Définir le niveau de compression 1-22 (commande de création)
- `--streaming` : Activer le mode streaming pour un traitement efficace en mémoire
- `--filter FILTER` : Filtre de sécurité pour l'extraction (data/tar/fully_trusted)
- `--no-atomic` : Désactiver les opérations de fichiers atomiques (non recommandé)
### Filtres de sécurité
```bash
# Extraire avec sécurité maximale (défaut)
tzst x archive.tzst --filter data
# Extraire avec compatibilité tar standard
tzst x archive.tzst --filter tar
# Extraire avec confiance totale (dangereux - uniquement pour les archives de confiance)
tzst x archive.tzst --filter fully_trusted
```
**Options de filtre de sécurité :**
- `data` (défaut) : Le plus sécurisé. Bloque les fichiers dangereux, les chemins absolus et les chemins en dehors du répertoire d'extraction
- `tar` : Compatibilité tar standard. Bloque les chemins absolus et la traversée de répertoires
- `fully_trusted` : Aucune restriction de sécurité. À utiliser uniquement avec des archives entièrement fiables
## API Python
### Classe TzstArchive
```python
from tzst import TzstArchive
# Créer une nouvelle archive
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# Lire une archive existante
with TzstArchive("archive.tzst", "r") as archive:
# Lister le contenu
contents = archive.list(verbose=True)
# Extraire avec filtre de sécurité
archive.extract("file.txt", "output/", filter="data")
# Tester l'intégrité
is_valid = archive.test()
# Pour de grandes archives, utiliser le mode streaming
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**Limitations importantes :**
- **Mode d'ajout non supporté** : Créez plusieurs archives ou recréez l'archive entière à la place
### Fonctions de convenance
#### create_archive()
```python
from tzst import create_archive
# Créer avec opérations atomiques (défaut)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# Extraire avec sécurité (défaut : filtre 'data')
extract_archive("backup.tzst", "restore/")
# Extraire des fichiers spécifiques
extract_archive("backup.tzst", "restore/", members=["config.txt"])
# Aplatir la structure des répertoires
extract_archive("backup.tzst", "restore/", flatten=True)
# Utiliser le streaming pour de grandes archives
extract_archive("large_backup.tzst", "restore/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# Liste simple
files = list_archive("backup.tzst")
# Liste détaillée
files = list_archive("backup.tzst", verbose=True)
# Streaming pour de grandes archives
files = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# Test d'intégrité de base
if test_archive("backup.tzst"):
print("L'archive est valide")
# Tester avec streaming
if test_archive("large_backup.tzst", streaming=True):
print("La grande archive est valide")
```
## Fonctionnalités avancées
### Extensions de fichiers
La bibliothèque gère automatiquement les extensions de fichiers avec normalisation intelligente :
- `.tzst` - Extension principale pour les archives tar+zstandard
- `.tar.zst` - Extension standard alternative
- Détection automatique lors de l'ouverture d'archives existantes
- Ajout automatique d'extension lors de la création d'archives
```python
# Toutes ces créent des archives valides
create_archive("backup.tzst", files) # Crée backup.tzst
create_archive("backup.tar.zst", files) # Crée backup.tar.zst
create_archive("backup", files) # Crée backup.tzst
create_archive("backup.txt", files) # Crée backup.tzst (normalisé)
```
### Niveaux de compression
Les niveaux de compression Zstandard vont de 1 (le plus rapide) à 22 (meilleure compression) :
- **Niveau 1-3** : Compression rapide, fichiers plus volumineux
- **Niveau 3** (défaut) : Bon équilibre entre vitesse et compression
- **Niveau 10-15** : Meilleure compression, plus lent
- **Niveau 20-22** : Compression maximale, beaucoup plus lent
### Mode streaming
Utilisez le mode streaming pour un traitement efficace en mémoire de grandes archives :
**Avantages :**
- Utilisation mémoire considérablement réduite
- Meilleures performances pour les archives qui ne tiennent pas en mémoire
- Nettoyage automatique des ressources
**Quand utiliser :**
- Archives supérieures à 100MB
- Environnements à mémoire limitée
- Traitement d'archives avec de nombreux gros fichiers
```python
# Exemple : Traitement d'une grande archive de sauvegarde
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# Opérations efficaces en mémoire
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### Opérations atomiques
Toutes les opérations de création de fichiers utilisent des opérations de fichiers atomiques par défaut :
- Archives créées dans des fichiers temporaires d'abord, puis déplacées atomiquement
- Nettoyage automatique si le processus est interrompu
- Aucun risque d'archives corrompues ou incomplètes
- Compatibilité multi-plateforme
```python
# Opérations atomiques activées par défaut
create_archive("important.tzst", files) # Sûr contre les interruptions
# Peut être désactivé si nécessaire (non recommandé)
create_archive("test.tzst", files, use_temp_file=False)
```
### Gestion des erreurs
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("Échec de la décompression de l'archive")
except TzstFileNotFoundError:
print("Fichier d'archive non trouvé")
except KeyboardInterrupt:
print("Opération interrompue par l'utilisateur")
# Le nettoyage est géré automatiquement
```
## Performance et comparaison
### Conseils de performance
1. **Niveaux de compression** : Le niveau 3 est optimal pour la plupart des cas d'usage
2. **Streaming** : Utilisez pour les archives supérieures à 100MB
3. **Opérations par lots** : Ajoutez plusieurs fichiers en une seule session
4. **Types de fichiers** : Les fichiers déjà compressés ne se compresseront pas beaucoup plus
### vs Autres outils
**vs tar + gzip :**
- Meilleurs taux de compression
- Décompression plus rapide
- Algorithme moderne
**vs tar + xz :**
- Compression significativement plus rapide
- Taux de compression similaires
- Meilleur compromis vitesse/compression
**vs zip :**
- Meilleure compression
- Préserve les permissions Unix et métadonnées
- Meilleur support de streaming
## Exigences
- Python 3.12 ou supérieur
- zstandard >= 0.19.0
## Développement
### Configuration de l'environnement de développement
Ce projet utilise les standards modernes d'empaquetage Python :
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### Exécution des tests
```bash
# Exécuter les tests avec couverture
pytest --cov=tzst --cov-report=html
# Ou utiliser la commande plus simple (paramètres de couverture dans pyproject.toml)
pytest
```
### Qualité du code
```bash
# Vérifier la qualité du code
ruff check src tests
# Formater le code
ruff format src tests
```
## Contribution
Nous accueillons les contributions ! Veuillez lire notre [Guide de contribution](CONTRIBUTING.md) pour :
- Configuration de développement et structure du projet
- Directives de style de code et meilleures pratiques
- Exigences de test et écriture de tests
- Processus de pull request et workflow de révision
### Démarrage rapide pour les contributeurs
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### Types de contributions bienvenues
- **Corrections de bugs** - Corriger les problèmes dans la fonctionnalité existante
- **Fonctionnalités** - Ajouter de nouvelles capacités à la bibliothèque
- **Documentation** - Améliorer ou ajouter de la documentation
- **Tests** - Ajouter ou améliorer la couverture de tests
- **Performance** - Optimiser le code existant
- **Sécurité** - Traiter les vulnérabilités de sécurité
## Remerciements
- [Meta Zstandard](https://github.com/facebook/zstd) pour l'excellent algorithme de compression
- [python-zstandard](https://github.com/indygreg/python-zstandard) pour les liaisons Python
- La communauté Python pour l'inspiration et les retours
## Licence
Droits d'auteur &copy; [Xi Xu](https://xi-xu.me). Tous droits réservés.
Sous licence [BSD 3-Clause](LICENSE).
+526
View File
@@ -0,0 +1,526 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | **🇯🇵 日本語** | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
**tzst** は、Python 3.12+ 向けに `.tzst` / `.tar.zst` アーカイブの作成、展開、一覧表示、整合性確認を行うためのライブラリ兼 CLI です。tar 互換性、Zstandard 圧縮、ストリーミング処理、アトミック書き込み、デフォルトで安全な展開を、運用向けの簡潔なインターフェースにまとめています。
> [!NOTE]
> 技術解説記事: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**。
## 特徴
- **高圧縮率**: Zstandard 圧縮による優れた圧縮率と速度
- **Tar 互換性**: Zstandard で圧縮された標準 tar アーカイブを作成
- **コマンドラインインターフェース**: ストリーミング対応の直感的な CLI と包括的なオプション
- **Python API**: プログラム利用のためのクリーンで Pythonic な API
- **クロスプラットフォーム**: Windows 、 macOS 、 Linux で動作
- **複数拡張子対応**: `.tzst` と `.tar.zst` の両方の拡張子をサポート
- **メモリ効率**: 大容量アーカイブを最小メモリ使用量で処理するストリーミングモード
- **アトミック操作**: 中断時にも安全な自動クリーンアップ付きファイル操作
- **デフォルトで安全**: 展開時の最大セキュリティのために「data」フィルタを使用
- **強化されたエラーハンドリング**: 代替案を示す明確なエラーメッセージ
## インストール
### GitHub リリースから
Python インストール不要のスタンドアロン実行ファイルをダウンロード:
#### サポート対象プラットフォーム
| プラットフォーム | アーキテクチャ | ファイル |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{バージョン}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{バージョン}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{バージョン}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{バージョン}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{バージョン}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{バージョン}-darwin-arm64.zip` |
#### インストール手順
1. **ダウンロード**: [最新リリースページ](https://github.com/xixu-me/tzst/releases/latest)からお使いのプラットフォームに合ったアーカイブをダウンロード
2. **展開**: アーカイブを展開し、 `tzst` 実行ファイル(Windows の場合は `tzst.exe` )を取得
3. **移動**: 実行ファイルを PATH が通ったディレクトリに移動:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: `tzst.exe` を含むディレクトリを PATH 環境変数に追加
4. **確認**: インストールを検証: `tzst --help`
#### バイナリインストールの利点
- **Python 不要** - スタンドアロン実行ファイル
- **高速起動** - Python インタプリタのオーバーヘッドなし
- **簡単なデプロイ** - 単一ファイル配布
- **一貫した動作** - 依存関係をバンドル
### PyPI から
pip を使用:
```bash
pip install tzst
```
または uv を使用(推奨):
```bash
uv tool install tzst
```
### ソースから
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### 開発用インストール
このプロジェクトはモダンな Python パッケージング標準を使用します:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## クイックスタート
### コマンドラインの使い方
```bash
# アーカイブ作成
tzst a archive.tzst file1.txt file2.txt directory/
# アーカイブ展開
tzst x archive.tzst
# アーカイブ内容一覧
tzst l archive.tzst
# アーカイブ整合性テスト
tzst t archive.tzst
```
### Python API の使い方
```python
from tzst import create_archive, extract_archive, list_archive
# アーカイブ作成
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# アーカイブ展開
extract_archive("archive.tzst", "output_directory/")
# アーカイブ内容一覧
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## コマンドラインインターフェース
### アーカイブ操作
#### アーカイブ作成
```bash
# 基本使用法
tzst a archive.tzst file1.txt file2.txt
# 圧縮レベル指定 (1-22, デフォルト: 3)
tzst a archive.tzst files/ -l 15
# 代替コマンド
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### アーカイブ展開
```bash
# 完全なディレクトリ構造で展開
tzst x archive.tzst
# 特定ディレクトリに展開
tzst x archive.tzst -o output/
# 特定ファイルのみ展開
tzst x archive.tzst file1.txt dir/file2.txt
# ディレクトリ構造なしで展開 (フラット)
tzst e archive.tzst -o output/
# 大容量アーカイブ用ストリーミングモード
tzst x archive.tzst --streaming -o output/
```
#### 内容一覧
```bash
# シンプルな一覧表示
tzst l archive.tzst
# 詳細情報付き一覧表示
tzst l archive.tzst -v
# 大容量アーカイブ用ストリーミングモード
tzst l archive.tzst --streaming -v
```
#### 整合性テスト
```bash
# アーカイブ整合性テスト
tzst t archive.tzst
# ストリーミングモードでテスト
tzst t archive.tzst --streaming
```
### コマンドリファレンス
| コマンド | エイリアス | 説明 | ストリーミングサポート |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | アーカイブ作成または追加 | N/A |
| `x` | `extract` | 完全パスで展開 | ✓ `--streaming` |
| `e` | `extract-flat` | ディレクトリ構造なしで展開 | ✓ `--streaming` |
| `l` | `list` | アーカイブ内容一覧 | ✓ `--streaming` |
| `t` | `test` | アーカイブ整合性テスト | ✓ `--streaming` |
### CLI オプション
- `-v, --verbose`: 詳細出力を有効化
- `-o, --output DIR`: 出力ディレクトリ指定 (展開コマンド)
- `-l, --level LEVEL`: 圧縮レベル設定 1-22 (作成コマンド)
- `--streaming`: メモリ効率処理のためのストリーミングモードを有効化
- `--filter FILTER`: 展開用セキュリティフィルタ (data/tar/fully_trusted)
- `--no-atomic`: アトミックファイル操作を無効化 (非推奨)
### セキュリティフィルタ
```bash
# 最大セキュリティで展開 (デフォルト)
tzst x archive.tzst --filter data
# 標準 tar 互換で展開
tzst x archive.tzst --filter tar
# 完全信頼で展開 (危険 - 信頼済みアーカイブ専用)
tzst x archive.tzst --filter fully_trusted
```
**セキュリティフィルタオプション:**
- `data` (デフォルト): 最強のセキュリティ。危険なファイル、絶対パス、展開ディレクトリ外のパスをブロック
- `tar`: 標準 tar 互換。絶対パスとディレクトリトラバーサルをブロック
- `fully_trusted`: セキュリティ制限なし。完全に信頼できるアーカイブ専用
## Python API
### TzstArchive クラス
```python
from tzst import TzstArchive
# 新規アーカイブ作成
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# 既存アーカイブ読み込み
with TzstArchive("archive.tzst", "r") as archive:
# 内容一覧
contents = archive.list(verbose=True)
# セキュリティフィルタ付き展開
archive.extract("file.txt", "output/", filter="data")
# 整合性テスト
is_valid = archive.test()
# 大容量アーカイブ用ストリーミングモード
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**重要な制限事項:**
- **追加モード非対応**: 複数アーカイブを作成するか、アーカイブ全体を再作成してください
### 便利関数
#### create_archive()
```python
from tzst import create_archive
# アトミック操作で作成 (デフォルト)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# セキュリティ付き展開 (デフォルト: 'data' フィルタ)
extract_archive("backup.tzst", "restore/")
# 特定ファイルのみ展開
extract_archive("backup.tzst", "restore/", members=["config.txt"])
# ディレクトリ構造をフラット化
extract_archive("backup.tzst", "restore/", flatten=True)
# 大容量アーカイブ用ストリーミングモード
extract_archive("large_backup.tzst", "restore/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# シンプルな一覧
files = list_archive("backup.tzst")
# 詳細一覧
files = list_archive("backup.tzst", verbose=True)
# 大容量アーカイブ用ストリーミングモード
files = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# 基本的な整合性テスト
if test_archive("backup.tzst"):
print("アーカイブは有効です")
# ストリーミングでテスト
if test_archive("large_backup.tzst", streaming=True):
print("大容量アーカイブは有効です")
```
## 高度な機能
### ファイル拡張子
ライブラリはインテリジェントな正規化でファイル拡張子を自動処理:
- `.tzst` - tar + zstandard アーカイブの主要拡張子
- `.tar.zst` - 代替標準拡張子
- 既存アーカイブを開く際の自動検出
- アーカイブ作成時の自動拡張子追加
```python
# すべて有効なアーカイブを作成
create_archive("backup.tzst", files) # backup.tzst を作成
create_archive("backup.tar.zst", files) # backup.tar.zst を作成
create_archive("backup", files) # backup.tzst を作成
create_archive("backup.txt", files) # backup.tzst を作成 (正規化)
```
### 圧縮レベル
Zstandard 圧縮レベルは 1 (最速) から 22 (最高圧縮) の範囲:
- **レベル 1-3**: 高速圧縮、ファイルサイズ大
- **レベル 3** (デフォルト): 速度と圧縮率の良いバランス
- **レベル 10-15**: 高圧縮、低速
- **レベル 20-22**: 最高圧縮、大幅に低速
### ストリーミングモード
大容量アーカイブのメモリ効率処理にストリーミングモードを使用:
**利点:**
- メモリ使用量の大幅削減
- メモリに収まらないアーカイブのパフォーマンス向上
- リソースの自動クリーンアップ
**使用推奨ケース:**
- 100 MB を超えるアーカイブ
- メモリ制限環境
- 多数の大容量ファイルを含むアーカイブ処理
```python
# 例: 大容量バックアップアーカイブ処理
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# メモリ効率の良い操作
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### アトミック操作
すべてのファイル作成操作はデフォルトでアトミックファイル操作を使用:
- 一時ファイルでアーカイブ作成後、アトミック移動
- プロセス中断時の自動クリーンアップ
- 破損/不完全なアーカイブのリスクなし
- クロスプラットフォーム互換性
```python
# デフォルトでアトミック操作有効
create_archive("important.tzst", files) # 中断から安全
# 必要時に無効化可能 (非推奨)
create_archive("test.tzst", files, use_temp_file=False)
```
### エラーハンドリング
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("アーカイブの解凍に失敗しました")
except TzstFileNotFoundError:
print("アーカイブファイルが見つかりません")
except KeyboardInterrupt:
print("ユーザーにより操作中断")
# クリーンアップは自動処理
```
## パフォーマンスと比較
### パフォーマンスのヒント
1. **圧縮レベル**: ほとんどのユースケースでレベル 3 が最適
2. **ストリーミング**: 100 MB を超えるアーカイブで使用
3. **バッチ操作**: 単一セッションで複数ファイル追加
4. **ファイルタイプ**: 既に圧縮されたファイルはそれ以上圧縮されない
### 他のツールとの比較
**vs tar + gzip:**
- より高い圧縮率
- 高速な解凍
- モダンなアルゴリズム
**vs tar + xz:**
- 大幅に高速な圧縮
- 同等の圧縮率
- 速度/圧縮率のトレードオフが優れる
**vs zip:**
- より高い圧縮率
- Unix 権限とメタデータを保持
- 優れたストリーミングサポート
## 要件
- Python 3.12 以上
- zstandard >= 0.19.0
## 開発
### 開発環境セットアップ
このプロジェクトはモダンな Python パッケージング標準を使用:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### テスト実行
```bash
# カバレッジ付きテスト実行
pytest --cov=tzst --cov-report=html
# またはシンプルなコマンド (カバレッジ設定は pyproject.toml 内)
pytest
```
### コード品質
```bash
# コード品質チェック
ruff check src tests
# コードフォーマット
ruff format src tests
```
## 貢献
貢献を歓迎します!以下の内容については[貢献ガイド](CONTRIBUTING.md)をお読みください:
- 開発セットアップとプロジェクト構造
- コードスタイルガイドラインとベストプラクティス
- テスト要件とテスト作成
- プルリクエストプロセスとレビューワークフロー
### 貢献者向けクイックスタート
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### 歓迎する貢献の種類
- **バグ修正** - 既存機能の問題修正
- **機能** - ライブラリへの新機能追加
- **ドキュメント** - ドキュメントの改善・追加
- **テスト** - テストカバレッジの追加・改善
- **パフォーマンス** - 既存コードの最適化
- **セキュリティ** - セキュリティ脆弱性への対応
## 謝辞
- [Meta Zstandard](https://github.com/facebook/zstd) - 優れた圧縮アルゴリズム
- [python-zstandard](https://github.com/indygreg/python-zstandard) - Python バインディング
- インスピレーションとフィードバックを提供した Python コミュニティ
## ライセンス
著作権 &copy; [Xi Xu](https://xi-xu.me)。全著作権を保留します。
[BSD 3-Clause](LICENSE) ライセンスのもとで公開されています。
+526
View File
@@ -0,0 +1,526 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | **🇰🇷 한국어** | [🇧🇷 português](./README.pt.md)
**tzst**는 Python 3.12+에서 `.tzst` 및 `.tar.zst` 아카이브를 생성, 추출, 나열, 검사하기 위한 라이브러리이자 CLI입니다. tar 호환성, Zstandard 압축, 스트리밍 처리, 원자적 쓰기, 기본 안전 추출을 운영 환경에 적합한 간결한 인터페이스로 제공합니다.
> [!NOTE]
> 상세 기술 문서: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## 기능
- **고압축률**: 우수한 압축률과 속도를 위한 Zstandard 압축
- **Tar 호환성**: Zstandard로 압축된 표준 tar 아카이브 생성
- **명령줄 인터페이스**: 스트리밍 지원과 포괄적인 옵션을 갖춘 직관적인 CLI
- **Python API**: 프로그램적 사용을 위한 깔끔하고 Python 스타일의 API
- **크로스 플랫폼**: Windows, macOS, Linux에서 작동
- **다중 확장자**: `.tzst` 및 `.tar.zst` 확장자 모두 지원
- **메모리 효율적**: 최소 메모리 사용으로 대용량 아카이브 처리 가능한 스트리밍 모드
- **원자적 작업**: 중단 시 자동 정리 기능을 통한 안전한 파일 작업
- **기본 보안**: 추출 시 최대 보안을 위해 'data' 필터 사용
- **향상된 오류 처리**: 유용한 대안 제시와 함께 명확한 오류 메시지
## 설치
### GitHub 릴리스에서
Python 설치가 필요 없는 독립형 실행 파일 다운로드:
#### 지원 플랫폼
| 플랫폼 | 아키텍처 | 파일 |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{버전}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{버전}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{버전}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{버전}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{버전}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{버전}-darwin-arm64.zip` |
#### 설치 단계
1. [최신 릴리스 페이지](https://github.com/xixu-me/tzst/releases/latest)에서 플랫폼에 맞는 아카이브 **다운로드**
2. 아카이브를 **추출**하여 `tzst` 실행 파일 획득 (Windows는 `tzst.exe`)
3. 실행 파일을 PATH 환경 변수 디렉터리로 **이동**:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: `tzst.exe`가 포함된 디렉터리를 PATH 환경 변수에 추가
4. 설치 **확인**: `tzst --help`
#### 바이너리 설치의 장점
- **Python 불필요** - 독립형 실행 파일
- **빠른 시작** - Python 인터프리터 오버헤드 없음
- **쉬운 배포** - 단일 파일 배포
- **일관된 동작** - 번들링된 의존성
### PyPI에서
pip 사용:
```bash
pip install tzst
```
또는 uv 사용 (권장):
```bash
uv tool install tzst
```
### 소스에서
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### 개발 설치
최신 Python 패키징 표준 사용:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## 빠른 시작
### 명령줄 사용법
```bash
# 아카이브 생성
tzst a archive.tzst file1.txt file2.txt directory/
# 아카이브 추출
tzst x archive.tzst
# 아카이브 내용 목록
tzst l archive.tzst
# 아카이브 무결성 테스트
tzst t archive.tzst
```
### Python API 사용법
```python
from tzst import create_archive, extract_archive, list_archive
# 아카이브 생성
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# 아카이브 추출
extract_archive("archive.tzst", "output_directory/")
# 아카이브 내용 목록
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## 명령줄 인터페이스
### 아카이브 작업
#### 아카이브 생성
```bash
# 기본 사용법
tzst a archive.tzst file1.txt file2.txt
# 압축 레벨 지정 (1-22, 기본값: 3)
tzst a archive.tzst files/ -l 15
# 대체 명령어
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### 아카이브 추출
```bash
# 전체 디렉터리 구조 유지하며 추출
tzst x archive.tzst
# 특정 디렉터리로 추출
tzst x archive.tzst -o output/
# 특정 파일 추출
tzst x archive.tzst file1.txt dir/file2.txt
# 디렉터리 구조 없이 추출 (플랫)
tzst e archive.tzst -o output/
# 대용량 아카이브에 스트리밍 모드 사용
tzst x archive.tzst --streaming -o output/
```
#### 내용 목록
```bash
# 간단한 목록
tzst l archive.tzst
# 상세 정보 포함 목록
tzst l archive.tzst -v
# 대용량 아카이브에 스트리밍 모드 사용
tzst l archive.tzst --streaming -v
```
#### 무결성 테스트
```bash
# 아카이브 무결성 테스트
tzst t archive.tzst
# 스트리밍 모드로 테스트
tzst t archive.tzst --streaming
```
### 명령어 참조
| 명령어 | 별칭 | 설명 | 스트리밍 지원 |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | 아카이브 생성 또는 추가 | N/A |
| `x` | `extract` | 전체 경로로 추출 | ✓ `--streaming` |
| `e` | `extract-flat` | 디렉터리 구조 없이 추출 | ✓ `--streaming` |
| `l` | `list` | 아카이브 내용 목록 | ✓ `--streaming` |
| `t` | `test` | 아카이브 무결성 테스트 | ✓ `--streaming` |
### CLI 옵션
- `-v, --verbose`: 상세 출력 활성화
- `-o, --output DIR`: 출력 디렉터리 지정 (추출 명령어)
- `-l, --level LEVEL`: 압축 레벨 1-22 설정 (생성 명령어)
- `--streaming`: 메모리 효율적 처리를 위한 스트리밍 모드 활성화
- `--filter FILTER`: 추출을 위한 보안 필터 (data/tar/fully_trusted)
- `--no-atomic`: 원자적 파일 작업 비활성화 (권장하지 않음)
### 보안 필터
```bash
# 최대 보안으로 추출 (기본값)
tzst x archive.tzst --filter data
# 표준 tar 호환성으로 추출
tzst x archive.tzst --filter tar
# 완전 신뢰 모드로 추출 (위험 - 신뢰할 수 있는 아카이브 전용)
tzst x archive.tzst --filter fully_trusted
```
**보안 필터 옵션:**
- `data` (기본값): 가장 안전. 위험한 파일, 절대 경로, 추출 디렉터리 외부 경로 차단
- `tar`: 표준 tar 호환성. 절대 경로 및 디렉터리 순회 차단
- `fully_trusted`: 보안 제한 없음. 완전히 신뢰할 수 있는 아카이브에서만 사용
## Python API
### TzstArchive 클래스
```python
from tzst import TzstArchive
# 새 아카이브 생성
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# 기존 아카이브 읽기
with TzstArchive("archive.tzst", "r") as archive:
# 내용 목록
contents = archive.list(verbose=True)
# 보안 필터 적용 추출
archive.extract("file.txt", "output/", filter="data")
# 무결성 테스트
is_valid = archive.test()
# 대용량 아카이브에 스트리밍 모드 사용
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**중요한 제한 사항:**
- **추가 모드 미지원**: 여러 아카이브 생성 또는 전체 아카이브 재생성 필요
### 편의 함수
#### create_archive()
```python
from tzst import create_archive
# 원자적 작업으로 생성 (기본값)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# 보안 추출 (기본값: 'data' 필터)
extract_archive("backup.tzst", "restore/")
# 특정 파일 추출
extract_archive("backup.tzst", "restore/", members=["config.txt"])
# 디렉터리 구조 평탄화
extract_archive("backup.tzst", "restore/", flatten=True)
# 대용량 아카이브에 스트리밍 사용
extract_archive("large_backup.tzst", "restore/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# 간단한 목록
files = list_archive("backup.tzst")
# 상세 목록
files = list_archive("backup.tzst", verbose=True)
# 대용량 아카이브에 스트리밍 사용
files = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# 기본 무결성 테스트
if test_archive("backup.tzst"):
print("아카이브가 유효합니다")
# 스트리밍으로 테스트
if test_archive("large_backup.tzst", streaming=True):
print("대용량 아카이브가 유효합니다")
```
## 고급 기능
### 파일 확장자
라이브러리는 지능적인 정규화로 파일 확장자를 자동 처리합니다:
- `.tzst` - tar+zstandard 아카이브의 기본 확장자
- `.tar.zst` - 대체 표준 확장자
- 기존 아카이브 열 때 자동 감지
- 아카이브 생성 시 자동 확장자 추가
```python
# 모두 유효한 아카이브 생성
create_archive("backup.tzst", files) # backup.tzst 생성
create_archive("backup.tar.zst", files) # backup.tar.zst 생성
create_archive("backup", files) # backup.tzst 생성
create_archive("backup.txt", files) # backup.tzst 생성 (정규화됨)
```
### 압축 레벨
Zstandard 압축 레벨 범위: 1 (가장 빠름) ~ 22 (최대 압축):
- **레벨 1-3**: 빠른 압축, 파일 크기 큼
- **레벨 3** (기본값): 속도와 압축률의 균형
- **레벨 10-15**: 더 나은 압축, 느림
- **레벨 20-22**: 최대 압축, 매우 느림
### 스트리밍 모드
대용량 아카이브의 메모리 효율적 처리를 위해 스트리밍 모드 사용:
**장점:**
- 메모리 사용량 현저히 감소
- 메모리에 맞지 않는 대용량 아카이브 처리 성능 향상
- 리소스 자동 정리
**사용 시기:**
- 100MB 이상의 아카이브
- 메모리가 제한된 환경
- 대용량 파일이 많은 아카이브 처리
```python
# 예제: 대용량 백업 아카이브 처리
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# 메모리 효율적 작업
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### 원자적 작업
모든 파일 생성 작업은 기본적으로 원자적 파일 작업을 사용합니다:
- 임시 파일에 먼저 생성 후 원자적 이동
- 프로세스 중단 시 자동 정리
- 손상되거나 불완전한 아카이브 위험 없음
- 크로스 플랫폼 호환성
```python
# 기본적으로 원자적 작업 활성화
create_archive("important.tzst", files) # 중단으로부터 안전
# 필요한 경우 비활성화 가능 (권장하지 않음)
create_archive("test.tzst", files, use_temp_file=False)
```
### 오류 처리
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("아카이브 압축 해제 실패")
except TzstFileNotFoundError:
print("아카이브 파일을 찾을 수 없음")
except KeyboardInterrupt:
print("사용자에 의해 작업 중단됨")
# 자동으로 정리됨
```
## 성능 및 비교
### 성능 팁
1. **압축 레벨**: 대부분의 경우 레벨 3이 최적
2. **스트리밍**: 100MB 이상 아카이브에 사용
3. **일괄 작업**: 단일 세션에서 여러 파일 추가
4. **파일 유형**: 이미 압축된 파일은 추가 압축이 거의 안됨
### 다른 도구와 비교
**vs tar + gzip:**
- 더 나은 압축률
- 더 빠른 압축 해제
- 현대적인 알고리즘
**vs tar + xz:**
- 현저히 빠른 압축
- 유사한 압축률
- 더 나은 속도/압축률 균형
**vs zip:**
- 더 나은 압축
- Unix 권한 및 메타데이터 보존
- 더 나은 스트리밍 지원
## 요구 사항
- Python 3.12 이상
- zstandard >= 0.19.0
## 개발
### 개발 환경 설정
최신 Python 패키징 표준 사용:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### 테스트 실행
```bash
# 커버리지 포함 테스트 실행
pytest --cov=tzst --cov-report=html
# 또는 간단한 명령어 사용 (커버리지 설정은 pyproject.toml에 있음)
pytest
```
### 코드 품질
```bash
# 코드 품질 확인
ruff check src tests
# 코드 포맷팅
ruff format src tests
```
## 기여
기여를 환영합니다! 다음 사항을 위해 [기여 가이드](CONTRIBUTING.md)를 읽어주세요:
- 개발 설정 및 프로젝트 구조
- 코드 스타일 가이드라인 및 모범 사례
- 테스트 요구 사항 및 테스트 작성 방법
- 풀 리퀘스트 프로세스 및 리뷰 워크플로
### 기여자 빠른 시작
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### 환영하는 기여 유형
- **버그 수정** - 기존 기능의 문제 해결
- **기능** - 라이브러리에 새로운 기능 추가
- **문서** - 문서 개선 또는 추가
- **테스트** - 테스트 커버리지 추가 또는 개선
- **성능** - 기존 코드 최적화
- **보안** - 보안 취약점 해결
## 감사의 말
- 우수한 압축 알고리즘을 제공한 [Meta Zstandard](https://github.com/facebook/zstd)
- Python 바인딩을 제공한 [python-zstandard](https://github.com/indygreg/python-zstandard)
- 영감과 피드백을 준 Python 커뮤니티
## 라이선스
저작권 &copy; [시 쉬](https://xi-xu.me). 모든 권리 보유.
[BSD 3-Clause](LICENSE) 라이선스로 사용이 허가되었습니다.
+67 -33
View File
@@ -1,13 +1,25 @@
# tzst
> [!TIP]
> 欢迎加入“Xget 开源与 AI 交流群”,一起交流开源项目、AI 应用、工程实践、效率工具和独立开发;如果你也在做产品、写代码、折腾项目或者对开源和 AI 感兴趣,欢迎[**进群**](https://file.xi-xu.me/QR%20Codes/%E7%BE%A4%E4%BA%8C%E7%BB%B4%E7%A0%81.png)认识更多认真做事、乐于分享的朋友。
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
**tzst** is a next-generation Python library engineered for modern archive management, leveraging cutting-edge Zstandard compression to deliver superior performance, security, and reliability. Built exclusively for Python 3.12+, this enterprise-grade solution combines atomic operations, streaming efficiency, and a meticulously crafted API to redefine how developers handle `.tzst`/`.tar.zst` archives in production environments.
**🇺🇸 English** | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
**tzst** is a Python 3.12+ library and CLI for creating, extracting, listing, and testing `.tzst` and `.tar.zst` archives. It combines tar compatibility, Zstandard compression, streaming support, atomic writes, and safe-by-default extraction in a compact, production-ready interface.
> [!NOTE]
> In-depth technical article: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## Features
@@ -24,12 +36,51 @@
## Installation
### From GitHub Releases
Download standalone executables that don't require Python installation:
#### Supported Platforms
| Platform | Architecture | File |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{version}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{version}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{version}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{version}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{version}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{version}-darwin-arm64.zip` |
#### Installation Steps
1. **Download** the appropriate archive for your platform from the [latest releases page](https://github.com/xixu-me/tzst/releases/latest)
2. **Extract** the archive to get the `tzst` executable (or `tzst.exe` on Windows)
3. **Move** the executable to a directory in your PATH:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: Add the directory containing `tzst.exe` to your PATH environment variable
4. **Verify** installation: `tzst --help`
#### Benefits of Binary Installation
- **No Python required** - Standalone executable
- **Faster startup** - No Python interpreter overhead
- **Easy deployment** - Single file distribution
- **Consistent behavior** - Bundled dependencies
### From PyPI
Using pip:
```bash
pip install tzst
```
Or using uv (recommended):
```bash
uv tool install tzst
```
### From Source
```bash
@@ -40,7 +91,7 @@ pip install .
### Development Installation
This project uses [Hatch](https://hatch.pypa.io/) as the build system:
This project uses modern Python packaging standards:
```bash
git clone https://github.com/xixu-me/tzst.git
@@ -48,19 +99,10 @@ cd tzst
pip install -e .[dev]
```
Alternatively, with [Hatch](https://hatch.pypa.io/) installed:
```bash
hatch env create
hatch shell
```
## Quick Start
### Command Line Usage
> **Recommended**: Use `uvx tzst` for running without installation and better performance. See [uv documentation](https://docs.astral.sh/uv/) for details.
```bash
# Create an archive
tzst a archive.tzst file1.txt file2.txt directory/
@@ -412,14 +454,14 @@ except KeyboardInterrupt:
## Requirements
- Python 3.12 or higher
- Python 3.12 or higher (tested on 3.12-3.14)
- zstandard >= 0.19.0
## Development
### Setting up Development Environment
This project uses **Hatch** as the build system:
This project uses modern Python packaging standards:
```bash
git clone https://github.com/xixu-me/tzst.git
@@ -427,22 +469,14 @@ cd tzst
pip install -e .[dev]
```
Or with Hatch:
```bash
pip install hatch
hatch env create
hatch shell
```
### Running Tests
```bash
# Using pytest
# Run tests with coverage
pytest --cov=tzst --cov-report=html
# Using Hatch
hatch run pytest --cov=tzst --cov-report=html
# Or use the simpler command (coverage settings are in pyproject.toml)
pytest
```
### Code Quality
@@ -460,7 +494,7 @@ ruff format src tests
We welcome contributions! Please read our [Contributing Guide](CONTRIBUTING.md) for:
- Development setup and project structure
- Code style guidelines and best practices
- Code style guidelines and best practices
- Testing requirements and writing tests
- Pull request process and review workflow
@@ -475,12 +509,12 @@ python -m pytest tests/
### Types of Contributions Welcome
- 🐛 **Bug fixes** - Fix issues in existing functionality
- ✨ **Features** - Add new capabilities to the library
- 📚 **Documentation** - Improve or add documentation
- 🧪 **Tests** - Add or improve test coverage
- ⚡ **Performance** - Optimize existing code
- 🔒 **Security** - Address security vulnerabilities
- **Bug fixes** - Fix issues in existing functionality
- **Features** - Add new capabilities to the library
- **Documentation** - Improve or add documentation
- **Tests** - Add or improve test coverage
- **Performance** - Optimize existing code
- **Security** - Address security vulnerabilities
## Acknowledgments
@@ -490,6 +524,6 @@ python -m pytest tests/
## License
Copyright &copy; 2025 [Xi Xu](https://xi-xu.me). All rights reserved.
Copyright &copy; [Xi Xu](https://xi-xu.me). All rights reserved.
Licensed under the [BSD 3-Clause](LICENSE) license.
+526
View File
@@ -0,0 +1,526 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | **🇧🇷 português**
**tzst** é uma biblioteca e CLI para Python 3.12+ voltada para criar, extrair, listar e validar arquivos `.tzst` e `.tar.zst`. Ela combina compatibilidade com tar, compressão Zstandard, modo streaming, gravações atômicas e extração segura por padrão em uma interface compacta pronta para produção.
> [!NOTE]
> Artigo técnico detalhado: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## Recursos
- **Alta Compressão**: Compressão Zstandard para excelentes taxas de compressão e velocidade
- **Compatibilidade com Tar**: Cria arquivos tar padrão comprimidos com Zstandard
- **Interface de Linha de Comando**: CLI intuitiva com suporte a streaming e opções abrangentes
- **API Python**: API limpa e pythônica para uso programático
- **Multiplataforma**: Funciona no Windows, macOS e Linux
- **Múltiplas Extensões**: Suporta tanto extensões `.tzst` quanto `.tar.zst`
- **Eficiente em Memória**: Modo streaming para lidar com grandes arquivos com uso mínimo de memória
- **Operações Atômicas**: Operações de arquivo seguras com limpeza automática em caso de interrupção
- **Seguro por Padrão**: Usa o filtro 'data' para máxima segurança durante a extração
- **Tratamento de Erros Aprimorado**: Mensagens de erro claras com alternativas úteis
## Instalação
### Dos Releases do GitHub
Baixe executáveis independentes que não requerem instalação do Python:
#### Plataformas Suportadas
| Plataforma | Arquitetura | Arquivo |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{versão}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{versão}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{versão}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{versão}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{versão}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{versão}-darwin-arm64.zip` |
#### Passos de Instalação
1. **Baixe** o arquivo apropriado para sua plataforma da [página de releases mais recentes](https://github.com/xixu-me/tzst/releases/latest)
2. **Extraia** o arquivo para obter o executável `tzst` (ou `tzst.exe` no Windows)
3. **Mova** o executável para um diretório em seu PATH:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: Adicione o diretório contendo `tzst.exe` à sua variável de ambiente PATH
4. **Verifique** a instalação: `tzst --help`
#### Benefícios da Instalação Binária
- **Python não é necessário** - Executável independente
- **Inicialização mais rápida** - Sem overhead do interpretador Python
- **Implantação fácil** - Distribuição de arquivo único
- **Comportamento consistente** - Dependências incluídas
### Do PyPI
Usando pip:
```bash
pip install tzst
```
Ou usando uv (recomendado):
```bash
uv tool install tzst
```
### Do Código Fonte
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### Instalação para Desenvolvimento
Este projeto usa padrões modernos de empacotamento Python:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## Início Rápido
### Uso da Linha de Comando
```bash
# Criar um arquivo
tzst a archive.tzst file1.txt file2.txt directory/
# Extrair um arquivo
tzst x archive.tzst
# Listar conteúdo do arquivo
tzst l archive.tzst
# Testar integridade do arquivo
tzst t archive.tzst
```
### Uso da API Python
```python
from tzst import create_archive, extract_archive, list_archive
# Criar um arquivo
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# Extrair um arquivo
extract_archive("archive.tzst", "output_directory/")
# Listar conteúdo do arquivo
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## Interface de Linha de Comando
### Operações de Arquivo
#### Criar Arquivo
```bash
# Uso básico
tzst a archive.tzst file1.txt file2.txt
# Com nível de compressão (1-22, padrão: 3)
tzst a archive.tzst files/ -l 15
# Comandos alternativos
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### Extrair Arquivo
```bash
# Extrair com estrutura completa de diretórios
tzst x archive.tzst
# Extrair para diretório específico
tzst x archive.tzst -o output/
# Extrair arquivos específicos
tzst x archive.tzst file1.txt dir/file2.txt
# Extrair sem estrutura de diretórios (plano)
tzst e archive.tzst -o output/
# Usar modo streaming para grandes arquivos
tzst x archive.tzst --streaming -o output/
```
#### Listar Conteúdo
```bash
# Listagem simples
tzst l archive.tzst
# Listagem detalhada com informações
tzst l archive.tzst -v
# Usar modo streaming para grandes arquivos
tzst l archive.tzst --streaming -v
```
#### Testar Integridade
```bash
# Testar integridade do arquivo
tzst t archive.tzst
# Testar com modo streaming
tzst t archive.tzst --streaming
```
### Referência de Comandos
| Comando | Aliases | Descrição | Suporte a Streaming |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | Criar ou adicionar ao arquivo | N/A |
| `x` | `extract` | Extrair com caminhos completos | ✓ `--streaming` |
| `e` | `extract-flat` | Extrair sem estrutura de diretórios | ✓ `--streaming` |
| `l` | `list` | Listar conteúdo do arquivo | ✓ `--streaming` |
| `t` | `test` | Testar integridade do arquivo | ✓ `--streaming` |
### Opções da CLI
- `-v, --verbose`: Ativar saída detalhada
- `-o, --output DIR`: Especificar diretório de saída (comandos de extração)
- `-l, --level LEVEL`: Definir nível de compressão 1-22 (comando de criação)
- `--streaming`: Ativar modo streaming para processamento eficiente em memória
- `--filter FILTER`: Filtro de segurança para extração (data/tar/fully_trusted)
- `--no-atomic`: Desativar operações de arquivo atômicas (não recomendado)
### Filtros de Segurança
```bash
# Extrair com máxima segurança (padrão)
tzst x archive.tzst --filter data
# Extrair com compatibilidade tar padrão
tzst x archive.tzst --filter tar
# Extrair com confiança total (perigoso - apenas para arquivos confiáveis)
tzst x archive.tzst --filter fully_trusted
```
**Opções de Filtro de Segurança:**
- `data` (padrão): Mais seguro. Bloqueia arquivos perigosos, caminhos absolutos e caminhos fora do diretório de extração
- `tar`: Compatibilidade tar padrão. Bloqueia caminhos absolutos e travessia de diretórios
- `fully_trusted`: Sem restrições de segurança. Use apenas com arquivos completamente confiáveis
## API Python
### Classe TzstArchive
```python
from tzst import TzstArchive
# Criar um novo arquivo
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# Ler um arquivo existente
with TzstArchive("archive.tzst", "r") as archive:
# Listar conteúdo
contents = archive.list(verbose=True)
# Extrair com filtro de segurança
archive.extract("file.txt", "output/", filter="data")
# Testar integridade
is_valid = archive.test()
# Para grandes arquivos, usar modo streaming
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**Limitações Importantes:**
- **Modo de anexação não suportado**: Crie múltiplos arquivos ou recrie o arquivo inteiro em vez disso
### Funções de Conveniência
#### create_archive()
```python
from tzst import create_archive
# Criar com operações atômicas (padrão)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# Extrair com segurança (padrão: filtro 'data')
extract_archive("backup.tzst", "restore/")
# Extrair arquivos específicos
extract_archive("backup.tzst", "restore/", members=["config.txt"])
# Achatar estrutura de diretórios
extract_archive("backup.tzst", "restore/", flatten=True)
# Usar streaming para grandes arquivos
extract_archive("large_backup.tzst", "restore/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# Listagem simples
files = list_archive("backup.tzst")
# Listagem detalhada
files = list_archive("backup.tzst", verbose=True)
# Streaming para grandes arquivos
files = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# Teste básico de integridade
if test_archive("backup.tzst"):
print("Arquivo é válido")
# Testar com streaming
if test_archive("large_backup.tzst", streaming=True):
print("Grande arquivo é válido")
```
## Recursos Avançados
### Extensões de Arquivo
A biblioteca automaticamente lida com extensões de arquivo com normalização inteligente:
- `.tzst` - Extensão primária para arquivos tar+zstandard
- `.tar.zst` - Extensão padrão alternativa
- Detecção automática ao abrir arquivos existentes
- Adição automática de extensão ao criar arquivos
```python
# Todos estes criam arquivos válidos
create_archive("backup.tzst", files) # Cria backup.tzst
create_archive("backup.tar.zst", files) # Cria backup.tar.zst
create_archive("backup", files) # Cria backup.tzst
create_archive("backup.txt", files) # Cria backup.tzst (normalizado)
```
### Níveis de Compressão
Os níveis de compressão Zstandard variam de 1 (mais rápido) a 22 (melhor compressão):
- **Nível 1-3**: Compressão rápida, arquivos maiores
- **Nível 3** (padrão): Bom equilíbrio entre velocidade e compressão
- **Nível 10-15**: Melhor compressão, mais lento
- **Nível 20-22**: Compressão máxima, muito mais lento
### Modo Streaming
Use o modo streaming para processamento eficiente em memória de grandes arquivos:
**Benefícios:**
- Uso de memória significativamente reduzido
- Melhor desempenho para arquivos que não cabem na memória
- Limpeza automática de recursos
**Quando usar:**
- Arquivos maiores que 100MB
- Ambientes com memória limitada
- Processamento de arquivos com muitos arquivos grandes
```python
# Exemplo: Processando um grande arquivo de backup
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# Operações eficientes em memória
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### Operações Atômicas
Todas as operações de criação de arquivo usam operações de arquivo atômicas por padrão:
- Arquivos criados em arquivos temporários primeiro, depois movidos atomicamente
- Limpeza automática se o processo for interrompido
- Nenhum risco de arquivos corrompidos ou incompletos
- Compatibilidade multiplataforma
```python
# Operações atômicas habilitadas por padrão
create_archive("important.tzst", files) # Seguro contra interrupção
# Pode ser desabilitado se necessário (não recomendado)
create_archive("test.tzst", files, use_temp_file=False)
```
### Tratamento de Erros
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("Falha ao descomprimir arquivo")
except TzstFileNotFoundError:
print("Arquivo de arquivo não encontrado")
except KeyboardInterrupt:
print("Operação interrompida pelo usuário")
# Limpeza é tratada automaticamente
```
## Desempenho e Comparação
### Dicas de Desempenho
1. **Níveis de compressão**: Nível 3 é ótimo para a maioria dos casos de uso
2. **Streaming**: Use para arquivos maiores que 100MB
3. **Operações em lote**: Adicione múltiplos arquivos em uma única sessão
4. **Tipos de arquivo**: Arquivos já comprimidos não comprimirão muito mais
### vs Outras Ferramentas
**vs tar + gzip:**
- Melhores taxas de compressão
- Descompressão mais rápida
- Algoritmo moderno
**vs tar + xz:**
- Compressão significativamente mais rápida
- Taxas de compressão similares
- Melhor compromisso velocidade/compressão
**vs zip:**
- Melhor compressão
- Preserva permissões Unix e metadados
- Melhor suporte a streaming
## Requisitos
- Python 3.12 ou superior
- zstandard >= 0.19.0
## Desenvolvimento
### Configurando Ambiente de Desenvolvimento
Este projeto usa padrões modernos de empacotamento Python:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### Executando Testes
```bash
# Executar testes com cobertura
pytest --cov=tzst --cov-report=html
# Ou usar o comando mais simples (configurações de cobertura estão em pyproject.toml)
pytest
```
### Qualidade do Código
```bash
# Verificar qualidade do código
ruff check src tests
# Formatar código
ruff format src tests
```
## Contribuindo
Nós recebemos contribuições! Por favor, leia nosso [Guia de Contribuição](CONTRIBUTING.md) para:
- Configuração de desenvolvimento e estrutura do projeto
- Diretrizes de estilo de código e melhores práticas
- Requisitos de teste e escrita de testes
- Processo de pull request e fluxo de revisão
### Início Rápido para Colaboradores
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### Tipos de Contribuições Bem-vindas
- **Correções de bugs** - Corrigir problemas na funcionalidade existente
- **Recursos** - Adicionar novas capacidades à biblioteca
- **Documentação** - Melhorar ou adicionar documentação
- **Testes** - Adicionar ou melhorar cobertura de testes
- **Desempenho** - Otimizar código existente
- **Segurança** - Abordar vulnerabilidades de segurança
## Agradecimentos
- [Meta Zstandard](https://github.com/facebook/zstd) pelo excelente algoritmo de compressão
- [python-zstandard](https://github.com/indygreg/python-zstandard) pelas ligações Python
- A comunidade Python pela inspiração e feedback
## Licença
Direitos autorais &copy; [Xi Xu](https://xi-xu.me). Todos os direitos reservados.
Licenciado sob a licença [BSD 3-Clause](LICENSE).
+526
View File
@@ -0,0 +1,526 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | [🇨🇳 汉语](./README.zh.md) | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | **🇷🇺 русский** | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
**tzst** — это библиотека и CLI для Python 3.12+, предназначенные для создания, извлечения, просмотра и проверки архивов `.tzst` и `.tar.zst`. Она объединяет совместимость с tar, сжатие Zstandard, потоковый режим, атомарную запись и безопасное извлечение по умолчанию в компактном интерфейсе для production.
> [!NOTE]
> Подробная техническая статья: **[Deep Dive into tzst: A Modern Python Archiving Library Based on Zstandard](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst-en.html)**.
## Особенности
- **Высокое сжатие**: Сжатие Zstandard для отличных коэффициентов сжатия и скорости
- **Совместимость с Tar**: Создаёт стандартные tar-архивы, сжатые с помощью Zstandard
- **Интерфейс командной строки**: Интуитивный CLI с поддержкой потоковой передачи и всесторонними опциями
- **Python API**: Чистый, pythonic API для программного использования
- **Кроссплатформенность**: Работает на Windows, macOS и Linux
- **Множественные расширения**: Поддерживает как `.tzst`, так и `.tar.zst` расширения
- **Эффективность памяти**: Режим потоковой передачи для обработки больших архивов с минимальным использованием памяти
- **Атомарные операции**: Безопасные файловые операции с автоматической очисткой при прерывании
- **Безопасность по умолчанию**: Использует фильтр 'data' для максимальной безопасности при извлечении
- **Улучшенная обработка ошибок**: Чёткие сообщения об ошибках с полезными альтернативами
## Установка
### Из релизов GitHub
Скачайте автономные исполняемые файлы, которые не требуют установки Python:
#### Поддерживаемые платформы
| Платформа | Архитектура | Файл |
|----------|-------------|------|
| **Linux** | x86_64 | `tzst-{версия}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{версия}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{версия}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{версия}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{версия}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{версия}-darwin-arm64.zip` |
#### Шаги установки
1. **Скачайте** подходящий архив для вашей платформы со [страницы последних релизов](https://github.com/xixu-me/tzst/releases/latest)
2. **Извлеките** архив, чтобы получить исполняемый файл `tzst` (или `tzst.exe` на Windows)
3. **Переместите** исполняемый файл в директорию в вашем PATH:
- **Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **Windows**: Добавьте директорию, содержащую `tzst.exe`, в переменную окружения PATH
4. **Проверьте** установку: `tzst --help`
#### Преимущества бинарной установки
- **Python не требуется** - Автономный исполняемый файл
- **Быстрый запуск** - Нет накладных расходов интерпретатора Python
- **Лёгкое развёртывание** - Распространение одним файлом
- **Последовательное поведение** - Встроенные зависимости
### Из PyPI
Используя pip:
```bash
pip install tzst
```
Или используя uv (рекомендуется):
```bash
uv tool install tzst
```
### Из исходного кода
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### Установка для разработки
Этот проект использует современные стандарты упаковки Python:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## Быстрый старт
### Использование командной строки
```bash
# Создать архив
tzst a archive.tzst file1.txt file2.txt directory/
# Извлечь архив
tzst x archive.tzst
# Список содержимого архива
tzst l archive.tzst
# Проверить целостность архива
tzst t archive.tzst
```
### Использование Python API
```python
from tzst import create_archive, extract_archive, list_archive
# Создать архив
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# Извлечь архив
extract_archive("archive.tzst", "output_directory/")
# Список содержимого архива
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## Интерфейс командной строки
### Операции с архивами
#### Создать архив
```bash
# Базовое использование
tzst a archive.tzst file1.txt file2.txt
# С уровнем сжатия (1-22, по умолчанию: 3)
tzst a archive.tzst files/ -l 15
# Альтернативные команды
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### Извлечь архив
```bash
# Извлечь с полной структурой директорий
tzst x archive.tzst
# Извлечь в определённую директорию
tzst x archive.tzst -o output/
# Извлечь определённые файлы
tzst x archive.tzst file1.txt dir/file2.txt
# Извлечь без структуры директорий (плоско)
tzst e archive.tzst -o output/
# Использовать режим потоковой передачи для больших архивов
tzst x archive.tzst --streaming -o output/
```
#### Список содержимого
```bash
# Простой список
tzst l archive.tzst
# Подробный список с деталями
tzst l archive.tzst -v
# Использовать режим потоковой передачи для больших архивов
tzst l archive.tzst --streaming -v
```
#### Проверка целостности
```bash
# Проверить целостность архива
tzst t archive.tzst
# Проверить с режимом потоковой передачи
tzst t archive.tzst --streaming
```
### Справочник команд
| Команда | Псевдонимы | Описание | Поддержка потоковой передачи |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | Создать или добавить в архив | N/A |
| `x` | `extract` | Извлечь с полными путями | ✓ `--streaming` |
| `e` | `extract-flat` | Извлечь без структуры директорий | ✓ `--streaming` |
| `l` | `list` | Список содержимого архива | ✓ `--streaming` |
| `t` | `test` | Проверить целостность архива | ✓ `--streaming` |
### Опции CLI
- `-v, --verbose`: Включить подробный вывод
- `-o, --output DIR`: Указать выходную директорию (команды извлечения)
- `-l, --level LEVEL`: Установить уровень сжатия 1-22 (команда создания)
- `--streaming`: Включить режим потоковой передачи для эффективной обработки памяти
- `--filter FILTER`: Фильтр безопасности для извлечения (data/tar/fully_trusted)
- `--no-atomic`: Отключить атомарные файловые операции (не рекомендуется)
### Фильтры безопасности
```bash
# Извлечь с максимальной безопасностью (по умолчанию)
tzst x archive.tzst --filter data
# Извлечь со стандартной совместимостью tar
tzst x archive.tzst --filter tar
# Извлечь с полным доверием (опасно - только для доверенных архивов)
tzst x archive.tzst --filter fully_trusted
```
**Опции фильтра безопасности:**
- `data` (по умолчанию): Наиболее безопасно. Блокирует опасные файлы, абсолютные пути и пути вне директории извлечения
- `tar`: Стандартная совместимость tar. Блокирует абсолютные пути и обход директорий
- `fully_trusted`: Никаких ограничений безопасности. Используйте только с полностью доверенными архивами
## Python API
### Класс TzstArchive
```python
from tzst import TzstArchive
# Создать новый архив
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# Прочитать существующий архив
with TzstArchive("archive.tzst", "r") as archive:
# Список содержимого
contents = archive.list(verbose=True)
# Извлечь с фильтром безопасности
archive.extract("file.txt", "output/", filter="data")
# Проверить целостность
is_valid = archive.test()
# Для больших архивов используйте режим потоковой передачи
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**Важные ограничения:**
- **Режим добавления не поддерживается**: Создавайте множественные архивы или пересоздавайте весь архив вместо этого
### Удобные функции
#### create_archive()
```python
from tzst import create_archive
# Создать с атомарными операциями (по умолчанию)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# Извлечь с безопасностью (по умолчанию: фильтр 'data')
extract_archive("backup.tzst", "restore/")
# Извлечь определённые файлы
extract_archive("backup.tzst", "restore/", members=["config.txt"])
# Сплющить структуру директорий
extract_archive("backup.tzst", "restore/", flatten=True)
# Использовать потоковую передачу для больших архивов
extract_archive("large_backup.tzst", "restore/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# Простой список
files = list_archive("backup.tzst")
# Подробный список
files = list_archive("backup.tzst", verbose=True)
# Потоковая передача для больших архивов
files = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# Базовая проверка целостности
if test_archive("backup.tzst"):
print("Архив действителен")
# Проверка с потоковой передачей
if test_archive("large_backup.tzst", streaming=True):
print("Большой архив действителен")
```
## Продвинутые возможности
### Расширения файлов
Библиотека автоматически обрабатывает расширения файлов с интеллектуальной нормализацией:
- `.tzst` - Основное расширение для архивов tar+zstandard
- `.tar.zst` - Альтернативное стандартное расширение
- Автоопределение при открытии существующих архивов
- Автоматическое добавление расширения при создании архивов
```python
# Все это создаёт действительные архивы
create_archive("backup.tzst", files) # Создаёт backup.tzst
create_archive("backup.tar.zst", files) # Создаёт backup.tar.zst
create_archive("backup", files) # Создаёт backup.tzst
create_archive("backup.txt", files) # Создаёт backup.tzst (нормализовано)
```
### Уровни сжатия
Уровни сжатия Zstandard варьируются от 1 (самый быстрый) до 22 (лучшее сжатие):
- **Уровень 1-3**: Быстрое сжатие, большие файлы
- **Уровень 3** (по умолчанию): Хороший баланс скорости и сжатия
- **Уровень 10-15**: Лучшее сжатие, медленнее
- **Уровень 20-22**: Максимальное сжатие, намного медленнее
### Режим потоковой передачи
Используйте режим потоковой передачи для эффективной обработки больших архивов в памяти:
**Преимущества:**
- Значительно сниженное использование памяти
- Лучшая производительность для архивов, которые не помещаются в память
- Автоматическая очистка ресурсов
**Когда использовать:**
- Архивы больше 100MB
- Среды с ограниченной памятью
- Обработка архивов с множеством больших файлов
```python
# Пример: Обработка большого архива резервной копии
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# Операции, эффективные по памяти
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore/", streaming=True)
```
### Атомарные операции
Все операции создания файлов используют атомарные файловые операции по умолчанию:
- Архивы создаются сначала во временных файлах, затем атомарно перемещаются
- Автоматическая очистка при прерывании процесса
- Никакого риска повреждённых или неполных архивов
- Кроссплатформенная совместимость
```python
# Атомарные операции включены по умолчанию
create_archive("important.tzst", files) # Безопасно от прерывания
# Может быть отключено при необходимости (не рекомендуется)
create_archive("test.tzst", files, use_temp_file=False)
```
### Обработка ошибок
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("Не удалось распаковать архив")
except TzstFileNotFoundError:
print("Файл архива не найден")
except KeyboardInterrupt:
print("Операция прервана пользователем")
# Очистка обрабатывается автоматически
```
## Производительность и сравнение
### Советы по производительности
1. **Уровни сжатия**: Уровень 3 оптимален для большинства случаев использования
2. **Потоковая передача**: Используйте для архивов больше 100MB
3. **Пакетные операции**: Добавляйте множественные файлы в одной сессии
4. **Типы файлов**: Уже сжатые файлы не будут сжиматься намного дальше
### против других инструментов
**против tar + gzip:**
- Лучшие коэффициенты сжатия
- Быстрее распаковка
- Современный алгоритм
**против tar + xz:**
- Значительно быстрее сжатие
- Похожие коэффициенты сжатия
- Лучший компромисс скорость/сжатие
**против zip:**
- Лучшее сжатие
- Сохраняет разрешения Unix и метаданные
- Лучшая поддержка потоковой передачи
## Требования
- Python 3.12 или выше
- zstandard >= 0.19.0
## Разработка
### Настройка среды разработки
Этот проект использует современные стандарты упаковки Python:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### Запуск тестов
```bash
# Запустить тесты с покрытием
pytest --cov=tzst --cov-report=html
# Или использовать более простую команду (настройки покрытия в pyproject.toml)
pytest
```
### Качество кода
```bash
# Проверить качество кода
ruff check src tests
# Форматировать код
ruff format src tests
```
## Вклад
Мы приветствуем вклады! Пожалуйста, прочитайте наше [Руководство по вкладу](CONTRIBUTING.md) для:
- Настройки разработки и структуры проекта
- Руководящих принципов стиля кода и лучших практик
- Требований к тестированию и написанию тестов
- Процесса pull request'ов и рабочего процесса обзора
### Быстрый старт для участников
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### Типы приветствуемых вкладов
- **Исправления ошибок** - Исправить проблемы в существующей функциональности
- **Возможности** - Добавить новые возможности в библиотеку
- **Документация** - Улучшить или добавить документацию
- **Тесты** - Добавить или улучшить покрытие тестами
- **Производительность** - Оптимизировать существующий код
- **Безопасность** - Устранить уязвимости безопасности
## Благодарности
- [Meta Zstandard](https://github.com/facebook/zstd) за отличный алгоритм сжатия
- [python-zstandard](https://github.com/indygreg/python-zstandard) за связи Python
- Сообществу Python за вдохновение и обратную связь
## Лицензия
Авторские права &copy; [Си Сюй](https://xi-xu.me). Все права защищены.
Лицензировано под лицензией [BSD 3-Clause](LICENSE).
+525
View File
@@ -0,0 +1,525 @@
> [!TIP]
> 欢迎加入“Xget 开源与 AI 交流群”,一起交流开源项目、AI 应用、工程实践、效率工具和独立开发;如果你也在做产品、写代码、折腾项目或者对开源和 AI 感兴趣,欢迎[**进群**](https://file.xi-xu.me/QR%20Codes/%E7%BE%A4%E4%BA%8C%E7%BB%B4%E7%A0%81.png)认识更多认真做事、乐于分享的朋友。
<h1 align="center">
<img src="https://raw.githubusercontent.com/xixu-me/tzst/refs/heads/main/docs/_static/tzst-logo.png" width="300">
</h1><br>
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://tzst.xi-xu.me)
[🇺🇸 English](./README.md) | **🇨🇳 汉语** | [🇪🇸 español](./README.es.md) | [🇯🇵 日本語](./README.ja.md) | [🇦🇪 العربية](./README.ar.md) | [🇷🇺 русский](./README.ru.md) | [🇩🇪 Deutsch](./README.de.md) | [🇫🇷 français](./README.fr.md) | [🇰🇷 한국어](./README.ko.md) | [🇧🇷 português](./README.pt.md)
**tzst** 是一个面向 Python 3.12+ 的归档库和命令行工具,用于创建、提取、列出和校验 `.tzst` 与 `.tar.zst` 归档。它将 tar 兼容性、Zstandard 压缩、流式处理、原子写入和默认安全提取整合为一套适合生产环境的简洁接口。
> [!NOTE]
> 技术深度解析:**[《深入解析 tzst:一个基于 Zstandard 的现代 Python 归档库》](https://blog.xi-xu.me/2025/11/01/deep-dive-into-tzst.html)**。
## 功能特性
- **高效压缩**:采用 Zstandard 压缩算法,实现优异的压缩率和速度
- **Tar 兼容性**:创建符合标准的 tar 归档并使用 Zstandard 压缩
- **命令行界面**:直观的 CLI,支持流式处理和全面选项
- **Python API**:简洁、符合 Python 风格的编程接口
- **跨平台支持**:兼容 Windows、macOS 和 Linux
- **多扩展名支持**:同时支持 `.tzst` 和 `.tar.zst` 扩展名
- **内存高效**:流模式可高效处理大型归档文件
- **原子操作**:安全的文件操作,中断时自动清理
- **默认安全**:提取时使用 'data' 过滤器确保最高安全性
- **增强的错误处理**:清晰的错误信息和实用建议
## 安装指南
### 从 GitHub Releases 安装
下载无需 Python 环境的独立可执行文件:
#### 支持平台
| 平台 | 架构 | 文件 |
|------|------|------|
| **Linux** | x86_64 | `tzst-{版本}-linux-amd64.zip` |
| **Linux** | ARM64 | `tzst-{版本}-linux-arm64.zip` |
| **Windows** | x64 | `tzst-{版本}-windows-amd64.zip` |
| **Windows** | ARM64 | `tzst-{版本}-windows-arm64.zip` |
| **macOS** | Intel | `tzst-{版本}-darwin-amd64.zip` |
| **macOS** | Apple Silicon | `tzst-{版本}-darwin-arm64.zip` |
#### 安装步骤
1. **下载**:从[最新发布页面](https://github.com/xixu-me/tzst/releases/latest)下载适合您平台的压缩包
2. **解压**:解压获取 `tzst` 可执行文件(Windows 为 `tzst.exe`)
3. **移动**:将可执行文件添加到 PATH 环境变量:
- **Linux/macOS**:`sudo mv tzst /usr/local/bin/`
- **Windows**:将包含 `tzst.exe` 的目录添加到 PATH
4. **验证**:运行 `tzst --help` 确认安装成功
#### 二进制安装优势
- **无需 Python** - 独立可执行文件
- **启动更快** - 无 Python 解释器开销
- **易于部署** - 单文件分发
- **行为一致** - 依赖项已打包
### 通过 PyPI 安装
使用 pip:
```bash
pip install tzst
```
或使用 uv(推荐):
```bash
uv tool install tzst
```
### 从源码安装
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### 开发环境安装
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
## 快速开始
### 命令行使用
```bash
# 创建归档
tzst a archive.tzst file1.txt file2.txt directory/
# 提取归档
tzst x archive.tzst
# 列出归档内容
tzst l archive.tzst
# 测试归档完整性
tzst t archive.tzst
```
### Python API 使用
```python
from tzst import create_archive, extract_archive, list_archive
# 创建归档
create_archive("archive.tzst", ["file1.txt", "file2.txt", "directory/"])
# 提取归档
extract_archive("archive.tzst", "output_dir/")
# 列出归档内容
contents = list_archive("archive.tzst", verbose=True)
for item in contents:
print(f"{item['name']}: {item['size']} bytes")
```
## 命令行接口
### 归档操作
#### 创建归档
```bash
# 基本用法
tzst a archive.tzst file1.txt file2.txt
# 指定压缩级别 (1-22, 默认: 3)
tzst a archive.tzst files/ -l 15
# 等效命令
tzst add archive.tzst files/
tzst create archive.tzst files/
```
#### 提取归档
```bash
# 完整目录结构提取
tzst x archive.tzst
# 提取到指定目录
tzst x archive.tzst -o output_dir/
# 提取特定文件
tzst x archive.tzst file1.txt dir/file2.txt
# 扁平化提取(无目录结构)
tzst e archive.tzst -o output_dir/
# 大文件使用流模式
tzst x archive.tzst --streaming -o output_dir/
```
#### 列出内容
```bash
# 简单列表
tzst l archive.tzst
# 详细列表
tzst l archive.tzst -v
# 大文件使用流模式
tzst l archive.tzst --streaming -v
```
#### 测试完整性
```bash
# 测试归档完整性
tzst t archive.tzst
# 流模式测试
tzst t archive.tzst --streaming
```
### 命令参考
| 命令 | 等效命令 | 描述 | 是否支持流模式 |
|------|----------|------|----------------|
| `a` | `add`, `create` | 创建或添加文件到归档 | 不支持 |
| `x` | `extract` | 完整路径提取 | ✓ `--streaming` |
| `e` | `extract-flat` | 扁平化提取 | ✓ `--streaming` |
| `l` | `list` | 列出归档内容 | ✓ `--streaming` |
| `t` | `test` | 测试归档完整性 | ✓ `--streaming` |
### CLI 选项
- `-v, --verbose`:启用详细输出
- `-o, --output DIR`:指定输出目录(提取命令)
- `-l, --level LEVEL`:设置压缩级别 1-22(创建命令)
- `--streaming`:启用流模式实现内存高效处理
- `--filter FILTER`:提取安全过滤器(data/tar/fully_trusted)
- `--no-atomic`:禁用原子文件操作(不推荐)
### 安全过滤器
```bash
# 最高安全性提取(默认)
tzst x archive.tzst --filter data
# 标准tar兼容性提取
tzst x archive.tzst --filter tar
# 完全信任模式(危险 - 仅适用于可信归档)
tzst x archive.tzst --filter fully_trusted
```
**安全过滤器选项:**
- `data` (默认):最安全。阻止危险文件、绝对路径和提取目录外路径
- `tar`:标准 tar 兼容性。阻止绝对路径和目录遍历
- `fully_trusted`:无安全限制。仅适用于完全可信的归档
## Python API
### TzstArchive 类
```python
from tzst import TzstArchive
# 创建新归档
with TzstArchive("archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# 读取现有归档
with TzstArchive("archive.tzst", "r") as archive:
# 列出内容
contents = archive.list(verbose=True)
# 安全提取
archive.extract("file.txt", "output/", filter="data")
# 测试完整性
is_valid = archive.test()
# 大文件使用流模式
with TzstArchive("large_archive.tzst", "r", streaming=True) as archive:
archive.extract(path="output/")
```
**重要限制:**
- **不支持追加模式**:需创建新归档或重建整个归档
### 便捷函数
#### create_archive()
```python
from tzst import create_archive
# 原子操作创建(默认)
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
#### extract_archive()
```python
from tzst import extract_archive
# 安全提取(默认:'data'过滤器)
extract_archive("backup.tzst", "restore_dir/")
# 提取特定文件
extract_archive("backup.tzst", "restore_dir/", members=["config.txt"])
# 扁平化提取
extract_archive("backup.tzst", "restore_dir/", flatten=True)
# 大文件使用流模式
extract_archive("large_backup.tzst", "restore_dir/", streaming=True)
```
#### list_archive()
```python
from tzst import list_archive
# 简单列表
file_list = list_archive("backup.tzst")
# 详细列表
file_details = list_archive("backup.tzst", verbose=True)
# 大文件使用流模式
large_list = list_archive("large_backup.tzst", streaming=True)
```
#### test_archive()
```python
from tzst import test_archive
# 基本完整性测试
if test_archive("backup.tzst"):
print("Archive is valid")
# 流模式测试
if test_archive("large_backup.tzst", streaming=True):
print("Large archive is valid")
```
## 高级功能
### 文件扩展名
库自动处理文件扩展名并智能标准化:
- `.tzst` - tar + zstandard 归档主扩展名
- `.tar.zst` - 替代标准扩展名
- 打开现有归档时自动检测
- 创建归档时自动添加扩展名
```python
# 以下创建方式均有效
create_archive("backup.tzst", files) # 创建 backup.tzst
create_archive("backup.tar.zst", files) # 创建 backup.tar.zst
create_archive("backup", files) # 创建 backup.tzst
create_archive("backup.txt", files) # 创建 backup.tzst (标准化)
```
### 压缩级别
Zstandard 压缩级别范围从 1(最快)到 22(最佳压缩):
- **级别 1-3**:快速压缩,文件较大
- **级别 3**(默认):速度与压缩率的良好平衡
- **级别 10-15**:更好的压缩率,速度较慢
- **级别 20-22**:最高压缩率,速度显著变慢
### 流模式
使用流模式实现大归档文件的内存高效处理:
**优势:**
- 显著降低内存使用
- 对内存无法容纳的大文件性能更好
- 资源自动清理
**适用场景:**
- 大于 100MB 的归档文件
- 内存有限的环境
- 处理包含多个大文件的归档
```python
# 示例:处理大型备份归档
from tzst import extract_archive, list_archive, test_archive
large_archive = "backup_500gb.tzst"
# 内存高效操作
is_valid = test_archive(large_archive, streaming=True)
contents = list_archive(large_archive, streaming=True, verbose=True)
extract_archive(large_archive, "restore_dir/", streaming=True)
```
### 原子操作
所有文件创建操作默认使用原子操作:
- 归档先在临时文件创建,然后原子移动
- 进程中断时自动清理
- 无损坏或不完整归档风险
- 跨平台兼容
```python
# 默认启用原子操作
create_archive("important.tzst", files) # 中断时安全
# 可禁用(不推荐)
create_archive("test.tzst", files, use_temp_file=False)
```
### 错误处理
```python
from tzst import TzstArchive
from tzst.exceptions import (
TzstError,
TzstArchiveError,
TzstCompressionError,
TzstDecompressionError,
TzstFileNotFoundError
)
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract()
except TzstDecompressionError:
print("Failed to decompress archive")
except TzstFileNotFoundError:
print("Archive file not found")
except KeyboardInterrupt:
print("Operation interrupted by user")
# Cleanup handled automatically
```
## 性能与对比
### 性能优化建议
1. **压缩级别**:级别 3 适用于大多数场景
2. **流模式**:归档大于 100MB 时使用
3. **批量操作**:单次会话添加多个文件
4. **文件类型**:已压缩文件不会进一步压缩
### 与其他工具对比
**对比 tar + gzip:**
- 更好的压缩率
- 更快的解压速度
- 现代算法
**对比 tar + xz:**
- 显著更快的压缩速度
- 相似的压缩率
- 更好的速度/压缩率平衡
**对比 zip:**
- 更好的压缩率
- 保留 Unix 权限和元数据
- 更好的流处理支持
## 系统要求
- Python 3.12 或更高版本(已测试 3.12-3.14)
- zstandard >= 0.19.0
## 开发指南
### 设置开发环境
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
### 运行测试
```bash
# 带覆盖率的测试
pytest --cov=tzst --cov-report=html
# 简化命令 (覆盖配置在 pyproject.toml)
pytest
```
### 代码质量
```bash
# 代码检查
ruff check src tests
# 代码格式化
ruff format src tests
```
## 贡献指南
欢迎贡献!请阅读[贡献指南](CONTRIBUTING.md)了解:
- 开发设置和项目结构
- 代码风格指南和最佳实践
- 测试要求和编写测试
- PR流程和审核规范
### 贡献者快速入门
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
python -m pytest tests/
```
### 欢迎贡献类型
- **缺陷修复** - 修复现有功能问题
- **新功能** - 扩展库的功能
- **文档** - 改进或新增文档
- **测试** - 增加或改进测试覆盖
- **性能** - 优化现有代码
- **安全** - 修复安全漏洞
## 致谢
- [Meta Zstandard](https://github.com/facebook/zstd) 提供的优秀压缩算法
- [python-zstandard](https://github.com/indygreg/python-zstandard) 的 Python 绑定
- Python 社区的宝贵反馈和启发
## 许可证
版权所有 &copy; [Xi Xu](https://xi-xu.me)。保留所有权利。
采用 [BSD 3-Clause](LICENSE) 许可证授权。
+313
View File
@@ -0,0 +1,313 @@
# Security Policy
## Overview
The tzst project takes security seriously and is committed to providing a secure archive management library. This document outlines our security policies, supported versions, vulnerability reporting procedures, and security best practices.
## Supported Versions
| Version | Supported |
|---------|-----------|
| 1.x.x | ✅ Yes |
| < 1.0 | ❌ No |
We provide security updates for the latest major version. Users are strongly encouraged to keep their installations up to date.
## Security Features
### Built-in Security by Default
tzst is designed with security as a primary concern and implements multiple layers of protection:
#### 🔒 Secure Extraction Filters
tzst provides three security filter levels for extraction operations:
- **`data` (default)**: Maximum security level
- Blocks dangerous files (device files, named pipes, etc.)
- Prevents absolute path extraction
- Blocks directory traversal attacks (`../` sequences)
- Restricts extraction to the specified directory
- **Recommended for untrusted archives**
- **`tar`**: Standard tar compatibility
- Blocks absolute paths
- Prevents directory traversal
- Allows Unix-specific features (symlinks, permissions)
- **Use for trusted archives requiring tar features**
- **`fully_trusted`**: No security restrictions
- Allows all archive features
- **Only use with completely trusted archives**
- ⚠️ **Warning**: Can be dangerous with untrusted content
#### ⚡ Atomic Operations
All file creation operations use atomic file operations by default:
- Archives are created in temporary files first, then atomically moved
- Automatic cleanup if the process is interrupted
- Prevents corrupted or incomplete archives
- Cross-platform compatibility
- Use `use_temp_file=False` only when necessary (not recommended)
#### 🛡️ Path Traversal Protection
- Validates all file paths before extraction
- Normalizes paths to prevent directory traversal
- Blocks extraction outside target directory
- Handles edge cases across different operating systems
#### 🔍 Input Validation
- Validates compression levels (1-22)
- Validates archive formats
- Validates file paths and names
- Comprehensive error handling with clear messages
## Best Practices for Users
### 1. Always Use Secure Defaults
```python
from tzst import extract_archive
# ✅ Good: Uses secure 'data' filter by default
extract_archive("untrusted.tzst", "output/")
# ✅ Good: Explicitly specify secure filter
extract_archive("untrusted.tzst", "output/", filter="data")
```
### 2. Choose Appropriate Security Filters
```python
# For untrusted archives (recommended)
extract_archive("untrusted.tzst", "output/", filter="data")
# For trusted archives needing tar features
extract_archive("trusted.tzst", "output/", filter="tar")
# Only for completely trusted archives (use with caution)
extract_archive("internal.tzst", "output/", filter="fully_trusted")
```
### 3. Validate Archive Sources
- Only process archives from trusted sources
- Verify archive integrity before extraction
- Use appropriate security filters based on trust level
- Consider implementing additional validation layers
### 4. Use Safe Extraction Practices
```python
import tempfile
from pathlib import Path
from tzst import extract_archive, test_archive
def safe_extract(archive_path, trust_level="untrusted"):
"""Safely extract an archive with appropriate security measures."""
# Test archive integrity first
if not test_archive(archive_path):
raise ValueError("Archive integrity check failed")
# Choose security filter based on trust level
filters = {
"untrusted": "data",
"trusted": "tar",
"internal": "fully_trusted"
}
security_filter = filters.get(trust_level, "data")
# Extract to temporary directory first
with tempfile.TemporaryDirectory() as temp_dir:
extract_archive(
archive_path,
temp_dir,
filter=security_filter
)
# Process extracted files safely
# Move to final destination if validation passes
```
### 5. Error Handling
```python
from tzst import TzstArchiveError, TzstDecompressionError
try:
extract_archive("archive.tzst", "output/")
except TzstDecompressionError:
# Handle corrupted or invalid archives
print("Archive appears to be corrupted")
except TzstArchiveError as e:
# Handle general archive errors
print(f"Archive operation failed: {e}")
except PermissionError:
# Handle permission issues
print("Insufficient permissions")
```
## Security Considerations for Different Use Cases
### Processing Untrusted Archives
When processing archives from untrusted sources (internet downloads, user uploads, etc.):
1. **Always use `data` filter** (default behavior)
2. **Extract to isolated directory** with limited permissions
3. **Validate extracted content** before use
4. **Use resource limits** to prevent DoS attacks
5. **Run in sandboxed environment** when possible
### Enterprise/Internal Use
For trusted internal archives:
1. Use `tar` filter for standard compatibility
2. Implement organizational security policies
3. Use secure transport channels
4. Maintain audit logs of archive operations
5. Regular security assessments
### Development/Testing
Even in development:
1. Use secure defaults
2. Don't disable security features without understanding implications
3. Test with malicious archives (in isolated environments)
4. Validate security assumptions
## Reporting Security Vulnerabilities
We take security vulnerabilities seriously and appreciate responsible disclosure.
### How to Report
**DO NOT** report security vulnerabilities through public GitHub issues.
Instead, please report security vulnerabilities via email to:
📧 **[i@xi-xu.me](mailto:i@xi-xu.me)**
### Information to Include
Please include as much of the following information as possible:
1. **Description** of the vulnerability
2. **Steps to reproduce** the issue
3. **Potential impact** and attack scenarios
4. **Affected versions** (if known)
5. **Suggested fix** (if you have one)
6. **Your contact information** for follow-up
### What to Expect
1. **Acknowledgment**: We'll acknowledge receipt within 48 hours
2. **Initial Assessment**: We'll provide an initial assessment within 5 business days
3. **Communication**: We'll keep you informed throughout the investigation
4. **Resolution**: We'll work to resolve confirmed vulnerabilities promptly
5. **Credit**: We'll credit you in security advisories (unless you prefer anonymity)
### Response Timeline
- **Critical vulnerabilities**: Patch within 7 days
- **High-severity vulnerabilities**: Patch within 14 days
- **Medium/Low-severity vulnerabilities**: Patch within 30 days
## Security Updates and Advisories
### How We Communicate Security Issues
1. **GitHub Security Advisories**: For confirmed vulnerabilities
2. **Release Notes**: Security fixes are prominently mentioned
3. **PyPI**: Updated packages with security fixes
4. **Documentation**: Security best practices updates
### Staying Informed
To stay informed about security updates:
1. **Watch the repository** for release notifications
2. **Subscribe to GitHub Security Advisories**
3. **Follow our release notes** for security mentions
4. **Use dependency scanners** to identify outdated versions
## Development Security Practices
### Code Review Process
- All changes undergo security-focused code review
- Security-sensitive changes require additional review
- Automated security scanning in CI/CD pipeline
- Regular dependency vulnerability scanning
### Testing
- Comprehensive security test suite
- Fuzzing with malformed archives
- Path traversal attack simulations
- Permission and access control testing
### Dependencies
- Minimal dependency footprint
- Regular dependency updates
- Automated vulnerability scanning
- Pinned versions for reproducible builds
## Known Security Considerations
### Archive Bombs
While tzst includes basic protections, be aware of:
- **Zip bombs**: Archives with extreme compression ratios
- **Memory exhaustion**: Very large expanded archives
- **Resource consumption**: Processing time attacks
**Mitigation**: Use streaming mode for large archives and implement resource limits.
### Symbolic Links
Different security filters handle symbolic links differently:
- `data` filter: Generally restricts symbolic links
- `tar` filter: Preserves symbolic links with basic safety checks
- `fully_trusted` filter: Allows all symbolic link operations
**Recommendation**: Use `data` filter for untrusted content.
### File Permissions
Extracted file permissions depend on:
- Source archive permissions
- Extraction filter settings
- Operating system capabilities
- User privileges
**Recommendation**: Review and validate extracted file permissions.
## Contact
For security-related questions or concerns:
- **Security issues**: [i@xi-xu.me](mailto:i@xi-xu.me) (private)
- **General questions**: [GitHub Discussions](https://github.com/xixu-me/tzst/discussions)
- **Documentation**: [Project Documentation](https://tzst.xi-xu.me)
## Acknowledgments
We thank the security research community for their responsible disclosure of vulnerabilities and continuous efforts to improve software security.
---
**Last Updated**: June 2025
**Version**: 1.0
For the most current security information, please check our [GitHub repository](https://github.com/xixu-me/tzst) and [official documentation](https://tzst.xi-xu.me).
+1 -1
View File
@@ -1,6 +1,6 @@
# Sphinx build outputs
_build/
_build_simple/
_build_*/
# Sphinx auto-generated files
_autosummary/
+76
View File
@@ -0,0 +1,76 @@
---
myst:
html_meta:
description: "Page not found - tzst documentation. Return to the main documentation or search for what you're looking for."
keywords: "404, page not found, tzst documentation, error"
og:title: "Page Not Found - tzst Documentation"
og:description: "The requested page could not be found. Visit the tzst documentation homepage or use the search feature."
twitter:title: "Page Not Found - tzst Documentation"
twitter:description: "The requested page could not be found. Visit the tzst documentation homepage or use the search feature."
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# 404 - Page Not Found
## Oops! The page you're looking for doesn't exist
The URL you requested could not be found in the tzst documentation. This might happen if:
- The page has been moved or renamed
- You followed a broken link
- There's a typo in the URL
- The page has been removed
## Where would you like to go?
### Popular Pages
- **{doc}`index`** - Documentation homepage
- **{doc}`quickstart`** - Get started with tzst
- **{doc}`performance`** - Learn about performance optimizations
- **{doc}`examples`** - Practical examples and use cases
- **{doc}`api/index`** - Complete API reference
- **{doc}`development`** - Contributing to tzst
- **{ref}`genindex`** - Index of all documented items
### Quick Navigation
- **Installation Guide** - {ref}`installation`
- **Basic Usage** - {ref}`basic-usage`
- **Security Features** - {ref}`security-and-filtering`
- **Error Handling** - {ref}`error-handling`
## Search Documentation
Use the search box in the top navigation to find what you're looking for, or browse through these sections:
### Core Features
- **{doc}`api/core`** - Core TzstArchive class and functions
- **{doc}`api/cli`** - Command-line interface
- **{doc}`api/exceptions`** - Exception handling
### Examples & Tutorials
- **Archive Creation** - {ref}`basic-archive-operations`
- **Security Best Practices** - {ref}`security-and-filtering`
- **Integration Examples** - {ref}`integration-examples`
- **Performance Optimization** - {ref}`performance-optimization`
## Additional Resources
- [GitHub Repository](https://github.com/xixu-me/tzst) - Source code and issue tracker
- [PyPI Package](https://pypi.org/project/tzst/) - Download and installation
- [Release Notes](https://github.com/xixu-me/tzst/releases) - Latest updates and changes
## Report an Issue
If you believe this is a broken link within our documentation, please [report it on GitHub](https://github.com/xixu-me/tzst/issues).
---
**Need help?** Check our {doc}`quickstart` guide or browse the {doc}`examples` for common use cases.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+21
View File
@@ -0,0 +1,21 @@
# robots.txt for tzst documentation
User-agent: *
Allow: /
# Sitemap location
Sitemap: https://tzst.xi-xu.me/sitemap.xml
# Disallow build artifacts and internal directories
Disallow: /_sources/
Disallow: /_static/*.js$
Disallow: /_images/
# Allow static assets like CSS and images
Allow: /_static/*.css$
Allow: /_static/*.png$
Allow: /_static/*.jpg$
Allow: /_static/*.ico$
Allow: /_static/*.svg$
# Crawl delay (optional, considerate to search engines)
Crawl-delay: 1
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

+200
View File
@@ -0,0 +1,200 @@
{% extends "!layout.html" %}
{% block extrahead %}
{{ super() }}
<!-- Additional SEO and social meta tags -->
<meta name="application-name" content="tzst" />
<meta name="generator" content="Sphinx {{ sphinx_version }}" />
<meta name="rating" content="General" />
<meta name="revisit-after" content="7 days" />
<!-- Schema.org markup for search engines -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "tzst",
"description": "A Python library for creating and extracting tar.zst archives with high performance and comprehensive features",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Cross-platform",
"programmingLanguage": "Python",
"license": "https://opensource.org/licenses/BSD-3-Clause",
"url": "https://tzst.xi-xu.me/",
"downloadUrl": "https://pypi.org/project/tzst/",
"codeRepository": "https://github.com/xixu-me/tzst",
"softwareVersion": "{{ version }}",
"author": {
"@type": "Person",
"name": "Xi Xu",
"url": "https://xi-xu.me"
},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5",
"reviewCount": "1"
},
"keywords": "tzst, tar, zstandard, compression, archive, python, extraction, backup"
}
</script>
<!-- Breadcrumb Schema -->
{% if pagename != 'index' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://tzst.xi-xu.me/"
},
{
"@type": "ListItem",
"position": 2,
"name": "{{ title|striptags }}",
"item": "https://tzst.xi-xu.me/{{ pagename }}.html"
}
]
}
</script>
{% endif %}
<!-- Article/TechArticle Schema for documentation pages -->
{% if pagename in ['quickstart', 'examples', 'performance', 'development'] %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "{{ title|striptags }}",
"description": "{{ metatags|striptags }}",
"author": {
"@type": "Person",
"name": "Xi Xu",
"url": "https://xi-xu.me"
},
"publisher": {
"@type": "Person",
"name": "Xi Xu"
},
"datePublished": "2025-01-01",
"dateModified": "2025-01-12",
"url": "https://tzst.xi-xu.me/{{ pagename }}.html",
"inLanguage": "en-US",
"about": {
"@type": "SoftwareApplication",
"name": "tzst"
}
}
</script>
{% endif %}
<!-- FAQ Schema for pages with common questions -->
{% if pagename == 'quickstart' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "How do I install tzst?",
"acceptedAnswer": {
"@type": "Answer",
"text": "You can install tzst using pip (pip install tzst), download standalone binaries from GitHub Releases, use uvx for no-installation usage (uvx tzst), or install from source."
}
},
{
"@type": "Question",
"name": "What compression levels does tzst support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "tzst supports compression levels from 1 to 22. Level 1 is fastest with lower compression, level 3 is the default balance, and level 22 provides maximum compression but is slower."
}
},
{
"@type": "Question",
"name": "Is tzst secure for extracting untrusted archives?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, tzst uses the 'data' security filter by default, which protects against path traversal attacks and blocks dangerous files. This makes it safe for extracting untrusted archives."
}
},
{
"@type": "Question",
"name": "When should I use streaming mode?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use streaming mode for archives larger than 100MB to reduce memory usage. Streaming mode is memory-efficient but has limitations such as no random access or specific file extraction."
}
},
{
"@type": "Question",
"name": "What file extensions does tzst support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "tzst supports both .tzst and .tar.zst file extensions. The library automatically handles extension detection and normalization when creating or opening archives."
}
}
]
}
</script>
{% endif %}
<!-- HowTo Schema for examples page -->
{% if pagename == 'examples' %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to use tzst for archive management",
"description": "Comprehensive examples of using tzst for creating, extracting, and managing tar.zst archives",
"image": "https://tzst.xi-xu.me/_static/tzst-logo.png",
"step": [
{
"@type": "HowToStep",
"name": "Create an archive",
"text": "Use create_archive() to create a new tzst archive with your files and directories",
"url": "https://tzst.xi-xu.me/examples.html#basic-operations"
},
{
"@type": "HowToStep",
"name": "Extract an archive",
"text": "Use extract_archive() to safely extract files from a tzst archive with security filters",
"url": "https://tzst.xi-xu.me/examples.html#flexible-extraction"
},
{
"@type": "HowToStep",
"name": "List archive contents",
"text": "Use list_archive() to view the contents of an archive without extracting",
"url": "https://tzst.xi-xu.me/examples.html#basic-operations"
},
{
"@type": "HowToStep",
"name": "Test archive integrity",
"text": "Use test_archive() to verify the integrity of your archive files",
"url": "https://tzst.xi-xu.me/examples.html#basic-operations"
}
]
}
</script>
{% endif %}
<!-- Canonical URL for better SEO -->
{% if pagename != 'index' %}
<link rel="canonical" href="https://tzst.xi-xu.me/{{ pagename }}.html" />
{% else %}
<link rel="canonical" href="https://tzst.xi-xu.me/" />
{% endif %}
<!-- Preconnect to external domains for performance -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://cdnjs.cloudflare.com" />
<link rel="dns-prefetch" href="https://pypi.org" />
<link rel="dns-prefetch" href="https://github.com" />
{% endblock %}
+179 -1
View File
@@ -1,14 +1,53 @@
---
myst:
html_meta:
description: "tzst CLI API - Command-line interface functions and utilities for tar.zst archive operations"
keywords: "tzst CLI API, command line interface, Python CLI, tar.zst commands"
og:title: "tzst CLI API Reference"
og:description: "CLI API documentation for tzst - Command-line interface functions and utilities"
twitter:title: "tzst CLI API Reference"
twitter:description: "CLI API documentation for tzst - Command-line interface functions and utilities"
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# CLI API
The command-line interface module provides functions for the tzst CLI tool.
The command-line interface module provides comprehensive functionality for the tzst CLI tool, including argument parsing, command execution, and interactive features.
```{eval-rst}
.. automodule:: tzst.cli
:members:
:undoc-members:
:show-inheritance:
:no-index:
```
## Overview
The tzst CLI provides a powerful command-line interface for archive operations with intuitive commands and comprehensive options. The interface is designed for both interactive use and scripting, with robust error handling and user-friendly output.
### Core Commands
| Command | Aliases | Description | Streaming Support |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | Create or add to archive | N/A |
| `x` | `extract` | Extract with full paths | `--streaming` |
| `e` | `extract-flat` | Extract without directory structure | `--streaming` |
| `l` | `list` | List archive contents | `--streaming` |
| `t` | `test` | Test archive integrity | `--streaming` |
### Key Features
- **Intuitive Commands**: Simple, memorable command aliases (a, x, e, l, t)
- **Streaming Support**: Memory-efficient processing for large archives
- **Interactive Conflict Resolution**: User-friendly prompts for handling file conflicts
- **Comprehensive Options**: Fine-grained control over compression, extraction, and security
- **Cross-Platform**: Consistent behavior across Windows, macOS, and Linux
## Main Functions
### main
@@ -17,12 +56,118 @@ The command-line interface module provides functions for the tzst CLI tool.
.. autofunction:: tzst.cli.main
```
The main entry point for the CLI application. Handles argument parsing, command execution, and comprehensive error reporting.
**Key Features:**
- Robust argument validation and error handling
- Support for all archive operations
- Consistent exit codes for scripting
- User-friendly error messages
**Exit Codes:**
- `0`: Success
- `1`: General error (file not found, archive corruption, etc.)
- `2`: Argument parsing error
- `130`: Interrupted by user (Ctrl+C)
### create_parser
```{eval-rst}
.. autofunction:: tzst.cli.create_parser
```
Creates and configures the comprehensive argument parser for the CLI interface.
**Supported Arguments:**
- **Global**: `--version`, `--help`
- **Archive Creation**: `-l/--level`, `--no-atomic`
- **Extraction**: `-o/--output`, `--streaming`, `--filter`, `--conflict-resolution`
- **Listing**: `-v/--verbose`, `--streaming`
- **Testing**: `--streaming`
## Command Handlers
The CLI implements dedicated command handlers for each operation, providing specialized functionality and error handling.
### Archive Creation Commands
#### cmd_add
Creates new archives from files and directories with configurable compression and atomic operations.
**Features:**
- Configurable compression levels (1-22)
- Atomic file operations (default) for safe creation
- Recursive directory processing
- Path validation and normalization
**Usage Examples:**
```bash
# Basic archive creation
tzst a backup.tzst documents/ photos/
# High compression with atomic disabled
tzst a backup.tzst files/ -l 15 --no-atomic
```
### Extraction Commands
#### cmd_extract_full
Extracts archives preserving complete directory structure with advanced conflict resolution.
**Features:**
- Preserves full directory paths
- Multiple conflict resolution strategies
- Security filters for safe extraction
- Selective file extraction
- Streaming mode for large archives
#### cmd_extract_flat
Extracts archives flattening all files to a single directory, useful for consolidating files.
**Features:**
- Flattens directory structure
- Automatic conflict resolution for filename collisions
- Preserves file content while simplifying structure
- Same security and streaming features as full extraction
### Management Commands
#### cmd_list
Lists archive contents with optional detailed information and streaming support.
**Features:**
- Simple or verbose listing modes
- Human-readable file sizes
- Modification timestamps
- Streaming mode for memory efficiency
#### cmd_test
Tests archive integrity and validity with comprehensive error reporting.
**Features:**
- Complete archive validation
- Streaming mode support
- Detailed error reporting
- Exit codes for automated testing
#### cmd_version
Displays version information and system details.
## Utility Functions
### print_banner
@@ -31,14 +176,47 @@ The command-line interface module provides functions for the tzst CLI tool.
.. autofunction:: tzst.cli.print_banner
```
Displays the application banner with version and copyright information.
### format_size
```{eval-rst}
.. autofunction:: tzst.cli.format_size
```
Formats file sizes in a human-readable format (bytes, KB, MB, GB).
### validate_compression_level
```{eval-rst}
.. autofunction:: tzst.cli.validate_compression_level
```
Validates compression level arguments and converts them to integers.
## Interactive Features
The CLI includes interactive conflict resolution for file extraction conflicts, allowing users to choose how to handle existing files during extraction operations.
### Conflict Resolution Options
- **Replace**: Overwrite the existing file
- **Skip**: Keep the existing file, skip extraction
- **Replace All**: Apply replace to all subsequent conflicts
- **Skip All**: Apply skip to all subsequent conflicts
- **Auto-rename All**: Automatically rename conflicting files
- **Exit**: Stop extraction process
### Security Considerations
The CLI implements multiple security filters for safe extraction:
- **`data` filter** (default): Safest option, blocks potentially dangerous archive members
- **`tar` filter**: Preserves more tar features while maintaining basic security
- **`fully_trusted` filter**: No restrictions, use only with completely trusted archives
### Performance Options
- **Streaming Mode**: Use `--streaming` for memory-efficient processing of large archives (>100MB)
- **Compression Levels**: Choose from 1 (fastest) to 22 (maximum compression)
- **Atomic Operations**: Default behavior uses temporary files for safe archive creation
+96 -3
View File
@@ -1,17 +1,34 @@
---
myst:
html_meta:
description: "tzst Core API - TzstArchive class and convenience functions for tar.zst archive operations"
keywords: "tzst core API, TzstArchive, Python archive class, tar.zst functions"
og:title: "tzst Core API Reference"
og:description: "Core API documentation for tzst - TzstArchive class and convenience functions"
twitter:title: "tzst Core API Reference"
twitter:description: "Core API documentation for tzst - TzstArchive class and convenience functions"
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# Core API
The core module provides the main functionality for working with tzst archives.
The core module provides the main functionality for working with tzst archives, including the primary `TzstArchive` class and high-level convenience functions.
```{eval-rst}
.. automodule:: tzst.core
:members:
:undoc-members:
:show-inheritance:
:no-index:
```
## TzstArchive Class
The main class for handling `.tzst`/`.tar.zst` archives.
The main class for handling `.tzst`/`.tar.zst` archives with comprehensive functionality for creation, extraction, and manipulation.
```{eval-rst}
.. autoclass:: tzst.TzstArchive
@@ -21,9 +38,32 @@ The main class for handling `.tzst`/`.tar.zst` archives.
:special-members: __init__, __enter__, __exit__
```
### Key Features
- **Context Manager Support**: Use with `with` statements for automatic resource management
- **Multiple Access Modes**: Read ('r'), write ('w'), and append ('a') modes
- **Streaming Support**: Memory-efficient processing for large archives
- **Security Features**: Built-in protection against path traversal attacks
- **Flexible Extraction**: Support for selective extraction and conflict resolution
### Usage Examples
```python
# Create a new archive
with TzstArchive("backup.tzst", "w", compression_level=6) as archive:
archive.add("important_file.txt")
archive.add("documents/", recursive=True)
# Read an existing archive
with TzstArchive("backup.tzst", "r") as archive:
contents = archive.list(verbose=True)
is_valid = archive.test()
archive.extractall("restore/")
```
## Convenience Functions
High-level functions for common archive operations.
High-level functions for common archive operations without needing to instantiate the `TzstArchive` class directly.
### create_archive
@@ -31,20 +71,73 @@ High-level functions for common archive operations.
.. autofunction:: tzst.create_archive
```
Creates a new tzst archive from the specified files and directories.
**Key Features:**
- Configurable compression levels (1-22)
- Atomic creation using temporary files
- Automatic path validation and normalization
- Support for both files and directories
### extract_archive
```{eval-rst}
.. autofunction:: tzst.extract_archive
```
Extracts files from a tzst archive with advanced options for handling conflicts and filtering.
**Key Features:**
- Selective extraction with member filtering
- Multiple conflict resolution strategies
- Flatten option to extract all files to a single directory
- Streaming mode for memory efficiency
- Security filters to prevent path traversal attacks
### list_archive
```{eval-rst}
.. autofunction:: tzst.list_archive
```
Lists the contents of a tzst archive with optional detailed information.
**Returns:**
- List of dictionaries containing file information
- Each entry includes name, size, modification time, and type
- Verbose mode provides additional metadata
### test_archive
```{eval-rst}
.. autofunction:: tzst.test_archive
```
Tests the integrity of a tzst archive to verify it can be successfully decompressed.
**Returns:**
- `True` if the archive is valid and can be extracted
- `False` if the archive is corrupted or cannot be processed
## Enums and Supporting Classes
### ConflictResolution
Enumeration for handling file conflicts during extraction:
- `REPLACE`: Overwrite existing files
- `SKIP`: Skip existing files
- `REPLACE_ALL`: Overwrite all existing files without prompting
- `SKIP_ALL`: Skip all existing files without prompting
- `AUTO_RENAME`: Automatically rename conflicting files
- `AUTO_RENAME_ALL`: Automatically rename all conflicting files
- `ASK`: Prompt user for each conflict (interactive mode)
- `EXIT`: Stop extraction on first conflict
### ConflictResolutionState
State management class for tracking conflict resolution decisions during batch operations.
+253 -3
View File
@@ -1,22 +1,272 @@
# Exceptions
---
myst:
html_meta:
description: "Complete reference for tzst exception classes and error handling. Learn about TzstError, TzstArchiveError, and other custom exceptions for robust archive operations."
keywords: "tzst exceptions, Python exceptions, error handling, TzstError, TzstArchiveError, archive errors, compression errors"
og:title: "tzst Exceptions API Reference"
og:description: "Complete reference for tzst exception classes and error handling. Learn about TzstError, TzstArchiveError, and other custom exceptions for robust archive operations."
og:type: "article"
twitter:title: "tzst Exceptions API Reference"
twitter:description: "Complete reference for tzst exception classes and error handling. Learn about TzstError, TzstArchiveError, and other custom exceptions for robust archive operations."
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
Custom exception classes used by tzst.
# Exceptions API
Custom exception classes used by tzst for comprehensive error handling and debugging.
```{eval-rst}
.. automodule:: tzst.exceptions
:members:
:undoc-members:
:show-inheritance:
:no-index:
```
## Exception Hierarchy
## Overview
The tzst library provides a comprehensive hierarchy of exceptions to help identify and handle different types of errors that may occur during archive operations. All exceptions inherit from the base `TzstError` class, making it easy to catch all tzst-related errors with a single exception handler.
### Exception Hierarchy
```text
TzstError (base exception)
├── TzstArchiveError (archive operation failures)
├── TzstCompressionError (compression failures)
└── TzstDecompressionError (decompression failures)
```
## Exception Classes
### Base Exception
#### TzstError
```{eval-rst}
.. autoexception:: tzst.exceptions.TzstError
:members:
:show-inheritance:
```
The base exception class for all tzst operations. Catch this exception to handle any tzst-related error in your application.
**Usage:**
```python
from tzst import create_archive, TzstError
try:
create_archive("backup.tzst", ["files/"])
except TzstError as e:
print(f"tzst operation failed: {e}")
```
### Archive Operation Exceptions
#### TzstArchiveError
```{eval-rst}
.. autoexception:: tzst.exceptions.TzstArchiveError
:members:
:show-inheritance:
```
Raised when archive operations fail, such as:
- Archive file cannot be opened or created
- File permissions prevent archive access
- Archive structure is malformed
- Tar operations fail within the archive
- Atomic file operations fail during creation
**Common Scenarios:**
- Invalid archive file path
- Insufficient disk space
- File permission errors
- Corrupt archive structure
### Compression Exceptions
#### TzstCompressionError
```{eval-rst}
.. autoexception:: tzst.exceptions.TzstCompressionError
:members:
:show-inheritance:
```
Raised when compression operations fail, including:
- Invalid compression level is specified
- Disk space is insufficient during compression
- Input data cannot be compressed due to corruption
- Zstandard compression encounters an internal error
**Common Scenarios:**
- Compression level out of range (1-22)
- Insufficient disk space during compression
- Source file corruption
- Zstandard library errors
### Decompression Exceptions
#### TzstDecompressionError
```{eval-rst}
.. autoexception:: tzst.exceptions.TzstDecompressionError
:members:
:show-inheritance:
```
Raised when decompression operations fail, such as:
- Archive file is corrupted or incomplete
- Archive was not created with zstandard compression
- Decompression buffer overflows or underflows
- Archive format is invalid or unsupported
**Common Scenarios:**
- Corrupted or truncated archive files
- Non-zstandard compressed archives
- Invalid tar structure within archive
- Archive format version mismatches
## Error Handling Best Practices
### Basic Error Handling
```python
from tzst import create_archive, TzstArchiveError, TzstCompressionError
try:
create_archive("backup.tzst", ["documents/"])
except TzstCompressionError as e:
print(f"Compression failed: {e}")
except TzstArchiveError as e:
print(f"Archive operation failed: {e}")
```
### Comprehensive Error Handling
```python
from tzst import extract_archive, TzstError
try:
extract_archive("backup.tzst", "restore/")
except TzstError as e:
# Catch any tzst-related error
print(f"Operation failed: {e}")
# Perform cleanup or fallback operations
```
### Specific Exception Handling
```python
from tzst import TzstArchive, TzstDecompressionError, TzstArchiveError
def safe_extract(archive_path, output_dir):
try:
with TzstArchive(archive_path, "r") as archive:
# Test integrity first
if not archive.test():
print("Archive integrity check failed")
return False
# Extract files
archive.extractall(output_dir)
return True
except TzstDecompressionError as e:
print(f"Archive is corrupted or invalid: {e}")
return False
except TzstArchiveError as e:
print(f"Archive operation failed: {e}")
return False
except FileNotFoundError: print(f"Archive file not found: {archive_path}")
return False
except PermissionError:
print(f"Permission denied accessing: {archive_path}")
return False
```
### Logging Integration
```python
import logging
from tzst import test_archive, TzstDecompressionError, TzstError
logger = logging.getLogger(__name__)
def verify_archive(archive_path):
"""Verify archive integrity with comprehensive logging."""
try:
if test_archive(archive_path):
logger.info(f"Archive {archive_path} is valid")
return True
except TzstDecompressionError as e:
logger.error(f"Archive {archive_path} is corrupted: {e}")
except TzstError as e:
logger.error(f"tzst error for {archive_path}: {e}")
except Exception as e:
logger.error(f"Unexpected error testing {archive_path}: {e}")
return False
```
### Error Recovery Patterns
```python
from tzst import create_archive, extract_archive, TzstError
from pathlib import Path
import tempfile
import shutil
def robust_backup_and_restore(source_dir, backup_path, restore_dir):
"""Robust backup with error recovery and validation."""
temp_backup = None
try:
# Create backup with temporary file for atomicity
with tempfile.NamedTemporaryFile(suffix='.tzst', delete=False) as temp_file:
temp_backup = Path(temp_file.name)
# Create archive
create_archive(temp_backup, [source_dir], compression_level=6)
# Verify archive before moving to final location
if not test_archive(temp_backup):
raise TzstArchiveError("Created archive failed integrity check")
# Move to final location atomically
shutil.move(temp_backup, backup_path)
temp_backup = None # Successfully moved
# Test restoration
extract_archive(backup_path, restore_dir)
print(f"Backup and restore completed successfully")
return True
except TzstError as e:
print(f"tzst operation failed: {e}")
# Cleanup and recovery logic
if restore_dir.exists():
shutil.rmtree(restore_dir)
return False
except Exception as e:
print(f"Unexpected error: {e}")
return False
finally:
# Cleanup temporary files
if temp_backup and temp_backup.exists():
temp_backup.unlink()
```
+83 -6
View File
@@ -1,6 +1,22 @@
---
myst:
html_meta:
description: "Complete tzst API reference - Core functions, CLI tools, and exception handling for tar.zst archives"
keywords: "tzst API, Python API documentation, tar.zst API reference, archive API"
og:title: "tzst API Reference"
og:description: "Complete API reference for tzst - Core functions, CLI tools, and exception handling"
twitter:title: "tzst API Reference"
twitter:description: "Complete API reference for tzst - Core functions, CLI tools, and exception handling"
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# API Reference
This section contains the complete API documentation for tzst.
This section contains the complete API documentation for tzst, providing detailed information about classes, functions, and exceptions.
```{toctree}
:maxdepth: 2
@@ -12,13 +28,22 @@ exceptions
## Overview
The tzst library provides both high-level convenience functions and a comprehensive class-based API for working with `.tzst`/`.tar.zst` archives.
The tzst library provides both high-level convenience functions and a comprehensive class-based API for working with `.tzst`/`.tar.zst` archives. The library is designed with security, performance, and ease of use in mind.
### Main Components
- **{doc}`core`**: Core functionality including `TzstArchive` class and convenience functions
- **{doc}`cli`**: Command-line interface functions and utilities
- **{doc}`exceptions`**: Custom exception classes for error handling
- **{doc}`core`**: Core functionality including `TzstArchive` class and convenience functions for archive operations
- **{doc}`cli`**: Command-line interface functions and utilities for batch operations
- **{doc}`exceptions`**: Custom exception classes for comprehensive error handling and debugging
### Architecture Overview
The tzst library follows a layered architecture:
1. **High-Level API**: Convenience functions for common operations
2. **Class-Based API**: `TzstArchive` class for advanced control
3. **CLI Interface**: Command-line tools for interactive and scripted use
4. **Exception System**: Comprehensive error handling for robust applications
### Quick Reference
@@ -33,6 +58,8 @@ The tzst library provides both high-level convenience functions and a comprehens
TzstArchive
```
The main class for archive manipulation with context manager support and comprehensive functionality.
#### Convenience Functions
```{eval-rst}
@@ -45,7 +72,26 @@ The tzst library provides both high-level convenience functions and a comprehens
test_archive
```
#### Exceptions
High-level functions that provide simple interfaces for common archive operations.
#### CLI Functions
```{eval-rst}
.. currentmodule:: tzst.cli
.. autosummary::
:nosignatures:
main
create_parser
print_banner
format_size
validate_compression_level
```
Command-line interface utilities for interactive and batch operations.
#### Exception Classes
```{eval-rst}
.. currentmodule:: tzst.exceptions
@@ -53,6 +99,37 @@ The tzst library provides both high-level convenience functions and a comprehens
.. autosummary::
:nosignatures:
TzstError
TzstArchiveError
TzstCompressionError
TzstDecompressionError
```
Exception hierarchy for comprehensive error handling and debugging support.
## Key Features
### Security First
- Built-in path traversal protection
- Multiple security filter options
- Safe extraction by default
### High Performance
- Zstandard compression with configurable levels
- Streaming support for large archives
- Memory-efficient operations
### Developer Friendly
- Clean, Pythonic API
- Comprehensive error handling
- Context manager support
- Extensive documentation and examples
### Cross-Platform
- Works on Windows, macOS, and Linux
- Consistent behavior across platforms
- Native performance optimizations
+37 -31
View File
@@ -10,10 +10,10 @@ from pathlib import Path
def run_command(cmd, cwd=None):
"""Run a shell command and return the result.""" try:
"""Run a shell command and return the result."""
try:
result = subprocess.run(
cmd, shell=True, check=True, cwd=cwd,
capture_output=True, text=True
cmd, shell=True, check=True, cwd=cwd, capture_output=True, text=True
)
return result.returncode == 0, result.stdout, result.stderr
except subprocess.CalledProcessError as e:
@@ -33,19 +33,21 @@ def build_docs(source_dir, build_dir, watch=False):
print("Starting live reload server...")
print("Visit http://localhost:8000 to view the documentation")
print("Press Ctrl+C to stop the server")
cmd = f"sphinx-autobuild {source_dir} {build_dir} --host 0.0.0.0 --port 8000"
success, stdout, stderr = run_command(cmd)
if not success:
print("Failed to start live reload server.")
print("Make sure sphinx-autobuild is installed: pip install sphinx-autobuild")
print(
"Make sure sphinx-autobuild is installed: pip install sphinx-autobuild"
)
return False
else:
print(f"Building documentation: {source_dir} -> {build_dir}")
cmd = f"python -m sphinx -b html {source_dir} {build_dir}"
success, stdout, stderr = run_command(cmd)
if success:
print("Documentation built successfully!")
index_file = build_dir / "index.html"
@@ -63,13 +65,13 @@ def serve_docs(build_dir, port=8000):
if not build_dir.exists():
print(f"Build directory {build_dir} does not exist. Build the docs first.")
return False
print(f"Serving documentation at http://localhost:{port}")
print("Press Ctrl+C to stop the server")
cmd = f"python -m http.server {port}"
success, stdout, stderr = run_command(cmd, cwd=build_dir)
return success
@@ -77,79 +79,83 @@ def check_dependencies():
"""Check if required dependencies are installed."""
try:
import sphinx
print(f"Sphinx version: {sphinx.__version__}")
except ImportError:
print("Sphinx is not installed. Install with: pip install sphinx")
return False
try:
import tzst
print(f"tzst version: {tzst.__version__}")
except ImportError:
print("tzst package is not installed. Install with: pip install -e ..")
return False
return True
def main():
parser = argparse.ArgumentParser(description="Build and serve tzst documentation")
parser.add_argument(
"command",
"command",
choices=["build", "clean", "serve", "watch", "check"],
help="Command to execute"
help="Command to execute",
)
parser.add_argument(
"--port", "-p",
"--port",
"-p",
type=int,
default=8000,
help="Port for serving documentation (default: 8000)"
help="Port for serving documentation (default: 8000)",
)
parser.add_argument(
"--open", "-o",
"--open",
"-o",
action="store_true",
help="Open documentation in browser after building/serving"
help="Open documentation in browser after building/serving",
)
args = parser.parse_args()
# Get directories
script_dir = Path(__file__).parent
source_dir = script_dir
build_dir = script_dir / "_build"
if args.command == "check":
success = check_dependencies()
sys.exit(0 if success else 1)
elif args.command == "clean":
clean_build(build_dir)
elif args.command == "build":
if not check_dependencies():
sys.exit(1)
success = build_docs(source_dir, build_dir)
if success and args.open:
index_file = build_dir / "index.html"
webbrowser.open(f"file://{index_file.absolute()}")
sys.exit(0 if success else 1)
elif args.command == "watch":
if not check_dependencies():
sys.exit(1)
success = build_docs(source_dir, build_dir, watch=True)
sys.exit(0 if success else 1)
elif args.command == "serve":
success = serve_docs(build_dir, args.port)
if args.open:
webbrowser.open(f"http://localhost:{args.port}")
sys.exit(0 if success else 1)
+65 -4
View File
@@ -2,6 +2,7 @@
import os
import sys
from datetime import datetime
# Add the source directory to the Python path
sys.path.insert(0, os.path.abspath("../src"))
@@ -11,7 +12,7 @@ from tzst import __version__
# -- Project information -----------------------------------------------------
project = "tzst"
copyright = "2025, Xi Xu"
copyright = f"{datetime.now().year}, Xi Xu"
author = "Xi Xu"
release = __version__
version = __version__
@@ -23,33 +24,82 @@ extensions = [
"sphinx.ext.viewcode",
"sphinx.ext.intersphinx",
"sphinx.ext.autosummary",
"sphinx.ext.coverage",
"myst_parser",
"sphinx_sitemap",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# Base URL for sitemap generation
html_baseurl = "https://tzst.xi-xu.me/"
# Sitemap configuration
sitemap_url_scheme = "{link}"
sitemap_filename = "sitemap.xml"
# -- Options for HTML output -------------------------------------------------
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
html_title = f"tzst {version} Documentation"
html_short_title = "tzst"
# HTML meta tags
html_meta = {
"description": "tzst - A Python library for creating and extracting tar.zst archives with high performance and comprehensive features",
"keywords": "tzst, tar, zstandard, compression, archive, python, extraction, backup",
"author": "Xi Xu",
"robots": "index, follow",
"language": "en",
"viewport": "width=device-width, initial-scale=1.0",
"theme-color": "#2980B9",
"msapplication-TileColor": "#2980B9",
"og:title": "tzst Documentation",
"og:description": "tzst - A Python library for creating and extracting tar.zst archives with high performance and comprehensive features",
"og:type": "website",
"og:url": "https://tzst.xi-xu.me/",
"og:image": "https://tzst.xi-xu.me/_static/tzst-logo.png",
"og:site_name": "tzst Documentation",
"og:locale": "en_US",
"twitter:card": "summary_large_image",
"twitter:title": "tzst Documentation",
"twitter:description": "tzst - A Python library for creating and extracting tar.zst archives with high performance and comprehensive features",
"twitter:image": "https://tzst.xi-xu.me/_static/tzst-logo.png",
"twitter:site": "@xixu_me",
"twitter:creator": "@xixu_me",
"article:author": "Xi Xu",
"article:publisher": "https://xi-xu.me",
}
# Theme options
html_theme_options = {
"canonical_url": "https://xixu-me.github.io/tzst/",
"canonical_url": "https://tzst.xi-xu.me/",
"logo_only": False,
"display_version": True,
"prev_next_buttons_location": "bottom",
"style_external_links": False,
"style_nav_header_background": "#2980B9",
"collapse_navigation": True,
"sticky_navigation": True,
"navigation_depth": 4,
"includehidden": True,
"includehidden": False,
"titles_only": False,
}
# Additional HTML options
html_favicon = "_static/favicon.ico" # Will show warning until favicon is created
html_logo = "_static/tzst-logo.png" # Will show warning until logo is created
html_use_opensearch = "https://tzst.xi-xu.me/"
# HTML context for custom template variables
html_context = {
"display_github": True,
"github_user": "xixu-me",
"github_repo": "tzst",
"github_version": "main",
"conf_py_path": "/docs/",
}
# -- Extension configuration -------------------------------------------------
autodoc_default_options = {
"members": True,
@@ -88,3 +138,14 @@ myst_enable_extensions = [
"substitution",
"tasklist",
]
# SEO optimization settings
html_copy_source = False # Don't copy source files to _sources (reduces crawl)
html_show_sourcelink = False # Hide "View page source" links
html_show_sphinx = False # Don't show "Created using Sphinx" in footer
# Additional HTML files to include (robots.txt will be copied from _static)
html_extra_path = []
# Language for content autogenerated by Sphinx
language = "en"
+369
View File
@@ -0,0 +1,369 @@
---
myst:
html_meta:
description: "Complete development guide for tzst - Setup, testing, contribution guidelines, and best practices"
keywords: "tzst development, Python development, contributing to tzst, testing guide, documentation"
og:title: "tzst Development Guide"
og:description: "Complete development guide for tzst - Setup, testing, contribution guidelines, and best practices"
twitter:title: "tzst Development Guide"
twitter:description: "Complete development guide for tzst - Setup, testing, contribution guidelines, and best practices"
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/development.html"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# Development Guide
This guide provides comprehensive information for developers contributing to or working with the tzst library.
## Setting up Development Environment
This project uses modern Python packaging standards:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
The development installation includes all necessary tools:
- **pytest** - Testing framework
- **ruff** - Linting and formatting
- **coverage** - Code coverage analysis
- **sphinx** - Documentation generation
## Running Tests
### Basic Test Commands
```bash
# Run all tests
python -m pytest
# Run tests with coverage
pytest --cov=tzst --cov-report=html
# Or use the simpler command (coverage settings are in pyproject.toml)
pytest
# Run with verbose output
python -m pytest -v
# Run specific test file
python -m pytest tests/test_core.py
# Run integration tests only
python -m pytest -m integration
```
### Test Structure
- **Unit tests**: Test individual functions and methods
- **Integration tests**: Test component interactions
- **CLI tests**: Test command-line interface
- **Platform-specific tests**: Test OS-specific functionality
### Writing Tests
1. **Use descriptive test names:**
```python
def test_create_archive_with_compression_level_9():
```
2. **Use fixtures for common test data:**
```python
def test_extract_archive(sample_archive_path, temp_dir):
```
3. **Test edge cases:**
- Empty files
- Large files
- Invalid inputs
- Corrupted archives
4. **Add markers for test categorization:**
```python
@pytest.mark.integration
def test_full_archive_workflow():
```
## Code Quality
### Running Code Style Tools
```bash
# Check code quality
ruff check src tests
# Fix auto-fixable issues
ruff check --fix src tests
# Format code
ruff format src tests
# Check formatting without making changes
ruff format --check src tests
```
### Configuration
Settings are defined in `pyproject.toml`:
- Line length: 88 characters
- Target Python version: 3.12+ (tested on 3.12-3.14)
- Import sorting with isort
- Quote style: double quotes
### Code Style Guidelines
1. **Follow PEP 8** with project-specific modifications
2. **Use type hints** for all public APIs
3. **Write docstrings** for classes and public methods
4. **Keep functions focused** and reasonably sized
5. **Use meaningful variable names**
6. **Add comments** for complex logic
## Documentation
### Building Documentation
```bash
# Navigate to docs directory
cd docs
# Install documentation dependencies
pip install -r requirements.txt
# Build HTML documentation
make html
# On Windows, use:
make.bat html
# View built documentation
# Open docs/_build/html/index.html in your browser
```
### Documentation Structure
```
docs/
├── index.md # Main documentation landing page
├── quickstart.md # Getting started guide
├── performance.md # Performance guide and comparisons
├── examples.md # Usage examples
├── development.md # This development guide
├── api/ # API reference documentation
│ ├── index.md
│ ├── core.md
│ ├── cli.md
│ └── exceptions.md
├── conf.py # Sphinx configuration
└── requirements.txt # Documentation dependencies
```
### Writing Documentation
- Use **MyST Markdown** format
- Include **code examples** for new features
- Add **cross-references** using proper syntax
- Test all **code snippets** to ensure they work
## Project Structure
```
tzst/
├── src/tzst/ # Main package source code
│ ├── __init__.py # Package initialization and exports
│ ├── __main__.py # CLI entry point
│ ├── cli.py # Command-line interface
│ ├── core.py # Core archive functionality
│ └── exceptions.py # Custom exceptions
├── tests/ # Test suite
│ ├── conftest.py # Pytest configuration and fixtures
│ ├── test_core.py # Core functionality tests
│ ├── test_cli.py # CLI tests
│ └── test_*.py # Additional test modules
├── docs/ # Documentation source
├── .github/ # GitHub workflows and templates
├── pyproject.toml # Project configuration
├── README.md # Project Readme
├── LICENSE # BSD 3-Clause License
└── CONTRIBUTING.md # Contribution guidelines
```
## Contributing Workflow
### 1. Making Changes
#### Types of Contributions
- **Bug fixes**: Fix issues in existing functionality
- **Features**: Add new capabilities to the library
- **Documentation**: Improve or add documentation
- **Tests**: Add or improve test coverage
- **Performance**: Optimize existing code
- **Security**: Address security vulnerabilities
#### Branch Naming
Use descriptive branch names:
- `feature/add-streaming-mode`
- `fix/handle-corrupted-archives`
- `docs/improve-api-documentation`
- `test/add-compression-tests`
### 2. Commit Messages
Follow conventional commit format:
```
type(scope): description
[optional body]
[optional footer]
```
**Types:**
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `test`: Adding or modifying tests
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `chore`: Build process or auxiliary tool changes
**Examples:**
```
feat(core): add streaming compression support
fix(cli): handle invalid archive paths gracefully
docs(readme): update installation instructions
```
### 3. Pull Request Process
1. **Create a feature branch:**
```bash
git checkout -b feature/your-feature-name
```
2. **Make your changes** following the guidelines above
3. **Add tests** for new functionality
4. **Update documentation** if needed
5. **Run the test suite:**
```bash
python -m pytest
ruff check .
ruff format --check .
```
6. **Commit your changes:**
```bash
git add .
git commit -m "feat: add your feature description"
```
7. **Push to your fork:**
```bash
git push origin feature/your-feature-name
```
8. **Create a pull request** using the provided template
### 4. Pull Request Guidelines
- **Fill out the PR template** completely
- **Link related issues** using keywords (fixes #123)
- **Keep PRs focused** - one feature/fix per PR
- **Ensure all CI checks pass**
- **Respond to review feedback** promptly
## Development Tips
### Performance Considerations
- Use streaming for large files
- Consider memory usage patterns
- Profile code for bottlenecks
- Test with various file sizes
### Security Considerations
- Validate all user inputs
- Use secure defaults (e.g., 'data' filter)
- Handle malicious archives safely
- Be cautious with file paths
### Compatibility
- Support Python 3.12+ with CI coverage for 3.12-3.14
- Test on multiple platforms (Windows, macOS, Linux)
- Consider different filesystem behaviors
- Maintain backwards compatibility when possible
## Release Process
Releases are handled by maintainers:
1. Update version in `src/tzst/__init__.py`
2. Create a release tag
3. Automated CI/CD publishes to PyPI
## Getting Help
### Resources
- **Issues**: [GitHub Issues](https://github.com/xixu-me/tzst/issues)
- **Discussions**: Use GitHub Discussions for questions
- **Documentation**: Check the README and code comments
### Reporting Issues
When reporting bugs:
1. **Use the bug report template**
2. **Provide a minimal reproduction case**
3. **Include system information** (OS, Python version)
4. **Attach relevant files** if possible (archives, logs)
### Suggesting Features
When suggesting features:
1. **Use the feature request template**
2. **Explain the use case** and motivation
3. **Consider backwards compatibility**
4. **Provide implementation ideas** if you have them
## Code of Conduct
This project follows the principles of respectful collaboration. Please be kind, constructive, and professional in all interactions.
## Recognition
Contributors are recognized in several ways:
- Listed in release notes for significant contributions
- Mentioned in README acknowledgments
- GitHub contributor statistics
Thank you for contributing to tzst! Your efforts help make this library better for everyone.
+1094 -452
View File
File diff suppressed because it is too large. Load diff
+150 -24
View File
@@ -1,5 +1,29 @@
---
myst:
html_meta:
description: "tzst - Next-generation Python library for tar.zst archives with Zstandard compression. Fast, secure, and reliable archive management."
keywords: "tzst, Python, tar.zst, Zstandard, compression, archive, backup, file management"
og:title: "tzst - Next-Generation Archive Management"
og:description: "Fast, secure, and reliable Python library for tar.zst archives with Zstandard compression"
twitter:title: "tzst - Next-Generation Archive Management"
twitter:description: "Fast, secure, and reliable Python library for tar.zst archives with Zstandard compression"
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# tzst Documentation
[![codecov](https://codecov.io/gh/xixu-me/tzst/graph/badge.svg?token=2AIN1559WU)](https://codecov.io/gh/xixu-me/tzst)
[![CodeQL](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
[![CI/CD](https://github.com/xixu-me/tzst/actions/workflows/ci.yml/badge.svg)](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
[![PyPI - Version](https://img.shields.io/pypi/v/tzst)](https://pypi.org/project/tzst/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/tzst)](https://pypistats.org/packages/tzst)
[![GitHub License](https://img.shields.io/github/license/xixu-me/tzst)](https://github.com/xixu-me/tzst/blob/main/LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-violet)](https://xi-xu.me/#sponsorships)
Welcome to **tzst**, the next-generation Python library engineered for modern archive management, leveraging cutting-edge Zstandard compression to deliver superior performance, security, and reliability.
```{toctree}
@@ -7,55 +31,157 @@ Welcome to **tzst**, the next-generation Python library engineered for modern ar
:caption: Contents:
quickstart
api/index
performance
examples
changelog
api/index
development
genindex
```
```{toctree}
:hidden:
404
README
```
## What is tzst?
**tzst** is a Python library built exclusively for Python 3.12+ that provides enterprise-grade solutions for handling `.tzst`/`.tar.zst` archives. It combines atomic operations, streaming efficiency, and a meticulously crafted API to redefine how developers handle compressed archives in production environments.
**tzst** is a modern Python library built exclusively for Python 3.12+ that provides comprehensive support for creating, extracting, and managing `.tzst` and `.tar.zst` archives. It combines the proven reliability of the tar format with the superior compression efficiency of Zstandard (zstd) to deliver:
- **Superior Performance**: Fast compression and decompression with excellent compression ratios
- **Enterprise-Grade Security**: Safe extraction with built-in protections against path traversal attacks
- **Memory Efficiency**: Streaming mode for handling large archives with minimal memory usage
- **Cross-Platform Compatibility**: Works seamlessly on Windows, macOS, and Linux
- **Developer-Friendly**: Clean, Pythonic API with comprehensive error handling
## Key Features
- **🚀 High Performance**: Leverages Zstandard compression for superior speed and compression ratios
- **🔒 Security First**: Built-in extraction filters protect against malicious archives
- **⚡ Streaming Support**: Memory-efficient handling of large archives
- **🛡️ Atomic Operations**: Ensures data integrity with fail-safe file operations
- **🎯 Modern API**: Clean, intuitive interface designed for Python 3.12+
- **📦 CLI Tools**: Comprehensive command-line interface for everyday tasks
### Advanced Compression
- **Zstandard Compression**: Best-in-class compression algorithm with configurable levels (1-22)
- **Multiple Extensions**: Support for both `.tzst` and `.tar.zst` file extensions
- **Streaming Support**: Memory-efficient processing for large archives
### Security First
- **Safe by Default**: Uses 'data' filter for secure extraction without dangerous path traversal
- **Multiple Filter Options**: Choose from 'data', 'tar', or 'fully_trusted' filters based on your security needs
- **Atomic Operations**: All file operations use temporary files with atomic moves to prevent corruption
### Dual Interfaces
- **Command Line**: Intuitive CLI with comprehensive options for batch operations
- **Python API**: Clean, object-oriented interface for programmatic use
- **Convenience Functions**: High-level functions for common operations
### High Performance
- **Optimized I/O**: Efficient buffering and streaming for large files
- **Conflict Resolution**: Intelligent handling of file conflicts during extraction
- **Cross-Platform**: Native performance on all major operating systems
## Quick Example
```python
from tzst import TzstArchive
from tzst import TzstArchive, create_archive, extract_archive
# Create a new archive
with TzstArchive("backup.tzst", "w", compression_level=5) as archive:
archive.add("documents/")
archive.add("photos/", recursive=True)
# Create an archive
create_archive("backup.tzst", ["documents/", "photos/"], compression_level=5)
# Extract with security
with TzstArchive("backup.tzst", "r") as archive:
archive.extract("documents/", filter="data")
# Extract an archive
extract_archive("backup.tzst", "restore/")
# Work with archives programmatically
with TzstArchive("data.tzst", "r") as archive:
contents = archive.list(verbose=True)
archive.extract("important.txt", "output/")
is_valid = archive.test()
```
## Installation
Install tzst from PyPI:
For detailed installation instructions, including standalone binaries and source installation, please refer to the {doc}`quickstart` guide.
```bash
# Install from PyPI
pip install tzst
# Or using uv (recommended)
uv tool install tzst
```
## Getting Started
For a quick introduction to using tzst, see the {doc}`quickstart` guide.
For a quick introduction, see the {doc}`quickstart` guide. For comprehensive usage examples, explore the {doc}`examples` section.
For detailed API documentation, browse the {doc}`api/index` section.
### API Documentation
## Indices and tables
Complete API documentation is available in the {doc}`api/index` section, covering:
- {ref}`genindex`
- {ref}`modindex`
- {ref}`search`
- {doc}`api/core`: Main classes and functions
- {doc}`api/cli`: Command-line interface
- {doc}`api/exceptions`: Error handling
## Development
For comprehensive development information, see the {doc}`development` guide, which covers:
- Setting up development environment
- Running tests and code quality checks
- Documentation building
- Contributing workflow and guidelines
- Project structure and best practices
### Quick Start
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
pytest
```
## Contributing
We welcome contributions! Please read our [Contributing Guide](https://github.com/xixu-me/tzst/blob/main/CONTRIBUTING.md) for:
- Development setup and project structure
- Code style guidelines and best practices
- Testing requirements and writing tests
- Pull request process and review workflow
### Types of Contributions Welcome
- **Bug fixes** - Fix issues in existing functionality
- **Features** - Add new capabilities to the library
- **Documentation** - Improve or add documentation
- **Tests** - Add or improve test coverage
- **Performance** - Optimize existing code
- **Security** - Address security vulnerabilities
## Acknowledgments
- [Meta Zstandard](https://github.com/facebook/zstd) for the excellent compression algorithm
- [python-zstandard](https://github.com/indygreg/python-zstandard) for Python bindings
- The Python community for inspiration and feedback
## License
Copyright © [Xi Xu](https://xi-xu.me). All rights reserved.
Licensed under the [BSD 3-Clause](https://github.com/xixu-me/tzst/blob/main/LICENSE) license.
## Documentation Guide
1. **{doc}`quickstart`** - Get up and running quickly with basic examples
2. **{doc}`performance`** - Performance optimization guide and comparisons
3. **{doc}`examples`** - Comprehensive usage examples and patterns
4. **{doc}`api/index`** - Complete API reference documentation
5. **{doc}`development`** - Development and contribution guidelines
6. **{ref}`genindex`** - Index of all documented items
## Requirements
- Python 3.12 or higher (tested on 3.12-3.14)
- zstandard >= 0.19.0
+283
View File
@@ -0,0 +1,283 @@
---
myst:
html_meta:
description: "tzst Performance Guide - Compression level optimization, performance tips, and comparison with other archive tools"
keywords: "tzst performance, compression benchmarks, tar gzip comparison, archive performance optimization"
og:title: "tzst Performance Guide"
og:description: "Performance optimization tips and comparison with other archive tools for tzst"
twitter:title: "tzst Performance Guide"
twitter:description: "Performance optimization tips and comparison with other archive tools for tzst"
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# Performance Guide
This guide covers performance optimization techniques and provides detailed comparisons with other archive tools.
## Performance Tips
### 1. Compression Levels
Choose the right compression level for your use case:
- **Level 1-3**: Fast compression, larger files (good for temporary archives or real-time processing)
- **Level 3** (default): Optimal balance for most use cases
- **Level 6-9**: Higher compression, moderate speed (good for regular backups)
- **Level 15-22**: Maximum compression, slower (for long-term storage or bandwidth-limited scenarios)
```python
from tzst import create_archive
# For temporary files or frequent operations
create_archive("temp.tzst", files, compression_level=1)
# Balanced default (recommended)
create_archive("backup.tzst", files, compression_level=3)
# Long-term storage
create_archive("archive.tzst", files, compression_level=9)
# Maximum compression for critical space savings
create_archive("minimal.tzst", files, compression_level=22)
```
### 2. Streaming
Use streaming mode for archives larger than 100MB:
```python
from tzst import extract_archive, list_archive, test_archive
# Memory-efficient operations for large archives
extract_archive("large-backup.tzst", "restore/", streaming=True)
contents = list_archive("large-backup.tzst", streaming=True)
is_valid = test_archive("large-backup.tzst", streaming=True)
```
**Streaming Benefits:**
- Significantly reduced memory usage
- Better performance for large archives
- Handles archives that don't fit in memory
### 3. Batch Operations
Add multiple files in a single session when possible:
```python
from tzst import TzstArchive
# Efficient: Single archive session
with TzstArchive("backup.tzst", "w") as archive:
archive.add("file1.txt")
archive.add("file2.txt")
archive.add("directory/", recursive=True)
# Less efficient: Multiple separate operations
create_archive("backup1.tzst", ["file1.txt"])
create_archive("backup2.tzst", ["file2.txt"])
```
### 4. File Type Considerations
- Already compressed files (`.jpg`, `.png`, `.mp4`, `.pdf`) won't compress much further
- Text files, source code, and logs compress very well
- Consider compression level based on your data types
## Comparison with Other Tools
### vs tar + gzip
**tzst Advantages:**
- **Better compression ratios**: 10-40% smaller archives
- **Faster decompression**: 2-3x faster extraction
- **Modern algorithm**: Better handling of various file types
- **Streaming support**: Better memory efficiency
**When to use tar + gzip:**
- Legacy system compatibility requirements
- Very old systems without zstd support
### vs tar + xz
**tzst Advantages:**
- **Significantly faster compression**: 3-10x faster creation
- **Faster decompression**: 2-4x faster extraction
- **Better speed/compression trade-off**: Similar compression with much better speed
- **More compression levels**: Fine-grained control (22 levels vs 9)
**When to use tar + xz:**
- Maximum compression is critical and time is not a factor
- Systems that don't support zstd
### vs zip
**tzst Advantages:**
- **Better compression**: 15-30% smaller archives
- **Preserves Unix permissions and metadata**: Full POSIX compatibility
- **Better streaming support**: Memory-efficient for large archives
- **Better directory handling**: Preserves directory structure and timestamps
**When to use zip:**
- Cross-platform compatibility with very old systems
- Individual file access without full extraction is required
- Windows-centric environments with no command-line tools
## Benchmarking Examples
### Compression Level Benchmark
```python
import time
from pathlib import Path
from tzst import create_archive
def benchmark_compression_levels(files, output_prefix="benchmark"):
"""Compare different compression levels."""
levels_to_test = [1, 3, 6, 9, 15, 22]
results = []
for level in levels_to_test:
output_file = f"{output_prefix}_level_{level}.tzst"
# Measure compression time
start_time = time.time()
create_archive(output_file, files, compression_level=level)
compress_time = time.time() - start_time
# Get file size
file_size = Path(output_file).stat().st_size
results.append({
'level': level,
'time': compress_time,
'size': file_size,
'size_mb': file_size / (1024 * 1024)
})
print(f"Level {level}: {compress_time:.2f}s, {file_size/1024/1024:.1f} MB")
return results
# Example usage
files = ["documents/", "projects/"]
results = benchmark_compression_levels(files)
```
### Memory Usage Comparison
```python
import psutil
import os
from tzst import extract_archive
def monitor_memory_usage(func, *args, **kwargs):
"""Monitor memory usage during function execution."""
process = psutil.Process(os.getpid())
initial_memory = process.memory_info().rss / 1024 / 1024 # MB
func(*args, **kwargs)
peak_memory = process.memory_info().rss / 1024 / 1024 # MB
return peak_memory - initial_memory
# Compare streaming vs non-streaming extraction
large_archive = "large-dataset.tzst"
memory_normal = monitor_memory_usage(extract_archive, large_archive, "output1/")
memory_streaming = monitor_memory_usage(extract_archive, large_archive, "output2/", streaming=True)
print(f"Normal extraction: {memory_normal:.1f} MB")
print(f"Streaming extraction: {memory_streaming:.1f} MB")
print(f"Memory savings: {memory_normal - memory_streaming:.1f} MB")
```
## Best Practices
### For Development
```python
# Fast compression for frequent builds
create_archive("build-artifacts.tzst", ["build/"], compression_level=1)
```
### For Backups
```python
# Balanced compression for regular backups
create_archive("daily-backup.tzst", ["data/"], compression_level=6)
```
### For Distribution
```python
# Higher compression for software distribution
create_archive("software-package.tzst", ["app/"], compression_level=9)
```
### For Archival Storage
```python
# Maximum compression for long-term storage
create_archive("archive-2024.tzst", ["historical-data/"], compression_level=22)
```
## Hardware Considerations
### CPU Usage
- Higher compression levels use more CPU but for shorter time periods
- Modern multi-core systems handle zstd compression very efficiently
- Consider system load when choosing compression levels
### Memory Usage
- Streaming mode: ~16-32 MB memory usage regardless of archive size
- Normal mode: Memory usage proportional to archive size
- Use streaming for archives >100 MB or on memory-constrained systems
### Storage
- SSDs benefit from higher compression (less I/O)
- HDDs may prefer lower compression levels (CPU vs I/O trade-off)
- Network storage benefits from higher compression (bandwidth savings)
## Integration with Build Systems
### Makefile Example
```makefile
# Fast compression for development
build-dev:
tzst a build-dev.tzst build/ -l 1
# Production compression
build-prod:
tzst a build-prod.tzst build/ -l 9
# CI/CD artifacts
artifacts:
tzst a artifacts.tzst dist/ logs/ -l 6
```
### GitHub Actions Example
```yaml
- name: Create release archive
run: |
tzst a release-${{ github.ref_name }}.tzst \
build/ docs/ \
--compression-level 9
```
This performance guide helps you choose the right settings for your specific use case and understand how tzst compares to alternative archive tools.
+373 -156
View File
@@ -1,222 +1,439 @@
---
myst:
html_meta:
description: "Quick start guide for tzst - Learn how to install and use the Python tar.zst archive library in minutes"
keywords: "tzst tutorial, Python archive tutorial, tar.zst guide, Zstandard compression guide"
og:title: "tzst Quick Start Guide"
og:description: "Learn how to install and use tzst for Python tar.zst archive management in minutes"
twitter:title: "tzst Quick Start Guide"
twitter:description: "Learn how to install and use tzst for Python tar.zst archive management in minutes"
og:type: "website"
og:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
og:url: "https://tzst.xi-xu.me/"
twitter:card: "summary_large_image"
twitter:image: "https://tzst.xi-xu.me/_static/tzst-square-logo.png"
---
# Quick Start Guide
This guide will help you get started with tzst quickly and efficiently.
This guide will get you up and running with tzst in just a few minutes.
(installation)=
## Installation
Install tzst using pip:
Choose your preferred installation method:
### From GitHub Releases
Download standalone executables that don't require Python installation:
#### Supported Platforms
| Platform | Architecture | File |
|----------|-------------|------|
| **🐧 Linux** | x86_64 | `tzst-{version}-linux-amd64.zip` |
| **🐧 Linux** | ARM64 | `tzst-{version}-linux-arm64.zip` |
| **🪟 Windows** | x64 | `tzst-{version}-windows-amd64.zip` |
| **🪟 Windows** | ARM64 | `tzst-{version}-windows-arm64.zip` |
| **🍎 macOS** | Intel | `tzst-{version}-darwin-amd64.zip` |
| **🍎 macOS** | Apple Silicon | `tzst-{version}-darwin-arm64.zip` |
#### 🛠️ Installation Steps
1. **📥 Download** the appropriate archive for your platform from the [latest releases page](https://github.com/xixu-me/tzst/releases/latest)
2. **📦 Extract** the archive to get the `tzst` executable (or `tzst.exe` on Windows)
3. **📂 Move** the executable to a directory in your PATH:
- **🐧 Linux/macOS**: `sudo mv tzst /usr/local/bin/`
- **🪟 Windows**: Add the directory containing `tzst.exe` to your PATH environment variable
4. **✅ Verify** installation: `tzst --help`
#### 🎯 Benefits of Binary Installation
- ✅ **No Python required** - Standalone executable
- ✅ **Faster startup** - No Python interpreter overhead
- ✅ **Easy deployment** - Single file distribution
- ✅ **Consistent behavior** - Bundled dependencies
### From PyPI
Using pip:
```bash
pip install tzst
```
Or using uv (recommended):
```bash
uv tool install tzst
```
### From Source
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install .
```
### Development Installation
This project uses modern Python packaging standards:
```bash
git clone https://github.com/xixu-me/tzst.git
cd tzst
pip install -e .[dev]
```
(basic-usage)=
## Basic Usage
### Creating Archives
### Command Line Interface
Use the `TzstArchive` class or convenience functions to create archives:
> **Note**: Download the [standalone binary](installation) for the best performance and no Python dependency. Alternatively, use `uvx tzst` for running without installation. See [uv documentation](https://docs.astral.sh/uv/) for details.
```python
from tzst import TzstArchive, create_archive
# Using TzstArchive class
with TzstArchive("my_archive.tzst", "w", compression_level=5) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# Using convenience function
create_archive(
archive_path="backup.tzst",
files=["documents/", "photos/", "config.txt"],
compression_level=10
)
```
### Extracting Archives
Extract archives safely with built-in security filters:
```python
from tzst import TzstArchive, extract_archive
# Using TzstArchive class
with TzstArchive("my_archive.tzst", "r") as archive:
# Extract all files with security filter
archive.extract("output/", filter="data")
# Extract specific files
archive.extract("output/", members=["file.txt"], filter="data")
# Using convenience function
extract_archive("backup.tzst", "restore/")
```
### Listing Archive Contents
View what's inside an archive:
```python
from tzst import TzstArchive, list_archive
# Using TzstArchive class
with TzstArchive("my_archive.tzst", "r") as archive:
contents = archive.list(verbose=True)
for item in contents:
print(f"{item['name']} - {item['size']} bytes")
# Using convenience function
files = list_archive("backup.tzst", verbose=True)
```
### Testing Archive Integrity
Verify that an archive is valid:
```python
from tzst import TzstArchive, test_archive
# Using TzstArchive class
with TzstArchive("my_archive.tzst", "r") as archive:
is_valid = archive.test()
print(f"Archive is {'valid' if is_valid else 'corrupted'}")
# Using convenience function
if test_archive("backup.tzst"):
print("Archive is valid")
```
## Command Line Interface
tzst provides a comprehensive CLI for archive operations:
### Creating Archives
The CLI provides four main operations:
```bash
# Create an archive with multiple files
tzst a backup.tzst documents/ photos/ config.txt
# Create an archive
tzst a archive.tzst file1.txt file2.txt directory/
# Extract an archive
tzst x archive.tzst
# List archive contents
tzst l archive.tzst
# Test archive integrity
tzst t archive.tzst
```
### Command Reference
| Command | Aliases | Description | Streaming Support |
|---------|---------|-------------|-------------------|
| `a` | `add`, `create` | Create or add to archive | N/A |
| `x` | `extract` | Extract with full paths | `--streaming` |
| `e` | `extract-flat` | Extract without directory structure | `--streaming` |
| `l` | `list` | List archive contents | `--streaming` |
| `t` | `test` | Test archive integrity | `--streaming` |
### CLI Options
- `-v, --verbose`: Enable verbose output
- `-o, --output DIR`: Specify output directory (extract commands)
- `-l, --level LEVEL`: Set compression level 1-22 (create command)
- `--streaming`: Enable streaming mode for memory-efficient processing
- `--filter FILTER`: Security filter for extraction (data/tar/fully_trusted)
- `--no-atomic`: Disable atomic file operations (not recommended)
#### Create Archives
```bash
# Create archive with default compression (level 3)
tzst a backup.tzst documents/ photos/
# Create with high compression
tzst a -l 15 backup.tzst large_files/
tzst a backup.tzst documents/ photos/ --compression-level 9
# Create without atomic operations (faster, less safe)
tzst a --no-atomic backup.tzst files/
# Create from current directory
tzst a project.tzst .
# Specify different output location
tzst a /backups/data.tzst /home/user/important/
```
### Extracting Archives
#### Extract Archives
```bash
# Extract all files (default: safe extraction)
# Extract to current directory
tzst x backup.tzst
# Extract to specific directory
tzst x backup.tzst -o restore/
tzst x backup.tzst --output /restore/
# Extract specific files only
tzst x backup.tzst config.txt documents/
tzst x backup.tzst documents/report.pdf photos/vacation.jpg
# Extract with streaming (memory efficient)
tzst x backup.tzst --streaming
# Extract with conflict resolution
tzst x backup.tzst --conflict-resolution skip
```
### Listing Contents
#### List Contents
```bash
# Simple listing
tzst l backup.tzst
# Detailed listing with file info
tzst l backup.tzst -v
tzst l backup.tzst --verbose
# Streaming mode for large archives
tzst l backup.tzst --streaming
# Stream large archives efficiently
tzst l huge-archive.tzst --streaming
```
### Testing Archives
### Python API
```bash
# Test archive integrity
tzst t backup.tzst
# Test with streaming
tzst t backup.tzst --streaming
```
## Security Considerations
tzst includes built-in security features to protect against malicious archives:
### Extraction Filters
Always use appropriate filters when extracting archives from untrusted sources:
- **`data`** (default): Safest option, only extracts regular files and directories
- **`tar`**: Honors most tar features but still secure
- **`fully_trusted`**: No restrictions (only use with completely trusted archives)
#### Quick Start
```python
# Safe extraction (recommended)
archive.extract("output/", filter="data")
from tzst import create_archive, extract_archive, list_archive, test_archive
# Command line
tzst x archive.tzst --filter=data
# Create an archive
create_archive("backup.tzst", ["documents/", "photos/"], compression_level=5)
# Extract an archive
extract_archive("backup.tzst", "restore/")
# List contents
contents = list_archive("backup.tzst", verbose=True)
for item in contents:
print(f"{item['name']} - {item['size']} bytes")
# Test integrity
is_valid = test_archive("backup.tzst")
print(f"Archive is {'valid' if is_valid else 'corrupted'}")
```
### Best Practices
1. **Always use the default `data` filter** for untrusted archives
2. **Enable atomic operations** (default) for data integrity
3. **Use streaming mode** for very large archives to save memory
4. **Validate archives** with `test()` before processing
5. **Specify output directories** explicitly to avoid overwrites
## Performance Tips
### Memory Efficiency
For large archives, use streaming mode:
#### Using the TzstArchive Class
```python
# Streaming mode uses less memory
with TzstArchive("large.tzst", "r", streaming=True) as archive:
archive.extract("output/")
from tzst import TzstArchive
# Create a new archive
with TzstArchive("data.tzst", "w", compression_level=6) as archive:
archive.add("file.txt")
archive.add("directory/", recursive=True)
# Add with custom archive name
archive.add("config/prod.yaml", arcname="config.yaml")
# Read an existing archive
with TzstArchive("data.tzst", "r") as archive:
# List contents
contents = archive.list(verbose=True)
for item in contents:
print(f"{item['name']} - {item['size']} bytes")
# Test integrity
is_valid = archive.test()
print(f"Archive is {'valid' if is_valid else 'corrupted'}")
# Extract specific files
archive.extract("file.txt", "output/")
# Extract all files
archive.extractall("restore/")
```
### Compression Levels
## Advanced Features
Choose appropriate compression levels based on your needs:
- **Level 1-3**: Fast compression, larger files
- **Level 3-6**: Balanced (default: 3)
- **Level 7-15**: Better compression, slower
- **Level 16-22**: Maximum compression, much slower
### Security and Filtering
```python
# Fast compression for temporary files
TzstArchive("temp.tzst", "w", compression_level=1)
from tzst import extract_archive
# Maximum compression for long-term storage
TzstArchive("backup.tzst", "w", compression_level=15)
# Safe extraction with built-in security (default)
extract_archive("untrusted.tzst", "safe-output/", filter="data")
# For trusted archives with special features
extract_archive("trusted.tzst", "output/", filter="tar")
```
### Security Filters
tzst provides three security filter options for extraction:
```python
from tzst import extract_archive
# Extract with maximum security (default)
extract_archive("archive.tzst", "output/", filter="data")
# Extract with standard tar compatibility
extract_archive("archive.tzst", "output/", filter="tar")
# Extract with full trust (dangerous - only for trusted archives)
extract_archive("archive.tzst", "output/", filter="fully_trusted")
```
**Security Filter Options:**
- `data` (default): Most secure. Blocks dangerous files, absolute paths, and paths outside extraction directory
- `tar`: Standard tar compatibility. Blocks absolute paths and directory traversal
- `fully_trusted`: No security restrictions. Only use with completely trusted archives
### Conflict Resolution
```python
from tzst import extract_archive, ConflictResolution
# Skip existing files
extract_archive("archive.tzst", "output/",
conflict_resolution=ConflictResolution.SKIP_ALL)
# Auto-rename conflicting files
extract_archive("archive.tzst", "output/",
conflict_resolution=ConflictResolution.AUTO_RENAME_ALL)
```
### Performance Optimization
```python
from tzst import create_archive, extract_archive
# Create with different compression levels
create_archive("fast.tzst", files, compression_level=1) # Fastest
create_archive("balanced.tzst", files, compression_level=6) # Balanced
create_archive("best.tzst", files, compression_level=22) # Best compression
# Memory-efficient operations for large archives
extract_archive("huge-archive.tzst", "output/", streaming=True)
```
### Streaming Mode
For large archives (>100MB), use streaming mode to reduce memory usage:
```python
# Memory-efficient operations
with TzstArchive("large-archive.tzst", "r", streaming=True) as archive:
contents = archive.list()
archive.extractall("output/")
is_valid = archive.test()
```
**Note**: Streaming mode has limitations - you cannot extract specific files or use random access operations.
### File Extensions
The library automatically handles file extensions with intelligent normalization:
- `.tzst` - Primary extension for tar+zstandard archives
- `.tar.zst` - Alternative standard extension
- Auto-detection when opening existing archives
- Automatic extension addition when creating archives
```python
from tzst import create_archive
# These all create valid archives
create_archive("backup.tzst", files) # Creates backup.tzst
create_archive("backup.tar.zst", files) # Creates backup.tar.zst
create_archive("backup", files) # Creates backup.tzst
create_archive("backup.txt", files) # Creates backup.tzst (normalized)
```
### Atomic Operations
All file creation operations use atomic file operations by default:
- Archives created in temporary files first, then atomically moved
- Automatic cleanup if process is interrupted
- No risk of corrupted or incomplete archives
- Cross-platform compatibility
```python
# Atomic operations enabled by default
create_archive("important.tzst", files) # Safe from interruption
# Can be disabled if needed (not recommended)
create_archive("test.tzst", files, use_temp_file=False)
```
## Error Handling
tzst provides specific exceptions for different error conditions:
```python
from tzst import TzstArchive
from tzst.exceptions import TzstArchiveError, TzstDecompressionError
from tzst import create_archive, TzstArchiveError, TzstCompressionError
try:
with TzstArchive("archive.tzst", "r") as archive:
archive.extract("output/")
create_archive("backup.tzst", ["documents/"])
except TzstCompressionError as e:
print(f"Compression failed: {e}")
except TzstArchiveError as e:
print(f"Archive error: {e}")
except TzstDecompressionError as e:
print(f"Decompression error: {e}")
print(f"Archive operation failed: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
```
## Next Steps
- Explore the complete {doc}`api/index` documentation
- Check out more {doc}`examples` and use cases
- Read about advanced features in the full documentation
- Explore comprehensive {doc}`examples` for real-world scenarios
- Check the {doc}`api/index` for detailed API documentation
- See advanced features like atomic operations and custom filters
- Learn about integration with web frameworks and automation tools
## Read an Existing Archive
```python
with TzstArchive("data.tzst", "r") as archive:
# List contents
contents = archive.list(verbose=True)
# Extract specific file
archive.extract("file.txt", "output/")
# Test integrity
is_valid = archive.test()
# Get raw member information
members = archive.getmembers()
```
## Common Patterns
### Backup Script
```python
#!/usr/bin/env python3
from pathlib import Path
from datetime import datetime
from tzst import create_archive
def create_backup():
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"backup_{timestamp}.tzst"
# Backup important directories
directories = ["documents/", "projects/", "config/"]
print(f"Creating backup: {backup_name}")
create_archive(backup_name, directories, compression_level=6)
print(f"Backup created: {Path(backup_name).stat().st_size / 1024 / 1024:.1f} MB")
if __name__ == "__main__":
create_backup()
```
### Archive Verification
```python
from tzst import test_archive, list_archive
def verify_archive(archive_path):
print(f"Verifying {archive_path}...")
# Test integrity
if not test_archive(archive_path):
print("Archive is corrupted!")
return False
# List contents
contents = list_archive(archive_path, verbose=True)
total_size = sum(item['size'] for item in contents if item['is_file'])
file_count = sum(1 for item in contents if item['is_file'])
print(f"Archive is valid")
print(f"Files: {file_count}")
print(f"Total size: {total_size / 1024 / 1024:.1f} MB")
return True
```
## Further Learning
- Explore {doc}`examples` for more advanced usage patterns
- Check {doc}`performance` for detailed performance guidance
- Refer to the {doc}`api/index` for complete API documentation
+9 -7
View File
@@ -1,17 +1,19 @@
# Documentation requirements for Sphinx
sphinx>=7.1.0
sphinx-rtd-theme>=2.0.0
myst-parser>=3.0.0
sphinx>=9.1.0
sphinx-rtd-theme>=3.1.0
myst-parser>=5.1.0
sphinxcontrib-napoleon>=0.7
linkify-it-py>=2.1.0
# Additional Sphinx extensions
sphinx-autobuild>=2021.3.14
sphinx-autobuild>=2025.8.25
sphinx-copybutton>=0.5.2
sphinxext-opengraph>=0.9.0
sphinx-autodoc-typehints>=1.25.0
sphinxext-opengraph>=0.13.0
sphinx-autodoc-typehints>=3.13.2
sphinx-sitemap>=2.9.0
# Alternative modern theme (optional)
furo>=2024.1.29
furo>=2025.12.19
# Main package dependencies (needed for autodoc to import modules)
zstandard>=0.19.0,<1.0.0
+12 -10
View File
@@ -18,13 +18,21 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: System :: Archiving :: Compression",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = ["zstandard>=0.19.0,<1.0.0"]
[project.optional-dependencies]
dev = ["pytest>=7.0.0", "pytest-cov>=4.0.0", "ruff>=0.1.0"]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"ruff>=0.1.0",
"pre-commit>=3.6.0",
"build>=1.0.0",
"twine>=4.0.0",
]
[project.urls]
Homepage = "https://github.com/xixu-me/tzst"
@@ -42,13 +50,7 @@ path = "src/tzst/__init__.py"
packages = ["src/tzst"]
[tool.hatch.build.targets.sdist]
include = [
"src",
"tests",
"README.md",
"LICENSE",
"CONTRIBUTING.md",
]
include = ["src", "tests", "README.md", "LICENSE", "CONTRIBUTING.md"]
[tool.pytest.ini_options]
testpaths = ["tests"]
@@ -76,8 +78,8 @@ select = [
"RUF", # ruff specific rules
]
ignore = [
"E501", # line too long
"E501", # line too long - handled by formatter
"C901", # function too complex - accepted for core functionality
]
fixable = ["ALL"]
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""Standalone entry point for tzst - used for PyInstaller builds."""
import os
import sys
# Add the src directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from tzst.cli import main
if __name__ == "__main__":
sys.exit(main())
+6 -2
View File
@@ -1,6 +1,10 @@
"""tzst - The next-generation Python library engineered for modern archive management, leveraging cutting-edge Zstandard compression to deliver superior performance, security, and reliability."""
"""tzst - The next-generation Python library engineered for modern archive management.
__version__ = "1.1.1"
Leveraging cutting-edge Zstandard compression to deliver superior performance,
security, and reliability.
"""
__version__ = "1.3.3"
from .core import (
TzstArchive,
+521 -106
View File
@@ -1,15 +1,88 @@
"""Command-line interface for tzst."""
import argparse
import json
import sys
from pathlib import Path
from typing import Literal, cast
from typing import Any, Literal, cast
from . import __version__
from .core import create_archive, extract_archive, list_archive, test_archive
from .core import (
ConflictResolution,
create_archive,
extract_archive,
list_archive,
test_archive,
)
from .exceptions import TzstArchiveError, TzstDecompressionError
def _normalize_archive_path(archive_path: Path) -> Path:
"""Normalize archive path by ensuring correct extension.
This exactly mirrors the logic in core.py's create_archive function to show
the correct final path in CLI output.
Args:
archive_path: Input archive path
Returns:
Path: Normalized path with correct extension
"""
# Convert to Path if it's not already
archive_path = Path(archive_path)
# Ensure archive has correct extension - this logic exactly matches core.py
if archive_path.suffix.lower() not in [".tzst", ".zst"]:
if archive_path.suffix.lower() == ".tar":
archive_path = archive_path.with_suffix(".tar.zst")
else:
archive_path = archive_path.with_suffix(archive_path.suffix + ".tzst")
return archive_path
def _interactive_conflict_callback(target_path: Path) -> ConflictResolution:
"""Interactive callback for handling file conflicts in CLI.
Args:
target_path: Path of the conflicting file
Returns:
ConflictResolution: User's choice for handling the conflict
"""
print(f"\nFile already exists: {target_path}")
print("Choose an action:")
print(" [R] Replace")
print(" [N] Do not replace (skip)")
print(" [A] Replace all")
print(" [S] Skip all")
print(" [U] Auto-rename all")
print(" [X] Exit")
while True:
try:
choice = input("Enter choice [R/N/A/S/U/X]: ").strip().upper()
if choice == "R":
return ConflictResolution.REPLACE
elif choice == "N":
return ConflictResolution.SKIP
elif choice == "A":
return ConflictResolution.REPLACE_ALL
elif choice == "S":
return ConflictResolution.SKIP_ALL
elif choice == "U":
return ConflictResolution.AUTO_RENAME_ALL
elif choice == "X":
return ConflictResolution.EXIT
else:
print("Invalid choice. Please enter R, N, A, S, U, or X.")
except (EOFError, KeyboardInterrupt):
print("\nOperation cancelled by user")
return ConflictResolution.EXIT
def print_banner() -> None:
"""Print the version and copyright banner.
@@ -20,10 +93,70 @@ def print_banner() -> None:
None
"""
print()
print(f"tzst {__version__} : Copyright (c) 2025 Xi Xu")
print(f"tzst {__version__} : Copyright (c) Xi Xu")
print()
def _wants_json_output(args) -> bool:
"""Return True when the caller requested machine-readable output."""
return bool(getattr(args, "json_output", False))
def _emit_json(payload: dict[str, Any], *, to_stderr: bool = False) -> None:
"""Emit a JSON payload to stdout or stderr."""
stream = sys.stderr if to_stderr else sys.stdout
print(json.dumps(payload, ensure_ascii=True), file=stream)
def _emit_error(
args,
message: str,
*,
error_type: str,
exit_code: int = 1,
details: dict[str, Any] | None = None,
) -> int:
"""Emit an error in text or JSON format and return the exit code."""
if _wants_json_output(args):
payload: dict[str, Any] = {
"ok": False,
"error": {"type": error_type, "message": message},
}
if details:
payload["error"]["details"] = details
_emit_json(payload, to_stderr=True)
else:
print(message, file=sys.stderr)
return exit_code
def _summarize_listing(contents: list[dict[str, Any]]) -> dict[str, int | str]:
"""Build the summary block used by list output."""
total_files = 0
total_dirs = 0
total_size = 0
for item in contents:
if item["is_file"]:
total_files += 1
total_size += item["size"]
elif item["is_dir"]:
total_dirs += 1
return {
"files": total_files,
"directories": total_dirs,
"total_size_bytes": total_size,
"total_size_human": format_size(total_size),
}
def _should_print_banner(argv: list[str] | None) -> bool:
"""Determine whether the human-facing banner should be displayed."""
cli_args = argv if argv is not None else sys.argv[1:]
return "--json" not in cli_args and "--no-banner" not in cli_args
def format_size(size: int) -> str:
"""Format file size in human-readable format.
@@ -148,18 +281,23 @@ def _prepare_archive_creation(args) -> tuple[Path, list[Path], int, bool] | int:
# Validate files
missing_files = _validate_files(files)
if missing_files:
print(
return _emit_error(
args,
f"Error: Files not found - {', '.join(map(str, missing_files))}",
file=sys.stderr,
error_type="files_not_found",
details={"missing_files": [str(path) for path in missing_files]},
)
return 1
compression_level, use_temp_file = _extract_add_params(args)
return archive_path, files, compression_level, use_temp_file
def _execute_archive_creation(
archive_path: Path, files: list[Path], compression_level: int, use_temp_file: bool
args,
archive_path: Path,
files: list[Path],
compression_level: int,
use_temp_file: bool,
) -> int:
"""Execute the archive creation process.
@@ -172,18 +310,37 @@ def _execute_archive_creation(
Returns:
int: Exit code (0 for success, non-zero for failure)
"""
print(f"Creating archive: {archive_path}")
for file_path in files:
print(f" Adding: {file_path}")
# Normalize archive path to show the correct final filename
normalized_archive_path = _normalize_archive_path(archive_path)
if not _wants_json_output(args):
print(f"Creating archive: {normalized_archive_path}")
for file_path in files:
print(f" Adding: {file_path}")
# Use atomic file operations by default for better reliability
# This creates the archive in a temporary file first, then moves it
create_archive(archive_path, files, compression_level, use_temp_file=use_temp_file)
print(f"Archive created successfully - {archive_path}")
if _wants_json_output(args):
_emit_json(
{
"ok": True,
"command": "add",
"archive": str(archive_path),
"normalized_archive": str(normalized_archive_path),
"added": [str(file_path) for file_path in files],
"compression_level": compression_level,
"atomic": use_temp_file,
}
)
else:
print(f"Archive created successfully - {normalized_archive_path}")
return 0
def _handle_archive_creation_exceptions(func, *args, **kwargs) -> int:
def _handle_archive_creation_exceptions(command_args, func, *args, **kwargs) -> int:
"""Handle exceptions during archive creation.
Args:
@@ -199,19 +356,32 @@ def _handle_archive_creation_exceptions(func, *args, **kwargs) -> int:
except OSError:
return 1 # Error already printed in _validate_files
except ValueError as e:
print(f"Error: Invalid parameter - {e}", file=sys.stderr)
return 1
return _emit_error(
command_args,
f"Error: Invalid parameter - {e}",
error_type="invalid_parameter",
)
except TzstArchiveError as e:
print(f"Error: Archive operation failed - {e}", file=sys.stderr)
return 1
return _emit_error(
command_args,
f"Error: Archive operation failed - {e}",
error_type="archive_operation_failed",
)
except KeyboardInterrupt:
print("\nOperation interrupted by user", file=sys.stderr)
return _emit_error(
command_args,
"Operation interrupted by user",
error_type="interrupted",
exit_code=130,
)
# Clean up any partial files - the atomic operations in create_archive
# handle this
return 130 # Standard exit code for SIGINT
except Exception as e:
print(f"Error: Failed to create archive - {e}", file=sys.stderr)
return 1
return _emit_error(
command_args,
f"Error: Failed to create archive - {e}",
error_type="create_failed",
)
def cmd_add(args) -> int:
@@ -251,10 +421,10 @@ def cmd_add(args) -> int:
archive_path, files, compression_level, use_temp_file = preparation_result
return _execute_archive_creation(
archive_path, files, compression_level, use_temp_file
args, archive_path, files, compression_level, use_temp_file
)
return _handle_archive_creation_exceptions(_create_archive_workflow)
return _handle_archive_creation_exceptions(args, _create_archive_workflow)
def cmd_extract_full(args) -> int:
@@ -289,8 +459,12 @@ def cmd_extract_full(args) -> int:
try:
archive_path = Path(args.archive)
if not archive_path.exists():
print(f"Error: Archive not found - {archive_path}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive not found - {archive_path}",
error_type="archive_not_found",
details={"archive": str(archive_path)},
)
output_dir = Path(args.output) if args.output else Path.cwd()
members = args.files if hasattr(args, "files") and args.files else None
@@ -299,12 +473,38 @@ def cmd_extract_full(args) -> int:
Literal["data", "tar", "fully_trusted"], getattr(args, "filter", "data")
)
print(f"Extracting from: {archive_path}")
print(f"Output directory: {output_dir}")
if streaming:
print("Using streaming mode (memory efficient)")
if filter_type != "data":
print(f"Using security filter: {filter_type}")
# Handle conflict resolution parameters
conflict_resolution_str = getattr(args, "conflict_resolution", "ask")
interactive_flag = getattr(args, "interactive", False)
# If --interactive is specified, use "ask" regardless of --conflict-resolution
if interactive_flag:
conflict_resolution_str = "ask"
# Convert string to ConflictResolution enum
conflict_resolution = ConflictResolution(conflict_resolution_str)
if _wants_json_output(args) and conflict_resolution == ConflictResolution.ASK:
return _emit_error(
args,
"Error: JSON mode does not support interactive conflict prompts",
error_type="interactive_conflict_not_supported",
)
# Set up interactive callback if needed
interactive_callback = None
if conflict_resolution == ConflictResolution.ASK:
interactive_callback = _interactive_conflict_callback
if not _wants_json_output(args):
print(f"Extracting from: {archive_path}")
print(f"Output directory: {output_dir}")
if streaming:
print("Using streaming mode (memory efficient)")
if filter_type != "data":
print(f"Using security filter: {filter_type}")
if conflict_resolution != ConflictResolution.REPLACE:
print(f"Conflict resolution: {conflict_resolution.value}")
extract_archive(
archive_path,
@@ -313,25 +513,59 @@ def cmd_extract_full(args) -> int:
flatten=False,
streaming=streaming,
filter=filter_type,
conflict_resolution=conflict_resolution,
interactive_callback=interactive_callback,
)
print("Extraction completed successfully")
if _wants_json_output(args):
_emit_json(
{
"ok": True,
"command": "extract",
"archive": str(archive_path),
"output_dir": str(output_dir),
"members": members or [],
"flatten": False,
"streaming": streaming,
"filter": filter_type,
"conflict_resolution": conflict_resolution.value,
}
)
else:
print("Extraction completed successfully")
return 0
except FileNotFoundError as e:
print(f"Error: File not found - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: File not found - {e}",
error_type="file_not_found",
)
except TzstDecompressionError as e:
print(f"Error: Archive decompression failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive decompression failed - {e}",
error_type="decompression_failed",
)
except TzstArchiveError as e:
print(f"Error: Archive operation failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive operation failed - {e}",
error_type="archive_operation_failed",
)
except KeyboardInterrupt:
print("\nOperation interrupted by user", file=sys.stderr)
return 130
return _emit_error(
args,
"Operation interrupted by user",
error_type="interrupted",
exit_code=130,
)
except Exception as e:
print(f"Error: Failed to extract archive - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Failed to extract archive - {e}",
error_type="extract_failed",
)
def cmd_extract_flat(args) -> int:
@@ -367,8 +601,12 @@ def cmd_extract_flat(args) -> int:
try:
archive_path = Path(args.archive)
if not archive_path.exists():
print(f"Error: Archive not found - {archive_path}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive not found - {archive_path}",
error_type="archive_not_found",
details={"archive": str(archive_path)},
)
output_dir = Path(args.output) if args.output else Path.cwd()
members = args.files if hasattr(args, "files") and args.files else None
@@ -377,10 +615,36 @@ def cmd_extract_flat(args) -> int:
Literal["data", "tar", "fully_trusted"], getattr(args, "filter", "data")
)
print(f"Extracting from: {archive_path}")
print(f"Output directory: {output_dir}")
if filter_type != "data":
print(f"Using security filter: {filter_type}")
# Handle conflict resolution parameters
conflict_resolution_str = getattr(args, "conflict_resolution", "ask")
interactive_flag = getattr(args, "interactive", False)
# If --interactive is specified, use "ask" regardless of --conflict-resolution
if interactive_flag:
conflict_resolution_str = "ask"
# Convert string to ConflictResolution enum
conflict_resolution = ConflictResolution(conflict_resolution_str)
if _wants_json_output(args) and conflict_resolution == ConflictResolution.ASK:
return _emit_error(
args,
"Error: JSON mode does not support interactive conflict prompts",
error_type="interactive_conflict_not_supported",
)
# Set up interactive callback if needed
interactive_callback = None
if conflict_resolution == ConflictResolution.ASK:
interactive_callback = _interactive_conflict_callback
if not _wants_json_output(args):
print(f"Extracting from: {archive_path}")
print(f"Output directory: {output_dir}")
if filter_type != "data":
print(f"Using security filter: {filter_type}")
if conflict_resolution != ConflictResolution.REPLACE:
print(f"Conflict resolution: {conflict_resolution.value}")
extract_archive(
archive_path,
@@ -389,22 +653,52 @@ def cmd_extract_flat(args) -> int:
flatten=True,
streaming=streaming,
filter=filter_type,
conflict_resolution=conflict_resolution,
interactive_callback=interactive_callback,
)
print("Extraction completed successfully")
if _wants_json_output(args):
_emit_json(
{
"ok": True,
"command": "extract-flat",
"archive": str(archive_path),
"output_dir": str(output_dir),
"members": members or [],
"flatten": True,
"streaming": streaming,
"filter": filter_type,
"conflict_resolution": conflict_resolution.value,
}
)
else:
print("Extraction completed successfully")
return 0
except FileNotFoundError as e:
print(f"Error: File not found - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: File not found - {e}",
error_type="file_not_found",
)
except TzstDecompressionError as e:
print(f"Error: Archive decompression failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive decompression failed - {e}",
error_type="decompression_failed",
)
except TzstArchiveError as e:
print(f"Error: Archive operation failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive operation failed - {e}",
error_type="archive_operation_failed",
)
except Exception as e:
print(f"Error: Failed to extract archive - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Failed to extract archive - {e}",
error_type="extract_failed",
)
def _print_verbose_listing(contents: list) -> None:
@@ -428,22 +722,14 @@ def _print_simple_listing(contents: list) -> None:
Args:
contents: List of archive items
"""
total_files = 0
total_dirs = 0
total_size = 0
for item in contents:
if item["is_file"]:
total_files += 1
total_size += item["size"]
elif item["is_dir"]:
total_dirs += 1
print(item["name"])
print()
summary = _summarize_listing(contents)
total_msg = (
f"Total: {total_files} files, {total_dirs} directories, "
f"{format_size(total_size)}"
f"Total: {summary['files']} files, {summary['directories']} directories, "
f"{summary['total_size_human']}"
)
print(total_msg)
@@ -477,20 +763,37 @@ def cmd_list(args) -> int:
try:
archive_path = Path(args.archive)
if not archive_path.exists():
print(f"Error: Archive not found - {archive_path}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive not found - {archive_path}",
error_type="archive_not_found",
details={"archive": str(archive_path)},
)
verbose = getattr(args, "verbose", False)
streaming = getattr(args, "streaming", False)
print(f"Listing contents of: {archive_path}")
if streaming:
print("Using streaming mode (memory efficient)")
print()
if not _wants_json_output(args):
print(f"Listing contents of: {archive_path}")
if streaming:
print("Using streaming mode (memory efficient)")
print()
contents = list_archive(archive_path, verbose=verbose, streaming=streaming)
if verbose:
if _wants_json_output(args):
_emit_json(
{
"ok": True,
"command": "list",
"archive": str(archive_path),
"verbose": verbose,
"streaming": streaming,
"contents": contents,
"summary": _summarize_listing(contents),
}
)
elif verbose:
_print_verbose_listing(contents)
else:
_print_simple_listing(contents)
@@ -498,20 +801,36 @@ def cmd_list(args) -> int:
return 0
except FileNotFoundError as e:
print(f"Error: File not found - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: File not found - {e}",
error_type="file_not_found",
)
except TzstDecompressionError as e:
print(f"Error: Archive decompression failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive decompression failed - {e}",
error_type="decompression_failed",
)
except TzstArchiveError as e:
print(f"Error: Archive operation failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive operation failed - {e}",
error_type="archive_operation_failed",
)
except KeyboardInterrupt:
print("\nOperation interrupted by user", file=sys.stderr)
return 130
return _emit_error(
args,
"Operation interrupted by user",
error_type="interrupted",
exit_code=130,
)
except Exception as e:
print(f"Error: Failed to list archive - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Failed to list archive - {e}",
error_type="list_failed",
)
def cmd_test(args) -> int:
@@ -543,37 +862,79 @@ def cmd_test(args) -> int:
try:
archive_path = Path(args.archive)
if not archive_path.exists():
print(f"Error: Archive not found - {archive_path}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive not found - {archive_path}",
error_type="archive_not_found",
details={"archive": str(archive_path)},
)
streaming = getattr(args, "streaming", False)
print(f"Testing archive: {archive_path}")
if streaming:
print("Using streaming mode (memory efficient)")
if not _wants_json_output(args):
print(f"Testing archive: {archive_path}")
if streaming:
print("Using streaming mode (memory efficient)")
if test_archive(archive_path, streaming=streaming):
print("Archive test passed - no errors detected")
healthy = test_archive(archive_path, streaming=streaming)
if healthy:
if _wants_json_output(args):
_emit_json(
{
"ok": True,
"command": "test",
"archive": str(archive_path),
"streaming": streaming,
"healthy": True,
}
)
else:
print("Archive test passed - no errors detected")
return 0
else:
print("Archive test failed - errors detected", file=sys.stderr)
return 1
return _emit_error(
args,
"Archive test failed - errors detected",
error_type="integrity_check_failed",
details={
"command": "test",
"archive": str(archive_path),
"streaming": streaming,
"healthy": False,
},
)
except FileNotFoundError as e:
print(f"Error: File not found - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: File not found - {e}",
error_type="file_not_found",
)
except TzstDecompressionError as e:
print(f"Error: Archive decompression failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive decompression failed - {e}",
error_type="decompression_failed",
)
except TzstArchiveError as e:
print(f"Error: Archive operation failed - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Archive operation failed - {e}",
error_type="archive_operation_failed",
)
except KeyboardInterrupt:
print("\nOperation interrupted by user", file=sys.stderr)
return 130
return _emit_error(
args,
"Operation interrupted by user",
error_type="interrupted",
exit_code=130,
)
except Exception as e:
print(f"Error: Failed to test archive - {e}", file=sys.stderr)
return 1
return _emit_error(
args,
f"Error: Failed to test archive - {e}",
error_type="test_failed",
)
def cmd_version(args) -> int:
@@ -582,7 +943,11 @@ def cmd_version(args) -> int:
Returns:
int: Exit code (always 0)
"""
# Version is already printed in print_banner(), so just exit
if _wants_json_output(args):
_emit_json({"ok": True, "command": "version", "version": __version__})
elif getattr(args, "no_banner", False):
print(f"tzst {__version__}")
return 0
@@ -637,7 +1002,7 @@ security note:
never use --filter=fully_trusted unless you completely trust the archive source
documentation:
https://github.com/xixu-me/tzst#readme
https://tzst.xi-xu.me
"""
parser = argparse.ArgumentParser(
prog="tzst",
@@ -648,6 +1013,17 @@ documentation:
parser.add_argument(
"--version", action="store_true", help="show version information and exit"
)
parser.add_argument(
"--json",
dest="json_output",
action="store_true",
help="emit machine-readable JSON output",
)
parser.add_argument(
"--no-banner",
action="store_true",
help="suppress the startup banner",
)
# Add global arguments
subparsers = parser.add_subparsers(
@@ -704,6 +1080,25 @@ documentation:
"'fully_trusted' honors all metadata"
),
)
parser_extract.add_argument(
"--conflict-resolution",
choices=[
"replace",
"skip",
"replace_all",
"skip_all",
"auto_rename",
"auto_rename_all",
"ask",
],
default="ask",
help=(
"How to handle file conflicts during extraction (default: ask). "
"'ask' prompts for each conflict, 'replace' overwrites existing files, "
"'skip' skips existing files, 'auto_rename' creates new names. "
"Adding '_all' applies the action to all subsequent conflicts."
),
)
parser_extract.set_defaults(func=cmd_extract_full)
# Extract flat command
@@ -734,6 +1129,25 @@ documentation:
"'fully_trusted' honors all metadata"
),
)
parser_extract_flat.add_argument(
"--conflict-resolution",
choices=[
"replace",
"skip",
"replace_all",
"skip_all",
"auto_rename",
"auto_rename_all",
"ask",
],
default="ask",
help=(
"How to handle file conflicts during extraction (default: ask). "
"'ask' prompts for each conflict, 'replace' overwrites existing files, "
"'skip' skips existing files, 'auto_rename' creates new names. "
"Adding '_all' applies the action to all subsequent conflicts."
),
)
parser_extract_flat.set_defaults(func=cmd_extract_flat)
# List command
@@ -998,7 +1412,8 @@ def main(argv: list[str] | None = None) -> int:
See Also:
:func:`create_parser`: Creates the argument parser used by this function
"""
print_banner()
if _should_print_banner(argv):
print_banner()
parser = create_parser()
args, error_code = _parse_arguments(parser, argv)
+281 -8
View File
@@ -6,6 +6,7 @@ import tarfile
import tempfile
import time
from collections.abc import Callable, Sequence
from enum import Enum
from pathlib import Path
from typing import BinaryIO
@@ -14,6 +15,143 @@ import zstandard as zstd
from .exceptions import TzstArchiveError, TzstDecompressionError
class ConflictResolution(Enum):
"""Enum for conflict resolution strategies."""
REPLACE = "replace"
SKIP = "skip"
REPLACE_ALL = "replace_all"
SKIP_ALL = "skip_all"
AUTO_RENAME = "auto_rename"
AUTO_RENAME_ALL = "auto_rename_all"
EXIT = "exit"
ASK = "ask"
class ConflictResolutionState:
"""State management for conflict resolution during extraction."""
def __init__(self, initial_resolution: ConflictResolution | None = None):
self.continue_extraction = True
self.global_resolution = initial_resolution
# If initial resolution is EXIT, set continue_extraction to False
if initial_resolution == ConflictResolution.EXIT:
self.continue_extraction = False
@property
def current_resolution(self) -> ConflictResolution | None:
"""Get the current resolution state."""
return self.global_resolution
def should_continue(self) -> bool:
"""Check if extraction should continue."""
return self.continue_extraction
@property
def apply_to_all(self) -> bool:
"""Check if the current resolution applies to all future conflicts."""
return self.global_resolution in (
ConflictResolution.REPLACE_ALL,
ConflictResolution.SKIP_ALL,
ConflictResolution.AUTO_RENAME_ALL,
)
def update_resolution(self, resolution: ConflictResolution) -> None:
"""Update the global resolution state."""
if resolution == ConflictResolution.EXIT:
self.continue_extraction = False
self.global_resolution = resolution
elif resolution in (
ConflictResolution.REPLACE_ALL,
ConflictResolution.SKIP_ALL,
ConflictResolution.AUTO_RENAME_ALL,
):
self.global_resolution = resolution
def _get_unique_filename(file_path: Path) -> Path:
"""Generate a unique filename by appending a number if the file exists."""
if not file_path.exists():
return file_path
parent = file_path.parent
stem = file_path.stem
suffix = file_path.suffix
counter = 1
while True:
new_name = f"{stem}_{counter}{suffix}"
new_path = parent / new_name
if not new_path.exists():
return new_path
counter += 1
def _move_file_cross_platform(src: Path, dst: Path) -> None:
"""Move a file from src to dst, handling cross-drive moves on Windows."""
try:
# Try the fast rename operation first
src.rename(dst)
except OSError as e:
# On Windows, rename fails across drives with error 17
# Fall back to copy + delete for cross-drive moves
import shutil
try:
shutil.copy2(src, dst)
src.unlink()
except Exception:
# If copy also fails, re-raise the original rename error
raise e from None
def _handle_file_conflict(
target_path: Path,
resolution: ConflictResolution | str,
interactive_callback: Callable[[Path], ConflictResolution] | None = None,
) -> tuple[ConflictResolution, Path | None]:
"""
Handle file conflicts during extraction.
Args:
target_path: The path where a conflict occurred
resolution: The conflict resolution strategy
interactive_callback: Optional callback for interactive resolution
Returns:
Tuple of (actual_resolution, final_path)"""
# Convert string resolution to enum if needed
if isinstance(resolution, str):
try:
resolution = ConflictResolution(resolution)
except ValueError:
# Invalid string, fallback to ASK for interactive handling
resolution = ConflictResolution.ASK
if resolution == ConflictResolution.ASK:
if interactive_callback:
resolution = interactive_callback(target_path)
else:
# No callback provided, default to REPLACE for consistency with tests
resolution = ConflictResolution.REPLACE
if resolution in (ConflictResolution.REPLACE, ConflictResolution.REPLACE_ALL):
return resolution, target_path
elif resolution in (ConflictResolution.SKIP, ConflictResolution.SKIP_ALL):
return resolution, None
elif resolution in (
ConflictResolution.AUTO_RENAME,
ConflictResolution.AUTO_RENAME_ALL,
):
unique_path = _get_unique_filename(target_path)
return resolution, unique_path
elif resolution == ConflictResolution.EXIT:
return resolution, None
else:
# Unknown resolution, default to REPLACE for robustness
return ConflictResolution.REPLACE, target_path
class TzstArchive:
"""A class for handling .tzst/.tar.zst archives."""
@@ -620,6 +758,8 @@ def extract_archive(
flatten: bool = False,
streaming: bool = False,
filter: str | Callable | None = "data",
conflict_resolution: ConflictResolution | str = ConflictResolution.REPLACE,
interactive_callback: Callable[[Path], ConflictResolution] | None = None,
) -> None:
"""
Extract files from a .tzst archive.
@@ -631,21 +771,31 @@ def extract_archive(
flatten: If True, extract without directory structure
streaming: If True, use streaming mode (memory efficient for large archives)
filter: Extraction filter for security. Can be:
- 'data': Safe filter for cross-platform data archives (default, recommended)
- 'data': Safe filter for cross-platform data archives (default)
- 'tar': Honor most tar features but block dangerous ones
- 'fully_trusted': Honor all metadata (use only for trusted archives)
- None: Use default behavior (may show deprecation warning in Python 3.12+)
- None: Use default behavior (may show deprecation warning)
- callable: Custom filter function
conflict_resolution: How to handle file conflicts during extraction
interactive_callback: Function to call for interactive conflict resolution
Warning:
Never extract archives from untrusted sources without proper filtering.
The 'data' filter is recommended for most use cases as it prevents
Never extract archives from untrusted sources without proper filtering. The 'data' filter is recommended for most use cases as it prevents
dangerous security issues like path traversal attacks.
See Also:
See Also:
:meth:`TzstArchive.extract`: Method for extracting from an open archive
"""
with TzstArchive(archive_path, "r", streaming=streaming) as archive:
# Convert string resolution to enum if needed
if isinstance(conflict_resolution, str):
try:
conflict_resolution = ConflictResolution(conflict_resolution)
except ValueError:
conflict_resolution = ConflictResolution.REPLACE
state = ConflictResolutionState(conflict_resolution)
if flatten:
# Extract files without directory structure
extract_dir = Path(extract_path)
@@ -657,20 +807,143 @@ def extract_archive(
member_list = archive.getmembers()
for member in member_list:
if not state.should_continue():
break
if member.isfile():
# Extract to flat directory
filename = Path(member.name).name
target_path = extract_dir / filename
# Handle conflicts
if target_path.exists():
current_resolution = (
state.global_resolution or conflict_resolution
)
actual_resolution, final_path = _handle_file_conflict(
target_path, current_resolution, interactive_callback
)
state.update_resolution(actual_resolution)
if actual_resolution in (
ConflictResolution.SKIP,
ConflictResolution.SKIP_ALL,
):
continue
elif actual_resolution == ConflictResolution.EXIT:
break
target_path = final_path
fileobj = archive.extractfile(member)
if fileobj:
with open(extract_dir / filename, "wb") as f:
with open(target_path, "wb") as f:
f.write(fileobj.read())
else:
# Extract with full directory structure
if members:
for member in members:
archive.extract(member, extract_path, filter=filter)
if not state.should_continue():
break
target_path = Path(extract_path) / member
# Handle conflicts
if target_path.exists():
current_resolution = (
state.global_resolution or conflict_resolution
)
actual_resolution, final_path = _handle_file_conflict(
target_path, current_resolution, interactive_callback
)
state.update_resolution(actual_resolution)
if actual_resolution in (
ConflictResolution.SKIP,
ConflictResolution.SKIP_ALL,
):
continue
elif actual_resolution == ConflictResolution.EXIT:
break # For AUTO_RENAME, we need to adjust the member path
if actual_resolution in (
ConflictResolution.AUTO_RENAME,
ConflictResolution.AUTO_RENAME_ALL,
):
# Create parent directories for renamed file
if final_path:
final_path.parent.mkdir(parents=True, exist_ok=True)
# Extract to temporary location, then move
temp_extract_path = Path(tempfile.mkdtemp())
try:
archive.extract(
member, temp_extract_path, filter=filter
)
temp_file = temp_extract_path / member
if final_path:
_move_file_cross_platform(temp_file, final_path)
finally:
# Clean up temp directory
import shutil
shutil.rmtree(temp_extract_path, ignore_errors=True)
else:
archive.extract(member, extract_path, filter=filter)
else:
archive.extract(member, extract_path, filter=filter)
else:
archive.extract(path=extract_path, filter=filter)
# For extractall, we need a different approach
# We'll extract to a temp location and handle conflicts file by file
temp_extract_path = Path(tempfile.mkdtemp())
try:
archive.extractall(temp_extract_path, filter=filter)
# Move files with conflict resolution
for temp_file in temp_extract_path.rglob("*"):
if not state.should_continue():
break
if temp_file.is_file():
rel_path = temp_file.relative_to(temp_extract_path)
target_path = Path(extract_path) / rel_path
# Create parent directories
target_path.parent.mkdir(
parents=True, exist_ok=True
) # Handle conflicts
if target_path.exists():
current_resolution = (
state.global_resolution or conflict_resolution
)
actual_resolution, final_path = _handle_file_conflict(
target_path,
current_resolution,
interactive_callback,
)
state.update_resolution(actual_resolution)
if actual_resolution in (
ConflictResolution.SKIP,
ConflictResolution.SKIP_ALL,
):
continue
elif actual_resolution == ConflictResolution.EXIT:
break
target_path = final_path
# Handle file replacement on Windows
if target_path and target_path.exists():
if actual_resolution in (
ConflictResolution.REPLACE,
ConflictResolution.REPLACE_ALL,
):
target_path.unlink() # Remove existing file
if target_path:
_move_file_cross_platform(temp_file, target_path)
finally:
# Clean up temp directory
import shutil
shutil.rmtree(temp_extract_path, ignore_errors=True)
def list_archive(
+200 -2
View File
@@ -5,6 +5,8 @@ and provide a single source of truth for CLI testing.
"""
import argparse
import json
from pathlib import Path
import pytest
@@ -17,6 +19,7 @@ from tzst.cli import (
)
@pytest.mark.cli
class TestUtilityFunctions:
"""Test CLI utility functions."""
@@ -95,7 +98,8 @@ class TestUtilityFunctions:
validate_compression_level("abc")
with pytest.raises(
argparse.ArgumentTypeError, match="Invalid compression level: '1.5'"
argparse.ArgumentTypeError,
match=r"Invalid compression level: '1\.5'\. Must be an integer between 1 and 22\.",
):
validate_compression_level("1.5")
@@ -144,6 +148,15 @@ class TestCLIParser:
assert args.command == "t"
assert args.archive == "test.tzst"
def test_global_machine_readable_flags(self):
"""Test parsing of machine-readable output flags."""
parser = create_parser()
args = parser.parse_args(["--json", "--no-banner", "l", "test.tzst"])
assert args.json_output is True
assert args.no_banner is True
assert args.command == "l"
class TestCLICommands:
"""Test CLI command execution."""
@@ -200,6 +213,59 @@ class TestCLICommands:
assert result == 0
def test_add_command_json_output(self, sample_files, temp_dir, capsys):
"""Test add command JSON output for sidecar integrations."""
archive_path = temp_dir / "json-add.tzst"
file_paths = [str(f) for f in sample_files if f.is_file()]
expected_added = [str(Path(path).resolve()) for path in file_paths]
result = main(["--json", "a", str(archive_path), *file_paths])
assert result == 0
payload = json.loads(capsys.readouterr().out)
assert payload["ok"] is True
assert payload["command"] == "add"
assert payload["normalized_archive"] == str(archive_path)
assert payload["added"] == expected_added
def test_list_command_json_output(self, sample_files, temp_dir, capsys):
"""Test list command JSON output for the desktop app."""
archive_path = temp_dir / "json-list.tzst"
file_paths = [str(f) for f in sample_files if f.is_file()]
create_result = main(["--no-banner", "a", str(archive_path), *file_paths])
assert create_result == 0
capsys.readouterr()
result = main(["--json", "l", str(archive_path)])
assert result == 0
payload = json.loads(capsys.readouterr().out)
assert payload["ok"] is True
assert payload["command"] == "list"
assert payload["summary"]["files"] >= 1
assert isinstance(payload["contents"], list)
def test_missing_archive_json_error(self, temp_dir, capsys):
"""Test JSON-formatted error output."""
fake_archive = temp_dir / "missing.tzst"
result = main(["--json", "l", str(fake_archive)])
assert result == 1
payload = json.loads(capsys.readouterr().err)
assert payload["ok"] is False
assert payload["error"]["type"] == "archive_not_found"
def test_version_json_output(self, capsys):
"""Test JSON version output without the startup banner."""
result = main(["--json", "--version"])
assert result == 0
payload = json.loads(capsys.readouterr().out)
assert payload["ok"] is True
assert payload["command"] == "version"
class TestCLIErrorHandling:
"""Test CLI error handling."""
@@ -892,7 +958,7 @@ class TestCLIRealWorldScenarios:
# Test error operation returns non-zero
result = main(["l", "nonexistent_archive.tzst"])
assert result != 0 # Error should return non-zero
assert result != 0
class TestCLISecurityFilterParsing:
@@ -2289,3 +2355,135 @@ class TestCLIListingFunctionsCoverage:
assert "2 files" in captured.out
assert "2 directories" in captured.out
assert "300.0 B" in captured.out # Total size of files
class TestCLIEdgeCasesExtended:
"""Additional edge case tests for CLI functionality."""
def test_validate_files_os_error_handling(self, temp_dir):
"""Test OSError handling in validate_files function."""
from unittest.mock import patch
from tzst.cli import _validate_files
# Create a test file
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
# Mock Path.exists to raise OSError
with patch("pathlib.Path.exists", side_effect=OSError("Permission denied")):
# Should handle OSError gracefully and continue
try:
_validate_files([test_file])
except OSError:
pass # Expected to be caught and handled
def test_windows_specific_cli_functionality(self, temp_dir):
"""Test Windows-specific CLI functionality."""
import sys
if sys.platform != "win32":
pytest.skip("Windows-specific test")
# Test Windows reserved names
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test with Windows path separators
windows_style_path = str(archive_path).replace("/", "\\")
result = main(["l", windows_style_path])
assert result == 0
def test_compression_level_boundary_values(self, temp_dir):
"""Test compression level boundary values."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content for compression")
# Test minimum compression level
archive_path_min = temp_dir / "test_min.tzst"
result = main(["a", str(archive_path_min), str(test_file), "-c", "1"])
assert result == 0
# Test maximum compression level
archive_path_max = temp_dir / "test_max.tzst"
result = main(["a", str(archive_path_max), str(test_file), "-c", "22"])
assert result == 0
def test_output_directory_creation_edge_cases(self, temp_dir):
"""Test output directory creation edge cases."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test extraction to nested directory that doesn't exist
nested_extract_dir = temp_dir / "level1" / "level2" / "level3"
result = main(["x", str(archive_path), "-o", str(nested_extract_dir)])
assert result == 0
# Verify directory was created
assert nested_extract_dir.exists()
def test_special_file_handling_edge_cases(self, temp_dir):
"""Test special file handling edge cases."""
# Create files with special characteristics
empty_file = temp_dir / "empty.txt"
empty_file.touch()
whitespace_file = temp_dir / "whitespace.txt"
whitespace_file.write_text(" \n\t\n ")
binary_file = temp_dir / "binary.bin"
binary_file.write_bytes(b"\x00\x01\x02\x03\x04\x05")
archive_path = temp_dir / "special.tzst"
# Create archive with special files
result = main(
[
"a",
str(archive_path),
str(empty_file),
str(whitespace_file),
str(binary_file),
]
)
assert result == 0
# Extract and verify
extract_dir = temp_dir / "extracted"
result = main(["x", str(archive_path), "-o", str(extract_dir)])
assert result == 0
def test_performance_edge_cases(self, temp_dir):
"""Test performance-related edge cases."""
# Create many small files
files = []
for i in range(20): # Create 20 small files
file_path = temp_dir / f"small_{i:03d}.txt"
file_path.write_text(f"Content of file {i}")
files.append(file_path)
archive_path = temp_dir / "many_files.tzst"
# Create archive with many files
file_args = [str(f) for f in files]
result = main(["a", str(archive_path), *file_args])
assert result == 0
# Test listing (should handle many files efficiently)
result = main(["l", str(archive_path)])
assert result == 0
# Test extraction
extract_dir = temp_dir / "extracted_many"
result = main(["x", str(archive_path), "-o", str(extract_dir)])
assert result == 0
+246
View File
@@ -0,0 +1,246 @@
"""Targeted tests for remaining uncovered CLI branches."""
import json
import runpy
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
from tzst import create_archive
from tzst.cli import (
_interactive_conflict_callback,
_is_extreme_compression_level_in_argv,
_normalize_archive_path,
_validate_command_in_argv,
_validate_compression_level_in_argv,
_validate_filter_in_argv,
cmd_extract_flat,
cmd_extract_full,
cmd_version,
main,
)
class TestCliCoverageGaps:
"""Exercise the remaining CLI coverage hotspots."""
def test_normalize_archive_path_handles_tar_and_custom_extensions(self):
assert _normalize_archive_path(Path("bundle.tar")) == Path("bundle.tar.zst")
assert _normalize_archive_path(Path("bundle.backup")) == Path(
"bundle.backup.tzst"
)
@pytest.mark.parametrize(
("choice", "expected"),
[
("A", "replace_all"),
("S", "skip_all"),
("U", "auto_rename_all"),
],
)
def test_interactive_conflict_callback_covers_remaining_choices(
self, monkeypatch, temp_dir, choice, expected
):
monkeypatch.setattr("builtins.input", lambda _prompt: choice)
result = _interactive_conflict_callback(temp_dir / "existing.txt")
assert result.value == expected
def test_extract_full_rejects_json_interactive_conflicts(self, temp_dir):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
args = SimpleNamespace(
archive=str(archive_path),
output=None,
files=[],
streaming=False,
filter="data",
conflict_resolution="replace",
interactive=True,
json_output=True,
no_banner=True,
)
assert cmd_extract_full(args) == 1
def test_extract_full_json_success_payload(self, temp_dir, capsys):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
output_dir = temp_dir / "extract"
result = main(
[
"--json",
"x",
str(archive_path),
"-o",
str(output_dir),
"--conflict-resolution",
"replace",
]
)
assert result == 0
payload = json.loads(capsys.readouterr().out)
assert payload["command"] == "extract"
assert payload["flatten"] is False
def test_extract_full_handles_file_not_found_from_backend(
self, temp_dir, monkeypatch
):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
def fail_extract(*args, **kwargs):
raise FileNotFoundError("backend missing file")
monkeypatch.setattr("tzst.cli.extract_archive", fail_extract)
assert main(["x", str(archive_path)]) == 1
def test_extract_flat_rejects_json_interactive_conflicts(self, temp_dir):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
args = SimpleNamespace(
archive=str(archive_path),
output=None,
files=[],
streaming=False,
filter="data",
conflict_resolution="replace",
interactive=True,
json_output=True,
no_banner=True,
)
assert cmd_extract_flat(args) == 1
def test_extract_flat_json_success_payload(self, temp_dir, capsys):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
output_dir = temp_dir / "extract"
result = main(
[
"--json",
"e",
str(archive_path),
"-o",
str(output_dir),
"--conflict-resolution",
"replace",
]
)
assert result == 0
payload = json.loads(capsys.readouterr().out)
assert payload["command"] == "extract-flat"
assert payload["flatten"] is True
def test_extract_flat_handles_file_not_found_from_backend(
self, temp_dir, monkeypatch
):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
def fail_extract(*args, **kwargs):
raise FileNotFoundError("backend missing file")
monkeypatch.setattr("tzst.cli.extract_archive", fail_extract)
assert main(["e", str(archive_path)]) == 1
def test_list_handles_file_not_found_from_backend(self, temp_dir, monkeypatch):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
def fail_list(*args, **kwargs):
raise FileNotFoundError("backend missing file")
monkeypatch.setattr("tzst.cli.list_archive", fail_list)
assert main(["l", str(archive_path)]) == 1
def test_test_command_json_success_and_file_not_found_backend(
self, temp_dir, capsys, monkeypatch
):
archive_path = temp_dir / "archive.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("content")
create_archive(archive_path, [source_file], use_temp_file=False)
result = main(["--json", "t", str(archive_path)])
assert result == 0
payload = json.loads(capsys.readouterr().out)
assert payload["command"] == "test"
assert payload["healthy"] is True
def fail_test(*args, **kwargs):
raise FileNotFoundError("backend missing file")
monkeypatch.setattr("tzst.cli.test_archive", fail_test)
assert main(["t", str(archive_path)]) == 1
def test_cmd_version_prints_when_no_banner_requested(self, capsys):
args = SimpleNamespace(json_output=False, no_banner=True)
assert cmd_version(args) == 0
assert capsys.readouterr().out.startswith("tzst ")
def test_validate_compression_level_in_argv_covers_short_flag_and_index_error(
self, capsys
):
assert _validate_compression_level_in_argv(["-c", "0"]) is True
assert "Invalid compression level: 0" in capsys.readouterr().err
assert _validate_compression_level_in_argv(["-c"]) is False
def test_validate_filter_in_argv_handles_missing_value(self):
assert _validate_filter_in_argv(["--filter"]) is False
def test_validate_command_in_argv_handles_empty_and_invalid_input(self, capsys):
assert _validate_command_in_argv([]) is False
assert _validate_command_in_argv(["bogus"]) is True
assert "Invalid command: 'bogus'" in capsys.readouterr().err
def test_is_extreme_compression_level_in_argv_covers_all_remaining_paths(self):
assert _is_extreme_compression_level_in_argv([]) is False
assert _is_extreme_compression_level_in_argv(["-c", "1000"]) is True
assert _is_extreme_compression_level_in_argv(["-l", "abc"]) is False
assert _is_extreme_compression_level_in_argv(["--level"]) is False
def test_cli_module_main_guard_executes(self, monkeypatch):
monkeypatch.delitem(sys.modules, "tzst.cli", raising=False)
monkeypatch.setattr(sys, "argv", ["tzst", "--version"])
with pytest.raises(SystemExit) as exc_info:
runpy.run_module("tzst.cli", run_name="__main__")
assert exc_info.value.code == 0
def test_package_main_module_executes(self, monkeypatch):
monkeypatch.delitem(sys.modules, "tzst.__main__", raising=False)
monkeypatch.setattr(sys, "argv", ["python", "--version"])
with pytest.raises(SystemExit) as exc_info:
runpy.run_module("tzst.__main__", run_name="__main__")
assert exc_info.value.code == 0
+11
View File
@@ -7,6 +7,17 @@ from pathlib import Path
import pytest
def pytest_configure(config):
"""Configure pytest with custom markers."""
config.addinivalue_line("markers", "unit: Unit tests")
config.addinivalue_line("markers", "integration: Integration tests")
config.addinivalue_line("markers", "cli: CLI interface tests")
config.addinivalue_line("markers", "platform: Platform-specific tests")
config.addinivalue_line("markers", "windows: Windows-specific tests")
config.addinivalue_line("markers", "unix: Unix/Linux-specific tests")
config.addinivalue_line("markers", "slow: Slow running tests")
@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests."""
-405
View File
@@ -1,405 +0,0 @@
"""Tests to cover missing lines in CLI and improve overall coverage."""
import sys
from unittest.mock import patch
import pytest
from tzst.cli import _validate_files, main
class TestCLIMissingLines:
"""Test specific missing lines in CLI for improved coverage."""
def test_validate_files_os_error_handling(self, temp_dir):
"""Test OSError handling in validate_files function."""
# Create a test file
test_file = temp_dir / "test.txt"
test_file.write_text("test content") # Mock Path.exists to raise OSError
with patch(
"pathlib.Path.exists", side_effect=OSError("Permission denied")
): # Should handle OSError gracefully and continue
try:
_validate_files([test_file])
except OSError:
pass # Expected to be caught and handled
def test_main_function_edge_cases(self, temp_dir):
"""Test main function edge cases for missing line coverage."""
# Test with minimal arguments that might hit edge cases
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst" # Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test version command through main
with patch("sys.exit"):
try:
main(["--version"])
except SystemExit:
pass
# Test help command variations
with patch("sys.exit"):
try:
main(["--help"])
except SystemExit:
pass
def test_command_line_argument_edge_cases(self, temp_dir):
"""Test command line argument edge cases."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
# Test with various argument combinations that might hit missing lines
archive_path = temp_dir / "test.tzst"
# Create archive with specific compression level
result = main(["a", str(archive_path), str(test_file), "-c", "1"])
assert result == 0
# Test list with streaming
result = main(["l", str(archive_path), "--streaming"])
assert result == 0 # Test extract with specific options
extract_dir = temp_dir / "extracted"
result = main(["x", str(archive_path), "-o", str(extract_dir)])
assert result == 0
def test_error_handling_edge_cases(self, temp_dir):
"""Test error handling edge cases in CLI."""
# Test with invalid archive path
invalid_path = temp_dir / "nonexistent" / "test.tzst"
result = main(["l", str(invalid_path)])
assert result == 1
# Test with invalid compression level - should return argparse error code 2
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
result = main(["a", str(archive_path), str(test_file), "-c", "50"])
assert result == 2
def test_filter_option_edge_cases(self, temp_dir):
"""Test filter option edge cases."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test extract with different filters
for filter_type in ["data", "tar", "fully_trusted"]:
extract_dir = temp_dir / f"extracted_{filter_type}"
result = main(
[
"x",
str(archive_path),
"-o",
str(extract_dir),
"--filter",
filter_type,
]
)
assert result == 0
def test_atomic_operation_edge_cases(self, temp_dir):
"""Test atomic operation edge cases."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Test with --no-atomic flag
result = main(["a", str(archive_path), str(test_file), "--no-atomic"])
assert result == 0
# Verify archive was created
assert archive_path.exists()
def test_verbose_output_edge_cases(self, temp_dir, capsys):
"""Test verbose output edge cases."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst" # Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test verbose list
result = main(["l", str(archive_path), "-v"])
assert result == 0
captured = capsys.readouterr()
assert len(captured.out) > 0
def test_command_validation_edge_cases(self):
"""Test command validation edge cases."""
# Test with empty arguments
result = main([])
assert result == 1
# Test with invalid command - should return argparse error code 2
result = main(["invalid_command"])
assert result == 2
@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific test")
def test_windows_specific_functionality(self, temp_dir):
"""Test Windows-specific functionality."""
# Test Windows reserved names
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test with Windows path separators
windows_style_path = str(archive_path).replace("/", "\\")
result = main(["l", windows_style_path])
assert result == 0
def test_streaming_mode_edge_cases(self, temp_dir):
"""Test streaming mode edge cases."""
# Create a larger file for streaming tests
large_file = temp_dir / "large.txt"
large_file.write_text("x" * 10000) # 10KB file
archive_path = temp_dir / "streaming.tzst"
# Create archive
result = main(["a", str(archive_path), str(large_file)])
assert result == 0
# Test all commands with streaming
result = main(["l", str(archive_path), "--streaming"])
assert result == 0
result = main(["t", str(archive_path), "--streaming"])
assert result == 0
extract_dir = temp_dir / "extracted_streaming"
result = main(["x", str(archive_path), "-o", str(extract_dir), "--streaming"])
assert result == 0
def test_compression_level_boundary_values(self, temp_dir):
"""Test compression level boundary values."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
# Test minimum compression level
archive_path_min = temp_dir / "min_compression.tzst"
result = main(["a", str(archive_path_min), str(test_file), "-c", "1"])
assert result == 0
# Test maximum compression level
archive_path_max = temp_dir / "max_compression.tzst"
result = main(["a", str(archive_path_max), str(test_file), "-c", "22"])
assert result == 0
def test_output_directory_creation_edge_cases(self, temp_dir):
"""Test output directory creation edge cases."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test extraction to nested directory that doesn't exist
nested_extract_dir = temp_dir / "level1" / "level2" / "level3"
result = main(["x", str(archive_path), "-o", str(nested_extract_dir)])
assert result == 0
# Verify directory was created
assert nested_extract_dir.exists()
def test_special_file_handling_edge_cases(self, temp_dir):
"""Test special file handling edge cases."""
# Create files with special characteristics
empty_file = temp_dir / "empty.txt"
empty_file.touch()
binary_file = temp_dir / "binary.bin"
binary_file.write_bytes(b"\x00\x01\x02\x03\xff")
unicode_file = temp_dir / "unicode.txt"
unicode_file.write_text("Hello 世界 🌍", encoding="utf-8")
archive_path = temp_dir / "special.tzst"
# Create archive with special files
result = main(
[
"a",
str(archive_path),
str(empty_file),
str(binary_file),
str(unicode_file),
]
)
assert result == 0
# Test list and extract
result = main(["l", str(archive_path)])
assert result == 0
extract_dir = temp_dir / "extracted_special"
result = main(["x", str(archive_path), "-o", str(extract_dir)])
assert result == 0
class TestPlatformSpecificMissingLines:
"""Test platform-specific functionality to improve coverage."""
@pytest.mark.skipif(
sys.platform != "win32", reason="Windows-specific functionality"
)
def test_windows_long_path_edge_cases(self, temp_dir):
"""Test Windows long path handling edge cases."""
# Create a very deep directory structure
deep_dir = temp_dir
for i in range(10):
deep_dir = deep_dir / f"very_long_directory_name_{i}"
deep_dir.mkdir(parents=True, exist_ok=True)
deep_file = deep_dir / "deep_file.txt"
deep_file.write_text("Content in deeply nested file")
archive_path = temp_dir / "deep.tzst"
# Test archiving deep structure
result = main(["a", str(archive_path), str(deep_file)])
assert result == 0
# Test extraction
extract_dir = temp_dir / "extracted_deep"
result = main(["x", str(archive_path), "-o", str(extract_dir)])
assert result == 0
@pytest.mark.skipif(
sys.platform != "win32", reason="Windows-specific functionality"
)
def test_windows_reserved_names_edge_cases(self, temp_dir):
"""Test Windows reserved names edge cases."""
# Test with files that have problematic names on Windows
normal_file = temp_dir / "normal.txt"
normal_file.write_text("normal content")
# File with trailing space (problematic on Windows)
space_file = temp_dir / "file_with_space .txt"
space_file.write_text("space content")
archive_path = temp_dir / "reserved.tzst"
# Create archive
result = main(["a", str(archive_path), str(normal_file), str(space_file)])
assert result == 0
def test_unicode_handling_edge_cases(self, temp_dir):
"""Test unicode handling edge cases."""
# Create files with various unicode content
files_to_create = [
("chinese.txt", "你好世界"),
("emoji.txt", "🎉🌟💫"),
("mixed.txt", "Hello 世界! 🌍 Мир"),
("special_chars.txt", "àáâãäåæçèéêë"),
]
created_files = []
for filename, content in files_to_create:
file_path = temp_dir / filename
file_path.write_text(content, encoding="utf-8")
created_files.append(file_path)
archive_path = temp_dir / "unicode.tzst" # Create archive
file_args = [str(f) for f in created_files]
result = main(["a", str(archive_path), *file_args])
assert result == 0
# Test extraction
extract_dir = temp_dir / "extracted_unicode"
result = main(["x", str(archive_path), "-o", str(extract_dir)])
assert result == 0
# Verify unicode content is preserved
for filename, original_content in files_to_create:
extracted_file = extract_dir / filename
assert extracted_file.exists()
extracted_content = extracted_file.read_text(encoding="utf-8")
assert extracted_content == original_content
def test_performance_edge_cases(self, temp_dir):
"""Test performance-related edge cases."""
# Create many small files
files = []
for i in range(50): # Create 50 small files
file_path = temp_dir / f"small_{i:03d}.txt"
file_path.write_text(f"Content of file {i}")
files.append(file_path)
archive_path = temp_dir / "many_files.tzst" # Create archive with many files
file_args = [str(f) for f in files]
result = main(["a", str(archive_path), *file_args])
assert result == 0
# Test listing (should handle many files efficiently)
result = main(["l", str(archive_path)])
assert result == 0
# Test extraction
extract_dir = temp_dir / "extracted_many"
result = main(["x", str(archive_path), "-o", str(extract_dir)])
assert result == 0
def test_error_recovery_edge_cases(self, temp_dir):
"""Test error recovery edge cases."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Create archive
result = main(["a", str(archive_path), str(test_file)])
assert result == 0
# Test with readonly archive
archive_path.chmod(0o444) # Make read-only
try:
# Should handle read-only archive gracefully
result = main(["l", str(archive_path)])
assert result == 0
finally:
# Restore write permissions for cleanup
archive_path.chmod(0o644)
def test_cross_platform_compatibility(self, temp_dir):
"""Test cross-platform compatibility features."""
# Create files with various characteristics
text_file = temp_dir / "text.txt"
text_file.write_text("Cross-platform text content\n")
binary_file = temp_dir / "binary.dat"
binary_file.write_bytes(bytes(range(256)))
archive_path = temp_dir / "cross_platform.tzst"
# Create archive
result = main(["a", str(archive_path), str(text_file), str(binary_file)])
assert result == 0
# Test with different compression levels
for level in [1, 11, 22]:
archive_path_level = temp_dir / f"cross_platform_level_{level}.tzst"
result = main(
["a", str(archive_path_level), str(text_file), "-c", str(level)]
)
assert result == 0
# Verify can be read back
result = main(["t", str(archive_path_level)])
assert result == 0
+463
View File
@@ -0,0 +1,463 @@
# filepath: e:\GitHub\tzst\tests\test_conflict_resolution_clean.py
"""Comprehensive tests for conflict resolution functionality."""
from unittest.mock import Mock, patch
from tzst.cli import _interactive_conflict_callback
from tzst.core import (
ConflictResolution,
ConflictResolutionState,
TzstArchive,
_get_unique_filename,
_handle_file_conflict,
create_archive,
extract_archive,
)
class TestConflictResolution:
"""Test conflict resolution enum and basic functionality."""
def test_conflict_resolution_enum_values(self):
"""Test that all ConflictResolution enum values exist."""
assert ConflictResolution.REPLACE.value == "replace"
assert ConflictResolution.SKIP.value == "skip"
assert ConflictResolution.REPLACE_ALL.value == "replace_all"
assert ConflictResolution.SKIP_ALL.value == "skip_all"
assert ConflictResolution.AUTO_RENAME.value == "auto_rename"
assert ConflictResolution.AUTO_RENAME_ALL.value == "auto_rename_all"
assert ConflictResolution.EXIT.value == "exit"
assert ConflictResolution.ASK.value == "ask"
class TestUniqueFilename:
"""Test unique filename generation."""
def test_get_unique_filename_basic(self, temp_dir):
"""Test basic unique filename generation."""
# Create a file
original_file = temp_dir / "test.txt"
original_file.write_text("original")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / "test_1.txt"
assert unique_path == expected_path
assert not unique_path.exists()
def test_get_unique_filename_multiple_conflicts(self, temp_dir):
"""Test unique filename generation with multiple conflicts."""
# Create multiple files
original_file = temp_dir / "test.txt"
conflict1 = temp_dir / "test_1.txt"
conflict2 = temp_dir / "test_2.txt"
original_file.write_text("original")
conflict1.write_text("conflict1")
conflict2.write_text("conflict2")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / "test_3.txt"
assert unique_path == expected_path
assert not unique_path.exists()
def test_get_unique_filename_no_extension(self, temp_dir):
"""Test unique filename generation for files without extension."""
# Create a file without extension
original_file = temp_dir / "README"
original_file.write_text("readme content")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / "README_1"
assert unique_path == expected_path
assert not unique_path.exists()
def test_get_unique_filename_empty_stem(self, temp_dir):
"""Test unique filename generation for files with empty stem."""
# Create a file with empty stem (just extension)
original_file = temp_dir / ".gitignore"
original_file.write_text("git ignore")
# Get unique name
unique_path = _get_unique_filename(original_file)
expected_path = temp_dir / ".gitignore_1"
assert unique_path == expected_path
assert not unique_path.exists()
class TestHandleFileConflict:
"""Test file conflict handling function."""
def test_handle_file_conflict_replace(self, temp_dir):
"""Test REPLACE conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.REPLACE, None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
def test_handle_file_conflict_skip(self, temp_dir):
"""Test SKIP conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.SKIP, None
)
assert resolution == ConflictResolution.SKIP
assert final_path is None
def test_handle_file_conflict_replace_all(self, temp_dir):
"""Test REPLACE_ALL conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.REPLACE_ALL, None
)
assert resolution == ConflictResolution.REPLACE_ALL
assert final_path == target_path
def test_handle_file_conflict_skip_all(self, temp_dir):
"""Test SKIP_ALL conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.SKIP_ALL, None
)
assert resolution == ConflictResolution.SKIP_ALL
assert final_path is None
def test_handle_file_conflict_auto_rename(self, temp_dir):
"""Test AUTO_RENAME conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.AUTO_RENAME, None
)
assert resolution == ConflictResolution.AUTO_RENAME
assert final_path == temp_dir / "existing_1.txt"
assert not final_path.exists()
def test_handle_file_conflict_auto_rename_all(self, temp_dir):
"""Test AUTO_RENAME_ALL conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.AUTO_RENAME_ALL, None
)
assert resolution == ConflictResolution.AUTO_RENAME_ALL
assert final_path == temp_dir / "existing_1.txt"
assert not final_path.exists()
def test_handle_file_conflict_exit(self, temp_dir):
"""Test EXIT conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.EXIT, None
)
assert resolution == ConflictResolution.EXIT
assert final_path is None
def test_handle_file_conflict_ask_with_callback(self, temp_dir):
"""Test ASK conflict resolution with callback."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
mock_callback = Mock(return_value=ConflictResolution.REPLACE)
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.ASK, mock_callback
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
mock_callback.assert_called_once_with(target_path)
def test_handle_file_conflict_ask_no_callback(self, temp_dir):
"""Test ASK conflict resolution without callback defaults to REPLACE."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, ConflictResolution.ASK, None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
def test_handle_file_conflict_string_resolution_valid(self, temp_dir):
"""Test string-based conflict resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(target_path, "skip", None)
assert resolution == ConflictResolution.SKIP
assert final_path is None
def test_handle_file_conflict_string_resolution_invalid(self, temp_dir):
"""Test invalid string conflict resolution defaults to REPLACE."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
resolution, final_path = _handle_file_conflict(
target_path, "invalid_resolution", None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
def test_handle_file_conflict_unknown_resolution(self, temp_dir):
"""Test unknown conflict resolution defaults to REPLACE."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing")
# Pass something that's not a valid enum value
resolution, final_path = _handle_file_conflict(
target_path, "completely_unknown", None
)
assert resolution == ConflictResolution.REPLACE
assert final_path == target_path
class TestConflictResolutionState:
"""Test conflict resolution state management."""
def test_initial_state(self):
"""Test initial state with different resolutions."""
state1 = ConflictResolutionState(ConflictResolution.REPLACE)
assert state1.current_resolution == ConflictResolution.REPLACE
assert state1.should_continue() is True
state2 = ConflictResolutionState(ConflictResolution.EXIT)
assert state2.current_resolution == ConflictResolution.EXIT
assert state2.should_continue() is False
def test_update_resolution_replace_all(self):
"""Test updating to REPLACE_ALL."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.REPLACE_ALL)
assert state.current_resolution == ConflictResolution.REPLACE_ALL
assert state.should_continue() is True
def test_update_resolution_skip_all(self):
"""Test updating to SKIP_ALL."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.SKIP_ALL)
assert state.current_resolution == ConflictResolution.SKIP_ALL
assert state.should_continue() is True
def test_update_resolution_auto_rename_all(self):
"""Test updating to AUTO_RENAME_ALL."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.AUTO_RENAME_ALL)
assert state.current_resolution == ConflictResolution.AUTO_RENAME_ALL
assert state.should_continue() is True
def test_update_resolution_exit(self):
"""Test updating to EXIT."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.EXIT)
assert state.current_resolution == ConflictResolution.EXIT
assert state.should_continue() is False
def test_update_resolution_normal(self):
"""Test updating to normal resolution doesn't change state."""
state = ConflictResolutionState(ConflictResolution.ASK)
state.update_resolution(ConflictResolution.REPLACE)
assert state.current_resolution == ConflictResolution.ASK
assert state.should_continue() is True
class TestExtractArchiveConflictResolution:
"""Test extract_archive with conflict resolution."""
def test_extract_with_replace_conflict_resolution(self, temp_dir):
"""Test extraction with REPLACE conflict resolution."""
# Create archive
archive_path = temp_dir / "test.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
create_archive(archive_path, [source_file])
# Create extract directory with conflicting file
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
# Extract with REPLACE resolution
extract_archive(
archive_path, extract_dir, conflict_resolution=ConflictResolution.REPLACE
)
# Verify file was replaced
assert conflict_file.read_text() == "archive content"
def test_extract_with_skip_conflict_resolution(self, temp_dir):
"""Test extraction with SKIP conflict resolution."""
# Create archive
archive_path = temp_dir / "test.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
create_archive(archive_path, [source_file])
# Create extract directory with conflicting file
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
# Extract with SKIP resolution
extract_archive(
archive_path, extract_dir, conflict_resolution=ConflictResolution.SKIP
)
# Verify file was not replaced
assert conflict_file.read_text() == "existing content"
def test_extract_with_interactive_callback(self, temp_dir):
"""Test extraction with interactive callback."""
# Create archive
archive_path = temp_dir / "test.tzst"
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
create_archive(archive_path, [source_file])
# Create extract directory with conflicting file
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
mock_callback = Mock(return_value=ConflictResolution.REPLACE)
# Extract with interactive callback
extract_archive(
archive_path,
extract_dir,
conflict_resolution=ConflictResolution.ASK,
interactive_callback=mock_callback,
)
# Verify callback was called and file was replaced
mock_callback.assert_called_once()
assert conflict_file.read_text() == "archive content"
class TestTzstArchiveConflictResolution:
"""Test extract_archive function with conflict resolution (corrected)."""
def test_extract_archive_with_conflict_resolution(self, temp_dir):
"""Test extract_archive function with conflict resolution parameter."""
# Create archive
source_file = temp_dir / "source.txt"
source_file.write_text("archive content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(source_file), arcname="source.txt")
# Create conflicting file in extract directory
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
conflict_file = extract_dir / "source.txt"
conflict_file.write_text("existing content")
# Extract with REPLACE resolution using extract_archive function
extract_archive(
archive_path, extract_dir, conflict_resolution=ConflictResolution.REPLACE
)
# Should have replaced the file
assert conflict_file.read_text() == "archive content"
class TestInteractiveConflictCallback:
"""Test interactive conflict callback functionality."""
@patch("builtins.input")
def test_interactive_callback_replace(self, mock_input, temp_dir):
"""Test interactive callback with replace choice."""
mock_input.return_value = "r"
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.REPLACE
@patch("builtins.input")
def test_interactive_callback_skip(self, mock_input, temp_dir):
"""Test interactive callback with skip choice."""
mock_input.return_value = "n"
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.SKIP
@patch("builtins.input")
def test_interactive_callback_exit(self, mock_input, temp_dir):
"""Test interactive callback with exit choice."""
mock_input.return_value = "x"
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.EXIT
@patch("builtins.input")
def test_interactive_callback_invalid_then_valid(self, mock_input, temp_dir):
"""Test interactive callback with invalid then valid choice."""
mock_input.side_effect = ["invalid", "r"]
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.REPLACE
assert mock_input.call_count == 2
@patch("builtins.input")
def test_interactive_callback_keyboard_interrupt(self, mock_input, temp_dir):
"""Test interactive callback with KeyboardInterrupt."""
mock_input.side_effect = KeyboardInterrupt()
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.EXIT
@patch("builtins.input")
def test_interactive_callback_eof_error(self, mock_input, temp_dir):
"""Test interactive callback with EOFError."""
mock_input.side_effect = EOFError()
file_path = temp_dir / "test.txt"
result = _interactive_conflict_callback(file_path)
assert result == ConflictResolution.EXIT
-216
View File
@@ -1,216 +0,0 @@
"""Tests to exercise conftest.py fixtures and improve coverage."""
from tzst import create_archive, extract_archive, list_archive
from tzst.core import TzstArchive
class TestConftestFixtures:
"""Test all conftest.py fixtures to improve coverage."""
def test_comprehensive_test_files_fixture(self, comprehensive_test_files, temp_dir):
"""Test the comprehensive_test_files fixture."""
# Ensure we have the expected file types
assert len(comprehensive_test_files) >= 9
file_names = [f.name for f in comprehensive_test_files]
# Check for specific files that should be created
assert "empty_file.txt" in file_names
assert "whitespace_only.txt" in file_names
assert "newlines_only.txt" in file_names
assert "null_bytes.bin" in file_names
assert "large_file.txt" in file_names
assert "binary_data.bin" in file_names
assert "file with spaces.txt" in file_names
assert "unicode_content.txt" in file_names
assert "deepest_file.txt" in file_names
# Create archive with these comprehensive test files
archive_path = temp_dir / "comprehensive.tzst"
file_paths = [str(f) for f in comprehensive_test_files if f.is_file()]
create_archive(archive_path, file_paths)
# Verify archive was created and contains expected files
assert archive_path.exists()
contents = list_archive(archive_path)
assert len(contents) >= 9
def test_platform_specific_files_fixture(self, platform_specific_files, temp_dir):
"""Test the platform_specific_files fixture."""
# This fixture may return empty list on Windows, non-empty on Unix
# Just ensure it doesn't crash and returns a list
assert isinstance(platform_specific_files, list)
if platform_specific_files:
# If we have platform-specific files, create an archive with them
archive_path = temp_dir / "platform_specific.tzst"
file_paths = [str(f) for f in platform_specific_files if f.is_file()]
if file_paths:
create_archive(archive_path, file_paths)
assert archive_path.exists()
def test_compression_test_files_fixture(self, compression_test_files, temp_dir):
"""Test the compression_test_files fixture."""
assert len(compression_test_files) == 2
file_names = [f.name for f in compression_test_files]
assert "highly_compressible.txt" in file_names
assert "poorly_compressible.bin" in file_names
# Test different compression levels with these files
for level in [1, 11, 22]:
archive_path = temp_dir / f"compression_level_{level}.tzst"
with TzstArchive(
archive_path, mode="w", compression_level=level
) as archive:
for file_path in compression_test_files:
if file_path.is_file():
archive.add(str(file_path), arcname=file_path.name)
assert archive_path.exists()
contents = list_archive(archive_path)
assert len(contents) == 2
def test_combined_fixtures_workflow(
self, comprehensive_test_files, compression_test_files, temp_dir
):
"""Test using multiple fixtures together."""
all_files = comprehensive_test_files + compression_test_files
file_paths = [str(f) for f in all_files if f.is_file()]
# Create archive with all files
archive_path = temp_dir / "combined.tzst"
create_archive(archive_path, file_paths)
# Extract and verify
extract_dir = temp_dir / "extracted"
extract_archive(archive_path, extract_dir)
# Verify archive was created and extracted directory exists
assert archive_path.exists()
assert extract_dir.exists()
# Count files instead of checking exact names (due to nested structure)
extracted_files = list(extract_dir.rglob("*"))
extracted_file_count = len([f for f in extracted_files if f.is_file()])
original_file_count = len([f for f in all_files if f.is_file()])
# Should have extracted at least some files
assert extracted_file_count > 0
assert extracted_file_count <= original_file_count
def test_unicode_content_file_handling(self, comprehensive_test_files, temp_dir):
"""Test handling of unicode content specifically."""
unicode_files = [f for f in comprehensive_test_files if "unicode" in f.name]
assert len(unicode_files) >= 1
unicode_file = unicode_files[0]
content = unicode_file.read_text(encoding="utf-8")
assert "世界" in content
assert "🌍" in content
# Create archive and verify unicode handling
archive_path = temp_dir / "unicode.tzst"
create_archive(archive_path, [str(unicode_file)])
# Extract and verify content is preserved
extract_dir = temp_dir / "extracted_unicode"
extract_archive(archive_path, extract_dir)
extracted_file = extract_dir / unicode_file.name
extracted_content = extracted_file.read_text(encoding="utf-8")
assert extracted_content == content
def test_special_character_filenames(self, comprehensive_test_files, temp_dir):
"""Test files with special characters in names."""
special_files = [f for f in comprehensive_test_files if " " in f.name]
assert len(special_files) >= 1
archive_path = temp_dir / "special_chars.tzst"
file_paths = [str(f) for f in special_files if f.is_file()]
create_archive(archive_path, file_paths)
contents = list_archive(archive_path)
assert any(" " in item["name"] for item in contents)
def test_deeply_nested_structure(self, comprehensive_test_files, temp_dir):
"""Test deeply nested directory structure."""
nested_files = [f for f in comprehensive_test_files if "deepest" in f.name]
assert len(nested_files) >= 1
nested_file = nested_files[0]
assert "nested" in str(nested_file.parent)
# Create archive maintaining directory structure
archive_path = temp_dir / "nested.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(
str(nested_file), arcname=str(nested_file.relative_to(temp_dir))
)
contents = list_archive(archive_path)
assert any("nested" in item["name"] for item in contents)
def test_empty_and_whitespace_files(self, comprehensive_test_files, temp_dir):
"""Test empty and whitespace-only files."""
empty_files = [
f
for f in comprehensive_test_files
if "empty" in f.name or "whitespace" in f.name or "newlines" in f.name
]
assert len(empty_files) >= 3
archive_path = temp_dir / "empty_whitespace.tzst"
file_paths = [str(f) for f in empty_files if f.is_file()]
create_archive(archive_path, file_paths)
# Extract and verify these special cases are handled
extract_dir = temp_dir / "extracted_empty"
extract_archive(archive_path, extract_dir)
for file_path in empty_files:
if file_path.is_file():
extracted_file = extract_dir / file_path.name
assert extracted_file.exists()
def test_binary_data_handling(self, comprehensive_test_files, temp_dir):
"""Test binary files with null bytes and binary data."""
binary_files = [f for f in comprehensive_test_files if f.suffix == ".bin"]
assert len(binary_files) >= 2
archive_path = temp_dir / "binary.tzst"
file_paths = [str(f) for f in binary_files if f.is_file()]
create_archive(archive_path, file_paths)
# Extract and verify binary content is preserved
extract_dir = temp_dir / "extracted_binary"
extract_archive(archive_path, extract_dir)
for file_path in binary_files:
if file_path.is_file():
extracted_file = extract_dir / file_path.name
assert extracted_file.exists()
# Verify binary content is identical
original_content = file_path.read_bytes()
extracted_content = extracted_file.read_bytes()
assert original_content == extracted_content
def test_large_file_handling(self, comprehensive_test_files, temp_dir):
"""Test large file handling."""
large_files = [f for f in comprehensive_test_files if "large" in f.name]
assert len(large_files) >= 1
large_file = large_files[0]
# Verify it's actually large
assert large_file.stat().st_size > 100000 # Should be > 100KB
archive_path = temp_dir / "large.tzst"
create_archive(archive_path, [str(large_file)])
# Test with streaming mode
archive_path_streaming = temp_dir / "large_streaming.tzst"
with TzstArchive(archive_path_streaming, mode="w", streaming=True) as archive:
archive.add(str(large_file), arcname=large_file.name)
# Both archives should exist
assert archive_path.exists()
assert archive_path_streaming.exists()
+253
View File
@@ -0,0 +1,253 @@
"""Tests for edge cases and error conditions in core.py to improve coverage.
This test file targets the specific missing lines identified in the coverage report,
focusing on error handling, edge cases, and less common code paths.
"""
import tarfile
from unittest.mock import Mock, patch
import pytest
from tzst.core import (
ConflictResolution,
TzstArchive,
_handle_file_conflict,
create_archive,
extract_archive,
)
class TestConflictResolutionEdgeCases:
"""Test edge cases in conflict resolution handling."""
def test_handle_file_conflict_invalid_string_resolution(self, temp_dir):
"""Test _handle_file_conflict with invalid string resolution."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing content")
# Test with invalid string - should fallback to ASK
result_action, result_path = _handle_file_conflict(
target_path, "invalid_resolution", None
) # Should fallback to REPLACE when no interactive callback
assert result_action == ConflictResolution.REPLACE
assert result_path == target_path
def test_handle_file_conflict_ask_with_callback(self, temp_dir):
"""Test ASK resolution with interactive callback."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing content")
# Mock interactive callback that returns REPLACE
mock_callback = Mock(return_value=ConflictResolution.REPLACE)
result_action, result_path = _handle_file_conflict(
target_path, ConflictResolution.ASK, mock_callback
)
assert result_action == ConflictResolution.REPLACE
assert result_path == target_path
mock_callback.assert_called_once_with(target_path)
def test_handle_file_conflict_ask_without_callback(self, temp_dir):
"""Test ASK resolution without interactive callback."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing content")
result_action, result_path = _handle_file_conflict(
target_path, ConflictResolution.ASK, None
) # Should default to REPLACE when no callback available
assert result_action == ConflictResolution.REPLACE
assert result_path == target_path
def test_handle_file_conflict_unknown_resolution(self, temp_dir):
"""Test handling of unknown resolution types."""
target_path = temp_dir / "existing.txt"
target_path.write_text("existing content")
# Create a mock enum value that's not handled
unknown_resolution = Mock()
unknown_resolution.name = "UNKNOWN"
result_action, result_path = _handle_file_conflict(
target_path, unknown_resolution, None
)
# Should default to REPLACE for unknown resolutions
assert result_action == ConflictResolution.REPLACE
assert result_path == target_path
class TestArchiveErrorHandling:
"""Test error handling in archive operations."""
def test_archive_streaming_extraction_error(self, temp_dir):
"""Test extraction error in streaming mode."""
# Create a simple archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
create_archive(archive_path, [test_file])
# Mock tarfile to raise StreamError
with patch("tarfile.open") as mock_open:
mock_tarfile = Mock()
mock_open.return_value.__enter__.return_value = mock_tarfile
mock_tarfile.extractall.side_effect = tarfile.StreamError(
"seeking not allowed"
)
archive = TzstArchive(archive_path, "r", streaming=True)
archive._tarfile = mock_tarfile
archive.streaming = True
with pytest.raises(
RuntimeError, match="Extraction failed in streaming mode"
):
archive.extractall(temp_dir / "extract")
def test_getmembers_archive_not_open(self):
"""Test getmembers when archive is not open."""
archive = TzstArchive("dummy.tzst", "r")
archive._tarfile = None
with pytest.raises(RuntimeError, match="Archive not open"):
archive.getmembers()
def test_getmembers_wrong_mode(self, temp_dir):
"""Test getmembers when archive is not in read mode."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, "w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getmembers()
def test_getnames_archive_not_open(self):
"""Test getnames when archive is not open."""
archive = TzstArchive("dummy.tzst", "r")
archive._tarfile = None
with pytest.raises(RuntimeError, match="Archive not open"):
archive.getnames()
def test_getnames_wrong_mode(self, temp_dir):
"""Test getnames when archive is not in read mode."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, "w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getnames()
class TestCreateArchiveErrorHandling:
"""Test error handling in archive creation."""
def test_create_archive_cleanup_on_error(self, temp_dir):
"""Test archive creation cleans up on error."""
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
# Test that errors during archive creation are properly handled
# Simulate error by trying to create archive with invalid compression level
with pytest.raises(ValueError, match="Invalid compression level"):
create_archive(archive_path, [test_file], compression_level=50)
# Archive should not be created when error occurs
assert not archive_path.exists()
def test_create_archive_common_path_no_common_parent(self, temp_dir):
"""Test create_archive when files have no common parent path."""
file1 = temp_dir / "file1.txt"
file1.write_text("content1")
with patch("os.path.commonpath", side_effect=ValueError("no common path")):
archive_path = temp_dir / "test.tzst"
# Should use parent of first file as fallback
create_archive(archive_path, [file1])
assert archive_path.exists()
class TestExtractArchiveEdgeCases:
"""Test edge cases in archive extraction."""
def test_extract_archive_exit_resolution(self, temp_dir):
"""Test extraction with EXIT conflict resolution."""
# Create archive with multiple files
file1 = temp_dir / "file1.txt"
file1.write_text("content1")
file2 = temp_dir / "file2.txt"
file2.write_text("content2")
archive_path = temp_dir / "test.tzst"
create_archive(archive_path, [file1, file2])
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Create a conflicting file
conflict_file = extract_dir / "file1.txt"
conflict_file.write_text("existing content")
# Extract with EXIT resolution
extract_archive(
archive_path, extract_dir, conflict_resolution=ConflictResolution.EXIT
)
# Should stop on first conflict
assert conflict_file.read_text() == "existing content"
assert not (extract_dir / "file2.txt").exists()
def test_extract_archive_members_with_state_break(self, temp_dir):
"""Test extraction of specific members with state break."""
# Create archive with multiple files
file1 = temp_dir / "file1.txt"
file1.write_text("content1")
file2 = temp_dir / "file2.txt"
file2.write_text("content2")
archive_path = temp_dir / "test.tzst"
create_archive(archive_path, [file1, file2])
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Mock ConflictResolutionState to return False for should_continue
with patch("tzst.core.ConflictResolutionState") as mock_state_class:
mock_state = Mock()
mock_state.should_continue.return_value = False
mock_state.apply_to_all = False
mock_state_class.return_value = mock_state
extract_archive(
archive_path, extract_dir, members=["file1.txt", "file2.txt"]
)
# Should break early due to state.should_continue()
assert mock_state.should_continue.called
def test_extract_archive_with_filter_parameter(self, temp_dir):
"""Test extraction using filter parameter."""
# Create archive with a file
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
create_archive(archive_path, [test_file])
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Mock filter function
def mock_filter(member, path):
return member
# Extract with filter (tests the filter parameter branch)
extract_archive(archive_path, extract_dir, filter=mock_filter)
extracted_file = extract_dir / "test.txt"
assert extracted_file.exists()
assert extracted_file.read_text() == "test content"
-395
View File
@@ -1,395 +0,0 @@
"""Tests to cover missing lines in core.py for improved coverage."""
import tarfile
from unittest.mock import MagicMock, patch
import pytest
from tzst.core import TzstArchive
from tzst.exceptions import TzstArchiveError, TzstDecompressionError
class TestCoreMissingLines:
"""Test specific missing lines in core.py."""
def test_append_mode_error_handling(self, temp_dir):
"""Test append mode error handling (lines 126-137)."""
archive_path = temp_dir / "test.tzst"
# Test append mode raises NotImplementedError
with pytest.raises(
NotImplementedError, match="Append mode is not currently supported"
):
TzstArchive(archive_path, mode="a")
def test_invalid_mode_error_after_open(self, temp_dir):
"""Test invalid mode error in __enter__ method (line 137)."""
archive_path = temp_dir / "test.tzst"
# Create archive instance with invalid mode after validation passes
archive = TzstArchive.__new__(TzstArchive)
archive.filename = archive_path
archive.mode = "invalid" # Set invalid mode after construction
archive.compression_level = 3
archive.streaming = False
archive._tarfile = None
archive._fileobj = None
archive._compressed_stream = None
with pytest.raises(TzstArchiveError, match="Failed to open archive"):
archive.__enter__()
def test_zstd_error_handling_in_open(self, temp_dir):
"""Test zstd error handling during archive opening (lines 133-137)."""
archive_path = temp_dir / "test.tzst"
# Create a file that will cause zstd decompression error
archive_path.write_bytes(b"invalid zstd data")
# Try to open as read mode - should raise TzstDecompressionError
with pytest.raises(TzstDecompressionError, match="Failed to open archive"):
with TzstArchive(archive_path, mode="r"):
pass
def test_generic_error_handling_in_open(self, temp_dir):
"""Test generic error handling during archive opening."""
archive_path = temp_dir / "test.tzst"
# Mock to raise a generic exception (not zstd-related)
with patch("builtins.open", side_effect=PermissionError("Permission denied")):
with pytest.raises(TzstArchiveError, match="Failed to open archive"):
with TzstArchive(archive_path, mode="r"):
pass
def test_close_error_handling(self, temp_dir):
"""Test error handling in close method (lines 146-157)."""
archive_path = temp_dir / "test.tzst"
# Create archive and manually set objects that will raise on close
with TzstArchive(archive_path, mode="w") as archive:
pass
# Now manually create problematic objects
archive = TzstArchive.__new__(TzstArchive)
archive._tarfile = MagicMock()
archive._tarfile.close.side_effect = Exception("Close error")
archive._compressed_stream = MagicMock()
archive._compressed_stream.close.side_effect = Exception("Close error")
archive._fileobj = MagicMock()
archive._fileobj.close.side_effect = Exception("Close error")
# close() should handle exceptions gracefully
archive.close() # Should not raise
assert archive._tarfile is None
assert archive._compressed_stream is None
assert archive._fileobj is None
def test_archive_not_open_for_reading_errors(self, temp_dir):
"""Test RuntimeError for operations on archives not open for reading (lines 186, 188, 192)."""
archive_path = temp_dir / "test.tzst"
# Create archive in write mode
with TzstArchive(archive_path, mode="w") as archive:
# Test getmembers() on write mode
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getmembers()
# Test getnames() on write mode
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getnames()
# Test extractfile() on write mode
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.extractfile("test")
def test_streaming_member_extraction_error(self, temp_dir):
"""Test streaming mode member extraction error (lines 242, 249-254)."""
# Create a test archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
# Try to extract specific member in streaming mode
with TzstArchive(archive_path, mode="r", streaming=True) as archive:
members = archive.getmembers()
member = members[0]
extract_dir = temp_dir / "extract"
extract_dir.mkdir() # Should raise RuntimeError for specific member extraction in streaming mode
with pytest.raises(
RuntimeError,
match="Extracting specific members is not supported in streaming mode",
):
archive.extract(member=member.name, path=extract_dir)
def test_streaming_extraction_failure_handling(self, temp_dir):
"""Test streaming extraction failure handling (lines 263-272)."""
# Create archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(
str(test_file), arcname="test.txt"
) # Mock tarfile to raise StreamError
with TzstArchive(archive_path, mode="r", streaming=True) as archive:
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Mock extractall to raise StreamError with streaming-related message
with patch.object(
archive._tarfile,
"extractall",
side_effect=tarfile.StreamError("seeking not supported"),
):
with pytest.raises(
RuntimeError, match="Extraction failed in streaming mode"
):
archive.extract(path=extract_dir)
def test_extractfile_not_open_error(self, temp_dir):
"""Test extractfile when archive is not open (line 307)."""
archive_path = temp_dir / "test.tzst"
# Create closed archive
archive = TzstArchive(archive_path, mode="r")
# Don't open it
with pytest.raises(RuntimeError, match="Archive not open"):
archive.extractfile("test")
def test_extractfile_write_mode_error(self, temp_dir):
"""Test extractfile in write mode (already covered but ensuring line coverage)."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.extractfile("test")
def test_add_method_not_open_error(self, temp_dir):
"""Test add method when archive is not open (line 325, 327)."""
archive_path = temp_dir / "test.tzst"
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
# Create archive but don't open it
archive = TzstArchive(archive_path, mode="w")
with pytest.raises(RuntimeError, match="Archive not open"):
archive.add(str(test_file))
def test_add_method_read_mode_error(self, temp_dir):
"""Test add method in read mode (line 327)."""
# Create archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
# Try to add to archive in read mode
with TzstArchive(archive_path, mode="r") as archive:
with pytest.raises(RuntimeError, match="Archive not open for writing"):
archive.add(str(test_file))
def test_file_not_found_in_add(self, temp_dir):
"""Test file not found error in add method (line 373)."""
archive_path = temp_dir / "test.tzst"
missing_file = temp_dir / "missing.txt"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(FileNotFoundError):
archive.add(str(missing_file))
def test_add_method_generic_error_handling(self, temp_dir):
"""Test generic error handling in add method (line 375)."""
archive_path = temp_dir / "test.tzst"
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
with TzstArchive(archive_path, mode="w") as archive:
# Mock add to raise generic exception
with patch.object(
archive._tarfile,
"add",
side_effect=PermissionError("Permission denied"),
):
with pytest.raises(TzstArchiveError, match="Failed to add"):
archive.add(str(test_file))
def test_test_method_not_open_error(self, temp_dir):
"""Test test method when archive is not open (line 390-391)."""
archive_path = temp_dir / "test.tzst"
# Create archive but don't open it
archive = TzstArchive(archive_path, mode="r")
with pytest.raises(RuntimeError, match="Archive not open"):
archive.test()
def test_test_method_write_mode_error(self, temp_dir):
"""Test test method in write mode (line 391)."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.test()
def test_test_method_streaming_mode_info(self, temp_dir):
"""Test test method streaming mode information (line 427)."""
# Create archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
# Test in streaming mode - should provide different behavior info
with TzstArchive(archive_path, mode="r", streaming=True) as archive:
# This should work but may have streaming-specific behavior
result = archive.test()
assert isinstance(result, bool)
def test_list_method_not_open_error(self, temp_dir):
"""Test list method when archive is not open (line 454-455)."""
archive_path = temp_dir / "test.tzst"
# Create archive but don't open it
archive = TzstArchive(archive_path, mode="r")
with pytest.raises(RuntimeError, match="Archive not open"):
list(archive.list())
def test_list_method_write_mode_error(self, temp_dir):
"""Test list method in write mode (line 455)."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
list(archive.list())
def test_context_manager_exception_handling(self, temp_dir):
"""Test context manager exception handling (lines 502-504)."""
archive_path = temp_dir / "test.tzst"
# Store references for cleanup
fileobj = None
compressed_stream = None
tarfile_obj = None
# Test that close exceptions are suppressed during context manager exit
with patch("tzst.core.TzstArchive.close", side_effect=Exception("Close error")):
try:
with TzstArchive(archive_path, mode="w") as archive:
# Store references to underlying objects for manual cleanup
fileobj = archive._fileobj
compressed_stream = archive._compressed_stream
tarfile_obj = archive._tarfile
raise ValueError("Test exception")
except ValueError:
pass # Expected - the original exception should not be masked
finally:
# Manually clean up since mocked close() failed
try:
if tarfile_obj:
tarfile_obj.close()
except Exception:
pass
try:
if compressed_stream:
compressed_stream.close()
except Exception:
pass
try:
if fileobj:
fileobj.close()
except Exception:
pass
# Ensure the file is removed to prevent permission errors
try:
if archive_path.exists():
archive_path.unlink()
except (PermissionError, OSError):
pass
# The close exception should be suppressed by __exit__
def test_streaming_mode_directory_creation_error(self, temp_dir):
"""Test directory creation error in streaming mode (line 521)."""
# Create archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
with TzstArchive(archive_path, mode="r", streaming=True) as archive:
# Mock path creation to fail
extract_dir = temp_dir / "extract"
with patch("pathlib.Path.mkdir", side_effect=OSError("Permission denied")):
with pytest.raises(OSError):
archive.extractall(path=extract_dir)
def test_list_verbose_mode_edge_cases(self, temp_dir):
"""Test list method verbose mode edge cases (lines 573, 588-589)."""
# Create archive with special files
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
# Create a directory
test_dir = temp_dir / "test_dir"
test_dir.mkdir()
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
archive.add(str(test_dir), arcname="test_dir")
with TzstArchive(archive_path, mode="r") as archive:
# Test verbose listing
items = list(archive.list(verbose=True))
assert len(items) >= 2
# Should have both file and directory entries
file_items = [item for item in items if item.get("is_file", False)]
dir_items = [item for item in items if item.get("is_dir", False)]
assert len(file_items) >= 1
assert len(dir_items) >= 1
def test_extractall_with_members_parameter(self, temp_dir):
"""Test extractall with members parameter for selective extraction."""
# Create archive with multiple files
test_file1 = temp_dir / "test1.txt"
test_file1.write_text("content1")
test_file2 = temp_dir / "test2.txt"
test_file2.write_text("content2")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file1), arcname="test1.txt")
archive.add(str(test_file2), arcname="test2.txt")
# Extract only specific members
with TzstArchive(archive_path, mode="r") as archive:
members = archive.getmembers()
first_member = members[0]
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Extract only first member
archive.extractall(path=extract_dir, members=[first_member])
# Verify only one file was extracted
extracted_files = list(extract_dir.glob("*.txt"))
assert len(extracted_files) == 1
-374
View File
@@ -1,374 +0,0 @@
"""Tests to cover missing lines in core.py for improved coverage."""
import tarfile
from unittest.mock import MagicMock, patch
import pytest
from tzst.core import TzstArchive
from tzst.exceptions import TzstArchiveError, TzstDecompressionError
class TestCoreMissingLines:
"""Test specific missing lines in core.py."""
def test_append_mode_error_handling(self, temp_dir):
"""Test append mode error handling (lines 126-137)."""
archive_path = temp_dir / "test.tzst"
# Test append mode raises NotImplementedError
with pytest.raises(
NotImplementedError, match="Append mode is not currently supported"
):
TzstArchive(archive_path, mode="a")
def test_invalid_mode_error_after_open(self, temp_dir):
"""Test invalid mode error in __enter__ method (line 137)."""
archive_path = temp_dir / "test.tzst"
# Create archive instance with invalid mode after validation passes
archive = TzstArchive.__new__(TzstArchive)
archive.filename = archive_path
archive.mode = "invalid" # Set invalid mode after construction
archive.compression_level = 3
archive.streaming = False
archive._tarfile = None
archive._fileobj = None
archive._compressed_stream = None
with pytest.raises(TzstArchiveError, match="Failed to open archive"):
archive.__enter__()
def test_zstd_error_handling_in_open(self, temp_dir):
"""Test zstd error handling during archive opening (lines 133-137)."""
archive_path = temp_dir / "test.tzst"
# Create a file that will cause zstd decompression error
archive_path.write_bytes(b"invalid zstd data")
# Try to open as read mode - should raise TzstDecompressionError
with pytest.raises(TzstDecompressionError, match="Failed to open archive"):
with TzstArchive(archive_path, mode="r"):
pass
def test_generic_error_handling_in_open(self, temp_dir):
"""Test generic error handling during archive opening."""
archive_path = temp_dir / "test.tzst"
# Mock to raise a generic exception (not zstd-related)
with patch("builtins.open", side_effect=PermissionError("Permission denied")):
with pytest.raises(TzstArchiveError, match="Failed to open archive"):
with TzstArchive(archive_path, mode="r"):
pass
def test_close_error_handling(self, temp_dir):
"""Test error handling in close method (lines 146-157)."""
archive_path = temp_dir / "test.tzst"
# Create archive and manually set objects that will raise on close
with TzstArchive(archive_path, mode="w") as archive:
pass
# Now manually create problematic objects
archive = TzstArchive.__new__(TzstArchive)
archive._tarfile = MagicMock()
archive._tarfile.close.side_effect = Exception("Close error")
archive._compressed_stream = MagicMock()
archive._compressed_stream.close.side_effect = Exception("Close error")
archive._fileobj = MagicMock()
archive._fileobj.close.side_effect = Exception("Close error")
# close() should handle exceptions gracefully
archive.close() # Should not raise
assert archive._tarfile is None
assert archive._compressed_stream is None
assert archive._fileobj is None
def test_archive_not_open_for_reading_errors(self, temp_dir):
"""Test RuntimeError for operations on archives not open for reading."""
archive_path = temp_dir / "test.tzst"
# Create archive in write mode
with TzstArchive(archive_path, mode="w") as archive:
# Test getmembers() on write mode
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getmembers()
# Test getnames() on write mode
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getnames()
# Test extractfile() on write mode
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.extractfile("test")
def test_streaming_member_extraction_error(self, temp_dir):
"""Test streaming mode member extraction error."""
# Create a test archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
# Try to extract specific member in streaming mode
with TzstArchive(archive_path, mode="r", streaming=True) as archive:
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Should raise RuntimeError for specific member extraction in streaming mode
with pytest.raises(
RuntimeError,
match="Extracting specific members is not supported in streaming mode",
):
archive.extract(member="test.txt", path=extract_dir)
def test_streaming_extraction_failure_handling(self, temp_dir):
"""Test streaming extraction failure handling."""
# Create archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
# Mock tarfile to raise StreamError
with TzstArchive(archive_path, mode="r", streaming=True) as archive:
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Mock extract to raise StreamError with streaming-related message
with patch.object(
archive._tarfile,
"extractall",
side_effect=tarfile.StreamError("seeking not supported"),
):
with pytest.raises(
RuntimeError, match="Extraction failed in streaming mode"
):
archive.extract(path=extract_dir)
def test_extractfile_not_open_error(self, temp_dir):
"""Test extractfile when archive is not open."""
archive_path = temp_dir / "test.tzst"
# Create closed archive
archive = TzstArchive(archive_path, mode="r")
# Don't open it
with pytest.raises(RuntimeError, match="Archive not open"):
archive.extractfile("test")
def test_extractfile_write_mode_error(self, temp_dir):
"""Test extractfile in write mode."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.extractfile("test")
def test_add_method_not_open_error(self, temp_dir):
"""Test add method when archive is not open."""
archive_path = temp_dir / "test.tzst"
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
# Create archive but don't open it
archive = TzstArchive(archive_path, mode="w")
with pytest.raises(RuntimeError, match="Archive not open"):
archive.add(str(test_file))
def test_add_method_read_mode_error(self, temp_dir):
"""Test add method in read mode."""
# Create archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
# Try to add to archive in read mode
with TzstArchive(archive_path, mode="r") as archive:
with pytest.raises(RuntimeError, match="Archive not open for writing"):
archive.add(str(test_file))
def test_file_not_found_in_add(self, temp_dir):
"""Test file not found error in add method."""
archive_path = temp_dir / "test.tzst"
missing_file = temp_dir / "missing.txt"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(FileNotFoundError):
archive.add(str(missing_file))
def test_add_method_generic_error_handling(self, temp_dir):
"""Test generic error handling in add method."""
archive_path = temp_dir / "test.tzst"
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
with TzstArchive(archive_path, mode="w") as archive:
# Mock add to raise generic exception
with patch.object(
archive._tarfile,
"add",
side_effect=PermissionError("Permission denied"),
):
with pytest.raises(TzstArchiveError, match="Failed to add"):
archive.add(str(test_file))
def test_test_method_not_open_error(self, temp_dir):
"""Test test method when archive is not open."""
archive_path = temp_dir / "test.tzst"
# Create archive but don't open it
archive = TzstArchive(archive_path, mode="r")
with pytest.raises(RuntimeError, match="Archive not open"):
archive.test()
def test_test_method_write_mode_error(self, temp_dir):
"""Test test method in write mode."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.test()
def test_test_method_streaming_mode_info(self, temp_dir):
"""Test test method streaming mode information."""
# Create archive first
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
# Test in streaming mode - should provide different behavior info
with TzstArchive(archive_path, mode="r", streaming=True) as archive:
# This should work but may have streaming-specific behavior
result = archive.test()
assert isinstance(result, bool)
def test_list_method_not_open_error(self, temp_dir):
"""Test list method when archive is not open."""
archive_path = temp_dir / "test.tzst"
# Create archive but don't open it
archive = TzstArchive(archive_path, mode="r")
with pytest.raises(RuntimeError, match="Archive not open"):
list(archive.list())
def test_list_method_write_mode_error(self, temp_dir):
"""Test list method in write mode."""
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
list(archive.list())
def test_context_manager_exception_handling(self, temp_dir):
"""Test context manager exception handling."""
archive_path = temp_dir / "test.tzst"
# Store references for cleanup
fileobj = None
compressed_stream = None
tarfile_obj = None
# Test that close exceptions are suppressed during context manager exit
with patch("tzst.core.TzstArchive.close", side_effect=Exception("Close error")):
try:
with TzstArchive(archive_path, mode="w") as archive:
# Store references to underlying objects for manual cleanup
fileobj = archive._fileobj
compressed_stream = archive._compressed_stream
tarfile_obj = archive._tarfile
raise ValueError("Test exception")
except ValueError:
pass # Expected - the original exception should not be masked
finally:
# Manually clean up since mocked close() failed
try:
if tarfile_obj:
tarfile_obj.close()
except Exception:
pass
try:
if compressed_stream:
compressed_stream.close()
except Exception:
pass
try:
if fileobj:
fileobj.close()
except Exception:
pass
# Ensure the file is removed to prevent permission errors
try:
if archive_path.exists():
archive_path.unlink()
except (PermissionError, OSError):
pass
# The close exception should be suppressed by __exit__
def test_list_verbose_mode_edge_cases(self, temp_dir):
"""Test list method verbose mode edge cases."""
# Create archive with special files
test_file = temp_dir / "test.txt"
test_file.write_text("test content")
# Create a directory
test_dir = temp_dir / "test_dir"
test_dir.mkdir()
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file), arcname="test.txt")
archive.add(str(test_dir), arcname="test_dir")
with TzstArchive(archive_path, mode="r") as archive:
# Test verbose listing
items = list(archive.list(verbose=True))
assert len(items) >= 2
# Should have both file and directory entries
file_items = [item for item in items if item.get("is_file", False)]
dir_items = [item for item in items if item.get("is_dir", False)]
assert len(file_items) >= 1
assert len(dir_items) >= 1
def test_extract_with_members_parameter(self, temp_dir):
"""Test extract with specific member for selective extraction."""
# Create archive with multiple files
test_file1 = temp_dir / "test1.txt"
test_file1.write_text("content1")
test_file2 = temp_dir / "test2.txt"
test_file2.write_text("content2")
archive_path = temp_dir / "test.tzst"
with TzstArchive(archive_path, mode="w") as archive:
archive.add(str(test_file1), arcname="test1.txt")
archive.add(str(test_file2), arcname="test2.txt")
# Extract only specific member
with TzstArchive(archive_path, mode="r") as archive:
extract_dir = temp_dir / "extract"
extract_dir.mkdir()
# Extract only first member
archive.extract(member="test1.txt", path=extract_dir)
# Verify only one file was extracted
extracted_files = list(extract_dir.glob("*.txt"))
assert len(extracted_files) == 1
assert extracted_files[0].name == "test1.txt"
+5
View File
@@ -1,8 +1,11 @@
"""Tests for TzstArchive class core functionality."""
import pytest
from tzst import TzstArchive
@pytest.mark.unit
class TestBasicImportAndCreation:
"""Test basic import and creation functionality."""
@@ -17,6 +20,7 @@ class TestBasicImportAndCreation:
assert archive.mode == "r"
@pytest.mark.unit
class TestTzstArchiveBasics:
"""Test basic TzstArchive class functionality."""
@@ -107,6 +111,7 @@ class TestTzstArchiveBasics:
assert "gid" in item
@pytest.mark.unit
class TestTzstArchiveStreamingMode:
"""Test streaming mode functionality."""
+4
View File
@@ -6,6 +6,7 @@ from tzst import create_archive, extract_archive, list_archive
from tzst import test_archive as tzst_test_archive
@pytest.mark.unit
class TestConvenienceFunctions:
"""Test the convenience functions."""
@@ -111,6 +112,7 @@ class TestConvenienceFunctions:
assert extract_dir_streaming.exists()
@pytest.mark.unit
class TestAtomicOperations:
"""Test atomic file operations."""
@@ -154,6 +156,7 @@ class TestAtomicOperations:
assert len(temp_files) == 0
@pytest.mark.unit
class TestCompressionLevels:
"""Test compression level validation and functionality."""
@@ -180,6 +183,7 @@ class TestCompressionLevels:
assert "1" in str(exc_info.value) and "22" in str(exc_info.value)
@pytest.mark.unit
class TestEdgeCaseCoverage:
"""Test edge cases to improve coverage."""
+440
View File
@@ -0,0 +1,440 @@
"""Targeted tests for remaining uncovered core branches."""
import os
import tarfile
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import tzst.core as core_module
from tzst.core import (
ConflictResolution,
ConflictResolutionState,
TzstArchive,
_get_unique_filename,
_move_file_cross_platform,
create_archive,
extract_archive,
)
from tzst.exceptions import TzstArchiveError, TzstDecompressionError
def _make_read_archive() -> TzstArchive:
archive = TzstArchive("dummy.tzst", "r")
archive._tarfile = Mock()
archive.mode = "r"
return archive
def _make_write_archive() -> TzstArchive:
archive = TzstArchive("dummy.tzst", "w")
archive._tarfile = Mock()
archive.mode = "w"
return archive
class TestCoreCoverageGaps:
"""Exercise the remaining core coverage hotspots."""
def test_conflict_resolution_state_apply_to_all(self):
state = ConflictResolutionState(ConflictResolution.AUTO_RENAME_ALL)
assert state.apply_to_all is True
state = ConflictResolutionState(ConflictResolution.REPLACE)
assert state.apply_to_all is False
def test_get_unique_filename_returns_original_for_missing_path(self, temp_dir):
missing_path = temp_dir / "missing.txt"
assert _get_unique_filename(missing_path) == missing_path
def test_move_file_cross_platform_falls_back_to_copy_and_delete(
self, temp_dir, monkeypatch
):
src = temp_dir / "source.txt"
dst = temp_dir / "target.txt"
src.write_text("cross-drive content")
def fail_rename(self, target):
raise OSError("cross-device link")
monkeypatch.setattr(Path, "rename", fail_rename)
_move_file_cross_platform(src, dst)
assert dst.read_text() == "cross-drive content"
assert not src.exists()
def test_move_file_cross_platform_reraises_original_rename_error(
self, temp_dir, monkeypatch
):
src = temp_dir / "source.txt"
dst = temp_dir / "target.txt"
src.write_text("cross-drive content")
def fail_rename(self, target):
raise OSError("rename failed")
def fail_copy(*args, **kwargs):
raise RuntimeError("copy failed")
monkeypatch.setattr(Path, "rename", fail_rename)
monkeypatch.setattr("shutil.copy2", fail_copy)
with pytest.raises(OSError, match="rename failed"):
_move_file_cross_platform(src, dst)
def test_exit_suppresses_close_errors(self, monkeypatch):
archive = TzstArchive("dummy.tzst", "w")
monkeypatch.setattr(archive, "close", Mock(side_effect=RuntimeError("boom")))
archive.__exit__(None, None, None)
def test_open_wraps_zstd_failures(self, temp_dir, monkeypatch):
archive_path = temp_dir / "broken.tzst"
archive_path.write_bytes(b"not-a-valid-archive")
class BrokenDecompressor:
def stream_reader(self, fileobj):
raise RuntimeError("zstd decoder exploded")
monkeypatch.setattr(core_module.zstd, "ZstdDecompressor", BrokenDecompressor)
with pytest.raises(TzstDecompressionError, match="zstd decoder exploded"):
TzstArchive(archive_path, "r").open()
def test_open_wraps_append_mode_when_mode_changes_after_init(self):
archive = TzstArchive("dummy.tzst", "w")
archive.mode = "a"
with pytest.raises(
TzstArchiveError, match="Append mode is not currently supported"
):
archive.open()
def test_open_wraps_invalid_mode_when_mode_changes_after_init(self):
archive = TzstArchive("dummy.tzst", "w")
archive.mode = "invalid"
with pytest.raises(TzstArchiveError, match="Invalid mode: invalid"):
archive.open()
def test_add_requires_open_archive(self, temp_dir):
archive = TzstArchive(temp_dir / "archive.tzst", "w")
with pytest.raises(RuntimeError, match="Archive not open"):
archive.add(temp_dir / "file.txt")
def test_add_requires_write_mode(self, temp_dir):
archive = _make_write_archive()
archive.mode = "r"
file_path = temp_dir / "file.txt"
file_path.write_text("content")
with pytest.raises(RuntimeError, match="Archive not open for writing"):
archive.add(file_path)
def test_add_raises_file_not_found_for_missing_input(self, temp_dir):
archive = _make_write_archive()
with pytest.raises(FileNotFoundError, match="File not found"):
archive.add(temp_dir / "missing.txt")
def test_add_wraps_permission_errors(self, temp_dir):
archive = _make_write_archive()
archive._tarfile.add.side_effect = PermissionError("denied")
file_path = temp_dir / "file.txt"
file_path.write_text("content")
with pytest.raises(TzstArchiveError, match="Failed to add"):
archive.add(file_path)
def test_extract_validates_open_state_and_mode(self, temp_dir):
closed_archive = TzstArchive(temp_dir / "archive.tzst", "r")
with pytest.raises(RuntimeError, match="Archive not open"):
closed_archive.extract()
wrong_mode_archive = _make_write_archive()
with pytest.raises(RuntimeError, match="Archive not open for reading"):
wrong_mode_archive.extract()
def test_extract_rejects_specific_members_in_streaming_mode(self):
archive = _make_read_archive()
archive.streaming = True
with pytest.raises(RuntimeError, match="specific members is not supported"):
archive.extract("file.txt")
def test_extract_passes_member_specific_kwargs(self, temp_dir):
archive = _make_read_archive()
output_dir = temp_dir / "extract"
archive.extract(
"nested/file.txt",
output_dir,
set_attrs=False,
numeric_owner=True,
filter="tar",
)
archive._tarfile.extract.assert_called_once_with(
"nested/file.txt",
path=output_dir,
set_attrs=False,
numeric_owner=True,
filter="tar",
)
def test_extract_wraps_streaming_structure_errors(self, temp_dir):
archive = _make_read_archive()
archive.streaming = True
archive._tarfile.extractall.side_effect = tarfile.StreamError(
"stream seeking failed"
)
with pytest.raises(RuntimeError, match="Extraction failed in streaming mode"):
archive.extract(path=temp_dir / "extract")
def test_extract_reraises_non_streaming_errors(self, temp_dir):
archive = _make_read_archive()
archive._tarfile.extractall.side_effect = OSError("disk full")
with pytest.raises(OSError, match="disk full"):
archive.extract(path=temp_dir / "extract")
def test_extractall_validates_open_state_and_members_behavior(self, temp_dir):
closed_archive = TzstArchive(temp_dir / "archive.tzst", "r")
with pytest.raises(RuntimeError, match="Archive not open"):
closed_archive.extractall()
wrong_mode_archive = _make_write_archive()
with pytest.raises(RuntimeError, match="Archive not open for reading"):
wrong_mode_archive.extractall()
streaming_archive = _make_read_archive()
streaming_archive.streaming = True
with pytest.raises(RuntimeError, match="specific members is not supported"):
streaming_archive.extractall(members=[Mock()])
def test_extractall_passes_members_and_reraises_other_errors(self, temp_dir):
archive = _make_read_archive()
members = [SimpleNamespace(name="file.txt")]
output_dir = temp_dir / "extract"
archive.extractall(
output_dir, members=members, numeric_owner=True, filter="tar"
)
archive._tarfile.extractall.assert_called_once_with(
path=output_dir,
numeric_owner=True,
filter="tar",
members=members,
)
archive = _make_read_archive()
archive._tarfile.extractall.side_effect = OSError("permission denied")
with pytest.raises(OSError, match="permission denied"):
archive.extractall(output_dir)
def test_getnames_list_and_test_runtime_paths(self):
archive = _make_read_archive()
archive._tarfile.getnames.return_value = ["a.txt"]
assert archive.getnames() == ["a.txt"]
closed_archive = TzstArchive("dummy.tzst", "r")
with pytest.raises(RuntimeError, match="Archive not open"):
closed_archive.list()
wrong_mode_archive = _make_write_archive()
with pytest.raises(RuntimeError, match="Archive not open for reading"):
wrong_mode_archive.list()
failing_archive = _make_read_archive()
failing_archive.getmembers = Mock(side_effect=RuntimeError("boom"))
assert failing_archive.test() is False
def test_create_archive_supports_tar_extension(self, temp_dir):
source_file = temp_dir / "source.txt"
source_file.write_text("archive me")
create_archive(temp_dir / "bundle.tar", [source_file], use_temp_file=False)
assert (temp_dir / "bundle.tar.zst").exists()
def test_create_archive_ignores_unlink_cleanup_failures(
self, temp_dir, monkeypatch
):
source_file = temp_dir / "source.txt"
source_file.write_text("archive me")
archive_path = temp_dir / "broken.tzst"
temp_path = temp_dir / ".broken.tzst.tmp"
def fake_mkstemp(*args, **kwargs):
fd = os.open(temp_path, os.O_CREAT | os.O_RDWR)
return fd, str(temp_path)
def fail_unlink(self):
raise OSError("locked")
monkeypatch.setattr(core_module.tempfile, "mkstemp", fake_mkstemp)
monkeypatch.setattr(
core_module, "_create_archive_impl", Mock(side_effect=RuntimeError("boom"))
)
monkeypatch.setattr(Path, "unlink", fail_unlink)
with pytest.raises(RuntimeError, match="boom"):
create_archive(archive_path, [source_file])
assert temp_path.exists()
def test_extract_archive_handles_flatten_members_and_invalid_resolution(
self, temp_dir
):
file_path = temp_dir / "source.txt"
file_path.write_text("content")
archive_path = temp_dir / "archive.tzst"
create_archive(archive_path, [file_path], use_temp_file=False)
output_dir = temp_dir / "extract"
extract_archive(
archive_path,
output_dir,
members=["source.txt"],
flatten=True,
conflict_resolution="definitely-invalid",
)
assert (output_dir / "source.txt").read_text() == "content"
def test_extract_archive_flatten_respects_initial_exit_resolution(self, temp_dir):
file_path = temp_dir / "source.txt"
file_path.write_text("content")
archive_path = temp_dir / "archive.tzst"
create_archive(archive_path, [file_path], use_temp_file=False)
output_dir = temp_dir / "extract"
extract_archive(
archive_path,
output_dir,
flatten=True,
conflict_resolution=ConflictResolution.EXIT,
)
assert not (output_dir / "source.txt").exists()
def test_extract_archive_flatten_conflict_skip_exit_and_auto_rename(self, temp_dir):
first = temp_dir / "first.txt"
second = temp_dir / "second.txt"
first.write_text("first")
second.write_text("second")
archive_path = temp_dir / "archive.tzst"
create_archive(archive_path, [first, second], use_temp_file=False)
skip_output = temp_dir / "skip"
skip_output.mkdir()
(skip_output / "first.txt").write_text("existing")
extract_archive(
archive_path,
skip_output,
members=["first.txt"],
flatten=True,
conflict_resolution=ConflictResolution.SKIP,
)
assert (skip_output / "first.txt").read_text() == "existing"
rename_output = temp_dir / "rename"
rename_output.mkdir()
(rename_output / "first.txt").write_text("existing")
extract_archive(
archive_path,
rename_output,
members=["first.txt"],
flatten=True,
conflict_resolution=ConflictResolution.AUTO_RENAME,
)
assert (rename_output / "first.txt").read_text() == "existing"
assert (rename_output / "first_1.txt").read_text() == "first"
exit_output = temp_dir / "exit"
exit_output.mkdir()
(exit_output / "first.txt").write_text("existing")
extract_archive(
archive_path,
exit_output,
members=["first.txt", "second.txt"],
flatten=True,
conflict_resolution=ConflictResolution.ASK,
interactive_callback=lambda _path: ConflictResolution.EXIT,
)
assert (exit_output / "first.txt").read_text() == "existing"
assert not (exit_output / "second.txt").exists()
def test_extract_archive_members_support_auto_rename_and_plain_extract(
self, temp_dir
):
source_root = temp_dir / "source"
nested_dir = source_root / "nested"
nested_dir.mkdir(parents=True)
conflict_file = nested_dir / "conflict.txt"
plain_file = source_root / "plain.txt"
conflict_file.write_text("conflict-content")
plain_file.write_text("plain-content")
archive_path = temp_dir / "archive.tzst"
create_archive(archive_path, [conflict_file, plain_file], use_temp_file=False)
output_dir = temp_dir / "extract"
output_dir.mkdir()
target_conflict = output_dir / "nested" / "conflict.txt"
target_conflict.parent.mkdir(parents=True, exist_ok=True)
target_conflict.write_text("existing")
extract_archive(
archive_path,
output_dir,
members=["nested/conflict.txt", "plain.txt"],
flatten=False,
conflict_resolution=ConflictResolution.AUTO_RENAME,
)
assert target_conflict.read_text() == "existing"
assert (
output_dir / "nested" / "conflict_1.txt"
).read_text() == "conflict-content"
assert (output_dir / "plain.txt").read_text() == "plain-content"
def test_extract_archive_members_honor_initial_exit_state(self, temp_dir):
file_path = temp_dir / "source.txt"
file_path.write_text("content")
archive_path = temp_dir / "archive.tzst"
create_archive(archive_path, [file_path], use_temp_file=False)
output_dir = temp_dir / "extract"
extract_archive(
archive_path,
output_dir,
members=["source.txt"],
conflict_resolution=ConflictResolution.EXIT,
)
assert not (output_dir / "source.txt").exists()
def test_extract_archive_extractall_breaks_on_exit_conflict(self, temp_dir):
file_path = temp_dir / "source.txt"
file_path.write_text("content")
archive_path = temp_dir / "archive.tzst"
create_archive(archive_path, [file_path], use_temp_file=False)
output_dir = temp_dir / "extract"
output_dir.mkdir()
(output_dir / "source.txt").write_text("existing")
extract_archive(
archive_path,
output_dir,
conflict_resolution=ConflictResolution.ASK,
interactive_callback=lambda _path: ConflictResolution.EXIT,
)
assert (output_dir / "source.txt").read_text() == "existing"
+22
View File
@@ -0,0 +1,22 @@
"""Project metadata and CI support tests."""
import tomllib
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
def test_pyproject_declares_python_314_classifier():
"""Package metadata should advertise Python 3.14 support."""
pyproject = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text("utf-8"))
classifiers = pyproject["project"]["classifiers"]
assert "Programming Language :: Python :: 3.14" in classifiers
def test_ci_matrix_includes_python_314():
"""Main test workflow should run on Python 3.14."""
workflow = (REPO_ROOT / ".github" / "workflows" / "ci.yml").read_text("utf-8")
assert 'python-version: ["3.12", "3.13", "3.14"]' in workflow
+6 -4
View File
@@ -8,6 +8,7 @@ from tzst import TzstArchive, create_archive, extract_archive
from tzst import test_archive as tzst_test_archive
@pytest.mark.unit
class TestErrorHandling:
"""Test error handling."""
@@ -37,6 +38,7 @@ class TestErrorHandling:
create_archive(archive_path, [fake_file])
@pytest.mark.unit
class TestSecurityFiltering:
"""Test security filtering mechanisms."""
@@ -50,11 +52,11 @@ class TestSecurityFiltering:
# Extract with tar filter
extract_dir = temp_dir / "tar_filtered"
with patch("tzst.core.TzstArchive.extract") as mock_extract:
with patch("tzst.core.TzstArchive.extractall") as mock_extractall:
extract_archive(archive_path, extract_dir, filter="tar")
# Verify filter was passed
call_args = mock_extract.call_args
call_args = mock_extractall.call_args
assert call_args[1]["filter"] == "tar"
def test_data_filter_extraction(self, sample_files, temp_dir):
@@ -67,11 +69,11 @@ class TestSecurityFiltering:
# Extract with data filter (default for security)
extract_dir = temp_dir / "data_filtered"
with patch("tzst.core.TzstArchive.extract") as mock_extract:
with patch("tzst.core.TzstArchive.extractall") as mock_extractall:
extract_archive(archive_path, extract_dir, filter="data")
# Verify filter was passed
call_args = mock_extract.call_args
call_args = mock_extractall.call_args
assert call_args[1]["filter"] == "data"
def test_invalid_filter_raises_error(self, sample_files, temp_dir):