Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1efdd4c091 | ||
|
|
713530da75 | ||
|
|
eb89bb8f3b | ||
|
|
d2fa7bcc14 | ||
|
|
5f9f3f3e16 | ||
|
|
a75e865b45 | ||
|
|
2e34172f0c | ||
|
|
d07cd60c3a | ||
|
|
4a5b423e7c | ||
|
|
5165a71659 | ||
|
|
afa4e0e01b | ||
|
|
e59431b610 | ||
|
|
f5262c92c6 | ||
|
|
df46fe8a48 | ||
|
|
5f44b21e6a | ||
|
|
734a7817ff | ||
|
|
b43e152a75 | ||
|
|
427e422a54 | ||
|
|
1ec12195fb | ||
|
|
1e8ff36a9b | ||
|
|
b9f0382e08 | ||
|
|
feaafc4192 | ||
|
|
f033846a0f | ||
|
|
62cb41bb0c | ||
|
|
4d66fccd59 | ||
|
|
a0e7c08f95 | ||
|
|
0f9d0e7b89 | ||
|
|
db3f4e356c | ||
|
|
78cf17be4b | ||
|
|
065c566b5d | ||
|
|
903a748ab8 | ||
|
|
59401b4f88 | ||
|
|
e4bb2e3edb | ||
|
|
a6e1021a1b | ||
|
|
ce07d5cff0 | ||
|
|
87cbd15bc9 | ||
|
|
8d39b46a2c | ||
|
|
71ac857984 | ||
|
|
2e8e99a200 | ||
|
|
7d7e170fdc | ||
|
|
81ac67bf86 | ||
|
|
927226b839 | ||
|
|
a1a9c45ad8 | ||
|
|
68ec8bfbcf | ||
|
|
f0caba5c66 | ||
|
|
264578b866 | ||
|
|
324e227399 | ||
|
|
c6234e12af | ||
|
|
69f0dd3c06 | ||
|
|
535c48cb22 | ||
|
|
a2035596a5 | ||
|
|
5774eddbbb | ||
|
|
33aae43b40 | ||
|
|
148b3b3780 | ||
|
|
8a15c2d7db | ||
|
|
75447562cd | ||
|
|
502b3a6b78 | ||
|
|
e552fca0b2 | ||
|
|
698eb9cf09 | ||
|
|
8e5e1e3271 | ||
|
|
15e27f6aa6 | ||
|
|
a2ef8dd115 | ||
|
|
e0cf05dea0 | ||
|
|
a3350b1a27 | ||
|
|
0c52a51a15 | ||
|
|
96a13fd29a | ||
|
|
6ae689d945 | ||
|
|
7ac5b57496 | ||
|
|
4e0f04e10a | ||
|
|
cc98287944 | ||
|
|
fa82ed95bb | ||
|
|
1878584aa0 | ||
|
|
8048393440 | ||
|
|
750fb36643 | ||
|
|
879c1166b4 | ||
|
|
aa1b218d4a | ||
|
|
96fc060aeb | ||
|
|
e2fca92d4b | ||
|
|
08e3bdc849 | ||
|
|
a48631159f | ||
|
|
1f4b77ffc7 | ||
|
|
66fbe5852b | ||
|
|
0eb219965a | ||
|
|
13f14c847a | ||
|
|
40c7724bcb | ||
|
|
a316a8ac2e | ||
|
|
9e741b6f8f | ||
|
|
988cdc0306 | ||
|
|
c872ca623a | ||
|
|
d82f82bd5d | ||
|
|
20cfb855c8 | ||
|
|
662d84da27 | ||
|
|
81c92e3a33 | ||
|
|
37103bf869 | ||
|
|
7c4459dc5a | ||
|
|
1f3e9ef76e | ||
|
|
5ecb10411f | ||
|
|
2cb2b9eaa7 | ||
|
|
e5ad0336f3 | ||
|
|
6442ba91f3 | ||
|
|
e5f6fd2bc4 | ||
|
|
ff6c321f0d | ||
|
|
1fc8a85c64 | ||
|
|
0aae913613 | ||
|
|
4987bcd468 | ||
|
|
fd11edd877 | ||
|
|
aca7b5f42c | ||
|
|
e454f79e18 | ||
|
|
bcfb299bfb | ||
|
|
171c00c8ce | ||
|
|
e6f81ab68b | ||
|
|
0a827ec260 | ||
|
|
3645979315 | ||
|
|
3d85da8267 | ||
|
|
4e87976fde | ||
|
|
372ad5d653 | ||
|
|
ffa12db17b | ||
|
|
4ba2749e82 | ||
|
|
3fc59f248a | ||
|
|
308e7e3c12 | ||
|
|
68800be7a9 | ||
|
|
af3e657aa4 | ||
|
|
305c13119f | ||
|
|
efbcbbce99 | ||
|
|
320ed0a27a | ||
|
|
36ffd4fd2b | ||
|
|
8abad3616a | ||
|
|
ec8705a7cf | ||
|
|
89efc8e3e8 | ||
|
|
64f2dd6757 | ||
|
|
7b306554f7 | ||
|
|
c78d74768b | ||
|
|
37af38e8bd | ||
|
|
6476e2445a | ||
|
|
d9ca6d0d4e | ||
|
|
641256a133 | ||
|
|
0b37b707b2 | ||
|
|
069ac67eea | ||
|
|
7e3bb1cf6a | ||
|
|
57c5fd0e6c | ||
|
|
7dc51f18c1 | ||
|
|
2e40a77f29 | ||
|
|
e60f252abc | ||
|
|
56b39fea44 | ||
|
|
ce061aa396 | ||
|
|
8de8ec0356 | ||
|
|
6c48d80d9a | ||
|
|
44843e0035 | ||
|
|
42180498e0 | ||
|
|
9b9ad446c1 | ||
|
|
af7bef5371 | ||
|
|
27e1809bf9 | ||
|
|
4d3d34051f | ||
|
|
526ddb76ea | ||
|
|
199fe41293 | ||
|
|
6a9a783ad3 | ||
|
|
571809c36f | ||
|
|
8b39da60da | ||
|
|
b5f7fa8dca | ||
|
|
abbeab83f1 | ||
|
|
ef5d06cb0e | ||
|
|
d0e09347eb | ||
|
|
76e56c984f | ||
|
|
d3e5b4e064 | ||
|
|
23dcdab305 | ||
|
|
2777fea4fc | ||
|
|
2ca81e0918 | ||
|
|
eb3f66f85e | ||
|
|
028c7e3650 | ||
|
|
9e5a677155 | ||
|
|
19356b819f | ||
|
|
bac2f44072 | ||
|
|
ffb6981965 | ||
|
|
15d0828fae | ||
|
|
710c22f605 | ||
|
|
7c268be215 | ||
|
|
b92b5c8652 | ||
|
|
af0159a9fe | ||
|
|
f6d1ff631d | ||
|
|
b5a654c187 | ||
|
|
9b30a8657c | ||
|
|
7f2e53c9d0 | ||
|
|
58400c8f42 | ||
|
|
cd56764f84 | ||
|
|
8b3e4d2429 | ||
|
|
22a879a002 | ||
|
|
ac14a4bca7 | ||
|
|
f89a10ec50 | ||
|
|
518e90f0ee | ||
|
|
194037b87c | ||
|
|
47d6504f39 | ||
|
|
a15a497c52 | ||
|
|
f866f30b7e | ||
|
|
b2eebdedb2 | ||
|
|
1414138341 | ||
|
|
eb4d29e503 | ||
|
|
76963aaf9f | ||
|
|
8035a77c10 | ||
|
|
d95f173308 | ||
|
|
637f1d2562 | ||
|
|
d2f11636be | ||
|
|
69918b4be5 | ||
|
|
7d688d420b | ||
|
|
56a10bc038 | ||
|
|
1dd3cde654 | ||
|
|
a1de2e1855 | ||
|
|
6397968a4d | ||
|
|
cb5c198d16 | ||
|
|
afe5d738a6 | ||
|
|
06eac113fa |
No files matched your search
@@ -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 +1,2 @@
|
||||
custom: https://xi-xu.me/#sponsorships
|
||||
buy_me_a_coffee: xixu
|
||||
+235
-30
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 للإلهام والملاحظات
|
||||
|
||||
## الترخيص
|
||||
|
||||
حقوق النشر © [شي شو](https://xi-xu.me). جميع الحقوق محفوظة.
|
||||
|
||||
مرخص تحت ترخيص [BSD 3-Clause](LICENSE).
|
||||
|
||||
</div>
|
||||
+526
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 © [Xi Xu](https://xi-xu.me). Alle Rechte vorbehalten.
|
||||
|
||||
Lizenziert unter der [BSD 3-Clause](LICENSE) Lizenz.
|
||||
+526
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 © [Xi Xu](https://xi-xu.me). Todos los derechos reservados.
|
||||
|
||||
Licenciado bajo la licencia [BSD 3-Clause](LICENSE).
|
||||
+526
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 © [Xi Xu](https://xi-xu.me). Tous droits réservés.
|
||||
|
||||
Sous licence [BSD 3-Clause](LICENSE).
|
||||
+526
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 コミュニティ
|
||||
|
||||
## ライセンス
|
||||
|
||||
著作権 © [Xi Xu](https://xi-xu.me)。全著作権を保留します。
|
||||
|
||||
[BSD 3-Clause](LICENSE) ライセンスのもとで公開されています。
|
||||
+526
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 커뮤니티
|
||||
|
||||
## 라이선스
|
||||
|
||||
저작권 © [시 쉬](https://xi-xu.me). 모든 권리 보유.
|
||||
|
||||
[BSD 3-Clause](LICENSE) 라이선스로 사용이 허가되었습니다.
|
||||
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 © 2025 [Xi Xu](https://xi-xu.me). All rights reserved.
|
||||
Copyright © [Xi Xu](https://xi-xu.me). All rights reserved.
|
||||
|
||||
Licensed under the [BSD 3-Clause](LICENSE) license.
|
||||
+526
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 © [Xi Xu](https://xi-xu.me). Todos os direitos reservados.
|
||||
|
||||
Licenciado sob a licença [BSD 3-Clause](LICENSE).
|
||||
+526
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 за вдохновение и обратную связь
|
||||
|
||||
## Лицензия
|
||||
|
||||
Авторские права © [Си Сюй](https://xi-xu.me). Все права защищены.
|
||||
|
||||
Лицензировано под лицензией [BSD 3-Clause](LICENSE).
|
||||
+525
@@ -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>
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](LICENSE)
|
||||
[](https://xi-xu.me/#sponsorships)
|
||||
[](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 社区的宝贵反馈和启发
|
||||
|
||||
## 许可证
|
||||
|
||||
版权所有 © [Xi Xu](https://xi-xu.me)。保留所有权利。
|
||||
|
||||
采用 [BSD 3-Clause](LICENSE) 许可证授权。
|
||||
+313
@@ -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
@@ -1,6 +1,6 @@
|
||||
# Sphinx build outputs
|
||||
_build/
|
||||
_build_simple/
|
||||
_build_*/
|
||||
|
||||
# Sphinx auto-generated files
|
||||
_autosummary/
|
||||
|
||||
+76
@@ -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.
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Vendored
+21
@@ -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
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 513 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 995 KiB |
Vendored
+200
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+150
-24
@@ -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
|
||||
|
||||
[](https://codecov.io/gh/xixu-me/tzst)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://github.com/xixu-me/tzst/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/tzst/)
|
||||
[](https://pypistats.org/packages/tzst)
|
||||
[](https://github.com/xixu-me/tzst/blob/main/LICENSE)
|
||||
[](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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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())
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user