duplicity 3.0.7.dev0__tar.gz → 3.0.8.dev2__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.7.dev0 → duplicity-3.0.8.dev2}/CHANGELOG.md +26 -4
  2. {duplicity-3.0.7.dev0/duplicity.egg-info → duplicity-3.0.8.dev2}/PKG-INFO +1 -1
  3. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/__init__.py +2 -2
  4. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/s3_boto3_backend.py +19 -13
  5. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/webdavbackend.py +7 -3
  6. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/cli_data.py +2 -2
  7. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/cli_util.py +1 -1
  8. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/diffdir.py +2 -22
  9. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/dup_main.py +13 -6
  10. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/log.py +25 -11
  11. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/path.py +1 -22
  12. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/util.py +80 -13
  13. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2/duplicity.egg-info}/PKG-INFO +1 -1
  14. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/man/duplicity.1 +33 -33
  15. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/pyproject.toml +1 -1
  16. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/setup.py +1 -1
  17. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/AUTHORS.md +0 -0
  18. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/COPYING +0 -0
  19. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/README-LOG.md +0 -0
  20. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/README-REPO.md +0 -0
  21. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/README-TESTING.md +0 -0
  22. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/README.md +0 -0
  23. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/__main__.py +0 -0
  24. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/_librsyncmodule.c +0 -0
  25. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/argparse311.py +0 -0
  26. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backend.py +0 -0
  27. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backend_pool.py +0 -0
  28. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/__init__.py +0 -0
  29. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/_cf_cloudfiles.py +0 -0
  30. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/_cf_pyrax.py +0 -0
  31. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/_testbackend.py +0 -0
  32. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/adbackend.py +0 -0
  33. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/azurebackend.py +0 -0
  34. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/b2backend.py +0 -0
  35. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/boxbackend.py +0 -0
  36. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/cfbackend.py +0 -0
  37. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/dpbxbackend.py +0 -0
  38. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/gdocsbackend.py +0 -0
  39. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/gdrivebackend.py +0 -0
  40. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/giobackend.py +0 -0
  41. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/hsibackend.py +0 -0
  42. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/hubicbackend.py +0 -0
  43. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/idrivedbackend.py +0 -0
  44. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/imapbackend.py +0 -0
  45. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/jottacloudbackend.py +0 -0
  46. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/lftpbackend.py +0 -0
  47. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/localbackend.py +0 -0
  48. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/mediafirebackend.py +0 -0
  49. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/megabackend.py +0 -0
  50. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/megav2backend.py +0 -0
  51. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/megav3backend.py +0 -0
  52. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/multibackend.py +0 -0
  53. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/ncftpbackend.py +0 -0
  54. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/onedrivebackend.py +0 -0
  55. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/par2backend.py +0 -0
  56. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/pcabackend.py +0 -0
  57. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/pydrivebackend.py +0 -0
  58. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/pyrax_identity/__init__.py +0 -0
  59. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/pyrax_identity/hubic.py +0 -0
  60. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/rclonebackend.py +0 -0
  61. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/rsyncbackend.py +0 -0
  62. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/slatebackend.py +0 -0
  63. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/ssh_paramiko_backend.py +0 -0
  64. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/ssh_pexpect_backend.py +0 -0
  65. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/swiftbackend.py +0 -0
  66. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/sxbackend.py +0 -0
  67. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/tahoebackend.py +0 -0
  68. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/backends/xorrisobackend.py +0 -0
  69. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/cached_ops.py +0 -0
  70. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/cli_main.py +0 -0
  71. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/config.py +0 -0
  72. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/dup_collections.py +0 -0
  73. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/dup_tarfile.py +0 -0
  74. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/dup_temp.py +0 -0
  75. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/dup_time.py +0 -0
  76. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/errors.py +0 -0
  77. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/file_naming.py +0 -0
  78. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/filechunkio.py +0 -0
  79. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/globmatch.py +0 -0
  80. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/gpg.py +0 -0
  81. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/gpginterface.py +0 -0
  82. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/lazy.py +0 -0
  83. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/librsync.py +0 -0
  84. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/manifest.py +0 -0
  85. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/patchdir.py +0 -0
  86. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/progress.py +0 -0
  87. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/robust.py +0 -0
  88. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/selection.py +0 -0
  89. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/statistics.py +0 -0
  90. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity/tempdir.py +0 -0
  91. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity.egg-info/SOURCES.txt +0 -0
  92. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity.egg-info/dependency_links.txt +0 -0
  93. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity.egg-info/entry_points.txt +0 -0
  94. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity.egg-info/requires.txt +0 -0
  95. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/duplicity.egg-info/top_level.txt +0 -0
  96. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/af_ZA/duplicity.mo +0 -0
  97. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ar_SA/duplicity.mo +0 -0
  98. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ca_ES/duplicity.mo +0 -0
  99. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/cs_CZ/duplicity.mo +0 -0
  100. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/da_DK/duplicity.mo +0 -0
  101. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/de_AT/duplicity.mo +0 -0
  102. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/de_DE/duplicity.mo +0 -0
  103. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/el_GR/duplicity.mo +0 -0
  104. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/en_AU/duplicity.mo +0 -0
  105. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/en_GB/duplicity.mo +0 -0
  106. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/en_PR/duplicity.mo +0 -0
  107. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/en_US/duplicity.mo +0 -0
  108. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/es_EM/duplicity.mo +0 -0
  109. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/es_ES/duplicity.mo +0 -0
  110. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/es_MX/duplicity.mo +0 -0
  111. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/es_PR/duplicity.mo +0 -0
  112. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/es_US/duplicity.mo +0 -0
  113. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/fi_FI/duplicity.mo +0 -0
  114. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/fr_FR/duplicity.mo +0 -0
  115. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/he_IL/duplicity.mo +0 -0
  116. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/hu_HU/duplicity.mo +0 -0
  117. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/it_IT/duplicity.mo +0 -0
  118. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ja_JP/duplicity.mo +0 -0
  119. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ko_KR/duplicity.mo +0 -0
  120. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/nl_BE/duplicity.mo +0 -0
  121. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/nl_NL/duplicity.mo +0 -0
  122. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/nl_SR/duplicity.mo +0 -0
  123. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/no_NO/duplicity.mo +0 -0
  124. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/pl_PL/duplicity.mo +0 -0
  125. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/pt_BR/duplicity.mo +0 -0
  126. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/pt_PT/duplicity.mo +0 -0
  127. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ro_RO/duplicity.mo +0 -0
  128. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ru_BY/duplicity.mo +0 -0
  129. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ru_MD/duplicity.mo +0 -0
  130. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ru_RU/duplicity.mo +0 -0
  131. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/ru_UA/duplicity.mo +0 -0
  132. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/sr_SP/duplicity.mo +0 -0
  133. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/sv_SE/duplicity.mo +0 -0
  134. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/tr_TR/duplicity.mo +0 -0
  135. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/uk_UA/duplicity.mo +0 -0
  136. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/vi_VN/duplicity.mo +0 -0
  137. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/zh_CN/duplicity.mo +0 -0
  138. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/zh_HK/duplicity.mo +0 -0
  139. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/zh_MO/duplicity.mo +0 -0
  140. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/zh_SG/duplicity.mo +0 -0
  141. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/po/zh_TW/duplicity.mo +0 -0
  142. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/requirements.txt +0 -0
  143. {duplicity-3.0.7.dev0 → duplicity-3.0.8.dev2}/setup.cfg +0 -0
@@ -1,10 +1,28 @@
1
1
 
2
- (Unreleased) / 2025-11-26
2
+ (Unreleased) / 2025-12-31
3
3
  =========================
4
4
 
5
- * 29e319fa:chg: Add missing ':' between tags and subject.
6
- * 41e08e43:chg: Use chg|fix|new, not target part.
7
- * 3418970f:chg: Use `git changelog` instead of `gitchangelog`.
5
+
6
+
7
+ rel.3.0.7 / 2025-12-31
8
+ ======================
9
+
10
+ * ad175c22:fix: replace custom deltree with built-in shutil.rmtree.
11
+ * 101be7fd:fix: delete duplicate code in DirDelta.
12
+ * c0d3e72d:fix: webdavs with "--concurrency 1" failed because of missing auth header
13
+ * a08f8bef:fix: disable s3 checksum workaround, warn only...
14
+ * f7055a7a:fix: --log-timestamp no longer working.
15
+ * ac163d5e:fix: Combined fix to related issues 912 and 914
16
+
17
+ rel.3.0.6.3 / 2025-12-05
18
+ ========================
19
+
20
+ * dd45a92d:chg: Update check_tags to use current branch.
21
+ * 5bbb5813:fix: Change log level from Info to Notice in get_passphrase().
22
+ * a9c4fbad:chg: Add key_needs_passphrase(key).
23
+ * abaf8485:chg: Better error message from get_remote_file().
24
+ * b083fca8:fix: 'duplicity --no-check-remote inc' prints spurious warning "found missing difftar(s) in backup sets"
25
+ * 9400fc37:chg: Add check_tags minor fix to setversion.
8
26
  * 29227571:fix: delete unused diffdir.DirSig, diffdir.SigTarBlockIter, librsync.SigFile.
9
27
  * 29205145:fix: Delete unused IndexedTuple.
10
28
  * b0653566:fix: Delete unused Patch, patch_diff_tarfile, PathPatcher.
@@ -444,6 +462,10 @@ rel.2.0.1 / 2023-08-08
444
462
  * fc8da777:fix: Restore pre-parser. Fixes #727.
445
463
  * 295e641b:fix: Add missing import to cli_util.py. Fixes #730.
446
464
  * 97fd0957:fix: Add missing import to b2backend.py. Fixes #729.
465
+
466
+ rel.2.0.0 / 2023-08-07
467
+ ======================
468
+
447
469
  * 5cb97560:fix: Adjust version to build under LP.
448
470
  * 598352ba:fix: Adjust to build under LP Mantic.
449
471
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duplicity
3
- Version: 3.0.7.dev0
3
+ Version: 3.0.8.dev2
4
4
  Summary: Encrypted backup using rsync algorithm
5
5
  Author-email: Kenneth Loafman <kenneth@loafman.com>
6
6
  Maintainer: Edgar Soldin, Thomas Laubrock
@@ -21,7 +21,7 @@
21
21
 
22
22
  import gettext
23
23
 
24
- __version__: str = "3.0.7.dev0"
25
- __reldate__: str = "November 20, 2025"
24
+ __version__: str = "3.0.8.dev2"
25
+ __reldate__: str = "January 14, 2026"
26
26
 
27
27
  gettext.install("duplicity", names=["ngettext"])
@@ -74,19 +74,6 @@ class S3Boto3Backend(duplicity.backend.Backend):
74
74
  from boto3.s3.transfer import S3UploadFailedError, TransferConfig
75
75
  from botocore.exceptions import ClientError
76
76
 
77
- if not (boto3.__version__ < "1.36.0" and botocore.__version__ < "1.36.0"):
78
- # TODO: remove this workaround when issue #870 is fixed.
79
- # https://github.com/boto/boto3/issues/2913
80
- log.Warn(
81
- "WARNING: Using boto3 >= 1,36.0 may result in errors, so we qre applying\n"
82
- "the workaround for https://gitlab.com/duplicity/duplicity/-/issues/870\n"
83
- " export AWS_REQUEST_CHECKSUM_CALCULATION=when_required\n"
84
- " export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required\n"
85
- "NOTE: This workaround is temporary and will be removed when issue is fixed.\n."
86
- )
87
- os.environ["AWS_REQUEST_CHECKSUM_CALCULATION"] = "when_required"
88
- os.environ["AWS_RESPONSE_CHECKSUM_VALIDATION"] = "when_required"
89
-
90
77
  duplicity.backend.Backend.__init__(self, parsed_url)
91
78
 
92
79
  # This folds the null prefix and all null parts, which means that:
@@ -109,6 +96,25 @@ class S3Boto3Backend(duplicity.backend.Backend):
109
96
  self.bucket = None
110
97
  self.tracker = UploadProgressTracker()
111
98
 
99
+ if not (boto3.__version__ < "1.36.0" and botocore.__version__ < "1.36.0"):
100
+ # this is an issue with 3rd party s3 implementations only
101
+ # likely when an endpoint is given that resides not under amazonaws.com
102
+ # in time that workaround will probably not be needed anymore
103
+ # https://github.com/boto/boto3/issues/2913
104
+ import re
105
+
106
+ if config.s3_endpoint_url and not re.match(
107
+ pattern="(?i).*\\.amazonaws\\.com(/+)?$", string=config.s3_endpoint_url
108
+ ):
109
+ log.Warn(
110
+ "WARNING: Using boto3 >= 1,36.0 with non-amazon s3 services"
111
+ " may result in checksum errors."
112
+ " a workaround is to set the following env vars\n\n"
113
+ " export AWS_REQUEST_CHECKSUM_CALCULATION=when_required\n"
114
+ " export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required\n\n"
115
+ "see https://gitlab.com/duplicity/duplicity/-/issues/870 for details."
116
+ )
117
+
112
118
  def reset_connection(self):
113
119
  self.bucket = None
114
120
  self.s3 = boto3.resource(
@@ -194,9 +194,10 @@ class WebDAVBackend(duplicity.backend.Backend):
194
194
  if self.username or self.password:
195
195
  # Workaround cpython http.client issue
196
196
  # https://github.com/python/cpython/issues/70107
197
- self.conn.request("OPTIONS", self.directory, None)
198
- response = self.conn.getresponse()
199
- response.read()
197
+ # PUT may not return 401 when ran without basic-auth but throw SSL-EOF-Error or hang
198
+ # as a workaround we run an OPTIONS request that adds auth if needed and creates
199
+ # an authenticated connection to (re)use
200
+ response = self.request("OPTIONS", self.directory, None)
200
201
  response.close()
201
202
 
202
203
  def _close(self):
@@ -226,6 +227,8 @@ class WebDAVBackend(duplicity.backend.Backend):
226
227
 
227
228
  if self.digest_challenge is not None:
228
229
  self.headers["Authorization"] = self.get_digest_authorization(path)
230
+ elif self.username or self.password:
231
+ self.headers["Authorization"] = self.get_basic_authorization()
229
232
 
230
233
  log.Debug(_("WebDAV %s %s request with headers: %s ") % (method, quoted_path, munge_headers(self.headers)))
231
234
  log.Debug(_("WebDAV data length: %s ") % sys.getsizeof(data))
@@ -245,6 +248,7 @@ class WebDAVBackend(duplicity.backend.Backend):
245
248
  return self.request(method, self.directory, data, redirected + 1)
246
249
  else:
247
250
  raise FatalBackendException(_("WebDAV missing location header in redirect response."))
251
+ # mainly for digest-auth to recalculate with response values
248
252
  elif response.status == 401:
249
253
  response.read()
250
254
  response.close()
@@ -446,9 +446,9 @@ OptionKwargs = dict(
446
446
  type=set_log_file,
447
447
  help="Logging filename to use",
448
448
  ),
449
- # log_timestamp is directly applied in SetLogTimestampAction(), not saved in config
449
+ # log_timestamp is directly applied in set_log_timestamp(), not saved in config
450
450
  log_timestamp=dict(
451
- dest="",
451
+ nargs=0,
452
452
  action=SetLogTimestampAction,
453
453
  help="Whether to include timestamp and level in log",
454
454
  default=dflt(False),
@@ -160,7 +160,7 @@ class SetLogTimestampAction(argparse._StoreConstAction):
160
160
  super().__init__(option_strings, dest, **kwargs)
161
161
 
162
162
  def __call__(self, parser, namespace, values, option_string=None):
163
- log._log_timestamp = True
163
+ log.add_timestamp()
164
164
 
165
165
 
166
166
  def _check_int(val):
@@ -37,7 +37,7 @@ from duplicity import dup_tarfile
37
37
  from duplicity import util
38
38
  from duplicity.path import * # pylint: disable=unused-wildcard-import,redefined-builtin
39
39
 
40
- # A StatsObj will be written to this from DirDelta and DirDelta_WriteSig.
40
+ # A StatsObj will be written to this from DirDelta_WriteSig.
41
41
  stats = None
42
42
  tracker = None
43
43
 
@@ -55,7 +55,7 @@ def DirFull(path_iter):
55
55
  will be easy to split up the tar and make the volumes the same
56
56
  sizes).
57
57
  """
58
- return DirDelta(path_iter, io.StringIO(""))
58
+ return DirDelta_WriteSig(path_iter, io.StringIO(""), None)
59
59
 
60
60
 
61
61
  def DirFull_WriteSig(path_iter, sig_outfp):
@@ -65,26 +65,6 @@ def DirFull_WriteSig(path_iter, sig_outfp):
65
65
  return DirDelta_WriteSig(path_iter, io.StringIO(""), sig_outfp)
66
66
 
67
67
 
68
- def DirDelta(path_iter, dirsig_fileobj_list):
69
- """
70
- Produce tarblock diff given dirsig_fileobj_list and pathiter
71
-
72
- dirsig_fileobj_list should either be a tar fileobj or a list of
73
- those, sorted so the most recent is last.
74
- """
75
- global stats
76
- stats = statistics.StatsDeltaProcess()
77
- if isinstance(dirsig_fileobj_list, list):
78
- sig_iter = combine_path_iters([sigtar2path_iter(x) for x in dirsig_fileobj_list])
79
- else:
80
- sig_iter = sigtar2path_iter(dirsig_fileobj_list)
81
- delta_iter = get_delta_iter(path_iter, sig_iter)
82
- if config.dry_run or (config.progress and not progress.tracker.has_collected_evidence()):
83
- return DummyBlockIter(delta_iter)
84
- else:
85
- return DeltaTarBlockIter(delta_iter)
86
-
87
-
88
68
  def delta_iter_error_handler(exc, new_path, sig_path, sig_tar=None): # pylint: disable=unused-argument
89
69
  """
90
70
  Called by get_delta_iter, report error in getting delta
@@ -132,24 +132,31 @@ def get_passphrase(n, action, for_signing=False):
132
132
  log.Notice(_("Reuse configured SIGN_PASSPHRASE as PASSPHRASE"))
133
133
  return os.environ["SIGN_PASSPHRASE"]
134
134
 
135
- # Not in the environment, check if encryption passphrase is needed
135
+ # no passphrase if --no-encryption or --use-agent
136
+ if not config.encryption or config.use_agent:
137
+ return ""
138
+
139
+ # no passphrase if --passphrase* in --gpg-options
140
+ if "--passphrase" in config.gpg_options:
141
+ return ""
142
+
143
+ # Check if encryption passphrase is needed
136
144
  asymmetric = False
137
145
  need_passphrase = False
138
146
  profile = config.gpg_profile
139
147
  encrypt_keys = profile.recipients + profile.hidden_recipients
140
148
  if profile.sign_key:
141
149
  encrypt_keys.append(profile.sign_key)
142
- if encrypt_keys:
150
+ if encrypt_keys and config.check_remote:
143
151
  asymmetric = True
144
152
  for key in encrypt_keys:
145
- if util.key_needs_passphrase(key):
153
+ if util.key_needs_passphrase(config.gpg_binary, key):
146
154
  log.Notice(f"Key {key} needs passphrase.")
147
155
  need_passphrase = True
148
156
  break
149
157
  else:
150
158
  log.Notice("No encryption keys need passphrase.")
151
159
  else:
152
- symmetric = True
153
160
  need_passphrase = True
154
161
  log.Notice("No encryption keys configured.")
155
162
 
@@ -758,7 +765,7 @@ def incremental_backup(sig_chain, col_stats=None):
758
765
  if config.progress:
759
766
  progress.tracker = progress.ProgressTracker()
760
767
  # Fake a backup to compute total of moving bytes
761
- tarblock_iter = diffdir.DirDelta(config.select, sig_chain.get_fileobjs())
768
+ tarblock_iter = diffdir.DirDelta_WriteSig(config.select, sig_chain.get_fileobjs(), None)
762
769
  dummy_backup(tarblock_iter)
763
770
  # Store computed stats to compute progress later
764
771
  progress.tracker.set_evidence(diffdir.stats, False)
@@ -768,7 +775,7 @@ def incremental_backup(sig_chain, col_stats=None):
768
775
  progress.progress_thread = progress.LogProgressThread()
769
776
 
770
777
  if config.dry_run:
771
- tarblock_iter = diffdir.DirDelta(config.select, sig_chain.get_fileobjs())
778
+ tarblock_iter = diffdir.DirDelta_WriteSig(config.select, sig_chain.get_fileobjs(), None)
772
779
  bytes_written = dummy_backup(tarblock_iter)
773
780
  else:
774
781
  new_sig_outfp = get_sig_fileobj("new-sig")
@@ -40,7 +40,6 @@ MAX = 9
40
40
  PREFIX = ""
41
41
 
42
42
  _logger = None
43
- _log_timestamp = False
44
43
 
45
44
 
46
45
  def DupToLoggerLevel(verb):
@@ -321,7 +320,6 @@ def setup():
321
320
  Initialize logging
322
321
  """
323
322
  global _logger
324
- global _log_timestamp
325
323
  if _logger:
326
324
  return
327
325
 
@@ -333,18 +331,12 @@ def setup():
333
331
 
334
332
  # stdout and stderr are for different logging levels
335
333
  outHandler = logging.StreamHandler(sys.stdout)
336
- if _log_timestamp:
337
- outHandler.setFormatter(DetailFormatter())
338
- else:
339
- outHandler.setFormatter(PrettyProgressFormatter())
334
+ outHandler.setFormatter(PrettyProgressFormatter())
340
335
  outHandler.addFilter(OutFilter())
341
336
  _logger.addHandler(outHandler)
342
337
 
343
338
  errHandler = logging.StreamHandler(sys.stderr)
344
- if _log_timestamp:
345
- errHandler.setFormatter(DetailFormatter())
346
- else:
347
- errHandler.setFormatter(PrettyProgressFormatter())
339
+ errHandler.setFormatter(PrettyProgressFormatter())
348
340
  errHandler.addFilter(ErrFilter())
349
341
  _logger.addHandler(errHandler)
350
342
 
@@ -388,7 +380,7 @@ class DetailFormatter(logging.Formatter):
388
380
  # standard 'levelname'. This is because the standard 'levelname' can
389
381
  # be adjusted by any library anywhere in our stack without us knowing.
390
382
  # But we control 'levelName'.
391
- logging.Formatter.__init__(self, "%(asctime)s %(levelName)s %(message)s")
383
+ logging.Formatter.__init__(self, "%(asctime)s %(levelName)-6s %(message)s")
392
384
 
393
385
  def format(self, record):
394
386
  s = logging.Formatter.format(self, record)
@@ -452,6 +444,28 @@ def add_file(filename):
452
444
  _logger.addHandler(handler)
453
445
 
454
446
 
447
+ def add_timestamp():
448
+ """
449
+ Add timestamp to logs written
450
+ """
451
+ global _logger
452
+
453
+ # remove all handlers
454
+ for handler in _logger.handlers[:]:
455
+ _logger.removeHandler(handler)
456
+
457
+ # stdout and stderr are for different logging levels
458
+ outHandler = logging.StreamHandler(sys.stdout)
459
+ outHandler.setFormatter(DetailFormatter())
460
+ outHandler.addFilter(OutFilter())
461
+ _logger.addHandler(outHandler)
462
+
463
+ errHandler = logging.StreamHandler(sys.stderr)
464
+ errHandler.setFormatter(DetailFormatter())
465
+ errHandler.addFilter(ErrFilter())
466
+ _logger.addHandler(errHandler)
467
+
468
+
455
469
  def setverbosity(verb):
456
470
  """
457
471
  Set the verbosity level.
@@ -623,13 +623,8 @@ class Path(ROPath):
623
623
 
624
624
  def deltree(self):
625
625
  """Remove self by recursively deleting files under it"""
626
- from duplicity import selection # TODO: avoid circ. dep. issue
627
-
628
626
  log.Debug(_("Deleting tree %s") % self.uc_name)
629
- itr = IterTreeReducer(PathDeleter, [])
630
- for path in selection.Select(self).set_iter():
631
- itr(path.index, path)
632
- itr.Finish()
627
+ shutil.rmtree(self.name)
633
628
  self.setdata()
634
629
 
635
630
  def get_parent_dir(self):
@@ -807,19 +802,3 @@ class DupPath(Path):
807
802
  return gpg.GPGFile(True, self, gpg_profile)
808
803
  else:
809
804
  return self.open(mode)
810
-
811
-
812
- class PathDeleter(ITRBranch):
813
- """Delete a directory. Called by Path.deltree"""
814
-
815
- def start_process(self, index, path): # pylint: disable=unused-argument
816
- self.path = path
817
-
818
- def end_process(self):
819
- self.path.delete()
820
-
821
- def can_fast_process(self, index, path): # pylint: disable=unused-argument
822
- return not path.isdir()
823
-
824
- def fast_process(self, index, path): # pylint: disable=unused-argument
825
- path.delete()
@@ -27,11 +27,13 @@ import atexit
27
27
  import csv
28
28
  import errno
29
29
  import json
30
+ import locale
30
31
  import multiprocessing
31
32
  import os
32
33
  import socket
33
34
  import sys
34
35
  import traceback
36
+ from contextlib import contextmanager
35
37
  from io import StringIO
36
38
 
37
39
  import fasteners
@@ -203,28 +205,93 @@ def release_lockfile():
203
205
  pass
204
206
 
205
207
 
206
- def key_needs_passphrase(key):
208
+ def key_needs_passphrase(gpgbin, key, logfile=None):
207
209
  """
208
- Check if a key needs a passphrase.
210
+ Determine whether a GnuPG key requires a passphrase.
211
+
212
+ This helper invokes the specified GnuPG frontend in a non‑destructive
213
+ way to discover if the secret key is protected by a passphrase. It uses
214
+ `pexpect` to spawn the command and watch for prompts or agent errors,
215
+ never changing the key material itself.
216
+
217
+ How it works
218
+ - Runs: ``<gpgbin> --pinentry-mode cancel --dry-run --change-passphrase <key>``
219
+ with a C UTF‑8 locale to ensure predictable output.
220
+ - Interprets the interaction:
221
+ - If the process reaches EOF without a passphrase prompt, the key is
222
+ considered not to need a passphrase.
223
+ - If a passphrase prompt appears (matches ``passphrase.*:``), the key
224
+ is considered to need a passphrase.
225
+ - If ``gpg-agent`` fails to start or ignores an inquiry, we log an
226
+ error and return ``None`` to signal an indeterminate result.
227
+
228
+ Parameters
229
+ - gpgbin: str
230
+ The GnuPG command to execute, e.g. ``"gpg"`` or ``"gpgsm"``.
231
+ - key: str
232
+ The key identifier understood by the given binary. Examples:
233
+ - For ``gpg`` (OpenPGP): a key ID or fingerprint, e.g. ``"56538CCF"``.
234
+ - For ``gpgsm`` (S/MIME): a certificate keyref, e.g.
235
+ ``"\\&165F2FB4F58D..."``.
236
+ - logfile: a file-like object or ``None``
237
+ If provided, raw pexpect I/O is mirrored to this stream for debugging
238
+ (e.g. ``sys.stdout``). Defaults to ``None``.
239
+
240
+ Returns
241
+ - ``True`` if the key requires a passphrase.
242
+ - ``False`` if the key does not require a passphrase.
243
+ - ``None`` if the status cannot be determined due to a runtime error
244
+ (e.g., agent failed to start or pexpect raised an exception).
245
+
246
+ Notes
247
+ - The check is read‑only: ``--dry-run`` and ``--pinentry-mode cancel`` are
248
+ used to avoid modifying the key or prompting the user.
249
+ - Environment variables ``LANG`` and ``LC_ALL`` are forced to ``C.utf8``
250
+ to make output matching stable across locales.
251
+ - For end‑to‑end manual verification with the repository’s test keyring,
252
+ see ``testing/manual/needspass.py``.
209
253
  """
254
+
255
+ environ = {**os.environ, "LANG": "C.utf8", "LC_ALL": "C.utf8"}
256
+ cmd = f"{gpgbin} --pinentry-mode cancel --dry-run --change-passphrase {key} "
257
+
258
+ log.Debug(f"{cmd=}")
259
+
210
260
  try:
211
- child = pexpect.spawn("gpg", f"--pinentry-mode=loopback --dry-run --passwd {key}".split())
212
- except Exception:
213
- log.FatalError(f"Exception spawning gpg while checking if passphrase needed for key: {key}")
261
+ child = pexpect.spawn(cmd, encoding="utf-8", env=environ)
262
+ child.logfile = logfile
263
+ except pexpect.ExceptionPexpect as e:
264
+ log.Error(f"An unexpected error occurred: {e}")
265
+ return None
214
266
 
215
267
  try:
216
- got = child.expect(["passphrase.*:", pexpect.EOF])
217
- except Exception:
218
- log.FatalError(f"Exception while checking if passphrase needed for key: {key}: {str(child)}")
268
+ got = child.expect(
269
+ [
270
+ pexpect.EOF,
271
+ "passphrase.*:",
272
+ "failed to start gpg-agent",
273
+ "ignoring gpg-agent inquiry",
274
+ ]
275
+ )
276
+ except pexpect.ExceptionPexpect as e:
277
+ log.Error(f"Exception while checking if passphrase needed for: {key}:\n{e}")
278
+ return None
279
+
280
+ child.close()
281
+ log.Debug(f"{child.exitstatus=}, {child.signalstatus=}, {got=}, {child.after=}")
219
282
 
220
283
  if got == 0:
221
- log.Debug(f"Key {key} needs passphrase")
222
- child.close()
223
- return True
224
- elif got == 1:
225
284
  log.Debug(f"Key {key} does not need passphrase")
226
285
  return False
227
- return None
286
+ elif got == 1:
287
+ log.Debug(f"Key {key} needs passphrase")
288
+ return True
289
+ elif got == 2:
290
+ log.Error(f"gpg-agent failed to start.")
291
+ return None
292
+ elif got == 3:
293
+ log.Error(f"gpg-agent failed inquiry ignored.")
294
+ return None
228
295
 
229
296
 
230
297
  def copyfileobj(infp, outfp, byte_count=-1):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duplicity
3
- Version: 3.0.7.dev0
3
+ Version: 3.0.8.dev2
4
4
  Summary: Encrypted backup using rsync algorithm
5
5
  Author-email: Kenneth Loafman <kenneth@loafman.com>
6
6
  Maintainer: Edgar Soldin, Thomas Laubrock
@@ -1,4 +1,4 @@
1
- .TH DUPLICITY 1 "November 20, 2025" "Version 3.0.7.dev0" "User Manuals" \" -*- nroff -*-
1
+ .TH DUPLICITY 1 "January 14, 2026" "Version 3.0.8.dev2" "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
@@ -497,23 +497,6 @@ See the
497
497
  .B FILE SELECTION
498
498
  section for more information.
499
499
 
500
- .TP
501
- .BI "--files-from " filename
502
- Read a list of files to backup from filename rather than searching the entire
503
- backup source directory. Operation is otherwise normal, just on the specified
504
- subset of the backup source directory.
505
-
506
- Files must be specified one per line and relative to the backup source
507
- directory. Any absolute paths will raise an error. All characters per line are
508
- significant and treated as part of the path, including leading and trailing
509
- whitespace. Lines are separated by newlines or nulls, depending on whether the
510
- .B "--null-separator"
511
- switch was given.
512
-
513
- It is not necessary to include the parent directory of listed files, their
514
- inclusion is implied. However, the content of any explicitly listed directories
515
- is not implied. All required files must be listed when this option is used.
516
-
517
500
  .TP
518
501
  .BI "--file-prefix " prefix
519
502
  .PD 0
@@ -535,12 +518,21 @@ See also
535
518
  .B "A NOTE ON FILENAME PREFIXES"
536
519
 
537
520
  .TP
538
- .BI "--path-to-restore " path
539
- This option may be given in restore mode, causing only
540
- .I path
541
- to be restored instead of the entire contents of the backup archive.
542
- .I path
543
- should be given relative to the root of the directory backed up.
521
+ .BI "--files-from " filename
522
+ Read a list of files to backup from filename rather than searching the entire
523
+ backup source directory. Operation is otherwise normal, just on the specified
524
+ subset of the backup source directory.
525
+
526
+ Files must be specified one per line and relative to the backup source
527
+ directory. Any absolute paths will raise an error. All characters per line are
528
+ significant and treated as part of the path, including leading and trailing
529
+ whitespace. Lines are separated by newlines or nulls, depending on whether the
530
+ .B "--null-separator"
531
+ switch was given.
532
+
533
+ It is not necessary to include the parent directory of listed files, their
534
+ inclusion is implied. However, the content of any explicitly listed directories
535
+ is not implied. All required files must be listed when this option is used.
544
536
 
545
537
  .TP
546
538
  .BI --filter-globbing
@@ -572,15 +564,6 @@ See the
572
564
  .B FILE SELECTION
573
565
  section for more information.
574
566
 
575
- .TP
576
- .BI "--full-if-older-than " time
577
- Perform a full backup if an incremental backup is requested, but the
578
- latest full backup in the collection is older than the given
579
- .IR time .
580
- See the
581
- .B TIME FORMATS
582
- section for more information.
583
-
584
567
  .TP
585
568
  .BI --force
586
569
  Proceed even if data loss might result. Duplicity will let the user
@@ -596,6 +579,15 @@ out.
596
579
  .BI --ftp-regular
597
580
  Use regular (PORT) data connections.
598
581
 
582
+ .TP
583
+ .BI "--full-if-older-than " time
584
+ Perform a full backup if an incremental backup is requested, but the
585
+ latest full backup in the collection is older than the given
586
+ .IR time .
587
+ See the
588
+ .B TIME FORMATS
589
+ section for more information.
590
+
599
591
  .TP
600
592
  .BI --gio
601
593
  Use the GIO backend and interpret any URLs as GIO would.
@@ -862,6 +854,14 @@ for Par2 recovery files (default 10%).
862
854
  .BI "--par2-volumes " number
863
855
  Number of Par2 volumes to create (default 1).
864
856
 
857
+ .TP
858
+ .BI "--path-to-restore " path
859
+ This option may be given in restore mode, causing only
860
+ .I path
861
+ to be restored instead of the entire contents of the backup archive.
862
+ .I path
863
+ should be given relative to the root of the directory backed up.
864
+
865
865
  .TP
866
866
  .BI --progress
867
867
  When selected, duplicity will output the current upload progress and estimated
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "duplicity"
3
- version = "3.0.7.dev0"
3
+ version = "3.0.8.dev2"
4
4
  dynamic = ["dependencies"]
5
5
  description = "Encrypted backup using rsync algorithm"
6
6
  authors = [
@@ -42,7 +42,7 @@ elif not ((3, 9) <= sys.version_info[:2] <= (3, 14)):
42
42
  print("Sorry, duplicity requires version 3.9 thru 3.14 of Python.", file=sys.stderr)
43
43
  sys.exit(1)
44
44
 
45
- Version: str = "3.0.7.dev0"
45
+ Version: str = "3.0.8.dev2"
46
46
 
47
47
  # READTHEDOCS uses setup.py sdist but can't handle extensions
48
48
  ext_modules = list()
File without changes
File without changes
File without changes