duplicity 3.0.8.dev6__tar.gz → 3.1.0.dev0__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.
Files changed (143) hide show
  1. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/CHANGELOG.md +10 -1
  2. {duplicity-3.0.8.dev6/duplicity.egg-info → duplicity-3.1.0.dev0}/PKG-INFO +3 -3
  3. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/README.md +2 -1
  4. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/__init__.py +2 -2
  5. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/cli_main.py +18 -0
  6. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/dup_main.py +8 -1
  7. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/selection.py +159 -0
  8. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0/duplicity.egg-info}/PKG-INFO +3 -3
  9. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity.egg-info/requires.txt +0 -1
  10. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/man/duplicity.1 +33 -3
  11. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/pyproject.toml +43 -10
  12. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/requirements.txt +0 -1
  13. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/setup.py +1 -1
  14. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/AUTHORS.md +0 -0
  15. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/COPYING +0 -0
  16. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/README-LOG.md +0 -0
  17. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/README-REPO.md +0 -0
  18. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/README-TESTING.md +0 -0
  19. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/__main__.py +0 -0
  20. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/_librsyncmodule.c +0 -0
  21. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/argparse311.py +0 -0
  22. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backend.py +0 -0
  23. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backend_pool.py +0 -0
  24. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/__init__.py +0 -0
  25. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/_cf_cloudfiles.py +0 -0
  26. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/_cf_pyrax.py +0 -0
  27. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/_testbackend.py +0 -0
  28. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/adbackend.py +0 -0
  29. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/azurebackend.py +0 -0
  30. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/b2backend.py +0 -0
  31. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/boxbackend.py +0 -0
  32. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/cfbackend.py +0 -0
  33. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/dpbxbackend.py +0 -0
  34. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/gdocsbackend.py +0 -0
  35. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/gdrivebackend.py +0 -0
  36. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/giobackend.py +0 -0
  37. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/hsibackend.py +0 -0
  38. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/hubicbackend.py +0 -0
  39. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/idrivedbackend.py +0 -0
  40. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/imapbackend.py +0 -0
  41. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/jottacloudbackend.py +0 -0
  42. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/lftpbackend.py +0 -0
  43. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/localbackend.py +0 -0
  44. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/mediafirebackend.py +0 -0
  45. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/megabackend.py +0 -0
  46. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/megav2backend.py +0 -0
  47. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/megav3backend.py +0 -0
  48. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/multibackend.py +0 -0
  49. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/ncftpbackend.py +0 -0
  50. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/onedrivebackend.py +0 -0
  51. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/par2backend.py +0 -0
  52. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/pcabackend.py +0 -0
  53. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/pydrivebackend.py +0 -0
  54. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/pyrax_identity/__init__.py +0 -0
  55. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/pyrax_identity/hubic.py +0 -0
  56. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/rclonebackend.py +0 -0
  57. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/rsyncbackend.py +0 -0
  58. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/s3_boto3_backend.py +0 -0
  59. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/slatebackend.py +0 -0
  60. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/ssh_paramiko_backend.py +0 -0
  61. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/ssh_pexpect_backend.py +0 -0
  62. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/swiftbackend.py +0 -0
  63. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/sxbackend.py +0 -0
  64. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/tahoebackend.py +0 -0
  65. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/webdavbackend.py +0 -0
  66. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/backends/xorrisobackend.py +0 -0
  67. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/cached_ops.py +0 -0
  68. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/cli_data.py +0 -0
  69. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/cli_util.py +0 -0
  70. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/config.py +0 -0
  71. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/diffdir.py +0 -0
  72. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/dup_collections.py +0 -0
  73. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/dup_tarfile.py +0 -0
  74. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/dup_temp.py +0 -0
  75. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/dup_time.py +0 -0
  76. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/errors.py +0 -0
  77. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/file_naming.py +0 -0
  78. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/filechunkio.py +0 -0
  79. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/globmatch.py +0 -0
  80. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/gpg.py +0 -0
  81. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/gpginterface.py +0 -0
  82. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/lazy.py +0 -0
  83. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/librsync.py +0 -0
  84. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/log.py +0 -0
  85. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/manifest.py +0 -0
  86. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/patchdir.py +0 -0
  87. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/path.py +0 -0
  88. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/progress.py +0 -0
  89. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/robust.py +0 -0
  90. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/statistics.py +0 -0
  91. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/tempdir.py +0 -0
  92. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity/util.py +0 -0
  93. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity.egg-info/SOURCES.txt +0 -0
  94. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity.egg-info/dependency_links.txt +0 -0
  95. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity.egg-info/entry_points.txt +0 -0
  96. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/duplicity.egg-info/top_level.txt +0 -0
  97. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/af_ZA/duplicity.mo +0 -0
  98. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ar_SA/duplicity.mo +0 -0
  99. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ca_ES/duplicity.mo +0 -0
  100. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/cs_CZ/duplicity.mo +0 -0
  101. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/da_DK/duplicity.mo +0 -0
  102. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/de_AT/duplicity.mo +0 -0
  103. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/de_DE/duplicity.mo +0 -0
  104. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/el_GR/duplicity.mo +0 -0
  105. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/en_AU/duplicity.mo +0 -0
  106. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/en_GB/duplicity.mo +0 -0
  107. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/en_PR/duplicity.mo +0 -0
  108. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/en_US/duplicity.mo +0 -0
  109. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/es_EM/duplicity.mo +0 -0
  110. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/es_ES/duplicity.mo +0 -0
  111. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/es_MX/duplicity.mo +0 -0
  112. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/es_PR/duplicity.mo +0 -0
  113. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/es_US/duplicity.mo +0 -0
  114. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/fi_FI/duplicity.mo +0 -0
  115. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/fr_FR/duplicity.mo +0 -0
  116. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/he_IL/duplicity.mo +0 -0
  117. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/hu_HU/duplicity.mo +0 -0
  118. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/it_IT/duplicity.mo +0 -0
  119. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ja_JP/duplicity.mo +0 -0
  120. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ko_KR/duplicity.mo +0 -0
  121. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/nl_BE/duplicity.mo +0 -0
  122. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/nl_NL/duplicity.mo +0 -0
  123. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/nl_SR/duplicity.mo +0 -0
  124. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/no_NO/duplicity.mo +0 -0
  125. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/pl_PL/duplicity.mo +0 -0
  126. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/pt_BR/duplicity.mo +0 -0
  127. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/pt_PT/duplicity.mo +0 -0
  128. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ro_RO/duplicity.mo +0 -0
  129. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ru_BY/duplicity.mo +0 -0
  130. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ru_MD/duplicity.mo +0 -0
  131. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ru_RU/duplicity.mo +0 -0
  132. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/ru_UA/duplicity.mo +0 -0
  133. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/sr_SP/duplicity.mo +0 -0
  134. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/sv_SE/duplicity.mo +0 -0
  135. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/tr_TR/duplicity.mo +0 -0
  136. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/uk_UA/duplicity.mo +0 -0
  137. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/vi_VN/duplicity.mo +0 -0
  138. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/zh_CN/duplicity.mo +0 -0
  139. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/zh_HK/duplicity.mo +0 -0
  140. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/zh_MO/duplicity.mo +0 -0
  141. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/zh_SG/duplicity.mo +0 -0
  142. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/po/zh_TW/duplicity.mo +0 -0
  143. {duplicity-3.0.8.dev6 → duplicity-3.1.0.dev0}/setup.cfg +0 -0
@@ -1,9 +1,18 @@
1
1
 
2
- (Unreleased) / 2026-05-12
2
+ (Unreleased) / 2026-06-04
3
3
  =========================
4
4
 
5
5
 
6
6
 
7
+ rel.3.1.0.dev0 / 2026-06-04
8
+ ===========================
9
+
10
+ * 5c4cdc2f:new: Now do pip builds for macOS and Linux
11
+ * 0140a374:new: Add include/exclude restore filtering using archive-relative paths
12
+ * 46a4796b:chg: Update snapcraft.yaml for license, type, and typo fixes.
13
+ * 143373b8:chg: Use `--build-on` for snapcraft remote-build.
14
+ * 182d6e33:chg: Use `python3 -m build` for sdist in release-prep.
15
+
7
16
  rel.3.0.8.dev5 / 2026-05-12
8
17
  ===========================
9
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duplicity
3
- Version: 3.0.8.dev6
3
+ Version: 3.1.0.dev0
4
4
  Summary: Encrypted backup using rsync algorithm
5
5
  Author-email: Kenneth Loafman <kenneth@loafman.com>
6
6
  Maintainer: Edgar Soldin, Thomas Laubrock
@@ -44,7 +44,6 @@ Requires-Dist: httplib2
44
44
  Requires-Dist: pysocks
45
45
  Requires-Dist: jottalib
46
46
  Requires-Dist: keyring
47
- Requires-Dist: lxml
48
47
  Requires-Dist: mediafire
49
48
  Requires-Dist: megatools
50
49
  Requires-Dist: paramiko
@@ -74,6 +73,7 @@ sudo apt-get install -y \
74
73
  openssl \
75
74
  par2 \
76
75
  python3-dev \
76
+ python3-lxml \
77
77
  python3-pip \
78
78
  python3-venv \
79
79
  python3 \
@@ -101,7 +101,7 @@ packaging tools like this:
101
101
  ```shell
102
102
  sudo python3 -m pip install --upgrade pip pipx
103
103
  ````
104
- **NOTE: _Failure to upgrade will probably result in a failed install._ <--IMPORTANT!**
104
+ **NOTE: _Failure to upgrade will probably result in a failed install._ IMPORTANT!**
105
105
 
106
106
  To make sure the pipx dirs are on your path do:
107
107
  ```shell
@@ -15,6 +15,7 @@ sudo apt-get install -y \
15
15
  openssl \
16
16
  par2 \
17
17
  python3-dev \
18
+ python3-lxml \
18
19
  python3-pip \
19
20
  python3-venv \
20
21
  python3 \
@@ -42,7 +43,7 @@ packaging tools like this:
42
43
  ```shell
43
44
  sudo python3 -m pip install --upgrade pip pipx
44
45
  ````
45
- **NOTE: _Failure to upgrade will probably result in a failed install._ <--IMPORTANT!**
46
+ **NOTE: _Failure to upgrade will probably result in a failed install._ IMPORTANT!**
46
47
 
47
48
  To make sure the pipx dirs are on your path do:
48
49
  ```shell
@@ -21,7 +21,7 @@
21
21
 
22
22
  import gettext
23
23
 
24
- __version__: str = "3.0.8.dev6"
25
- __reldate__: str = "May 12, 2026"
24
+ __version__: str = "3.1.0.dev0"
25
+ __reldate__: str = "June 04, 2026"
26
26
 
27
27
  gettext.install("duplicity", names=["ngettext"])
@@ -320,6 +320,24 @@ def process_command_line(cmdline_list):
320
320
  # count is only used by the remove-* commands
321
321
  config.keep_chains = config.count
322
322
 
323
+ # restore selection uses archive-relative paths and is applied while restoring
324
+ if config.action == "restore" and config.select_opts:
325
+ if config.restore_path:
326
+ command_line_error(
327
+ "--path-to-restore cannot be combined with restore file selection options. "
328
+ "Use --path-to-restore for one archive path, or --include/--exclude for pattern-based restore."
329
+ )
330
+ unsupported_restore_selection = {
331
+ "--exclude-if-present",
332
+ "--exclude-other-filesystems",
333
+ "--files-from",
334
+ }
335
+ unsupported = sorted({opt for opt, _arg in config.select_opts if opt in unsupported_restore_selection})
336
+ if unsupported:
337
+ command_line_error(
338
+ "The following file selection options are not supported for restore: " + ", ".join(unsupported)
339
+ )
340
+
323
341
  # selection only applies to certain commands
324
342
  if config.action in ["full", "inc", "verify"]:
325
343
  set_selection()
@@ -57,6 +57,7 @@ from duplicity import (
57
57
  patchdir,
58
58
  path,
59
59
  progress,
60
+ selection,
60
61
  tempdir,
61
62
  util,
62
63
  )
@@ -908,6 +909,9 @@ def restore_get_patched_rop_iter(col_stats):
908
909
  f"available chains: {col_stats.all_backup_chains}"
909
910
  )
910
911
  backup_setlist = backup_chain.get_sets_at_time(time)
912
+ restore_selection = None
913
+ if config.action == "restore" and config.select_opts:
914
+ restore_selection = selection.get_restore_selection(config.select_opts, config.select_files)
911
915
  num_vols = 0
912
916
  for s in backup_setlist:
913
917
  num_vols += len(s)
@@ -957,7 +961,10 @@ def restore_get_patched_rop_iter(col_stats):
957
961
 
958
962
  fileobj_iters = list(map(get_fileobj_iter, backup_setlist))
959
963
  tarfiles = list(map(patchdir.TarFile_FromFileobjs, fileobj_iters))
960
- return patchdir.tarfiles2rop_iter(tarfiles, index)
964
+ rop_iter = patchdir.tarfiles2rop_iter(tarfiles, index)
965
+ if restore_selection:
966
+ rop_iter = selection.filter_restore_path_iter(rop_iter, restore_selection)
967
+ return rop_iter
961
968
 
962
969
 
963
970
  def restore_get_enc_fileobj(backend, filename, volume_info):
@@ -702,3 +702,162 @@ class Select(object):
702
702
  return None
703
703
 
704
704
  return test_fn
705
+
706
+
707
+ class _ArchivePath(object):
708
+ """Adapter exposing an archive ROPath with an archive-relative name."""
709
+
710
+ def __init__(self, ropath):
711
+ self.ropath = ropath
712
+ self.index = ropath.index
713
+ if self.index:
714
+ self.uc_name = os.fsdecode(b"/".join(self.index))
715
+ else:
716
+ self.uc_name = ""
717
+
718
+ def __getattr__(self, attr):
719
+ return getattr(self.ropath, attr)
720
+
721
+
722
+ class ArchiveSelect(Select):
723
+ """Apply selection rules to archive-relative restore paths.
724
+
725
+ Restore paths are matched in the namespace printed by list-current-files
726
+ and accepted by --path-to-restore, not in the original source filesystem
727
+ namespace used by backup selection.
728
+ """
729
+
730
+ def __init__(self):
731
+ super().__init__(Path(b"."))
732
+
733
+ @staticmethod
734
+ def _normalize_archive_pattern(pattern):
735
+ while pattern.startswith("./"):
736
+ pattern = pattern[2:]
737
+ if pattern.startswith("/"):
738
+ raise FilePrefixError(pattern)
739
+ if any(part in (".", "..") for part in pattern.split("/") if part):
740
+ raise FilePrefixError(pattern)
741
+ return pattern
742
+
743
+ def parse_catch_error(self, exc):
744
+ """Deal with restore archive-relative selection errors."""
745
+ if isinstance(exc, FilePrefixError):
746
+ log.FatalError(
747
+ dedent(_("""\
748
+ Fatal Error: Restore file selection pattern
749
+ %s
750
+ is invalid. Restore file selection patterns must be relative to the
751
+ archive root, using paths as printed by list-current-files, and must
752
+ not be absolute or contain '.' or '..' path elements.""")) % (exc,),
753
+ log.ErrorCode.file_prefix_error,
754
+ )
755
+ else:
756
+ super().parse_catch_error(exc)
757
+
758
+ def glob_get_sf(self, glob_str, include, ignore_case=False):
759
+ """Return archive-relative selection function based on glob_str."""
760
+ glob_str = self._normalize_archive_pattern(glob_str)
761
+ if glob_str == "**":
762
+ sel_func = lambda path: include
763
+ else:
764
+ sel_func = select_fn_from_glob(glob_str, include, ignore_case)
765
+
766
+ sel_func.exclude = not include
767
+ sel_func.name = (
768
+ f"archive shell glob {include and 'include' or 'exclude'} " f"{ignore_case and 'no-' or ''}case: {glob_str}"
769
+ )
770
+ return sel_func
771
+
772
+ def literal_get_sf(self, lit_str, include, ignore_case=False):
773
+ """Return archive-relative selection function based on literal string."""
774
+ lit_str = self._normalize_archive_pattern(lit_str)
775
+ sel_func = self.select_fn_from_literal(lit_str, include, ignore_case)
776
+ sel_func.exclude = not include
777
+ sel_func.name = (
778
+ f"archive literal string {include and 'include' or 'exclude'} "
779
+ f"{ignore_case and 'no-' or ''}case: {lit_str}"
780
+ )
781
+ return sel_func
782
+
783
+ def exclude_older_get_sf(self, date):
784
+ """Return archive-relative selection function based on archived mtime."""
785
+
786
+ def sel_func(path):
787
+ if not path.isreg():
788
+ return None
789
+ if path.getmtime() < date:
790
+ return 0
791
+ return None
792
+
793
+ sel_func.exclude = True
794
+ sel_func.name = f"Select older than {date}"
795
+ return sel_func
796
+
797
+
798
+ def get_restore_selection(argtuples, filelists):
799
+ """Return an archive-relative selector for restore filtering."""
800
+ sel = ArchiveSelect()
801
+ sel.ParseArgs(argtuples, filelists)
802
+ return sel
803
+
804
+
805
+ def filter_restore_path_iter(path_iter, sel):
806
+ """Yield selected restore ROPaths, including deferred parent directories."""
807
+ deferred_dirs = []
808
+ excluded_dirs = []
809
+ yielded_dirs = set()
810
+
811
+ def is_ancestor(parent_index, child_index):
812
+ return parent_index == child_index[: len(parent_index)]
813
+
814
+ def prune_deferred_dirs(index):
815
+ while deferred_dirs and not is_ancestor(deferred_dirs[-1].index, index):
816
+ deferred_dirs.pop()
817
+
818
+ def prune_excluded_dirs(index):
819
+ while excluded_dirs and not is_ancestor(excluded_dirs[-1], index):
820
+ excluded_dirs.pop()
821
+
822
+ def is_excluded_by_parent(index):
823
+ return excluded_dirs and is_ancestor(excluded_dirs[-1], index)
824
+
825
+ def yield_deferred_dirs():
826
+ for deferred_dir in deferred_dirs:
827
+ if deferred_dir.index not in yielded_dirs:
828
+ yielded_dirs.add(deferred_dir.index)
829
+ yield deferred_dir
830
+ del deferred_dirs[:]
831
+
832
+ def close_unselected(ropath):
833
+ fileobj = getattr(ropath, "fileobj", None)
834
+ if fileobj:
835
+ try:
836
+ fileobj.close()
837
+ except Exception:
838
+ pass
839
+
840
+ for ropath in path_iter:
841
+ prune_deferred_dirs(ropath.index)
842
+ prune_excluded_dirs(ropath.index)
843
+ if is_excluded_by_parent(ropath.index):
844
+ close_unselected(ropath)
845
+ continue
846
+
847
+ if ropath.index == ():
848
+ if ropath.index not in yielded_dirs:
849
+ deferred_dirs.append(ropath)
850
+ continue
851
+
852
+ result = sel.Select(_ArchivePath(ropath))
853
+ if result == 1:
854
+ yield from yield_deferred_dirs()
855
+ if ropath.isdir():
856
+ yielded_dirs.add(ropath.index)
857
+ yield ropath
858
+ elif result == 2 and ropath.isdir() and ropath.index not in yielded_dirs:
859
+ deferred_dirs.append(ropath)
860
+ else:
861
+ if result == 0 and ropath.isdir():
862
+ excluded_dirs.append(ropath.index)
863
+ close_unselected(ropath)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duplicity
3
- Version: 3.0.8.dev6
3
+ Version: 3.1.0.dev0
4
4
  Summary: Encrypted backup using rsync algorithm
5
5
  Author-email: Kenneth Loafman <kenneth@loafman.com>
6
6
  Maintainer: Edgar Soldin, Thomas Laubrock
@@ -44,7 +44,6 @@ Requires-Dist: httplib2
44
44
  Requires-Dist: pysocks
45
45
  Requires-Dist: jottalib
46
46
  Requires-Dist: keyring
47
- Requires-Dist: lxml
48
47
  Requires-Dist: mediafire
49
48
  Requires-Dist: megatools
50
49
  Requires-Dist: paramiko
@@ -74,6 +73,7 @@ sudo apt-get install -y \
74
73
  openssl \
75
74
  par2 \
76
75
  python3-dev \
76
+ python3-lxml \
77
77
  python3-pip \
78
78
  python3-venv \
79
79
  python3 \
@@ -101,7 +101,7 @@ packaging tools like this:
101
101
  ```shell
102
102
  sudo python3 -m pip install --upgrade pip pipx
103
103
  ````
104
- **NOTE: _Failure to upgrade will probably result in a failed install._ <--IMPORTANT!**
104
+ **NOTE: _Failure to upgrade will probably result in a failed install._ IMPORTANT!**
105
105
 
106
106
  To make sure the pipx dirs are on your path do:
107
107
  ```shell
@@ -14,7 +14,6 @@ httplib2
14
14
  pysocks
15
15
  jottalib
16
16
  keyring
17
- lxml
18
17
  mediafire
19
18
  megatools
20
19
  paramiko
@@ -1,4 +1,4 @@
1
- .TH DUPLICITY 1 "May 12, 2026" "Version 3.0.8.dev6" "User Manuals" \" -*- nroff -*-
1
+ .TH DUPLICITY 1 "June 04, 2026" "Version 3.1.0.dev0" "User Manuals" \" -*- nroff -*-
2
2
  .\" disable justification (adjust text to left margin only)
3
3
  .\" command line examples stay readable through that
4
4
  .ad l
@@ -29,7 +29,7 @@ source_url target_directory
29
29
  target_url
30
30
 
31
31
  .B duplicity [restore]
32
- .I [options] [--path-to-restore <relpath>] [--time time]
32
+ .I [options] [--path-to-restore <relpath> | file selection options] [--time time]
33
33
  source_url target_directory
34
34
 
35
35
  .B duplicity remove-older-than <time>
@@ -215,10 +215,21 @@ the other hand if the archive has been deleted or corrupted, this
215
215
  action will not detect it.
216
216
 
217
217
  .TP
218
- .BI "restore, rb " "[--path-to-restore <relpath>] [--time <time>] <url> <target_folder>"
218
+ .BI "restore, rb " "[--path-to-restore <relpath> | file selection options] [--time <time>] <url> <target_folder>"
219
219
  You can restore the full monty or selected folders/files from a specific time.
220
220
  Use the relative path as it is printed by
221
221
  .BR list-current-files .
222
+ .br
223
+ For pattern-based restore filtering, file selection options such as
224
+ .BR --include ,
225
+ .BR --exclude ,
226
+ .BR --include-regexp ,
227
+ and
228
+ .BR --exclude-regexp
229
+ also match paths relative to the archive root, as printed by
230
+ .BR list-current-files .
231
+ These file selection options cannot be combined with
232
+ .BR --path-to-restore .
222
233
  Usually not needed as duplicity enters restore mode when it detects that the URL
223
234
  comes before the local folder.
224
235
 
@@ -1658,6 +1669,25 @@ system, unless
1658
1669
  .B "--files-from"
1659
1670
  has been specified in which case the passed list of individual files
1660
1671
  is used instead.
1672
+ During restore, supported file selection options filter archive-relative
1673
+ paths, matching the names printed by
1674
+ .BR list-current-files ,
1675
+ and are mutually exclusive with
1676
+ .BR --path-to-restore .
1677
+ Restore file selection patterns must be relative to the archive root,
1678
+ may optionally start with
1679
+ .BR ./ ,
1680
+ and must not be absolute or contain
1681
+ .B .
1682
+ or
1683
+ .B ..
1684
+ path elements.
1685
+ Options requiring live source filesystem semantics, such as
1686
+ .BR --exclude-if-present ,
1687
+ .BR --exclude-other-filesystems ,
1688
+ and
1689
+ .BR --files-from ,
1690
+ are not supported during restore.
1661
1691
 
1662
1692
  The file selection system comprises a number of file
1663
1693
  selection conditions, which are set using one of the following command
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "duplicity"
3
- version = "3.0.8.dev6"
3
+ version = "3.1.0.dev0"
4
4
  dynamic = ["dependencies"]
5
5
  description = "Encrypted backup using rsync algorithm"
6
6
  authors = [
@@ -128,14 +128,16 @@ testpaths = [
128
128
 
129
129
 
130
130
  [tool.cibuildwheel]
131
-
132
- before-all = [
133
- # WARNING: wheel builds are RedHat based, not Debian.
134
- "yum update -y",
135
- "yum install -y gcc gcc-c++ make git intltool librsync-devel",
136
- "yum install -y libffi-devel openssl-devel openssl tzdata",
137
- "yum install -y libxml2-devel libxslt-devel",
138
- "python -m pip install -q -r requirements.txt",
131
+ before-build = [
132
+ # Clean up stale build folders and old egg-info before compiling each wheel
133
+ # to build_ext with proper python version. Build any extensions needed.
134
+ # make sure we build_ext with the current python
135
+ "rm -rf build/ *.egg-info",
136
+ "rm -rf .setuptools-cmake-build _skbuild",
137
+ # install all the python requirements but not lxml
138
+ "python -m pip install -q --upgrade pip",
139
+ "grep -v lxml requirements.txt > requirements.whl",
140
+ "python -m pip install -q -r requirements.whl",
139
141
  "python ./setup.py build_ext",
140
142
  ]
141
143
 
@@ -143,15 +145,46 @@ before-all = [
143
145
  build-option = "--use-pep517"
144
146
 
145
147
  [tool.cibuildwheel.linux]
148
+ before-all = [
149
+ # WARNING: wheel builds are RedHat-based, not Debian.
150
+ "yum update -y",
151
+ "yum install -y gcc gcc-c++ make git intltool librsync-devel",
152
+ "yum install -y libffi-devel openssl-devel openssl tzdata",
153
+ "yum install -y libxml2-devel libxslt-devel python3-lxml",
154
+ ]
146
155
  archs = [
147
- "x86_64",
148
156
  "aarch64",
157
+ # "armv7l", # is 32-bit
158
+ # "ppc64le", # lxml build fails
159
+ # "riscv64", # no librsync-devel
160
+ # "s390x", # lxml build fails
161
+ "x86_64",
149
162
  ]
150
163
  build = [
151
164
  "cp{310,311,312,313,314}-manylinux_aarch64",
165
+ # "cp{310,311,312,313,314}-manylinux_armv7l", # is 32-bit
166
+ # "cp{310,311,312,313,314}-manylinux_ppc64le", # lxml build fails
167
+ # "cp{310,311,312,313,314}-manylinux_riscv64", # no librsync-devel
168
+ # "cp{310,311,312,313,314}-manylinux_s390x", # lxml build fails
152
169
  "cp{310,311,312,313,314}-manylinux_x86_64",
153
170
  ]
154
171
 
172
+ [tool.cibuildwheel.macos]
173
+ environment = "LIBRSYNC_DIR=$HOMEBREW_PREFIX MACOSX_DEPLOYMENT_TARGET=26.0"
174
+ before-all = [
175
+ # install packages using homebrew
176
+ "echo $LIBRSYNC_DIR",
177
+ "brew install cmake intltool librsync",
178
+ ]
179
+ archs = [
180
+ "arm64",
181
+ "x86_64",
182
+ ]
183
+ build = [
184
+ "cp{310,311,312,313,314}-macosx_arm64",
185
+ "cp{310,311,312,313,314}-macosx_x86_64",
186
+ ]
187
+
155
188
 
156
189
  [tool.pylint]
157
190
  jobs = 0
@@ -20,7 +20,6 @@ httplib2
20
20
  pysocks # optional httplib2 dep, needed for proxy support
21
21
  jottalib
22
22
  keyring
23
- lxml
24
23
  mediafire
25
24
  megatools
26
25
  paramiko
@@ -38,7 +38,7 @@ elif not ((3, 10) <= sys.version_info[:2] <= (3, 14)):
38
38
  print("Sorry, duplicity requires Python version 3.10 thru 3.14.", file=sys.stderr)
39
39
  sys.exit(1)
40
40
 
41
- Version: str = "3.0.8.dev6"
41
+ Version: str = "3.1.0.dev0"
42
42
 
43
43
  # READTHEDOCS uses setup.py sdist but can't handle extensions
44
44
  ext_modules = list()
File without changes
File without changes