Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package duplicity for openSUSE:Factory checked in at 2026-01-09 17:04:31 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/duplicity (Old) and /work/SRC/openSUSE:Factory/.duplicity.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "duplicity" Fri Jan 9 17:04:31 2026 rev:88 rq:1326307 version:3.0.7 Changes: -------- --- /work/SRC/openSUSE:Factory/duplicity/duplicity.changes 2025-12-08 11:54:36.099114827 +0100 +++ /work/SRC/openSUSE:Factory/.duplicity.new.1928/duplicity.changes 2026-01-09 17:06:21.659135275 +0100 @@ -1,0 +2,12 @@ +Wed Dec 31 15:34:59 UTC 2025 - Michael Gorse <[email protected]> + +- Update to version 3.0.7: + * fix: replace custom deltree with built-in shutil.rmtree. + * fix: delete duplicate code in DirDelta. + * fix: webdavs with "--concurrency 1" failed because of missing + auth header. + * fix: disable s3 checksum workaround, warn only... + * fix: --log-timestamp no longer working. + * fix: Combined fix to related issues 912 and 914. + +------------------------------------------------------------------- Old: ---- duplicity-rel.3.0.6.3.tar.bz2 New: ---- duplicity-rel.3.0.7.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ duplicity.spec ++++++ --- /var/tmp/diff_new_pack.yNaeJw/_old 2026-01-09 17:06:22.583173577 +0100 +++ /var/tmp/diff_new_pack.yNaeJw/_new 2026-01-09 17:06:22.587173742 +0100 @@ -26,7 +26,7 @@ %define _python3_version %{?python311_version} %endif Name: duplicity -Version: 3.0.6.3 +Version: 3.0.7 Release: 0 Summary: Encrypted bandwidth-efficient backup using the rsync algorithm License: GPL-3.0-or-later ++++++ duplicity-rel.3.0.6.3.tar.bz2 -> duplicity-rel.3.0.7.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/CHANGELOG.md new/duplicity-rel.3.0.7/CHANGELOG.md --- old/duplicity-rel.3.0.6.3/CHANGELOG.md 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/CHANGELOG.md 2025-12-31 13:07:54.000000000 +0100 @@ -1,9 +1,19 @@ -(Unreleased) / 2025-12-05 +(Unreleased) / 2025-12-31 ========================= +rel.3.0.7 / 2025-12-31 +====================== + + * ad175c22:fix: replace custom deltree with built-in shutil.rmtree. + * 101be7fd:fix: delete duplicate code in DirDelta. + * c0d3e72d:fix: webdavs with "--concurrency 1" failed because of missing auth header + * a08f8bef:fix: disable s3 checksum workaround, warn only... + * f7055a7a:fix: --log-timestamp no longer working. + * ac163d5e:fix: Combined fix to related issues 912 and 914 + rel.3.0.6.3 / 2025-12-05 ======================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/__init__.py new/duplicity-rel.3.0.7/duplicity/__init__.py --- old/duplicity-rel.3.0.6.3/duplicity/__init__.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/__init__.py 2025-12-31 13:07:54.000000000 +0100 @@ -21,7 +21,7 @@ import gettext -__version__: str = "3.0.6.3" -__reldate__: str = "December 05, 2025" +__version__: str = "3.0.7" +__reldate__: str = "December 31, 2025" gettext.install("duplicity", names=["ngettext"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/backends/s3_boto3_backend.py new/duplicity-rel.3.0.7/duplicity/backends/s3_boto3_backend.py --- old/duplicity-rel.3.0.6.3/duplicity/backends/s3_boto3_backend.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/backends/s3_boto3_backend.py 2025-12-31 13:07:54.000000000 +0100 @@ -74,19 +74,6 @@ from boto3.s3.transfer import S3UploadFailedError, TransferConfig from botocore.exceptions import ClientError - if not (boto3.__version__ < "1.36.0" and botocore.__version__ < "1.36.0"): - # TODO: remove this workaround when issue #870 is fixed. - # https://github.com/boto/boto3/issues/2913 - log.Warn( - "WARNING: Using boto3 >= 1,36.0 may result in errors, so we qre applying\n" - "the workaround for https://gitlab.com/duplicity/duplicity/-/issues/870\n" - " export AWS_REQUEST_CHECKSUM_CALCULATION=when_required\n" - " export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required\n" - "NOTE: This workaround is temporary and will be removed when issue is fixed.\n." - ) - os.environ["AWS_REQUEST_CHECKSUM_CALCULATION"] = "when_required" - os.environ["AWS_RESPONSE_CHECKSUM_VALIDATION"] = "when_required" - duplicity.backend.Backend.__init__(self, parsed_url) # This folds the null prefix and all null parts, which means that: @@ -109,6 +96,25 @@ self.bucket = None self.tracker = UploadProgressTracker() + if not (boto3.__version__ < "1.36.0" and botocore.__version__ < "1.36.0"): + # this is an issue with 3rd party s3 implementations only + # likely when an endpoint is given that resides not under amazonaws.com + # in time that workaround will probably not be needed anymore + # https://github.com/boto/boto3/issues/2913 + import re + + if config.s3_endpoint_url and not re.match( + pattern="(?i).*\\.amazonaws\\.com(/+)?$", string=config.s3_endpoint_url + ): + log.Warn( + "WARNING: Using boto3 >= 1,36.0 with non-amazon s3 services" + " may result in checksum errors." + " a workaround is to set the following env vars\n\n" + " export AWS_REQUEST_CHECKSUM_CALCULATION=when_required\n" + " export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required\n\n" + "see https://gitlab.com/duplicity/duplicity/-/issues/870 for details." + ) + def reset_connection(self): self.bucket = None self.s3 = boto3.resource( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/backends/webdavbackend.py new/duplicity-rel.3.0.7/duplicity/backends/webdavbackend.py --- old/duplicity-rel.3.0.6.3/duplicity/backends/webdavbackend.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/backends/webdavbackend.py 2025-12-31 13:07:54.000000000 +0100 @@ -194,9 +194,10 @@ if self.username or self.password: # Workaround cpython http.client issue # https://github.com/python/cpython/issues/70107 - self.conn.request("OPTIONS", self.directory, None) - response = self.conn.getresponse() - response.read() + # PUT may not return 401 when ran without basic-auth but throw SSL-EOF-Error or hang + # as a workaround we run an OPTIONS request that adds auth if needed and creates + # an authenticated connection to (re)use + response = self.request("OPTIONS", self.directory, None) response.close() def _close(self): @@ -226,6 +227,8 @@ if self.digest_challenge is not None: self.headers["Authorization"] = self.get_digest_authorization(path) + elif self.username or self.password: + self.headers["Authorization"] = self.get_basic_authorization() log.Debug(_("WebDAV %s %s request with headers: %s ") % (method, quoted_path, munge_headers(self.headers))) log.Debug(_("WebDAV data length: %s ") % sys.getsizeof(data)) @@ -245,6 +248,7 @@ return self.request(method, self.directory, data, redirected + 1) else: raise FatalBackendException(_("WebDAV missing location header in redirect response.")) + # mainly for digest-auth to recalculate with response values elif response.status == 401: response.read() response.close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/cli_data.py new/duplicity-rel.3.0.7/duplicity/cli_data.py --- old/duplicity-rel.3.0.6.3/duplicity/cli_data.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/cli_data.py 2025-12-31 13:07:54.000000000 +0100 @@ -446,9 +446,9 @@ type=set_log_file, help="Logging filename to use", ), - # log_timestamp is directly applied in SetLogTimestampAction(), not saved in config + # log_timestamp is directly applied in set_log_timestamp(), not saved in config log_timestamp=dict( - dest="", + nargs=0, action=SetLogTimestampAction, help="Whether to include timestamp and level in log", default=dflt(False), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/cli_util.py new/duplicity-rel.3.0.7/duplicity/cli_util.py --- old/duplicity-rel.3.0.6.3/duplicity/cli_util.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/cli_util.py 2025-12-31 13:07:54.000000000 +0100 @@ -160,7 +160,7 @@ super().__init__(option_strings, dest, **kwargs) def __call__(self, parser, namespace, values, option_string=None): - log._log_timestamp = True + log.add_timestamp() def _check_int(val): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/diffdir.py new/duplicity-rel.3.0.7/duplicity/diffdir.py --- old/duplicity-rel.3.0.6.3/duplicity/diffdir.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/diffdir.py 2025-12-31 13:07:54.000000000 +0100 @@ -37,7 +37,7 @@ from duplicity import util from duplicity.path import * # pylint: disable=unused-wildcard-import,redefined-builtin -# A StatsObj will be written to this from DirDelta and DirDelta_WriteSig. +# A StatsObj will be written to this from DirDelta_WriteSig. stats = None tracker = None @@ -55,7 +55,7 @@ will be easy to split up the tar and make the volumes the same sizes). """ - return DirDelta(path_iter, io.StringIO("")) + return DirDelta_WriteSig(path_iter, io.StringIO(""), None) def DirFull_WriteSig(path_iter, sig_outfp): @@ -65,26 +65,6 @@ return DirDelta_WriteSig(path_iter, io.StringIO(""), sig_outfp) -def DirDelta(path_iter, dirsig_fileobj_list): - """ - Produce tarblock diff given dirsig_fileobj_list and pathiter - - dirsig_fileobj_list should either be a tar fileobj or a list of - those, sorted so the most recent is last. - """ - global stats - stats = statistics.StatsDeltaProcess() - if isinstance(dirsig_fileobj_list, list): - sig_iter = combine_path_iters([sigtar2path_iter(x) for x in dirsig_fileobj_list]) - else: - sig_iter = sigtar2path_iter(dirsig_fileobj_list) - delta_iter = get_delta_iter(path_iter, sig_iter) - if config.dry_run or (config.progress and not progress.tracker.has_collected_evidence()): - return DummyBlockIter(delta_iter) - else: - return DeltaTarBlockIter(delta_iter) - - def delta_iter_error_handler(exc, new_path, sig_path, sig_tar=None): # pylint: disable=unused-argument """ Called by get_delta_iter, report error in getting delta diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/dup_main.py new/duplicity-rel.3.0.7/duplicity/dup_main.py --- old/duplicity-rel.3.0.6.3/duplicity/dup_main.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/dup_main.py 2025-12-31 13:07:54.000000000 +0100 @@ -132,24 +132,31 @@ log.Notice(_("Reuse configured SIGN_PASSPHRASE as PASSPHRASE")) return os.environ["SIGN_PASSPHRASE"] - # Not in the environment, check if encryption passphrase is needed + # no passphrase if --no-encryption or --use-agent + if not config.encryption or config.use_agent: + return "" + + # no passphrase if --passphrase* in --gpg-options + if "--passphrase" in config.gpg_options: + return "" + + # Check if encryption passphrase is needed asymmetric = False need_passphrase = False profile = config.gpg_profile encrypt_keys = profile.recipients + profile.hidden_recipients if profile.sign_key: encrypt_keys.append(profile.sign_key) - if encrypt_keys: + if encrypt_keys and config.check_remote: asymmetric = True for key in encrypt_keys: - if util.key_needs_passphrase(key): + if util.key_needs_passphrase(config.gpg_binary, key): log.Notice(f"Key {key} needs passphrase.") need_passphrase = True break else: log.Notice("No encryption keys need passphrase.") else: - symmetric = True need_passphrase = True log.Notice("No encryption keys configured.") @@ -758,7 +765,7 @@ if config.progress: progress.tracker = progress.ProgressTracker() # Fake a backup to compute total of moving bytes - tarblock_iter = diffdir.DirDelta(config.select, sig_chain.get_fileobjs()) + tarblock_iter = diffdir.DirDelta_WriteSig(config.select, sig_chain.get_fileobjs(), None) dummy_backup(tarblock_iter) # Store computed stats to compute progress later progress.tracker.set_evidence(diffdir.stats, False) @@ -768,7 +775,7 @@ progress.progress_thread = progress.LogProgressThread() if config.dry_run: - tarblock_iter = diffdir.DirDelta(config.select, sig_chain.get_fileobjs()) + tarblock_iter = diffdir.DirDelta_WriteSig(config.select, sig_chain.get_fileobjs(), None) bytes_written = dummy_backup(tarblock_iter) else: new_sig_outfp = get_sig_fileobj("new-sig") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/log.py new/duplicity-rel.3.0.7/duplicity/log.py --- old/duplicity-rel.3.0.6.3/duplicity/log.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/log.py 2025-12-31 13:07:54.000000000 +0100 @@ -40,7 +40,6 @@ PREFIX = "" _logger = None -_log_timestamp = False def DupToLoggerLevel(verb): @@ -321,7 +320,6 @@ Initialize logging """ global _logger - global _log_timestamp if _logger: return @@ -333,18 +331,12 @@ # stdout and stderr are for different logging levels outHandler = logging.StreamHandler(sys.stdout) - if _log_timestamp: - outHandler.setFormatter(DetailFormatter()) - else: - outHandler.setFormatter(PrettyProgressFormatter()) + outHandler.setFormatter(PrettyProgressFormatter()) outHandler.addFilter(OutFilter()) _logger.addHandler(outHandler) errHandler = logging.StreamHandler(sys.stderr) - if _log_timestamp: - errHandler.setFormatter(DetailFormatter()) - else: - errHandler.setFormatter(PrettyProgressFormatter()) + errHandler.setFormatter(PrettyProgressFormatter()) errHandler.addFilter(ErrFilter()) _logger.addHandler(errHandler) @@ -388,7 +380,7 @@ # standard 'levelname'. This is because the standard 'levelname' can # be adjusted by any library anywhere in our stack without us knowing. # But we control 'levelName'. - logging.Formatter.__init__(self, "%(asctime)s %(levelName)s %(message)s") + logging.Formatter.__init__(self, "%(asctime)s %(levelName)-6s %(message)s") def format(self, record): s = logging.Formatter.format(self, record) @@ -452,6 +444,28 @@ _logger.addHandler(handler) +def add_timestamp(): + """ + Add timestamp to logs written + """ + global _logger + + # remove all handlers + for handler in _logger.handlers[:]: + _logger.removeHandler(handler) + + # stdout and stderr are for different logging levels + outHandler = logging.StreamHandler(sys.stdout) + outHandler.setFormatter(DetailFormatter()) + outHandler.addFilter(OutFilter()) + _logger.addHandler(outHandler) + + errHandler = logging.StreamHandler(sys.stderr) + errHandler.setFormatter(DetailFormatter()) + errHandler.addFilter(ErrFilter()) + _logger.addHandler(errHandler) + + def setverbosity(verb): """ Set the verbosity level. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/path.py new/duplicity-rel.3.0.7/duplicity/path.py --- old/duplicity-rel.3.0.6.3/duplicity/path.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/path.py 2025-12-31 13:07:54.000000000 +0100 @@ -623,13 +623,8 @@ def deltree(self): """Remove self by recursively deleting files under it""" - from duplicity import selection # TODO: avoid circ. dep. issue - log.Debug(_("Deleting tree %s") % self.uc_name) - itr = IterTreeReducer(PathDeleter, []) - for path in selection.Select(self).set_iter(): - itr(path.index, path) - itr.Finish() + shutil.rmtree(self.name) self.setdata() def get_parent_dir(self): @@ -807,19 +802,3 @@ return gpg.GPGFile(True, self, gpg_profile) else: return self.open(mode) - - -class PathDeleter(ITRBranch): - """Delete a directory. Called by Path.deltree""" - - def start_process(self, index, path): # pylint: disable=unused-argument - self.path = path - - def end_process(self): - self.path.delete() - - def can_fast_process(self, index, path): # pylint: disable=unused-argument - return not path.isdir() - - def fast_process(self, index, path): # pylint: disable=unused-argument - path.delete() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/duplicity/util.py new/duplicity-rel.3.0.7/duplicity/util.py --- old/duplicity-rel.3.0.6.3/duplicity/util.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/duplicity/util.py 2025-12-31 13:07:54.000000000 +0100 @@ -27,11 +27,13 @@ import csv import errno import json +import locale import multiprocessing import os import socket import sys import traceback +from contextlib import contextmanager from io import StringIO import fasteners @@ -203,28 +205,93 @@ pass -def key_needs_passphrase(key): +def key_needs_passphrase(gpgbin, key, logfile=None): """ - Check if a key needs a passphrase. + Determine whether a GnuPG key requires a passphrase. + + This helper invokes the specified GnuPG frontend in a non‑destructive + way to discover if the secret key is protected by a passphrase. It uses + `pexpect` to spawn the command and watch for prompts or agent errors, + never changing the key material itself. + + How it works + - Runs: ``<gpgbin> --pinentry-mode cancel --dry-run --change-passphrase <key>`` + with a C UTF‑8 locale to ensure predictable output. + - Interprets the interaction: + - If the process reaches EOF without a passphrase prompt, the key is + considered not to need a passphrase. + - If a passphrase prompt appears (matches ``passphrase.*:``), the key + is considered to need a passphrase. + - If ``gpg-agent`` fails to start or ignores an inquiry, we log an + error and return ``None`` to signal an indeterminate result. + + Parameters + - gpgbin: str + The GnuPG command to execute, e.g. ``"gpg"`` or ``"gpgsm"``. + - key: str + The key identifier understood by the given binary. Examples: + - For ``gpg`` (OpenPGP): a key ID or fingerprint, e.g. ``"56538CCF"``. + - For ``gpgsm`` (S/MIME): a certificate keyref, e.g. + ``"\\&165F2FB4F58D..."``. + - logfile: a file-like object or ``None`` + If provided, raw pexpect I/O is mirrored to this stream for debugging + (e.g. ``sys.stdout``). Defaults to ``None``. + + Returns + - ``True`` if the key requires a passphrase. + - ``False`` if the key does not require a passphrase. + - ``None`` if the status cannot be determined due to a runtime error + (e.g., agent failed to start or pexpect raised an exception). + + Notes + - The check is read‑only: ``--dry-run`` and ``--pinentry-mode cancel`` are + used to avoid modifying the key or prompting the user. + - Environment variables ``LANG`` and ``LC_ALL`` are forced to ``C.utf8`` + to make output matching stable across locales. + - For end‑to‑end manual verification with the repository’s test keyring, + see ``testing/manual/needspass.py``. """ + + environ = {**os.environ, "LANG": "C.utf8", "LC_ALL": "C.utf8"} + cmd = f"{gpgbin} --pinentry-mode cancel --dry-run --change-passphrase {key} " + + log.Debug(f"{cmd=}") + try: - child = pexpect.spawn("gpg", f"--pinentry-mode=loopback --dry-run --passwd {key}".split()) - except Exception: - log.FatalError(f"Exception spawning gpg while checking if passphrase needed for key: {key}") + child = pexpect.spawn(cmd, encoding="utf-8", env=environ) + child.logfile = logfile + except pexpect.ExceptionPexpect as e: + log.Error(f"An unexpected error occurred: {e}") + return None try: - got = child.expect(["passphrase.*:", pexpect.EOF]) - except Exception: - log.FatalError(f"Exception while checking if passphrase needed for key: {key}: {str(child)}") + got = child.expect( + [ + pexpect.EOF, + "passphrase.*:", + "failed to start gpg-agent", + "ignoring gpg-agent inquiry", + ] + ) + except pexpect.ExceptionPexpect as e: + log.Error(f"Exception while checking if passphrase needed for: {key}:\n{e}") + return None + + child.close() + log.Debug(f"{child.exitstatus=}, {child.signalstatus=}, {got=}, {child.after=}") if got == 0: - log.Debug(f"Key {key} needs passphrase") - child.close() - return True - elif got == 1: log.Debug(f"Key {key} does not need passphrase") return False - return None + elif got == 1: + log.Debug(f"Key {key} needs passphrase") + return True + elif got == 2: + log.Error(f"gpg-agent failed to start.") + return None + elif got == 3: + log.Error(f"gpg-agent failed inquiry ignored.") + return None def copyfileobj(infp, outfp, byte_count=-1): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/man/duplicity.1 new/duplicity-rel.3.0.7/man/duplicity.1 --- old/duplicity-rel.3.0.6.3/man/duplicity.1 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/man/duplicity.1 2025-12-31 13:07:54.000000000 +0100 @@ -1,4 +1,4 @@ -.TH DUPLICITY 1 "December 05, 2025" "Version 3.0.6.3" "User Manuals" \" -*- nroff -*- +.TH DUPLICITY 1 "December 31, 2025" "Version 3.0.7" "User Manuals" \" -*- nroff -*- .\" disable justification (adjust text to left margin only) .\" command line examples stay readable through that .ad l @@ -498,23 +498,6 @@ section for more information. .TP -.BI "--files-from " filename -Read a list of files to backup from filename rather than searching the entire -backup source directory. Operation is otherwise normal, just on the specified -subset of the backup source directory. - -Files must be specified one per line and relative to the backup source -directory. Any absolute paths will raise an error. All characters per line are -significant and treated as part of the path, including leading and trailing -whitespace. Lines are separated by newlines or nulls, depending on whether the -.B "--null-separator" -switch was given. - -It is not necessary to include the parent directory of listed files, their -inclusion is implied. However, the content of any explicitly listed directories -is not implied. All required files must be listed when this option is used. - -.TP .BI "--file-prefix " prefix .PD 0 .TP @@ -535,12 +518,21 @@ .B "A NOTE ON FILENAME PREFIXES" .TP -.BI "--path-to-restore " path -This option may be given in restore mode, causing only -.I path -to be restored instead of the entire contents of the backup archive. -.I path -should be given relative to the root of the directory backed up. +.BI "--files-from " filename +Read a list of files to backup from filename rather than searching the entire +backup source directory. Operation is otherwise normal, just on the specified +subset of the backup source directory. + +Files must be specified one per line and relative to the backup source +directory. Any absolute paths will raise an error. All characters per line are +significant and treated as part of the path, including leading and trailing +whitespace. Lines are separated by newlines or nulls, depending on whether the +.B "--null-separator" +switch was given. + +It is not necessary to include the parent directory of listed files, their +inclusion is implied. However, the content of any explicitly listed directories +is not implied. All required files must be listed when this option is used. .TP .BI --filter-globbing @@ -573,15 +565,6 @@ section for more information. .TP -.BI "--full-if-older-than " time -Perform a full backup if an incremental backup is requested, but the -latest full backup in the collection is older than the given -.IR time . -See the -.B TIME FORMATS -section for more information. - -.TP .BI --force Proceed even if data loss might result. Duplicity will let the user know when this option is required. @@ -597,6 +580,15 @@ Use regular (PORT) data connections. .TP +.BI "--full-if-older-than " time +Perform a full backup if an incremental backup is requested, but the +latest full backup in the collection is older than the given +.IR time . +See the +.B TIME FORMATS +section for more information. + +.TP .BI --gio Use the GIO backend and interpret any URLs as GIO would. @@ -863,6 +855,14 @@ Number of Par2 volumes to create (default 1). .TP +.BI "--path-to-restore " path +This option may be given in restore mode, causing only +.I path +to be restored instead of the entire contents of the backup archive. +.I path +should be given relative to the root of the directory backed up. + +.TP .BI --progress When selected, duplicity will output the current upload progress and estimated upload time. To annotate changes, it will perform a first dry-run before a full diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/pyproject.toml new/duplicity-rel.3.0.7/pyproject.toml --- old/duplicity-rel.3.0.6.3/pyproject.toml 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/pyproject.toml 2025-12-31 13:07:54.000000000 +0100 @@ -1,6 +1,6 @@ [project] name = "duplicity" -version = "3.0.6.3" +version = "3.0.7" dynamic = ["dependencies"] description = "Encrypted backup using rsync algorithm" authors = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/setup.py new/duplicity-rel.3.0.7/setup.py --- old/duplicity-rel.3.0.6.3/setup.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/setup.py 2025-12-31 13:07:54.000000000 +0100 @@ -42,7 +42,7 @@ print("Sorry, duplicity requires version 3.9 thru 3.14 of Python.", file=sys.stderr) sys.exit(1) -Version: str = "3.0.6.3" +Version: str = "3.0.7" # READTHEDOCS uses setup.py sdist but can't handle extensions ext_modules = list() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/snap/snapcraft.yaml new/duplicity-rel.3.0.7/snap/snapcraft.yaml --- old/duplicity-rel.3.0.6.3/snap/snapcraft.yaml 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/snap/snapcraft.yaml 2025-12-31 13:07:54.000000000 +0100 @@ -1,5 +1,5 @@ name: duplicity -version: 3.0.6.3 +version: 3.0.7 license: GPL-2.0 summary: Efficient, encrypted backup to local or remote hosts description: | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/functional/test_regression.py new/duplicity-rel.3.0.7/testing/functional/test_regression.py --- old/duplicity-rel.3.0.6.3/testing/functional/test_regression.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/testing/functional/test_regression.py 2025-12-31 13:07:54.000000000 +0100 @@ -126,6 +126,7 @@ ] ) + @unittest.skipIf(os.path.exists("/.dockerenv"), "Won't work on docker") def test_issue908(self): """ Test issue 908 - gpg: public key decryption failed: No passphrase given (3.0.6.2) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/functional/test_restart.py new/duplicity-rel.3.0.7/testing/functional/test_restart.py --- old/duplicity-rel.3.0.6.3/testing/functional/test_restart.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/testing/functional/test_restart.py 2025-12-31 13:07:54.000000000 +0100 @@ -88,6 +88,7 @@ self.backup("full", f"{_runtest_dir}/testfiles/largefiles") self.verify(f"{_runtest_dir}/testfiles/largefiles") + @unittest.skipIf(os.path.exists("/.dockerenv"), "Won't work on docker") def test_restart_encrypt_without_password(self): """ Test that we can successfully restart a encrypt-key-only backup without diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/manual/needspass.py new/duplicity-rel.3.0.7/testing/manual/needspass.py --- old/duplicity-rel.3.0.6.3/testing/manual/needspass.py 1970-01-01 01:00:00.000000000 +0100 +++ new/duplicity-rel.3.0.7/testing/manual/needspass.py 2025-12-31 13:07:54.000000000 +0100 @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Manual helper to verify whether specific GnuPG keys require a passphrase. + +What this script does +--------------------- +- Changes to the project root and sets `GNUPGHOME` to use the test keyring in + `testing/gnupg`. +- Calls `duplicity.util.key_needs_passphrase(gpgbin, key)` for a small set of + known test keys with both `gpg` and `gpgsm` and reports results. +- Prints PASS/FAIL per key and exits with the number of failures as the status + code (0 means all checks passed). + +How to run +---------- +- From the project root: + - `python3 testing/manual/needspass.py` + +Optional debugging +------------------ +- To see the interaction with `gpg-agent` via `pexpect`, set `logfile=sys.stdout` + where indicated in the code (search for the comment “set logfile=sys.stdout”). + +Prerequisites +------------- +- `gpg`, `gpgsm`, and a functioning `gpg-agent` available in PATH. +- No additional arguments are needed; the script is self‑contained for the + repository’s test keyring. + +Notes +----- +- The expected outcomes for the embedded test keys are specified in + `test_keys` below; modify or extend this list if you need to try other keys. +""" + +import os +import sys + +os.chdir(os.path.dirname(__file__) + "/../..") +os.environ["GNUPGHOME"] = "testing/gnupg" + +from duplicity import log +from duplicity import util + + +test_keys = [ + ("gpgsm", "\\&165F2FB4F58D537404FE223A603878F54CD444E5", True), + ("gpgsm", "\\&86E23738BB09B27C6C7E4F76C39DA0194586CF4B", True), + ("gpg", "56538CCF", True), + ("gpg", "B5FA894F", False), + ("gpg", "9B736B2A", True), +] + +log.setup() +log.setverbosity(log.DEBUG) + +passed = failed = errored = 0 + +for gpgbin, key, needs_passphrase in test_keys: + # set logfile=sys.stdout to see pexpect output + res = util.key_needs_passphrase(gpgbin, key, logfile=None) + if res is None: + log.Debug(f"HARD FAIL: gpg-agent failed for {key}") + errored += 1 + elif res == needs_passphrase: + log.Debug(f"PASS: {key} needs passphrase={needs_passphrase} OK") + passed += 1 + else: + log.Debug(f"FAIL: {key} needs passphrase={needs_passphrase} got {res=}") + failed += 1 + + log.Debug("\n========================================\n") + +log.Debug(f"{passed=}, {failed=}, {errored=}") + +sys.exit(failed) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/manual/roottest.py new/duplicity-rel.3.0.7/testing/manual/roottest.py --- old/duplicity-rel.3.0.6.3/testing/manual/roottest.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/testing/manual/roottest.py 2025-12-31 13:07:54.000000000 +0100 @@ -83,7 +83,7 @@ diffdir.write_block_iter(diffdir.DirSig(selection.Select(seq_path).set_iter()), sig) diffdir.write_block_iter( - diffdir.DirDelta(selection.Select(new_path).set_iter(), sig.open("rb")), + diffdir.DirDelta_WriteSig(selection.Select(new_path).set_iter(), sig.open("rb"), None), diff, ) @@ -105,7 +105,7 @@ diff = Path("/tmp/testfiles/output/diff.tar") diffdir.write_block_iter(diffdir.DirSig(self.get_sel(seq_path)), sig) - deltablock = diffdir.DirDelta(self.get_sel(new_path), sig.open("rb")) + deltablock = diffdir.DirDelta_WriteSig(self.get_sel(new_path), sig.open("rb"), None) diffdir.write_block_iter(deltablock, diff) patchdir.Patch(seq_path, diff.open("rb")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duplicity-rel.3.0.6.3/testing/unit/test_diffdir.py new/duplicity-rel.3.0.7/testing/unit/test_diffdir.py --- old/duplicity-rel.3.0.6.3/testing/unit/test_diffdir.py 2025-12-05 17:21:40.000000000 +0100 +++ new/duplicity-rel.3.0.7/testing/unit/test_diffdir.py 2025-12-31 13:07:54.000000000 +0100 @@ -72,7 +72,7 @@ select2 = selection.Select(Path(dirname)) select2.set_iter() diffdir.write_block_iter( - diffdir.DirDelta(select2, sigtar_fp), + diffdir.DirDelta_WriteSig(select2, sigtar_fp, None), f"{_runtest_dir}/testfiles/output/difftar", ) @@ -98,7 +98,7 @@ select2 = selection.Select(Path(f"{_runtest_dir}/testfiles/various_file_types")) select2.set_iter() diffdir.write_block_iter( - diffdir.DirDelta(select2, sigtar_fp), + diffdir.DirDelta_WriteSig(select2, sigtar_fp, None), f"{_runtest_dir}/testfiles/output/difftar", ) @@ -119,7 +119,7 @@ sigtar_fp = open(f"{_runtest_dir}/testfiles/output/dir1.sigtar", "rb") sel2 = selection.Select(Path(f"{_runtest_dir}/testfiles/dir2")) - delta_tar = diffdir.DirDelta(sel2.set_iter(), sigtar_fp) + delta_tar = diffdir.DirDelta_WriteSig(sel2.set_iter(), sigtar_fp, None) diffdir.write_block_iter(delta_tar, f"{_runtest_dir}/testfiles/output/dir1dir2.difftar") changed_files = [ @@ -147,7 +147,7 @@ sigtar_fp = open(f"{_runtest_dir}/testfiles/output/dir2.sigtar", "rb") sel2 = selection.Select(Path(f"{_runtest_dir}/testfiles/dir3")) - delta_tar = diffdir.DirDelta(sel2.set_iter(), sigtar_fp) + delta_tar = diffdir.DirDelta_WriteSig(sel2.set_iter(), sigtar_fp, None) diffdir.write_block_iter(delta_tar, f"{_runtest_dir}/testfiles/output/dir2dir3.difftar") buffer = b"" @@ -199,7 +199,9 @@ incsig = Path(f"{_runtest_dir}/testfiles/output/incsig." + dirname) # Write old-style delta to deltadir1 - diffdir.write_block_iter(diffdir.DirDelta(get_sel(cur_dir), old_full_sigs.open("rb")), delta1) + diffdir.write_block_iter( + diffdir.DirDelta_WriteSig(get_sel(cur_dir), old_full_sigs.open("rb"), None), delta1 + ) # Write new signature and delta to deltadir2 and sigdir2, compare block_iter = diffdir.DirDelta_WriteSig(
