11import argparse
22import errno
3- import fnmatch
4- import getpass
53import math
4+ import getpass
65import os
76import re
87import socket
98import sys
10- import unicodedata
11- from datetime import datetime as dt
9+ from datetime import datetime as dt , timedelta
1210from functools import reduce
1311from typing import Any , Callable , Iterable , List , Optional , Tuple , TypeVar
1412
15- import psutil
1613from 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
2019from vorta .borg ._compatibility import BorgCompatibility
2120from 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
2625DEFAULT_DIR_FLAG = object ()
2726METRIC_UNITS = ['' , 'K' , 'M' , 'G' , 'T' , 'P' , 'E' , 'Z' , 'Y' ]
2827NONMETRIC_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
15533def 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
240102Number = 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
248120def 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
338179def 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-
384212def 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):
431259SHELL_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
489263def is_system_tray_available ():
490264 app = QApplication .instance ()
0 commit comments