Skip to content

Commit 61fe1f3

Browse files
committed
Move slow code to async QThread workers (WIP)
1 parent d721011 commit 61fe1f3

13 files changed

+422
-346
lines changed

src/vorta/store/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
import peewee as pw
1313
from playhouse import signals
1414

15-
from vorta.utils import slugify
16-
from vorta.views.utils import get_exclusion_presets
15+
from vorta.store.utils import slugify
16+
# from vorta.views.utils import get_exclusion_presets
1717

1818
DB = pw.Proxy()
1919
logger = logging.getLogger(__name__)

src/vorta/store/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import unicodedata
2+
import re
3+
4+
def slugify(value):
5+
"""
6+
Converts to lowercase, removes non-word characters (alphanumerics and
7+
underscores) and converts spaces to hyphens. Also strips leading and
8+
trailing whitespace.
9+
10+
Copied from Django.
11+
"""
12+
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
13+
value = re.sub(r'[^\w\s-]', '', value).strip().lower()
14+
return re.sub(r'[-\s]+', '-', value)

src/vorta/utils.py

Lines changed: 18 additions & 244 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,33 @@
11
import argparse
22
import errno
3-
import fnmatch
4-
import getpass
53
import math
4+
import getpass
65
import os
76
import re
87
import socket
98
import sys
10-
import unicodedata
11-
from datetime import datetime as dt
9+
from datetime import datetime as dt, timedelta
1210
from functools import reduce
1311
from typing import Any, Callable, Iterable, List, Optional, Tuple, TypeVar
1412

15-
import psutil
1613
from PyQt6 import QtCore
17-
from PyQt6.QtCore import QFileInfo, QThread, pyqtSignal
18-
from PyQt6.QtWidgets import QApplication, QFileDialog, QSystemTrayIcon
14+
from PyQt6.QtCore import QFileInfo, QThread, pyqtSignal, Qt
15+
from PyQt6.QtWidgets import (QApplication, QFileDialog, QSystemTrayIcon,
16+
QListWidgetItem, QTableWidgetItem)
1917

18+
from vorta.network_status.abc import NetworkStatusMonitor
2019
from vorta.borg._compatibility import BorgCompatibility
2120
from vorta.log import logger
22-
from vorta.network_status.abc import NetworkStatusMonitor
21+
2322

2423
# Used to store whether a user wanted to override the
2524
# default directory for the --development flag
2625
DEFAULT_DIR_FLAG = object()
2726
METRIC_UNITS = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
2827
NONMETRIC_UNITS = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']
29-
30-
borg_compat = BorgCompatibility()
3128
_network_status_monitor = None
3229

33-
34-
class FilePathInfoAsync(QThread):
35-
signal = pyqtSignal(str, str, str)
36-
37-
def __init__(self, path, exclude_patterns_str):
38-
self.path = path
39-
QThread.__init__(self)
40-
self.exiting = False
41-
self.exclude_patterns = []
42-
for _line in (exclude_patterns_str or '').splitlines():
43-
line = _line.strip()
44-
if line != '':
45-
self.exclude_patterns.append(line)
46-
47-
def run(self):
48-
# logger.info("running thread to get path=%s...", self.path)
49-
self.size, self.files_count = get_path_datasize(self.path, self.exclude_patterns)
50-
self.signal.emit(self.path, str(self.size), str(self.files_count))
51-
52-
53-
def normalize_path(path):
54-
"""normalize paths for MacOS (but do nothing on other platforms)"""
55-
# HFS+ converts paths to a canonical form, so users shouldn't be required to enter an exact match.
56-
# Windows and Unix filesystems allow different forms, so users always have to enter an exact match.
57-
return unicodedata.normalize('NFD', path) if sys.platform == 'darwin' else path
58-
59-
60-
# prepare patterns as borg does
61-
# see `FnmatchPattern._prepare` at
62-
# https://github.com/borgbackup/borg/blob/master//src/borg/patterns.py
63-
def prepare_pattern(pattern):
64-
"""Prepare and process fnmatch patterns as borg does"""
65-
if pattern.endswith(os.path.sep):
66-
# trailing sep indicates that the contents should be excluded
67-
# but not the directory it self.
68-
pattern = os.path.normpath(pattern).rstrip(os.path.sep)
69-
pattern += os.path.sep + '*' + os.path.sep
70-
else:
71-
pattern = os.path.normpath(pattern) + os.path.sep + '*'
72-
73-
pattern = pattern.lstrip(os.path.sep) # sep at beginning is removed
74-
return re.compile(fnmatch.translate(pattern))
75-
76-
77-
def match(pattern: re.Pattern, path: str):
78-
"""Check whether a path matches the given pattern."""
79-
path = path.lstrip(os.path.sep) + os.path.sep
80-
return pattern.match(path) is not None
81-
82-
83-
def get_directory_size(dir_path, exclude_patterns):
84-
'''Get number of files only and total size in bytes from a path.
85-
Based off https://stackoverflow.com/a/17936789'''
86-
exclude_patterns = [prepare_pattern(p) for p in exclude_patterns]
87-
88-
data_size_filtered = 0
89-
seen = set()
90-
seen_filtered = set()
91-
92-
for dir_path, subdirectories, file_names in os.walk(dir_path, topdown=True):
93-
is_excluded = False
94-
for pattern in exclude_patterns:
95-
if match(pattern, dir_path):
96-
is_excluded = True
97-
break
98-
99-
if is_excluded:
100-
subdirectories.clear() # so that os.walk won't walk them
101-
continue
102-
103-
for file_name in file_names:
104-
file_path = os.path.join(dir_path, file_name)
105-
106-
# Ignore symbolic links, since borg doesn't follow them
107-
if os.path.islink(file_path):
108-
continue
109-
110-
is_excluded = False
111-
for pattern in exclude_patterns:
112-
if match(pattern, file_path):
113-
is_excluded = True
114-
break
115-
116-
try:
117-
stat = os.stat(file_path)
118-
if stat.st_ino not in seen: # Visit each file only once
119-
# this won't add the size of a hardlinked file
120-
seen.add(stat.st_ino)
121-
if not is_excluded:
122-
data_size_filtered += stat.st_size
123-
seen_filtered.add(stat.st_ino)
124-
except (FileNotFoundError, PermissionError):
125-
continue
126-
127-
files_count_filtered = len(seen_filtered)
128-
129-
return data_size_filtered, files_count_filtered
130-
131-
132-
def get_network_status_monitor():
133-
global _network_status_monitor
134-
if _network_status_monitor is None:
135-
_network_status_monitor = NetworkStatusMonitor.get_network_status_monitor()
136-
logger.info(
137-
'Using %s NetworkStatusMonitor implementation.',
138-
_network_status_monitor.__class__.__name__,
139-
)
140-
return _network_status_monitor
141-
142-
143-
def get_path_datasize(path, exclude_patterns):
144-
file_info = QFileInfo(path)
145-
146-
if file_info.isDir():
147-
data_size, files_count = get_directory_size(file_info.absoluteFilePath(), exclude_patterns)
148-
else:
149-
data_size = file_info.size()
150-
files_count = 1
151-
152-
return data_size, files_count
30+
borg_compat = BorgCompatibility()
15331

15432

15533
def nested_dict():
@@ -220,22 +98,6 @@ def get_private_keys() -> List[str]:
22098
return available_private_keys
22199

222100

223-
def sort_sizes(size_list):
224-
"""Sorts sizes with extensions. Assumes that size is already in largest unit possible"""
225-
final_list = []
226-
for suffix in [" B", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"]:
227-
sub_list = [
228-
float(size[: -len(suffix)])
229-
for size in size_list
230-
if size.endswith(suffix) and size[: -len(suffix)][-1].isnumeric()
231-
]
232-
sub_list.sort()
233-
final_list += [(str(size) + suffix) for size in sub_list]
234-
# Skip additional loops
235-
if len(final_list) == len(size_list):
236-
break
237-
return final_list
238-
239101

240102
Number = TypeVar("Number", int, float)
241103

@@ -244,6 +106,16 @@ def clamp(n: Number, min_: Number, max_: Number) -> Number:
244106
"""Restrict the number n inside a range"""
245107
return min(max_, max(n, min_))
246108

109+
def get_network_status_monitor():
110+
global _network_status_monitor
111+
if _network_status_monitor is None:
112+
_network_status_monitor = NetworkStatusMonitor.get_network_status_monitor()
113+
logger.info(
114+
'Using %s NetworkStatusMonitor implementation.',
115+
_network_status_monitor.__class__.__name__,
116+
)
117+
return _network_status_monitor
118+
247119

248120
def find_best_unit_for_sizes(sizes: Iterable[int], metric: bool = True, precision: int = 1) -> int:
249121
"""
@@ -303,37 +175,6 @@ def get_asset(path):
303175
return os.path.join(bundle_dir, path)
304176

305177

306-
def get_sorted_wifis(profile):
307-
"""
308-
Get Wifi networks known to the OS (only current one on macOS) and
309-
merge with networks from other profiles. Update last connected time.
310-
"""
311-
312-
from vorta.store.models import WifiSettingModel
313-
314-
# Pull networks known to OS and all other backup profiles
315-
system_wifis = get_network_status_monitor().get_known_wifis()
316-
from_other_profiles = WifiSettingModel.select().where(WifiSettingModel.profile != profile.id).execute()
317-
318-
for wifi in list(from_other_profiles) + system_wifis:
319-
db_wifi, created = WifiSettingModel.get_or_create(
320-
ssid=wifi.ssid,
321-
profile=profile.id,
322-
defaults={'last_connected': wifi.last_connected, 'allowed': True},
323-
)
324-
325-
# Update last connected time
326-
if not created and db_wifi.last_connected != wifi.last_connected:
327-
db_wifi.last_connected = wifi.last_connected
328-
db_wifi.save()
329-
330-
# Finally return list of networks and settings for that profile
331-
return (
332-
WifiSettingModel.select()
333-
.where(WifiSettingModel.profile == profile.id)
334-
.order_by(-WifiSettingModel.last_connected)
335-
)
336-
337178

338179
def parse_args():
339180
parser = argparse.ArgumentParser(description='Vorta Backup GUI for Borg.')
@@ -368,19 +209,6 @@ def parse_args():
368209
return parser.parse_known_args()[0]
369210

370211

371-
def slugify(value):
372-
"""
373-
Converts to lowercase, removes non-word characters (alphanumerics and
374-
underscores) and converts spaces to hyphens. Also strips leading and
375-
trailing whitespace.
376-
377-
Copied from Django.
378-
"""
379-
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
380-
value = re.sub(r'[^\w\s-]', '', value).strip().lower()
381-
return re.sub(r'[-\s]+', '-', value)
382-
383-
384212
def uses_dark_mode():
385213
"""
386214
This function detects if we are running in dark mode (e.g. macOS dark mode).
@@ -431,60 +259,6 @@ def format_archive_name(profile, archive_name_tpl):
431259
SHELL_PATTERN_ELEMENT = re.compile(r'([?\[\]*])')
432260

433261

434-
def get_mount_points(repo_url):
435-
mount_points = {}
436-
repo_mounts = []
437-
for proc in psutil.process_iter():
438-
try:
439-
name = proc.name()
440-
if name == 'borg' or name.startswith('python'):
441-
if 'mount' not in proc.cmdline():
442-
continue
443-
444-
if borg_compat.check('V2'):
445-
# command line syntax:
446-
# `borg mount -r <repo> <mountpoint> <path> (-a <archive_pattern>)`
447-
cmd = proc.cmdline()
448-
if repo_url in cmd:
449-
i = cmd.index(repo_url)
450-
if len(cmd) > i + 1:
451-
mount_point = cmd[i + 1]
452-
453-
# Archive mount?
454-
ao = '-a' in cmd
455-
if ao or '--match-archives' in cmd:
456-
i = cmd.index('-a' if ao else '--match-archives')
457-
if len(cmd) >= i + 1 and not SHELL_PATTERN_ELEMENT.search(cmd[i + 1]):
458-
mount_points[mount_point] = cmd[i + 1]
459-
else:
460-
repo_mounts.append(mount_point)
461-
else:
462-
for idx, parameter in enumerate(proc.cmdline()):
463-
if parameter.startswith(repo_url):
464-
# mount from this repo
465-
466-
# The borg mount command specifies that the mount_point
467-
# parameter comes after the archive name
468-
if len(proc.cmdline()) > idx + 1:
469-
mount_point = proc.cmdline()[idx + 1]
470-
471-
# archive or full mount?
472-
if parameter[len(repo_url) :].startswith('::'):
473-
archive_name = parameter[len(repo_url) + 2 :]
474-
mount_points[archive_name] = mount_point
475-
break
476-
else:
477-
# repo mount point
478-
repo_mounts.append(mount_point)
479-
480-
except (psutil.ZombieProcess, psutil.AccessDenied, psutil.NoSuchProcess):
481-
# Getting process details may fail (e.g. zombie process on macOS)
482-
# or because the process is owned by another user.
483-
# Also see https://github.com/giampaolo/psutil/issues/783
484-
continue
485-
486-
return mount_points, repo_mounts
487-
488262

489263
def is_system_tray_available():
490264
app = QApplication.instance()

0 commit comments

Comments
 (0)