1
1
import argparse
2
2
import errno
3
- import fnmatch
4
- import getpass
5
3
import math
4
+ import getpass
6
5
import os
7
6
import re
8
7
import socket
9
8
import sys
10
- import unicodedata
11
- from datetime import datetime as dt
9
+ from datetime import datetime as dt , timedelta
12
10
from functools import reduce
13
11
from typing import Any , Callable , Iterable , List , Optional , Tuple , TypeVar
14
12
15
- import psutil
16
13
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 )
19
17
18
+ from vorta .network_status .abc import NetworkStatusMonitor
20
19
from vorta .borg ._compatibility import BorgCompatibility
21
20
from vorta .log import logger
22
- from vorta . network_status . abc import NetworkStatusMonitor
21
+
23
22
24
23
# Used to store whether a user wanted to override the
25
24
# default directory for the --development flag
26
25
DEFAULT_DIR_FLAG = object ()
27
26
METRIC_UNITS = ['' , 'K' , 'M' , 'G' , 'T' , 'P' , 'E' , 'Z' , 'Y' ]
28
27
NONMETRIC_UNITS = ['' , 'Ki' , 'Mi' , 'Gi' , 'Ti' , 'Pi' , 'Ei' , 'Zi' , 'Yi' ]
29
-
30
- borg_compat = BorgCompatibility ()
31
28
_network_status_monitor = None
32
29
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 ()
153
31
154
32
155
33
def nested_dict ():
@@ -220,22 +98,6 @@ def get_private_keys() -> List[str]:
220
98
return available_private_keys
221
99
222
100
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
-
239
101
240
102
Number = TypeVar ("Number" , int , float )
241
103
@@ -244,6 +106,16 @@ def clamp(n: Number, min_: Number, max_: Number) -> Number:
244
106
"""Restrict the number n inside a range"""
245
107
return min (max_ , max (n , min_ ))
246
108
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
+
247
119
248
120
def find_best_unit_for_sizes (sizes : Iterable [int ], metric : bool = True , precision : int = 1 ) -> int :
249
121
"""
@@ -303,37 +175,6 @@ def get_asset(path):
303
175
return os .path .join (bundle_dir , path )
304
176
305
177
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
-
337
178
338
179
def parse_args ():
339
180
parser = argparse .ArgumentParser (description = 'Vorta Backup GUI for Borg.' )
@@ -368,19 +209,6 @@ def parse_args():
368
209
return parser .parse_known_args ()[0 ]
369
210
370
211
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
-
384
212
def uses_dark_mode ():
385
213
"""
386
214
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):
431
259
SHELL_PATTERN_ELEMENT = re .compile (r'([?\[\]*])' )
432
260
433
261
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
-
488
262
489
263
def is_system_tray_available ():
490
264
app = QApplication .instance ()
0 commit comments