Skip to content

Commit 0e533c0

Browse files
committed
Merge branch 'release_0.4.1'
2 parents 34010aa + 6c18835 commit 0e533c0

File tree

13 files changed

+272
-31
lines changed

13 files changed

+272
-31
lines changed

docs/actions/compress.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ This compression algorithm is fast and good-balanced in resources consumption. C
165165
| Name | Type | Description | Optional |
166166
|------|------|-------------|----------|
167167
| `compressionLevel` | `ìnt` | Compression level from 1 to 19 (default 3) | Yes |
168+
| `cpus` | `int` | Uses this amount of cores to compress the data | Yes |
168169

169170
**Description**
170171

docs/actions/encrypt.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
|------|------|-------------|----------|
1515
| `passphrase` | `str` | Defines a passphrase that will be used to decrypt the data | Yes |
1616
| `recipients` | `List[str]` | List of emails that will be able to decrypt the data | Yes |
17-
| `cipherAlgorithm` | `str` | Changes the cypher algorithm | Yes |
18-
| `compressAlgorithm` | `str` | Changes the compress algorithm | Yes |
17+
| `cipherAlgorithm` | `str` | Changes the cipher algorithm | Yes |
18+
| `cypherAlgorithm` | `str` | Alias for `cypherAlgorithm` | Yes |
19+
| `compressAlgorithm` | `str` | Selects the compress algorithm, if not set, then will be uncompressed (default) | Yes |
1920

2021
At least one recipient must be defined. If no recipients are defined, a passphrase must be provided. Both can be defined.
2122

docs/configuration.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ This allows you to auto-complete with the elements available in the configuratio
4646
"cloud": {
4747
"compression": {
4848
"method": "gz|xz|bz2|br|zst",
49-
"level": 8
49+
"level": 8,
50+
"cpus": 1
5051
},
51-
"cypher": {
52+
"encrypt": {
5253
"strategy": "gpg-keys|gpg-passphrase",
5354
"passphrase": "If using gpg-passphrase, this will be used as passphrase for the cypher",
5455
"keys": "If using gpg-keys, this will be used as recipients option for the gpg cypher (emails)",
@@ -114,8 +115,9 @@ cloud:
114115
compression:
115116
method: gz|xz|bz2|br|zst
116117
level: 8
118+
cpus: 1
117119

118-
cypher:
120+
encrypt:
119121
strategy: gpg-keys|gpg-passphrase
120122
passphrase: If using gpg-passphrase, this will be used as passphrase for the cypher
121123
keys: If using gpg-keys, this will be used as recipients option for the gpg cypher (emails)
@@ -217,7 +219,14 @@ In general, a lot of Linux distributions includes these commands, as well as in
217219

218220
The compression level. Higher values indicates better but slower compressions. Values accepted for `gzip` are from 1 to 9. Values accepted for `xz` are from 0 to 9 (by default is 6, 7-9 are not recommended).
219221

220-
### cypher
222+
#### cpus
223+
224+
The number of cpus/threads to use when compressing. By default, will use only one thread to compress, but this can be changed to any number. This value is ignored by some compression algorithms.
225+
226+
!!! Note "Threads and compression algorithms"
227+
Currently, `xz` and `zst` supports `cpus` setting, the rest will always use 1 thread.
228+
229+
### encrypt
221230

222231
If defined, when backups are uploaded to a storage provider, folders will be encrypted using this configuration.
223232

mdbackup/_commands/upload.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pathlib import Path
44
from typing import Iterable, List, Tuple
55

6-
from ..archive import archive_folder
6+
from ..archive import archive_file, archive_folder
77
from ..config import Config
88
from ..hooks import run_hook
99
from ..storage import create_storage_instance
@@ -24,17 +24,29 @@ def _process_results(config: Config, backup: Path, items: Iterable[Tuple[Path, d
2424

2525
# Compress directories
2626
for item, task in items:
27-
# Compress if it is a directory
2827
if item.is_dir():
28+
# Compress/encrypt if it is a directory
2929
filename = archive_folder(backup, item, config.cloud)
3030
final_items.append(backup / filename)
3131
items_to_remove.append(backup / filename)
3232
task['cloudResult'] = Path(filename)
3333
else:
34-
final_items.append(item)
34+
# Compress/encrypt if it is a file (if needed)
35+
filename = archive_file(backup, item, task, config.cloud)
36+
if filename is not None:
37+
final_items.append(backup / filename)
38+
items_to_remove.append(backup / filename)
39+
task['cloudResult'] = Path(filename)
40+
else:
41+
final_items.append(item)
3542

3643
# Add the manifest as well :)
37-
final_items.append(backup / '.manifest.yaml')
44+
manifest_filename = archive_file(backup, backup / '.manifest.yaml', {'actions': []}, config.cloud)
45+
if manifest_filename is not None:
46+
final_items.append(backup / manifest_filename)
47+
items_to_remove.append(backup / manifest_filename)
48+
else:
49+
final_items.append(backup / '.manifest.yaml')
3850
return final_items, items_to_remove
3951

4052

@@ -119,7 +131,7 @@ def main_upload(config: Config, backup: Path, force: bool = False):
119131
raise FileNotFoundError(backup)
120132
if not backup.is_dir():
121133
raise NotADirectoryError(backup)
122-
if not str(backup).startswith(str(config.backups_path)):
134+
if not str(backup).startswith(str(config.backups_path.resolve())):
123135
raise ValueError(f'Backup path {backup} is not inside the backups path')
124136
manifest_path = backup / '.manifest.yaml'
125137
if not manifest_path.exists():
@@ -143,5 +155,5 @@ def main_upload(config: Config, backup: Path, force: bool = False):
143155
finally:
144156
# Remove compressed directories
145157
for item in items_to_remove:
146-
logger.info(f'Removing file from compressed directory {item}')
158+
logger.info(f'Removing temporary file {item}')
147159
item.unlink()

mdbackup/actions/builtin/database.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def action_influxd_command(_, params: dict):
102102

103103
if docker:
104104
params['image'] = params.get('image', 'influxdb:alpine')
105+
params['user'] = None
105106
return action_docker(None, params)
106107
else:
107108
return action_command(None, params)

mdbackup/actions/builtin/encrypt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
def action_encrypt_opengpg(inp: InputDataStream, params) -> subprocess.Popen:
1111
passphrase = params.get('passphrase')
1212
recipients: list = params.get('recipients', [])
13-
algorithm = params.get('cipherAlgorithm')
14-
compress = params.get('compressAlgorithm', False)
13+
algorithm = params.get('cipherAlgorithm', params.get('cypherAlgorithm'))
14+
compress = params.get('compressAlgorithm', None)
1515

1616
raise_if_type_is_incorrect(passphrase, str, 'passphrase must be a string')
1717
raise_if_type_is_incorrect(recipients, list, 'recipients must be a list of strings')

mdbackup/actions/builtin/file.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def _checks(params: dict) -> Path:
102102

103103
def _file_has_changed(entry, old_file: Path) -> bool:
104104
mod_time_current = entry.st_mtime_ns
105-
mod_time_prev = old_file.stat().st_mtime_ns
105+
mod_time_prev = old_file.lstat().st_mtime_ns
106106
return mod_time_current != mod_time_prev
107107

108108

@@ -148,7 +148,7 @@ def action_copy_file(_, params: dict):
148148
logger = logging.getLogger(__name__).getChild('action_copy_file')
149149
orig_path = Path(params['from']) if params.get('from') is not None else None
150150
orig_stream = params.get('_stream')
151-
orig_stat = orig_path.stat() if orig_path is not None else params['_stat']
151+
orig_stat = orig_path.lstat() if orig_path is not None else params['_stat']
152152
dest_path = _checks(params)
153153
in_path = Path(params['to'])
154154
preserve_stats = params.get('preserveStats', 'utime')
@@ -200,7 +200,7 @@ def action_reverse_copy_file(_, params: dict):
200200
dest_path = Path(params['from'])
201201
orig_path = _checks(params) if 'to' in params or 'path' in params else None
202202
orig_stream = params.get('_stream')
203-
orig_stat = orig_path.stat() if orig_path is not None else params['_stat']
203+
orig_stat = orig_path.lstat() if orig_path is not None else params['_stat']
204204
preserve_stats = params.get('preserveStats', 'utime')
205205

206206
raise_if_type_is_incorrect(preserve_stats, (str, bool), 'preserveStats must be a string or a boolean')
@@ -241,7 +241,7 @@ def action_clone_file(_, params: dict):
241241
# https://stackoverflow.com/questions/52766388/how-can-i-use-the-copy-on-write-of-a-btrfs-from-c-code
242242
# https://github.com/coreutils/coreutils/blob/master/src/copy.c#L370
243243
if not dest_path.exists():
244-
dest_path.touch(orig_path.stat().st_mode)
244+
dest_path.touch(orig_path.lstat().st_mode)
245245
src_fd = os.open(orig_path, flags=os.O_RDONLY)
246246
dst_fd = os.open(dest_path, flags=os.O_WRONLY | os.O_TRUNC)
247247
try:
@@ -257,7 +257,7 @@ def action_clone_file(_, params: dict):
257257

258258
if preserve_stats:
259259
xattrs = _read_xattrs(orig_path)
260-
_preserve_stats(dest_path, orig_path.stat(), xattrs, preserve_stats)
260+
_preserve_stats(dest_path, orig_path.lstat(), xattrs, preserve_stats)
261261
else:
262262
cow_failed = True
263263

mdbackup/actions/ds.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, _type: str, path: Path, stats, **kwargs):
2727

2828
@staticmethod
2929
def from_real_path(path: Path, root_path: Optional[Path] = None, **kwargs) -> 'DirEntry':
30-
stats = path.stat()
30+
stats = path.lstat()
3131
rel_path = path.relative_to(root_path) if root_path is not None else path
3232
try:
3333
xattrs = _read_xattrs(path)

mdbackup/archive.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from pathlib import Path
3+
from typing import Optional
34

45
from mdbackup.actions.runner import run_task_actions
56
from mdbackup.config import CloudConfig
@@ -16,7 +17,7 @@ def archive_folder(backup_path: Path, folder: Path, cloud_config: CloudConfig) -
1617
1718
The returned value is the file name for the archived folder.
1819
"""
19-
logger = logging.getLogger(__name__)
20+
logger = logging.getLogger(__name__).getChild('archive_folder')
2021
filename = str(folder.relative_to(backup_path)) + '.tar'
2122
actions = [
2223
{'from-directory': str(folder)},
@@ -27,12 +28,13 @@ def archive_folder(backup_path: Path, folder: Path, cloud_config: CloudConfig) -
2728
actions.append({
2829
f'compress-{cloud_config.compression_strategy}': {
2930
'level': cloud_config.compression_level,
31+
'cpus': cloud_config.compression_cpus,
3032
},
3133
})
3234
filename += f'.{cloud_config.compression_strategy}'
3335
if cloud_config.cypher_strategy is not None:
3436
actions.append({
35-
f'encrypt-gpg': {
37+
'encrypt-gpg': {
3638
'passphrase': cloud_config.cypher_params.get('passphrase'),
3739
'recipients': cloud_config.cypher_params.get('keys', []),
3840
'algorithm': cloud_config.cypher_params.get('algorithm'),
@@ -42,8 +44,53 @@ def archive_folder(backup_path: Path, folder: Path, cloud_config: CloudConfig) -
4244

4345
actions.append({'to-file': {'_backup_path': backup_path, 'to': filename}})
4446

45-
# Do the compression
46-
logger.info(f'Compressing directory {folder} into {filename}')
47+
# Do the magic
48+
logger.info(f'Compressing/encrypting directory {folder} into {filename}')
4749
run_task_actions('archive-folder', actions)
4850

4951
return filename
52+
53+
54+
def archive_file(backup_path: Path, file_path: Path, task: dict, cloud_config: CloudConfig) -> Optional[str]:
55+
logger = logging.getLogger(__name__).getChild('archive_file')
56+
57+
# Gets the actions used in this file and check if it is compressed or encrypted using any action
58+
actions = [next(iter(item.keys())) for item in task['actions']]
59+
has_compress_action = len([item for item in actions if 'compress-' in item]) != 0
60+
has_encrypt_action = len([item for item in actions if 'encrypt-' in item]) != 0
61+
62+
filename = str(file_path.relative_to(backup_path))
63+
archive_actions = [
64+
{'from-file': str(file_path)},
65+
]
66+
67+
if cloud_config.compression_strategy is not None and not has_compress_action:
68+
archive_actions.append({
69+
f'compress-{cloud_config.compression_strategy}': {
70+
'level': cloud_config.compression_level,
71+
'cpus': cloud_config.compression_cpus,
72+
},
73+
})
74+
filename += f'.{cloud_config.compression_strategy}'
75+
if cloud_config.cypher_strategy is not None and not has_encrypt_action:
76+
archive_actions.append({
77+
'encrypt-gpg': {
78+
'passphrase': cloud_config.cypher_params.get('passphrase'),
79+
'recipients': cloud_config.cypher_params.get('keys', []),
80+
'algorithm': cloud_config.cypher_params.get('algorithm'),
81+
},
82+
})
83+
filename += '.asc'
84+
85+
# If no extra actions are required, then just return and do nothing
86+
if len(archive_actions) == 1:
87+
logger.info(f'File {file_path} will not compress/encrypt because it is not required')
88+
return None
89+
90+
archive_actions.append({'to-file': {'_backup_path': backup_path, 'to': filename}})
91+
92+
# Do the magic
93+
logger.info(f'Compressing/encrypting file {file_path} into {filename}')
94+
run_task_actions('archive-file', archive_actions)
95+
96+
return filename

mdbackup/config/cloud.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ def __init__(self, conf: Dict[str, str]):
1010
if 'compression' in conf:
1111
self.__compression_level = conf['compression'].get('level', 6)
1212
self.__compression_strategy = conf['compression']['method']
13+
self.__compression_cpus = conf['compression'].get('cpus', None)
1314
else:
1415
self.__compression_level = None
1516
self.__compression_strategy = None
17+
self.__compression_cpus = None
1618
if 'encrypt' in conf:
1719
self.__cypher_strategy = conf['encrypt']['strategy']
1820
self.__cypher_params = change_keys(conf['encrypt'])
@@ -42,6 +44,13 @@ def compression_level(self) -> Optional[int]:
4244
"""
4345
return self.__compression_level
4446

47+
@property
48+
def compression_cpus(self) -> Optional[int]:
49+
"""
50+
:return: The threads/cpus to use during compression
51+
"""
52+
return self.__compression_cpus
53+
4554
@property
4655
def cypher_strategy(self) -> Optional[str]:
4756
"""

mdbackup/json-schemas/cloud.compression.schema.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222
"level": {
2323
"$id": "#/properties/level",
2424
"type": "number",
25-
"title": "Selects the compression level",
26-
"default": 6,
27-
"enum": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
25+
"title": "Selects the compression level to use to compress files (value depends on compression algorithm)",
26+
"default": 6
27+
},
28+
"cpus": {
29+
"$id": "#/properties/cpus",
30+
"type": "number",
31+
"title": "Selects the number of threads to use to compress files (may be ignored by some algorithms)",
32+
"default": 1
2833
}
2934
}
3035
}

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import setuptools
44

5-
version = '0.4.0'
5+
version = '0.4.1'
66

77
with open("README.md", "r") as fh:
88
long_description = fh.read()

0 commit comments

Comments
 (0)