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
6 changes: 5 additions & 1 deletion src/vorta/borg/borg_job.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import json
import logging
import os
Expand All @@ -11,12 +13,14 @@
from datetime import datetime as dt
from subprocess import PIPE, Popen, TimeoutExpired
from threading import Lock
from typing import TYPE_CHECKING, Optional

from PyQt6 import QtCore
from PyQt6.QtCore import QT_TRANSLATE_NOOP
from PyQt6.QtWidgets import QApplication

from vorta import application
if TYPE_CHECKING:
from vorta import application
from vorta.borg.jobs_manager import JobInterface
from vorta.i18n import translate
from vorta.keyring.abc import VortaKeyring
Expand Down
46 changes: 35 additions & 11 deletions src/vorta/views/dialogs/archive/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,23 @@ def parse_json_lines(lines, model: "ExtractTree"):
for item in lines:
path = PurePath(item["path"])

size = item["size"]
mode = item["mode"]
file_type = FileType(mode[0])
user = item["user"]
group = item["group"]
health = item["healthy"]
source_path = item["source"] if "source" in item else None
size = item.get("size", 0)
mode = item.get("mode", "")
file_type = FileType(mode[0]) if mode else FileType.FILE
user = item.get("user", "")
group = item.get("group", "")

# Borg 1 uses boolean "healthy": true/false.
# Borg 2 uses string "health": "healthy"/"broken" from the {health} format placeholder.
health_raw = item.get("healthy")
if health_raw is None:
health_raw = item.get("health", True)
if isinstance(health_raw, str):
health = health_raw.lower() == "healthy"
else:
health = bool(health_raw)

source_path = item.get("source") or None

# For python >= 3.7 this would work
# modified = datetime.fromisoformat(item["mtime"]).ctime()
Expand All @@ -248,9 +258,15 @@ def parse_json_lines(lines, model: "ExtractTree"):
# except ValueError:
# modified = datetime.strptime(item["mtime"], "%Y-%m-%dT%H:%M:%S")

modified = QDateTime.fromString(
item['isomtime' if borg_compat.check('V122') else 'mtime'], Qt.DateFormat.ISODateWithMs
mtime_str = (
item.get("isomtime" if borg_compat.check("V122") else "mtime") or item.get("mtime") or item.get("isomtime")
)
if mtime_str:
modified = QDateTime.fromString(mtime_str, Qt.DateFormat.ISODateWithMs)
if not modified.isValid():
modified = QDateTime.fromString(mtime_str, Qt.DateFormat.ISODate)
else:
modified = QDateTime.currentDateTime()

items.append(
(
Expand All @@ -273,10 +289,18 @@ class ExtractSortProxyModel(FileTreeSortProxyModel):
def choose_data(self, index: QModelIndex):
"""Choose the data of index used for comparison."""
item: ExtractFileItem = index.internalPointer()
if item is None or item.data is None:
return ""
column = index.column()

if item is None or item.data is None:
if column == 1:
return QDateTime()
elif column == 2:
return -1
elif column == 3:
return False
else:
return ""

if column == 0:
# file name
return self.extract_path(index)
Expand Down
87 changes: 86 additions & 1 deletion tests/unit/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import re

import pytest
from PyQt6.QtCore import QModelIndex, Qt
from PyQt6.QtCore import QDateTime, QModelIndex, Qt

import vorta.borg
from vorta.borg.extract import BorgExtractJob, _include_pattern
from vorta.store.models import ArchiveModel, BackupProfileModel
from vorta.views.dialogs.archive.extract import (
ExtractDialog,
ExtractSortProxyModel,
ExtractTree,
FileData,
FileType,
Expand Down Expand Up @@ -267,3 +268,87 @@ def test_extract_prepare_newline_filename(qapp):
assert "+ fm:a/trailing?" in content
assert "+ a/plain" in content
assert "+ a" in content


def test_parser_borg2_format():
"""Test parsing json lines formatted by Borg 2.0 or with health strings."""
lines = [
{
"path": "healthy_file.txt",
"size": 1024,
"mode": "-rw-r--r--",
"user": "root",
"group": "root",
"health": "healthy",
"mtime": "2024-01-01T12:00:00",
},
{
"path": "broken_file.txt",
"size": 512,
"mode": "-rw-r--r--",
"user": "root",
"group": "root",
"health": "broken",
"mtime": "2024-01-01T12:00:00",
},
{
"path": "omitted_health.txt",
"size": 256,
"mode": "-rw-r--r--",
"mtime": "2024-01-01T12:00:00",
},
]

model = ExtractTree()
parse_json_lines(lines, model)

healthy_item = model.getItem(("healthy_file.txt",))
assert healthy_item is not None
assert healthy_item.data.health is True
assert healthy_item.data.size == 1024

broken_item = model.getItem(("broken_file.txt",))
assert broken_item is not None
assert broken_item.data.health is False

omitted_item = model.getItem(("omitted_health.txt",))
assert omitted_item is not None
assert omitted_item.data.health is True
assert omitted_item.data.user == ""
assert omitted_item.data.group == ""


def test_extract_sort_proxy_empty_data():
"""Test sorting proxy handles empty/None items without TypeError across columns."""
proxy = ExtractSortProxyModel()
model = ExtractTree()
proxy.setSourceModel(model)

# Invalid QModelIndex
invalid_index = QModelIndex()
assert proxy.choose_data(invalid_index) == ""

# Test columns with None internalPointer
idx0 = model.createIndex(0, 0, None)
idx1 = model.createIndex(0, 1, None)
idx2 = model.createIndex(0, 2, None)
idx3 = model.createIndex(0, 3, None)

assert proxy.choose_data(idx0) == ""
empty_dt = proxy.choose_data(idx1)
assert isinstance(empty_dt, QDateTime)
assert (empty_dt < QDateTime.currentDateTime()) is True
assert proxy.choose_data(idx2) == -1
assert (-1 < 1024) is True
assert proxy.choose_data(idx3) is False
assert (False < True) is True

# Test columns where item.data is None
fsi = FileSystemItem(["folder"], None)
fsi_idx1 = model.createIndex(0, 1, fsi)
fsi_idx2 = model.createIndex(0, 2, fsi)
fsi_idx3 = model.createIndex(0, 3, fsi)

assert isinstance(proxy.choose_data(fsi_idx1), QDateTime)
assert proxy.choose_data(fsi_idx2) == -1
assert proxy.choose_data(fsi_idx3) is False