dar-backup 0.5.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/.darrc +108 -0
- dar_backup/cleanup.py +197 -0
- dar_backup/config_settings.py +65 -0
- dar_backup/dar_backup.py +671 -0
- dar_backup/util.py +213 -0
- dar_backup-0.5.8.dist-info/METADATA +356 -0
- dar_backup-0.5.8.dist-info/RECORD +10 -0
- dar_backup-0.5.8.dist-info/WHEEL +5 -0
- dar_backup-0.5.8.dist-info/entry_points.txt +3 -0
- dar_backup-0.5.8.dist-info/top_level.txt +1 -0
dar_backup/.darrc
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Default configuration file for dar
|
|
2
|
+
# Place this file in the user's home directory as .darrc or specify it with the -B option
|
|
3
|
+
|
|
4
|
+
extract:
|
|
5
|
+
# don't restore File Specific Attributes
|
|
6
|
+
#--fsa-scope none
|
|
7
|
+
|
|
8
|
+
# ignore owner, useful when used by a non-privileged user
|
|
9
|
+
--comparison-field=ignore-owner
|
|
10
|
+
|
|
11
|
+
# First setting case insensitive mode on:
|
|
12
|
+
-an
|
|
13
|
+
-ag
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Exclude specific file types from compression
|
|
17
|
+
compress-exclusion:
|
|
18
|
+
-Z *.gz
|
|
19
|
+
-Z *.bz2
|
|
20
|
+
-Z *.xz
|
|
21
|
+
-Z *.zip
|
|
22
|
+
-Z *.rar
|
|
23
|
+
-Z *.7z
|
|
24
|
+
-Z *.tar
|
|
25
|
+
-Z *.tgz
|
|
26
|
+
-Z *.tbz2
|
|
27
|
+
-Z *.txz
|
|
28
|
+
# Exclude common image file types from compression
|
|
29
|
+
-Z *.jpg
|
|
30
|
+
-Z *.jpeg
|
|
31
|
+
-Z *.png
|
|
32
|
+
-Z *.gif
|
|
33
|
+
-Z *.bmp
|
|
34
|
+
-Z *.tiff
|
|
35
|
+
-Z *.svg
|
|
36
|
+
# Exclude common movie file types from compression
|
|
37
|
+
-Z *.mp4
|
|
38
|
+
-Z *.avi
|
|
39
|
+
-Z *.mkv
|
|
40
|
+
-Z *.mov
|
|
41
|
+
-Z *.wmv
|
|
42
|
+
-Z *.flv
|
|
43
|
+
-Z *.mpeg
|
|
44
|
+
-Z *.mpg
|
|
45
|
+
|
|
46
|
+
# These are zip files. Not all are compressed, but considering that they can
|
|
47
|
+
# get quite large it is probably more prudent to leave this uncommented.
|
|
48
|
+
-Z "*.pk3"
|
|
49
|
+
-Z "*.zip"
|
|
50
|
+
# You can get better compression on these files, but then you should be
|
|
51
|
+
# de/recompressing with an actual program, not dar.
|
|
52
|
+
-Z "*.lz4"
|
|
53
|
+
-Z "*.zoo"
|
|
54
|
+
|
|
55
|
+
# Other, in alphabetical order.
|
|
56
|
+
-Z "*.Po"
|
|
57
|
+
-Z "*.aar"
|
|
58
|
+
-Z "*.bx"
|
|
59
|
+
-Z "*.chm"
|
|
60
|
+
-Z "*.doc"
|
|
61
|
+
-Z "*.epub"
|
|
62
|
+
-Z "*.f3d"
|
|
63
|
+
-Z "*.gpg"
|
|
64
|
+
-Z "*.htmlz"
|
|
65
|
+
-Z "*.iix"
|
|
66
|
+
-Z "*.iso"
|
|
67
|
+
-Z "*.jin"
|
|
68
|
+
-Z "*.ods"
|
|
69
|
+
-Z "*.odt"
|
|
70
|
+
-Z "*.ser"
|
|
71
|
+
-Z "*.svgz"
|
|
72
|
+
-Z "*.swx"
|
|
73
|
+
-Z "*.sxi"
|
|
74
|
+
-Z "*.whl"
|
|
75
|
+
-Z "*.wings"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Dar archives (may be compressed).
|
|
79
|
+
-Z "*.dar"
|
|
80
|
+
|
|
81
|
+
# Now we swap back to case sensitive mode for masks which is the default
|
|
82
|
+
# mode:
|
|
83
|
+
-acase
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
##############################################################
|
|
87
|
+
# target: verbose
|
|
88
|
+
# remove comments belov for dar being more verbose
|
|
89
|
+
verbose:
|
|
90
|
+
|
|
91
|
+
# -vt show files teated due to filtering inclusion or no filtering at all
|
|
92
|
+
# -vt
|
|
93
|
+
|
|
94
|
+
# -vs show skipped files du to exclusion
|
|
95
|
+
# -vs
|
|
96
|
+
|
|
97
|
+
# -vd show diretory currently being processed
|
|
98
|
+
# -vd
|
|
99
|
+
|
|
100
|
+
# -vm show detailed messages, not related to files and directories
|
|
101
|
+
# -vm
|
|
102
|
+
|
|
103
|
+
# -vf show summary of each treated directory, including average compression
|
|
104
|
+
# -vf
|
|
105
|
+
|
|
106
|
+
# -va equivalent to "-vm -vs -vt"
|
|
107
|
+
# -va
|
|
108
|
+
|
dar_backup/cleanup.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
cleanup.py source code is here: https://github.com/per2jensen/dar-backup
|
|
5
|
+
|
|
6
|
+
Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
7
|
+
|
|
8
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW,
|
|
9
|
+
not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See section 15 and section 16 in the supplied "LICENSE" file
|
|
11
|
+
|
|
12
|
+
This script removes old DIFF and INCR archives + accompanying .par2 files according to the
|
|
13
|
+
[AGE] settings in the configuration file.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from datetime import datetime, timedelta
|
|
24
|
+
from time import time
|
|
25
|
+
|
|
26
|
+
from dar_backup.config_settings import ConfigSettings
|
|
27
|
+
from dar_backup.util import extract_error_lines
|
|
28
|
+
from dar_backup.util import list_backups
|
|
29
|
+
from dar_backup.util import setup_logging
|
|
30
|
+
|
|
31
|
+
VERSION = "aplha-0.5"
|
|
32
|
+
|
|
33
|
+
logger = None
|
|
34
|
+
|
|
35
|
+
def delete_old_backups(backup_dir, age, backup_type, backup_definition=None):
|
|
36
|
+
"""
|
|
37
|
+
Delete backups older than the specified age in days.
|
|
38
|
+
Only .dar and .par2 files are considered for deletion.
|
|
39
|
+
"""
|
|
40
|
+
logger.info(f"Deleting {backup_type} backups older than {age} days in {backup_dir} for backup definition: {backup_definition}")
|
|
41
|
+
|
|
42
|
+
if backup_type not in ['DIFF', 'INCR']:
|
|
43
|
+
logger.error(f"Invalid backup type: {backup_type}")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
now = datetime.now()
|
|
47
|
+
cutoff_date = now - timedelta(days=age)
|
|
48
|
+
|
|
49
|
+
for filename in sorted(os.listdir(backup_dir)):
|
|
50
|
+
if not (filename.endswith('.dar') or filename.endswith('.par2')):
|
|
51
|
+
continue
|
|
52
|
+
if backup_definition and not filename.startswith(backup_definition):
|
|
53
|
+
continue
|
|
54
|
+
if backup_type in filename:
|
|
55
|
+
try:
|
|
56
|
+
date_str = filename.split(f"_{backup_type}_")[1].split('.')[0]
|
|
57
|
+
file_date = datetime.strptime(date_str, '%Y-%m-%d')
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Error parsing date from filename {filename}: {e}")
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
if file_date < cutoff_date:
|
|
63
|
+
file_path = os.path.join(backup_dir, filename)
|
|
64
|
+
try:
|
|
65
|
+
os.remove(file_path)
|
|
66
|
+
logger.info(f"Deleted {backup_type} backup: {file_path}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Error deleting file {file_path}: {e}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def delete_archives(backup_dir, archive_name):
|
|
72
|
+
"""
|
|
73
|
+
Delete all .dar and .par2 files in the backup directory for the given archive name.
|
|
74
|
+
|
|
75
|
+
This function will delete any type of archive, including FULL.
|
|
76
|
+
"""
|
|
77
|
+
logger.info(f"Deleting all .dar and .par2 files for archive: `{archive_name}`")
|
|
78
|
+
# Regex to match the archive files according to the naming convention
|
|
79
|
+
archive_regex = re.compile(rf"^{re.escape(archive_name)}\.[0-9]+\.dar$")
|
|
80
|
+
|
|
81
|
+
# Delete the specified .dar files according to the naming convention
|
|
82
|
+
files_deleted = False
|
|
83
|
+
for filename in sorted(os.listdir(backup_dir)):
|
|
84
|
+
if archive_regex.match(filename):
|
|
85
|
+
file_path = os.path.join(backup_dir, filename)
|
|
86
|
+
try:
|
|
87
|
+
os.remove(file_path)
|
|
88
|
+
logger.info(f"Deleted archive slice: {file_path}")
|
|
89
|
+
files_deleted = True
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Error deleting archive slice {file_path}: {e}")
|
|
92
|
+
|
|
93
|
+
if not files_deleted:
|
|
94
|
+
logger.info("No .dar files matched the regex for deletion.")
|
|
95
|
+
|
|
96
|
+
# Delete associated .par2 files
|
|
97
|
+
par2_regex = re.compile(rf"^{re.escape(archive_name)}\.[0-9]+\.dar.*\.par2$")
|
|
98
|
+
files_deleted = False
|
|
99
|
+
for filename in sorted(os.listdir(backup_dir)):
|
|
100
|
+
if par2_regex.match(filename):
|
|
101
|
+
file_path = os.path.join(backup_dir, filename)
|
|
102
|
+
try:
|
|
103
|
+
os.remove(file_path)
|
|
104
|
+
logger.info(f"Deleted PAR2 file: {file_path}")
|
|
105
|
+
files_deleted = True
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Error deleting PAR2 file {file_path}: {e}")
|
|
108
|
+
|
|
109
|
+
if not files_deleted:
|
|
110
|
+
logger.info("No .par2 matched the regex for deletion.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def show_version():
|
|
114
|
+
script_name = os.path.basename(sys.argv[0])
|
|
115
|
+
print(f"{script_name} {VERSION}")
|
|
116
|
+
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
117
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
118
|
+
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
global logger
|
|
124
|
+
|
|
125
|
+
parser = argparse.ArgumentParser(description="Cleanup old backup files.")
|
|
126
|
+
parser.add_argument('--backup-definition', '-d', help="Specific backup definition to clean.")
|
|
127
|
+
parser.add_argument('--config-file', '-c', type=str, help="Path to 'dar-backup.conf'", default='~/.config/dar-backup/dar-backup.conf')
|
|
128
|
+
parser.add_argument('--version', '-v', action='store_true', help="Show version information.")
|
|
129
|
+
parser.add_argument('--alternate-archive-dir', type=str, help="Cleanup in this directory instead of the default one.")
|
|
130
|
+
parser.add_argument('--cleanup-specific-archive', type=str, help="Force delete all .dar and .par2 files in the backup directory for given archive name")
|
|
131
|
+
parser.add_argument('--list', action='store_true', help="List available archives.")
|
|
132
|
+
parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
|
|
133
|
+
args = parser.parse_args()
|
|
134
|
+
|
|
135
|
+
args.config_file = os.path.expanduser(args.config_file)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if args.version:
|
|
139
|
+
show_version()
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
|
|
142
|
+
config_settings = ConfigSettings(args.config_file)
|
|
143
|
+
|
|
144
|
+
start_time=int(time())
|
|
145
|
+
logger = setup_logging(config_settings.logfile_location, logging.INFO)
|
|
146
|
+
logger.info(f"=====================================")
|
|
147
|
+
logger.info(f"cleanup.py started, version: {VERSION}")
|
|
148
|
+
logger.info(f"START TIME: {start_time}")
|
|
149
|
+
logger.debug(f"`args`:\n{args}")
|
|
150
|
+
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
151
|
+
|
|
152
|
+
current_dir = os.path.normpath(os.path.dirname(__file__))
|
|
153
|
+
args.verbose and (print(f"Current directory: {current_dir}"))
|
|
154
|
+
args.verbose and (print(f"Config file: {args.config_file}"))
|
|
155
|
+
args.verbose and (print(f"Backup dir: {config_settings.backup_dir}"))
|
|
156
|
+
args.verbose and (print(f"Logfile location: {config_settings.logfile_location}"))
|
|
157
|
+
args.verbose and (print(f"--alternate-archive-dir: {args.alternate_archive_dir}"))
|
|
158
|
+
args.verbose and (print(f"--cleanup-specific-archive: {args.cleanup_specific_archive}"))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if args.alternate_archive_dir:
|
|
162
|
+
config_settings.backup_dir = args.alternate_archive_dir
|
|
163
|
+
|
|
164
|
+
if args.cleanup_specific_archive:
|
|
165
|
+
delete_archives(config_settings.backup_dir, args.cleanup_specific_archive)
|
|
166
|
+
sys.exit(0)
|
|
167
|
+
elif args.list:
|
|
168
|
+
list_backups(config_settings.backup_dir, args.backup_definition)
|
|
169
|
+
else:
|
|
170
|
+
backup_definitions = []
|
|
171
|
+
if args.backup_definition:
|
|
172
|
+
backup_definitions.append(args.backup_definition)
|
|
173
|
+
else:
|
|
174
|
+
for root, _, files in os.walk(config_settings.backup_d_dir):
|
|
175
|
+
for file in files:
|
|
176
|
+
backup_definitions.append(file.split('.')[0])
|
|
177
|
+
|
|
178
|
+
for definition in backup_definitions:
|
|
179
|
+
delete_old_backups(config_settings.backup_dir, config_settings.diff_age, 'DIFF', definition)
|
|
180
|
+
delete_old_backups(config_settings.backup_dir, config_settings.incr_age, 'INCR', definition)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
end_time=int(time())
|
|
184
|
+
logger.info(f"END TIME: {end_time}")
|
|
185
|
+
|
|
186
|
+
error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
|
|
187
|
+
if len(error_lines) > 0:
|
|
188
|
+
args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
|
|
189
|
+
for line in error_lines:
|
|
190
|
+
print(line)
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
else:
|
|
193
|
+
args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
|
|
194
|
+
sys.exit(0)
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
main()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import configparser
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ConfigSettings:
|
|
9
|
+
"""
|
|
10
|
+
A dataclass for holding configuration settings, initialized from a configuration file.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
logfile_location (str): The location of the log file.
|
|
14
|
+
max_size_verification_mb (int): The maximum size for verification in megabytes.
|
|
15
|
+
min_size_verification_mb (int): The minimum size for verification in megabytes.
|
|
16
|
+
no_files_verification (int): The number of files for verification.
|
|
17
|
+
backup_dir (str): The directory for backups.
|
|
18
|
+
test_restore_dir (str): The directory for test restores.
|
|
19
|
+
backup_d_dir (str): The directory for backup.d.
|
|
20
|
+
diff_age (int): The age for differential backups before deletion.
|
|
21
|
+
incr_age (int): The age for incremental backups before deletion.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config_file: str):
|
|
25
|
+
"""
|
|
26
|
+
Initializes the ConfigSettings instance by reading the specified configuration file.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config_file (str): The path to the configuration file.
|
|
30
|
+
"""
|
|
31
|
+
self.config = configparser.ConfigParser()
|
|
32
|
+
try:
|
|
33
|
+
self.config.read(config_file)
|
|
34
|
+
self.logfile_location = self.config['MISC']['LOGFILE_LOCATION']
|
|
35
|
+
self.max_size_verification_mb = int(self.config['MISC']['MAX_SIZE_VERIFICATION_MB'])
|
|
36
|
+
self.min_size_verification_mb = int(self.config['MISC']['MIN_SIZE_VERIFICATION_MB'])
|
|
37
|
+
self.no_files_verification = int(self.config['MISC']['NO_FILES_VERIFICATION'])
|
|
38
|
+
self.backup_dir = self.config['DIRECTORIES']['BACKUP_DIR']
|
|
39
|
+
self.test_restore_dir = self.config['DIRECTORIES']['TEST_RESTORE_DIR']
|
|
40
|
+
self.backup_d_dir = self.config['DIRECTORIES']['BACKUP.D_DIR']
|
|
41
|
+
self.diff_age = int(self.config['AGE']['DIFF_AGE'])
|
|
42
|
+
self.incr_age = int(self.config['AGE']['INCR_AGE'])
|
|
43
|
+
self.error_correction_percent = int(self.config['PAR2']['ERROR_CORRECTION_PERCENT'])
|
|
44
|
+
|
|
45
|
+
# Ensure the directories exist
|
|
46
|
+
Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
|
|
47
|
+
Path(self.test_restore_dir).mkdir(parents=True, exist_ok=True)
|
|
48
|
+
Path(self.backup_d_dir).mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
except FileNotFoundError as e:
|
|
51
|
+
logging.error(f"Configuration file not found: {config_file}")
|
|
52
|
+
logging.error(f"Error details: {e}")
|
|
53
|
+
sys.exit("Error: Configuration file not found.")
|
|
54
|
+
except PermissionError as e:
|
|
55
|
+
logging.error(f"Permission error while reading config file {config_file}")
|
|
56
|
+
logging.error(f"Error details: {e}")
|
|
57
|
+
sys.exit("Error: Permission error while reading config file.")
|
|
58
|
+
except KeyError as e:
|
|
59
|
+
logging.error(f"Missing mandatory configuration key: {e}")
|
|
60
|
+
logging.error(f"Error details: {e}")
|
|
61
|
+
sys.exit(f"Error: Missing mandatory configuration key: {e}.")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logging.exception(f"Unexpected error reading config file {config_file}: {e}")
|
|
64
|
+
logging.error(f"Error details: {e}")
|
|
65
|
+
sys.exit(f"Unexpected error reading config file: {e}.")
|