duplicity 3.0.7__tar.gz → 3.0.7.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.7 → duplicity-3.0.7.dev0}/CHANGELOG.md +4 -26
  2. {duplicity-3.0.7/duplicity.egg-info → duplicity-3.0.7.dev0}/PKG-INFO +1 -1
  3. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/__init__.py +2 -2
  4. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/s3_boto3_backend.py +13 -19
  5. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/webdavbackend.py +3 -7
  6. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/cli_data.py +2 -2
  7. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/cli_util.py +1 -1
  8. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/diffdir.py +22 -2
  9. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/dup_main.py +6 -13
  10. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/log.py +11 -25
  11. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/path.py +22 -1
  12. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/util.py +13 -80
  13. {duplicity-3.0.7 → duplicity-3.0.7.dev0/duplicity.egg-info}/PKG-INFO +1 -1
  14. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/man/duplicity.1 +33 -33
  15. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/pyproject.toml +1 -1
  16. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/setup.py +1 -1
  17. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/AUTHORS.md +0 -0
  18. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/COPYING +0 -0
  19. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/README-LOG.md +0 -0
  20. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/README-REPO.md +0 -0
  21. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/README-TESTING.md +0 -0
  22. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/README.md +0 -0
  23. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/__main__.py +0 -0
  24. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/_librsyncmodule.c +0 -0
  25. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/argparse311.py +0 -0
  26. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backend.py +0 -0
  27. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backend_pool.py +0 -0
  28. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/__init__.py +0 -0
  29. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/_cf_cloudfiles.py +0 -0
  30. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/_cf_pyrax.py +0 -0
  31. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/_testbackend.py +0 -0
  32. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/adbackend.py +0 -0
  33. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/azurebackend.py +0 -0
  34. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/b2backend.py +0 -0
  35. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/boxbackend.py +0 -0
  36. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/cfbackend.py +0 -0
  37. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/dpbxbackend.py +0 -0
  38. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/gdocsbackend.py +0 -0
  39. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/gdrivebackend.py +0 -0
  40. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/giobackend.py +0 -0
  41. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/hsibackend.py +0 -0
  42. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/hubicbackend.py +0 -0
  43. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/idrivedbackend.py +0 -0
  44. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/imapbackend.py +0 -0
  45. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/jottacloudbackend.py +0 -0
  46. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/lftpbackend.py +0 -0
  47. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/localbackend.py +0 -0
  48. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/mediafirebackend.py +0 -0
  49. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/megabackend.py +0 -0
  50. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/megav2backend.py +0 -0
  51. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/megav3backend.py +0 -0
  52. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/multibackend.py +0 -0
  53. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/ncftpbackend.py +0 -0
  54. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/onedrivebackend.py +0 -0
  55. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/par2backend.py +0 -0
  56. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/pcabackend.py +0 -0
  57. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/pydrivebackend.py +0 -0
  58. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/pyrax_identity/__init__.py +0 -0
  59. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/pyrax_identity/hubic.py +0 -0
  60. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/rclonebackend.py +0 -0
  61. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/rsyncbackend.py +0 -0
  62. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/slatebackend.py +0 -0
  63. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/ssh_paramiko_backend.py +0 -0
  64. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/ssh_pexpect_backend.py +0 -0
  65. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/swiftbackend.py +0 -0
  66. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/sxbackend.py +0 -0
  67. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/tahoebackend.py +0 -0
  68. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/backends/xorrisobackend.py +0 -0
  69. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/cached_ops.py +0 -0
  70. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/cli_main.py +0 -0
  71. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/config.py +0 -0
  72. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/dup_collections.py +0 -0
  73. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/dup_tarfile.py +0 -0
  74. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/dup_temp.py +0 -0
  75. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/dup_time.py +0 -0
  76. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/errors.py +0 -0
  77. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/file_naming.py +0 -0
  78. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/filechunkio.py +0 -0
  79. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/globmatch.py +0 -0
  80. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/gpg.py +0 -0
  81. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/gpginterface.py +0 -0
  82. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/lazy.py +0 -0
  83. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/librsync.py +0 -0
  84. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/manifest.py +0 -0
  85. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/patchdir.py +0 -0
  86. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/progress.py +0 -0
  87. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/robust.py +0 -0
  88. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/selection.py +0 -0
  89. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/statistics.py +0 -0
  90. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity/tempdir.py +0 -0
  91. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity.egg-info/SOURCES.txt +0 -0
  92. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity.egg-info/dependency_links.txt +0 -0
  93. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity.egg-info/entry_points.txt +0 -0
  94. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity.egg-info/requires.txt +0 -0
  95. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/duplicity.egg-info/top_level.txt +0 -0
  96. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/af_ZA/duplicity.mo +0 -0
  97. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ar_SA/duplicity.mo +0 -0
  98. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ca_ES/duplicity.mo +0 -0
  99. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/cs_CZ/duplicity.mo +0 -0
  100. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/da_DK/duplicity.mo +0 -0
  101. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/de_AT/duplicity.mo +0 -0
  102. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/de_DE/duplicity.mo +0 -0
  103. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/el_GR/duplicity.mo +0 -0
  104. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/en_AU/duplicity.mo +0 -0
  105. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/en_GB/duplicity.mo +0 -0
  106. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/en_PR/duplicity.mo +0 -0
  107. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/en_US/duplicity.mo +0 -0
  108. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/es_EM/duplicity.mo +0 -0
  109. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/es_ES/duplicity.mo +0 -0
  110. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/es_MX/duplicity.mo +0 -0
  111. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/es_PR/duplicity.mo +0 -0
  112. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/es_US/duplicity.mo +0 -0
  113. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/fi_FI/duplicity.mo +0 -0
  114. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/fr_FR/duplicity.mo +0 -0
  115. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/he_IL/duplicity.mo +0 -0
  116. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/hu_HU/duplicity.mo +0 -0
  117. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/it_IT/duplicity.mo +0 -0
  118. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ja_JP/duplicity.mo +0 -0
  119. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ko_KR/duplicity.mo +0 -0
  120. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/nl_BE/duplicity.mo +0 -0
  121. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/nl_NL/duplicity.mo +0 -0
  122. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/nl_SR/duplicity.mo +0 -0
  123. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/no_NO/duplicity.mo +0 -0
  124. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/pl_PL/duplicity.mo +0 -0
  125. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/pt_BR/duplicity.mo +0 -0
  126. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/pt_PT/duplicity.mo +0 -0
  127. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ro_RO/duplicity.mo +0 -0
  128. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ru_BY/duplicity.mo +0 -0
  129. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ru_MD/duplicity.mo +0 -0
  130. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ru_RU/duplicity.mo +0 -0
  131. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/ru_UA/duplicity.mo +0 -0
  132. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/sr_SP/duplicity.mo +0 -0
  133. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/sv_SE/duplicity.mo +0 -0
  134. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/tr_TR/duplicity.mo +0 -0
  135. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/uk_UA/duplicity.mo +0 -0
  136. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/vi_VN/duplicity.mo +0 -0
  137. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/zh_CN/duplicity.mo +0 -0
  138. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/zh_HK/duplicity.mo +0 -0
  139. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/zh_MO/duplicity.mo +0 -0
  140. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/zh_SG/duplicity.mo +0 -0
  141. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/po/zh_TW/duplicity.mo +0 -0
  142. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/requirements.txt +0 -0
  143. {duplicity-3.0.7 → duplicity-3.0.7.dev0}/setup.cfg +0 -0
@@ -1,28 +1,10 @@
1
1
 
2
- (Unreleased) / 2025-12-31
2
+ (Unreleased) / 2025-11-26
3
3
  =========================
4
4
 
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.
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`.
26
8
  * 29227571:fix: delete unused diffdir.DirSig, diffdir.SigTarBlockIter, librsync.SigFile.
27
9
  * 29205145:fix: Delete unused IndexedTuple.
28
10
  * b0653566:fix: Delete unused Patch, patch_diff_tarfile, PathPatcher.
@@ -462,10 +444,6 @@ rel.2.0.1 / 2023-08-08
462
444
  * fc8da777:fix: Restore pre-parser. Fixes #727.
463
445
  * 295e641b:fix: Add missing import to cli_util.py. Fixes #730.
464
446
  * 97fd0957:fix: Add missing import to b2backend.py. Fixes #729.
465
-
466
- rel.2.0.0 / 2023-08-07
467
- ======================
468
-
469
447
  * 5cb97560:fix: Adjust version to build under LP.
470
448
  * 598352ba:fix: Adjust to build under LP Mantic.
471
449
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: duplicity
3
- Version: 3.0.7
3
+ Version: 3.0.7.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
@@ -21,7 +21,7 @@
21
21
 
22
22
  import gettext
23
23
 
24
- __version__: str = "3.0.7"
25
- __reldate__: str = "December 31, 2025"
24
+ __version__: str = "3.0.7.dev0"
25
+ __reldate__: str = "November 20, 2025"
26
26
 
27
27
  gettext.install("duplicity", names=["ngettext"])
@@ -74,6 +74,19 @@ 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
+
77
90
  duplicity.backend.Backend.__init__(self, parsed_url)
78
91
 
79
92
  # This folds the null prefix and all null parts, which means that:
@@ -96,25 +109,6 @@ class S3Boto3Backend(duplicity.backend.Backend):
96
109
  self.bucket = None
97
110
  self.tracker = UploadProgressTracker()
98
111
 
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
-
118
112
  def reset_connection(self):
119
113
  self.bucket = None
120
114
  self.s3 = boto3.resource(
@@ -194,10 +194,9 @@ 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
- # 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)
197
+ self.conn.request("OPTIONS", self.directory, None)
198
+ response = self.conn.getresponse()
199
+ response.read()
201
200
  response.close()
202
201
 
203
202
  def _close(self):
@@ -227,8 +226,6 @@ class WebDAVBackend(duplicity.backend.Backend):
227
226
 
228
227
  if self.digest_challenge is not None:
229
228
  self.headers["Authorization"] = self.get_digest_authorization(path)
230
- elif self.username or self.password:
231
- self.headers["Authorization"] = self.get_basic_authorization()
232
229
 
233
230
  log.Debug(_("WebDAV %s %s request with headers: %s ") % (method, quoted_path, munge_headers(self.headers)))
234
231
  log.Debug(_("WebDAV data length: %s ") % sys.getsizeof(data))
@@ -248,7 +245,6 @@ class WebDAVBackend(duplicity.backend.Backend):
248
245
  return self.request(method, self.directory, data, redirected + 1)
249
246
  else:
250
247
  raise FatalBackendException(_("WebDAV missing location header in redirect response."))
251
- # mainly for digest-auth to recalculate with response values
252
248
  elif response.status == 401:
253
249
  response.read()
254
250
  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 set_log_timestamp(), not saved in config
449
+ # log_timestamp is directly applied in SetLogTimestampAction(), not saved in config
450
450
  log_timestamp=dict(
451
- nargs=0,
451
+ dest="",
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.add_timestamp()
163
+ log._log_timestamp = True
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_WriteSig.
40
+ # A StatsObj will be written to this from DirDelta and 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_WriteSig(path_iter, io.StringIO(""), None)
58
+ return DirDelta(path_iter, io.StringIO(""))
59
59
 
60
60
 
61
61
  def DirFull_WriteSig(path_iter, sig_outfp):
@@ -65,6 +65,26 @@ 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
+
68
88
  def delta_iter_error_handler(exc, new_path, sig_path, sig_tar=None): # pylint: disable=unused-argument
69
89
  """
70
90
  Called by get_delta_iter, report error in getting delta
@@ -132,31 +132,24 @@ 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
- # 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
135
+ # Not in the environment, check if encryption passphrase is needed
144
136
  asymmetric = False
145
137
  need_passphrase = False
146
138
  profile = config.gpg_profile
147
139
  encrypt_keys = profile.recipients + profile.hidden_recipients
148
140
  if profile.sign_key:
149
141
  encrypt_keys.append(profile.sign_key)
150
- if encrypt_keys and config.check_remote:
142
+ if encrypt_keys:
151
143
  asymmetric = True
152
144
  for key in encrypt_keys:
153
- if util.key_needs_passphrase(config.gpg_binary, key):
145
+ if util.key_needs_passphrase(key):
154
146
  log.Notice(f"Key {key} needs passphrase.")
155
147
  need_passphrase = True
156
148
  break
157
149
  else:
158
150
  log.Notice("No encryption keys need passphrase.")
159
151
  else:
152
+ symmetric = True
160
153
  need_passphrase = True
161
154
  log.Notice("No encryption keys configured.")
162
155
 
@@ -765,7 +758,7 @@ def incremental_backup(sig_chain, col_stats=None):
765
758
  if config.progress:
766
759
  progress.tracker = progress.ProgressTracker()
767
760
  # Fake a backup to compute total of moving bytes
768
- tarblock_iter = diffdir.DirDelta_WriteSig(config.select, sig_chain.get_fileobjs(), None)
761
+ tarblock_iter = diffdir.DirDelta(config.select, sig_chain.get_fileobjs())
769
762
  dummy_backup(tarblock_iter)
770
763
  # Store computed stats to compute progress later
771
764
  progress.tracker.set_evidence(diffdir.stats, False)
@@ -775,7 +768,7 @@ def incremental_backup(sig_chain, col_stats=None):
775
768
  progress.progress_thread = progress.LogProgressThread()
776
769
 
777
770
  if config.dry_run:
778
- tarblock_iter = diffdir.DirDelta_WriteSig(config.select, sig_chain.get_fileobjs(), None)
771
+ tarblock_iter = diffdir.DirDelta(config.select, sig_chain.get_fileobjs())
779
772
  bytes_written = dummy_backup(tarblock_iter)
780
773
  else:
781
774
  new_sig_outfp = get_sig_fileobj("new-sig")
@@ -40,6 +40,7 @@ MAX = 9
40
40
  PREFIX = ""
41
41
 
42
42
  _logger = None
43
+ _log_timestamp = False
43
44
 
44
45
 
45
46
  def DupToLoggerLevel(verb):
@@ -320,6 +321,7 @@ def setup():
320
321
  Initialize logging
321
322
  """
322
323
  global _logger
324
+ global _log_timestamp
323
325
  if _logger:
324
326
  return
325
327
 
@@ -331,12 +333,18 @@ def setup():
331
333
 
332
334
  # stdout and stderr are for different logging levels
333
335
  outHandler = logging.StreamHandler(sys.stdout)
334
- outHandler.setFormatter(PrettyProgressFormatter())
336
+ if _log_timestamp:
337
+ outHandler.setFormatter(DetailFormatter())
338
+ else:
339
+ outHandler.setFormatter(PrettyProgressFormatter())
335
340
  outHandler.addFilter(OutFilter())
336
341
  _logger.addHandler(outHandler)
337
342
 
338
343
  errHandler = logging.StreamHandler(sys.stderr)
339
- errHandler.setFormatter(PrettyProgressFormatter())
344
+ if _log_timestamp:
345
+ errHandler.setFormatter(DetailFormatter())
346
+ else:
347
+ errHandler.setFormatter(PrettyProgressFormatter())
340
348
  errHandler.addFilter(ErrFilter())
341
349
  _logger.addHandler(errHandler)
342
350
 
@@ -380,7 +388,7 @@ class DetailFormatter(logging.Formatter):
380
388
  # standard 'levelname'. This is because the standard 'levelname' can
381
389
  # be adjusted by any library anywhere in our stack without us knowing.
382
390
  # But we control 'levelName'.
383
- logging.Formatter.__init__(self, "%(asctime)s %(levelName)-6s %(message)s")
391
+ logging.Formatter.__init__(self, "%(asctime)s %(levelName)s %(message)s")
384
392
 
385
393
  def format(self, record):
386
394
  s = logging.Formatter.format(self, record)
@@ -444,28 +452,6 @@ def add_file(filename):
444
452
  _logger.addHandler(handler)
445
453
 
446
454
 
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
-
469
455
  def setverbosity(verb):
470
456
  """
471
457
  Set the verbosity level.
@@ -623,8 +623,13 @@ 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
+
626
628
  log.Debug(_("Deleting tree %s") % self.uc_name)
627
- shutil.rmtree(self.name)
629
+ itr = IterTreeReducer(PathDeleter, [])
630
+ for path in selection.Select(self).set_iter():
631
+ itr(path.index, path)
632
+ itr.Finish()
628
633
  self.setdata()
629
634
 
630
635
  def get_parent_dir(self):
@@ -802,3 +807,19 @@ class DupPath(Path):
802
807
  return gpg.GPGFile(True, self, gpg_profile)
803
808
  else:
804
809
  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,13 +27,11 @@ import atexit
27
27
  import csv
28
28
  import errno
29
29
  import json
30
- import locale
31
30
  import multiprocessing
32
31
  import os
33
32
  import socket
34
33
  import sys
35
34
  import traceback
36
- from contextlib import contextmanager
37
35
  from io import StringIO
38
36
 
39
37
  import fasteners
@@ -205,93 +203,28 @@ def release_lockfile():
205
203
  pass
206
204
 
207
205
 
208
- def key_needs_passphrase(gpgbin, key, logfile=None):
206
+ def key_needs_passphrase(key):
209
207
  """
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``.
208
+ Check if a key needs a passphrase.
253
209
  """
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
-
260
210
  try:
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
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}")
266
214
 
267
215
  try:
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=}")
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)}")
282
219
 
283
220
  if got == 0:
284
- log.Debug(f"Key {key} does not need passphrase")
285
- return False
286
- elif got == 1:
287
221
  log.Debug(f"Key {key} needs passphrase")
222
+ child.close()
288
223
  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
224
+ elif got == 1:
225
+ log.Debug(f"Key {key} does not need passphrase")
226
+ return False
227
+ return None
295
228
 
296
229
 
297
230
  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
3
+ Version: 3.0.7.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
@@ -1,4 +1,4 @@
1
- .TH DUPLICITY 1 "December 31, 2025" "Version 3.0.7" "User Manuals" \" -*- nroff -*-
1
+ .TH DUPLICITY 1 "November 20, 2025" "Version 3.0.7.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
@@ -497,6 +497,23 @@ 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
+
500
517
  .TP
501
518
  .BI "--file-prefix " prefix
502
519
  .PD 0
@@ -518,21 +535,12 @@ See also
518
535
  .B "A NOTE ON FILENAME PREFIXES"
519
536
 
520
537
  .TP
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.
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.
536
544
 
537
545
  .TP
538
546
  .BI --filter-globbing
@@ -564,6 +572,15 @@ See the
564
572
  .B FILE SELECTION
565
573
  section for more information.
566
574
 
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
+
567
584
  .TP
568
585
  .BI --force
569
586
  Proceed even if data loss might result. Duplicity will let the user
@@ -579,15 +596,6 @@ out.
579
596
  .BI --ftp-regular
580
597
  Use regular (PORT) data connections.
581
598
 
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
-
591
599
  .TP
592
600
  .BI --gio
593
601
  Use the GIO backend and interpret any URLs as GIO would.
@@ -854,14 +862,6 @@ for Par2 recovery files (default 10%).
854
862
  .BI "--par2-volumes " number
855
863
  Number of Par2 volumes to create (default 1).
856
864
 
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"
3
+ version = "3.0.7.dev0"
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"
45
+ Version: str = "3.0.7.dev0"
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
File without changes
File without changes
File without changes