yd-cli 0.9__tar.gz → 0.11__tar.gz
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.
- {yd_cli-0.9 → yd_cli-0.11}/PKG-INFO +25 -3
- {yd_cli-0.9 → yd_cli-0.11}/README.md +24 -2
- {yd_cli-0.9 → yd_cli-0.11}/pyproject.toml +1 -1
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/cli.py +42 -13
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/commands.py +81 -7
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/execution.py +1 -0
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/io.py +0 -5
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/types.py +1 -5
- {yd_cli-0.9 → yd_cli-0.11}/LICENSES/MIT.txt +0 -0
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/__init__.py +0 -0
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/__main__.py +0 -0
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/echo.py +0 -0
- {yd_cli-0.9 → yd_cli-0.11}/src/yd/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yd-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11
|
|
4
4
|
Summary: CLI tool to synchronize directories using rsync.
|
|
5
5
|
Author: Christian Heinze
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,6 +25,26 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
Build and execute `rsync` commands from *TOML* configuration files.
|
|
27
27
|
|
|
28
|
+
## (Un)Install
|
|
29
|
+
|
|
30
|
+
To install, run
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install yd-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then run
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
yd --install-completion
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
to install auto-completion in your shell.
|
|
43
|
+
|
|
44
|
+
In `bash`, the completion code is stored in `~/.bash_completions/yd.sh` and that file is sourced from `~/.bashrc`.
|
|
45
|
+
Remove both and call `uv tool uninstall yd-cli` to remove this tool.
|
|
46
|
+
The below mentioned configuration files need to be removed separately.
|
|
47
|
+
|
|
28
48
|
## Create a config
|
|
29
49
|
|
|
30
50
|
Create a new configuration called `photos` with:
|
|
@@ -72,7 +92,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
72
92
|
| Key | Meaning |
|
|
73
93
|
| --- | --- |
|
|
74
94
|
| `src_home` | Base directory for all `src` paths. Relative paths are resolved from your home directory. |
|
|
75
|
-
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory.
|
|
95
|
+
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory. |
|
|
76
96
|
| `mtp_target` | Use in-place syncing for MTP targets. |
|
|
77
97
|
| `backup` | Backup directory for replaced or deleted files. Relative paths are resolved from `target_home`. `strftime` placeholders are supported. |
|
|
78
98
|
| `exclude` | Exclude patterns applied to every command. |
|
|
@@ -84,7 +104,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
84
104
|
| Key | Meaning |
|
|
85
105
|
| --- | --- |
|
|
86
106
|
| `src` | Relative source directory below `src_home`. |
|
|
87
|
-
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders are supported. |
|
|
107
|
+
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders (`%Y`, `%m`, and `%d` only!) are supported. |
|
|
88
108
|
| `delete_extra` | Delete files in the target that do not exist in the source. Defaults to `true`. |
|
|
89
109
|
| `exclude` | Extra exclude patterns for this command only. |
|
|
90
110
|
|
|
@@ -95,6 +115,8 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
95
115
|
- Exactly one of `--src-home -` and `--target-home -` may read from standard input in a single run.
|
|
96
116
|
- If `backup` is omitted, `yd` creates a timestamped backup directory automatically unless `--no-backup` is specified.
|
|
97
117
|
- If a configured source directory exists but is empty, `yd` reports that no synchronization was performed for that command. E.g., forgetting to mount an external drive does not delete all corresponding copies on harddrive).
|
|
118
|
+
- If `strftime` placeholders are included in the `target`, then the `target_home` is scanned for directories that may have been created by this rule in the past. All matches are included as additional references in the synchronization: if a file in `src` is included in one of those reference directories, then it will not be copied to `target`.
|
|
119
|
+
- If the `target` directory did not exist before and no files are transferred into it, then it will be removed again.
|
|
98
120
|
|
|
99
121
|
## Run a config
|
|
100
122
|
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Build and execute `rsync` commands from *TOML* configuration files.
|
|
4
4
|
|
|
5
|
+
## (Un)Install
|
|
6
|
+
|
|
7
|
+
To install, run
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install yd-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yd --install-completion
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
to install auto-completion in your shell.
|
|
20
|
+
|
|
21
|
+
In `bash`, the completion code is stored in `~/.bash_completions/yd.sh` and that file is sourced from `~/.bashrc`.
|
|
22
|
+
Remove both and call `uv tool uninstall yd-cli` to remove this tool.
|
|
23
|
+
The below mentioned configuration files need to be removed separately.
|
|
24
|
+
|
|
5
25
|
## Create a config
|
|
6
26
|
|
|
7
27
|
Create a new configuration called `photos` with:
|
|
@@ -49,7 +69,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
49
69
|
| Key | Meaning |
|
|
50
70
|
| --- | --- |
|
|
51
71
|
| `src_home` | Base directory for all `src` paths. Relative paths are resolved from your home directory. |
|
|
52
|
-
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory.
|
|
72
|
+
| `target_home` | Base directory for all target paths. Relative paths are resolved from your home directory. |
|
|
53
73
|
| `mtp_target` | Use in-place syncing for MTP targets. |
|
|
54
74
|
| `backup` | Backup directory for replaced or deleted files. Relative paths are resolved from `target_home`. `strftime` placeholders are supported. |
|
|
55
75
|
| `exclude` | Exclude patterns applied to every command. |
|
|
@@ -61,7 +81,7 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
61
81
|
| Key | Meaning |
|
|
62
82
|
| --- | --- |
|
|
63
83
|
| `src` | Relative source directory below `src_home`. |
|
|
64
|
-
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders are supported. |
|
|
84
|
+
| `target` | Relative target directory below `target_home`; defaults to `src`. `strftime` placeholders (`%Y`, `%m`, and `%d` only!) are supported. |
|
|
65
85
|
| `delete_extra` | Delete files in the target that do not exist in the source. Defaults to `true`. |
|
|
66
86
|
| `exclude` | Extra exclude patterns for this command only. |
|
|
67
87
|
|
|
@@ -72,6 +92,8 @@ Leading comment lines directly at the top of the file are treated as the configu
|
|
|
72
92
|
- Exactly one of `--src-home -` and `--target-home -` may read from standard input in a single run.
|
|
73
93
|
- If `backup` is omitted, `yd` creates a timestamped backup directory automatically unless `--no-backup` is specified.
|
|
74
94
|
- If a configured source directory exists but is empty, `yd` reports that no synchronization was performed for that command. E.g., forgetting to mount an external drive does not delete all corresponding copies on harddrive).
|
|
95
|
+
- If `strftime` placeholders are included in the `target`, then the `target_home` is scanned for directories that may have been created by this rule in the past. All matches are included as additional references in the synchronization: if a file in `src` is included in one of those reference directories, then it will not be copied to `target`.
|
|
96
|
+
- If the `target` directory did not exist before and no files are transferred into it, then it will be removed again.
|
|
75
97
|
|
|
76
98
|
## Run a config
|
|
77
99
|
|
|
@@ -27,6 +27,8 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
from rich.text import Text
|
|
29
29
|
|
|
30
|
+
log = logging.getLogger(__name__)
|
|
31
|
+
|
|
30
32
|
env: yd.types.Environment
|
|
31
33
|
console: rich.console.Console
|
|
32
34
|
highlighter: StrEnumHighlighter
|
|
@@ -176,7 +178,6 @@ def _setup_io() -> None:
|
|
|
176
178
|
|
|
177
179
|
_encode_json = msgspec.json.Encoder().encode
|
|
178
180
|
|
|
179
|
-
@dataclasses.dataclass(slots=True)
|
|
180
181
|
class JsonLineFormatter(logging.Formatter):
|
|
181
182
|
def format(self, record: logging.LogRecord) -> str:
|
|
182
183
|
return _encode_json(
|
|
@@ -267,7 +268,9 @@ def run(
|
|
|
267
268
|
) -> int:
|
|
268
269
|
"""Run a saved configuration by name."""
|
|
269
270
|
import asyncio
|
|
270
|
-
import
|
|
271
|
+
import contextlib
|
|
272
|
+
import functools as ft
|
|
273
|
+
import shutil
|
|
271
274
|
from pathlib import Path
|
|
272
275
|
|
|
273
276
|
import rich.progress
|
|
@@ -291,7 +294,6 @@ def run(
|
|
|
291
294
|
keep_newer=keep_newer,
|
|
292
295
|
rename_speedup=rename_speedup,
|
|
293
296
|
no_backup=no_backup,
|
|
294
|
-
error_encoder=yd.io.encode_error,
|
|
295
297
|
)
|
|
296
298
|
)
|
|
297
299
|
except yd.types.CommandBuildError as err:
|
|
@@ -312,7 +314,7 @@ def run(
|
|
|
312
314
|
),
|
|
313
315
|
rich.progress.TextColumn(
|
|
314
316
|
"",
|
|
315
|
-
table_column=rich.table.Column(
|
|
317
|
+
table_column=rich.table.Column(min_width=8),
|
|
316
318
|
),
|
|
317
319
|
*it.chain.from_iterable(
|
|
318
320
|
(
|
|
@@ -333,6 +335,8 @@ def run(
|
|
|
333
335
|
}
|
|
334
336
|
),
|
|
335
337
|
console=console,
|
|
338
|
+
expand=True,
|
|
339
|
+
refresh_per_second=10,
|
|
336
340
|
) as progress,
|
|
337
341
|
):
|
|
338
342
|
template = "{action} {filename} ({transfer_bytes} bytes)"
|
|
@@ -371,6 +375,13 @@ def run(
|
|
|
371
375
|
**counter, # type: ignore[ty:invalid-argument-type]
|
|
372
376
|
total=1,
|
|
373
377
|
)
|
|
378
|
+
|
|
379
|
+
target = Path(args[-1])
|
|
380
|
+
target_existed = target.is_dir()
|
|
381
|
+
progress_updater = ft.partial(
|
|
382
|
+
show_progress, counter=counter, taskid=taskid, target=target
|
|
383
|
+
)
|
|
384
|
+
|
|
374
385
|
try:
|
|
375
386
|
runner.run(
|
|
376
387
|
yd.execution.run(
|
|
@@ -378,12 +389,7 @@ def run(
|
|
|
378
389
|
*args,
|
|
379
390
|
parser_stdout=yd.io.decode_from_stdout,
|
|
380
391
|
parser_stderr=yd.io.decode_from_stderr,
|
|
381
|
-
consumer=
|
|
382
|
-
show_progress,
|
|
383
|
-
counter=counter,
|
|
384
|
-
taskid=taskid,
|
|
385
|
-
target=Path(args[-1]),
|
|
386
|
-
),
|
|
392
|
+
consumer=progress_updater,
|
|
387
393
|
)
|
|
388
394
|
)
|
|
389
395
|
except (KeyboardInterrupt, asyncio.CancelledError) as err:
|
|
@@ -396,6 +402,28 @@ def run(
|
|
|
396
402
|
except (yd.types.OutputParseError, yd.types.OutputConsumeError) as err:
|
|
397
403
|
console.print("Failed to handle rsync output.", style="error")
|
|
398
404
|
raise typer.Exit(2) from err
|
|
405
|
+
|
|
406
|
+
# If target was created but no files were copied, then remove it.
|
|
407
|
+
if (
|
|
408
|
+
not target_existed
|
|
409
|
+
and target.is_dir() # Ensure rglob can be used.
|
|
410
|
+
and not any(
|
|
411
|
+
path.is_file() or path.is_symlink() for path in target.rglob("*")
|
|
412
|
+
)
|
|
413
|
+
):
|
|
414
|
+
with contextlib.suppress(OSError):
|
|
415
|
+
shutil.rmtree(target)
|
|
416
|
+
log.info("Removed empty but newly created target `%s`.", target)
|
|
417
|
+
runner.run(
|
|
418
|
+
progress_updater(
|
|
419
|
+
yd.types.Transaction(
|
|
420
|
+
filename=target.as_posix(),
|
|
421
|
+
transfer_bytes=0,
|
|
422
|
+
info="*deleting",
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
399
427
|
progress.update(taskid, completed=1)
|
|
400
428
|
|
|
401
429
|
return 0
|
|
@@ -437,17 +465,18 @@ def edit(
|
|
|
437
465
|
] = False,
|
|
438
466
|
) -> int:
|
|
439
467
|
"""Open an existing configuration or create a new one in the editor."""
|
|
440
|
-
import contextlib
|
|
441
468
|
import subprocess
|
|
442
469
|
|
|
443
470
|
if (editor_bin := env.editor_bin) is None:
|
|
444
471
|
raise typer.BadParameter("environment variable `EDITOR` is not set")
|
|
445
472
|
|
|
446
|
-
|
|
447
|
-
with contextlib.suppress(subprocess.CalledProcessError):
|
|
473
|
+
try:
|
|
448
474
|
vers = subprocess.run(
|
|
449
475
|
[editor_bin, "--version"], encoding="utf-8", capture_output=True, check=True
|
|
450
476
|
).stdout
|
|
477
|
+
except subprocess.CalledProcessError:
|
|
478
|
+
is_nvim = False
|
|
479
|
+
else:
|
|
451
480
|
is_nvim = re.match(r"\s*NVIM", vers) is not None
|
|
452
481
|
|
|
453
482
|
config_path = env.config_dir / f"{cfg_name}.toml"
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import datetime as dt
|
|
8
9
|
import itertools as it
|
|
9
10
|
import logging
|
|
11
|
+
import re
|
|
10
12
|
import sys
|
|
11
13
|
import uuid
|
|
14
|
+
from dataclasses import dataclass
|
|
12
15
|
from pathlib import Path
|
|
13
16
|
|
|
14
17
|
from yd import types
|
|
15
18
|
|
|
16
19
|
TYPE_CHECKING = False
|
|
17
20
|
if TYPE_CHECKING:
|
|
18
|
-
from collections.abc import
|
|
21
|
+
from collections.abc import Iterator, Sequence
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
log = logging.getLogger(__name__)
|
|
@@ -47,7 +50,57 @@ def _format_path(path: Path, /) -> str:
|
|
|
47
50
|
return f"{path.as_posix()}/"
|
|
48
51
|
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class _DatePathMatch:
|
|
55
|
+
path: Path
|
|
56
|
+
relative_path: str
|
|
57
|
+
datetime: dt.datetime | None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_PLACEHOLDER = re.compile("%Y|%m|%d")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _match_dirs(
|
|
64
|
+
*, rel_path_pattern: str, home: Path, env: types.Environment
|
|
65
|
+
) -> Iterator[_DatePathMatch]:
|
|
66
|
+
"""Find subdirectories matching a target pattern."""
|
|
67
|
+
if "%" not in rel_path_pattern:
|
|
68
|
+
log.debug("No date placeholder found in `%s`.", rel_path_pattern)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Crude filter. More careful checking below.
|
|
72
|
+
glob_pattern = re.sub(
|
|
73
|
+
"(?P<f>[^/])$", r"\g<f>/", _PLACEHOLDER.sub("*", rel_path_pattern)
|
|
74
|
+
)
|
|
75
|
+
if "%" in glob_pattern:
|
|
76
|
+
raise ValueError("pattern contains unsupported date placeholders")
|
|
77
|
+
rel_path_pattern = re.sub("/$", "", rel_path_pattern)
|
|
78
|
+
|
|
79
|
+
log.debug("Searching for sub-directories of the form `%s`.", home / glob_pattern)
|
|
80
|
+
for path in home.glob(glob_pattern):
|
|
81
|
+
rel_path = path.relative_to(home).as_posix()
|
|
82
|
+
log.debug("Scanning for date in `%s`.", rel_path)
|
|
83
|
+
try:
|
|
84
|
+
parsed = dt.datetime.strptime(rel_path, rel_path_pattern)
|
|
85
|
+
except ValueError:
|
|
86
|
+
log.debug("Found no date in `%s`.", rel_path)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
log.info("Found date `%s` in `%s`.", parsed.strftime("%Y-%m-%d"), rel_path)
|
|
90
|
+
if (rendered := parsed.strftime(rel_path_pattern)) != rel_path:
|
|
91
|
+
log.debug("Path pattern rendered to `%s` != `%s`.", rendered, rel_path)
|
|
92
|
+
continue
|
|
93
|
+
if parsed.date() >= env.current_time.date():
|
|
94
|
+
log.debug(
|
|
95
|
+
"Date `%s` not before current date `%s`.",
|
|
96
|
+
parsed.strftime("%Y-%m-%d"),
|
|
97
|
+
env.current_time.strftime("%Y-%m-%d"),
|
|
98
|
+
)
|
|
99
|
+
continue
|
|
100
|
+
yield _DatePathMatch(path=path, relative_path=rel_path, datetime=parsed)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _create_echo_command(
|
|
51
104
|
enc_msg: str, /, environment: types.Environment
|
|
52
105
|
) -> tuple[str, tuple[str, ...]]:
|
|
53
106
|
if environment.echo_bin is not None:
|
|
@@ -67,8 +120,7 @@ def create( # noqa: PLR0915
|
|
|
67
120
|
rename_speedup: bool,
|
|
68
121
|
no_backup: bool,
|
|
69
122
|
out_format: str = '{"filename": "%n", "transfer_bytes": %b, "info": "%i"}',
|
|
70
|
-
|
|
71
|
-
) -> Iterable[tuple[str, Sequence[str]]]:
|
|
123
|
+
) -> Iterator[tuple[str, Sequence[str]]]:
|
|
72
124
|
"""Build CLI commands.
|
|
73
125
|
|
|
74
126
|
If the source directory is empty, then an `echo` command will be created. If
|
|
@@ -91,7 +143,6 @@ def create( # noqa: PLR0915
|
|
|
91
143
|
if not (target_home := cg.target_home if target_home is None else target_home):
|
|
92
144
|
raise types.CommandBuildError("missing target home specification")
|
|
93
145
|
|
|
94
|
-
target_home = env.current_time.strftime(target_home)
|
|
95
146
|
src_hpath, target_hpath = (
|
|
96
147
|
_complete_path(Path(src_home), home=env.home_dir),
|
|
97
148
|
_complete_path(Path(target_home), home=env.home_dir),
|
|
@@ -160,9 +211,9 @@ def create( # noqa: PLR0915
|
|
|
160
211
|
msg = f"Source directory `{src}` is empty. No synchronization will be done."
|
|
161
212
|
if cmd.delete_extra:
|
|
162
213
|
msg += " Delete the folder contents manually to synchronize."
|
|
163
|
-
|
|
164
214
|
log.warning(msg)
|
|
165
|
-
|
|
215
|
+
|
|
216
|
+
yield _create_echo_command(msg, environment=env)
|
|
166
217
|
continue
|
|
167
218
|
|
|
168
219
|
target_spec = (
|
|
@@ -211,6 +262,29 @@ def create( # noqa: PLR0915
|
|
|
211
262
|
# A relative path will be considered relative to the destination directory.
|
|
212
263
|
args.extend(("--backup", "--backup-dir", _format_path(target_backup_path)))
|
|
213
264
|
|
|
265
|
+
if cmd.target is not None and target_spec != cmd.target:
|
|
266
|
+
try:
|
|
267
|
+
cmp_dests = sorted(
|
|
268
|
+
_match_dirs(
|
|
269
|
+
rel_path_pattern=cmd.target, home=target_hpath, env=env
|
|
270
|
+
),
|
|
271
|
+
key=lambda d: d.datetime,
|
|
272
|
+
reverse=True,
|
|
273
|
+
)
|
|
274
|
+
except ValueError as err:
|
|
275
|
+
raise types.CommandBuildError(
|
|
276
|
+
"unsupported date specifiers in target"
|
|
277
|
+
) from err
|
|
278
|
+
# rsync restricts the number of `--compare-dest` specs to 20.
|
|
279
|
+
cmp_dests = [d.path.as_posix() for i, d in enumerate(cmp_dests) if i < 20]
|
|
280
|
+
log.info(
|
|
281
|
+
"Found reference targets `%s`.",
|
|
282
|
+
"`, `".join(d for d in cmp_dests),
|
|
283
|
+
)
|
|
284
|
+
args.extend(
|
|
285
|
+
it.chain.from_iterable(("--compare-dest", d) for d in cmp_dests)
|
|
286
|
+
)
|
|
287
|
+
|
|
214
288
|
args.extend(
|
|
215
289
|
f"--exclude={pattern}" for pattern in it.chain(cmd.exclude, cg.exclude)
|
|
216
290
|
)
|
|
@@ -35,7 +35,6 @@ __all__ = [
|
|
|
35
35
|
"list_available_configs",
|
|
36
36
|
"decode_from_stdout",
|
|
37
37
|
"decode_from_stderr",
|
|
38
|
-
"encode_error",
|
|
39
38
|
]
|
|
40
39
|
|
|
41
40
|
|
|
@@ -195,7 +194,3 @@ def decode_from_stderr(text: str, /) -> types.Transaction | str | None:
|
|
|
195
194
|
if not (stripped_text := text.strip()):
|
|
196
195
|
return None
|
|
197
196
|
return stripped_text
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def encode_error(text: str, /) -> str:
|
|
201
|
-
return text
|
|
@@ -70,10 +70,6 @@ class CommandGroup(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
|
|
70
70
|
src_home/target_home
|
|
71
71
|
Absolute path used to turn relative src/target path in individual commands into
|
|
72
72
|
absolute paths.
|
|
73
|
-
|
|
74
|
-
The `target_home` is passed to `datetime.strftime` via the `format` parameter,
|
|
75
|
-
that is, placeholders for date/time components like `%Y`, `%m`, etc., are
|
|
76
|
-
replaced with the actual values at runtime.
|
|
77
73
|
mtp_target
|
|
78
74
|
Set True if communication with the target works with MTP. Due to
|
|
79
75
|
technical limitations of that protocol, synchronization needs to be done
|
|
@@ -84,7 +80,7 @@ class CommandGroup(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
|
|
84
80
|
Path to backup directory. Added to each command and if relative, then taken
|
|
85
81
|
as relative to target home. If None, a default relative path is used.
|
|
86
82
|
|
|
87
|
-
The `
|
|
83
|
+
The `backup` is passed to `datetime.strftime` via the `format` parameter, that
|
|
88
84
|
is, placeholders for date/time components like `%Y`, `%m`, etc., are replaced
|
|
89
85
|
with the actual values at runtime.
|
|
90
86
|
exclude
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|