From 07eeef69b34021e765ad9631042dd403af1cb1e6 Mon Sep 17 00:00:00 2001 From: Seraph91P Date: Wed, 4 Mar 2026 16:06:48 +0100 Subject: [PATCH] fix: derive tar mode from original filename when browsing encrypted backups The temp file for decrypted archives was always created with .tar.gz suffix, causing tarfile.open to fail with 'not a gzip file' when the backup was uncompressed (.tar.enc). - Derive temp suffix from original archive name (strip .enc) - Make _get_tar_mode use 'r:*' auto-detect as fallback - Fix encryption.py list_backup_contents: same suffix bug + use 'tar -tf' (auto-detect) instead of 'tar -tzf' (gzip-only) --- backend/app/api/backups.py | 24 ++++++++++++++++++++---- backend/app/encryption.py | 18 ++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/backend/app/api/backups.py b/backend/app/api/backups.py index a587ba0..149101e 100644 --- a/backend/app/api/backups.py +++ b/backend/app/api/backups.py @@ -1281,8 +1281,19 @@ def _try_key_candidate(candidate: str | None) -> str | None: detail="Encryption key file (.key) not found on disk or remote storage.", ) - # Decrypt to temp file - temp_fd, temp_name = tempfile.mkstemp(suffix=".tar.gz") + # Decrypt to temp file — derive suffix from the original archive name + # so _get_tar_mode picks the correct open mode (.tar vs .tar.gz etc.). + original_name = os.path.basename(backup.file_path or "") + if original_name.endswith(".enc"): + original_name = original_name[: -len(".enc")] + # Extract suffix(es) like .tar.gz, .tar, .tar.zst etc. + if ".tar." in original_name: + tar_suffix = original_name[original_name.index(".tar") :] + elif original_name.endswith(".tar"): + tar_suffix = ".tar" + else: + tar_suffix = ".tar.gz" # safe fallback + temp_fd, temp_name = tempfile.mkstemp(suffix=tar_suffix) os.close(temp_fd) temp_path = Path(temp_name) @@ -1309,17 +1320,22 @@ def _try_key_candidate(candidate: str | None) -> str | None: def _get_tar_mode(archive_path: str) -> str: - """Determine tarfile open mode from file extension.""" + """Determine tarfile open mode from file extension, with auto-detect fallback.""" if archive_path.endswith(".tar.gz") or archive_path.endswith(".tgz"): return "r:gz" elif archive_path.endswith(".tar.bz2"): return "r:bz2" elif archive_path.endswith(".tar.xz"): return "r:xz" + elif archive_path.endswith(".tar.zst") or archive_path.endswith(".tar.zstd"): + # zstd is not natively supported by tarfile; fall through to raw tar + # after external decompression, or handle separately + return "r:" elif archive_path.endswith(".tar"): return "r:" else: - raise HTTPException(status_code=400, detail="Unsupported archive format") + # Auto-detect: let tarfile figure out the compression + return "r:*" class BrowseEncryptedRequest(BaseModel): diff --git a/backend/app/encryption.py b/backend/app/encryption.py index e0247a8..9da9e47 100644 --- a/backend/app/encryption.py +++ b/backend/app/encryption.py @@ -453,18 +453,28 @@ async def list_backup_contents( Returns list of files with name, size, and type. """ - # Create temp file for decrypted backup - with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as f: + # Derive temp suffix from original archive name (strip .enc) + original_name = encrypted_path.name + if original_name.endswith(".enc"): + original_name = original_name[: -len(".enc")] + if ".tar." in original_name: + tar_suffix = original_name[original_name.index(".tar") :] + elif original_name.endswith(".tar"): + tar_suffix = ".tar" + else: + tar_suffix = ".tar.gz" + + with tempfile.NamedTemporaryFile(suffix=tar_suffix, delete=False) as f: temp_path = Path(f.name) try: # Decrypt to temp await decrypt_backup(encrypted_path, key_path, private_key, temp_path) - # List tar contents + # List tar contents — use -tf (no gzip flag) and let tar auto-detect process = await asyncio.create_subprocess_exec( "tar", - "-tzf", + "-tf", str(temp_path), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,