Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions backend/app/api/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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):
Expand Down
18 changes: 14 additions & 4 deletions backend/app/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading