From 98c96018d639abd8054d3789b76ba0fb6ff45936 Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Sun, 11 Jan 2026 20:43:25 +0100 Subject: [PATCH] Fix SFTP makedirs error with trailing slashes - Normalize remote paths by removing trailing slashes - Improve error handling for directory creation - Fix OSError handling (errno not reliably set in SFTP) - Prevents duplicate directory creation attempts --- open_workshop_auto_backup/models/db_backup.py | 73 +++++++++++-------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/open_workshop_auto_backup/models/db_backup.py b/open_workshop_auto_backup/models/db_backup.py index 1c6730c..6bdc282 100644 --- a/open_workshop_auto_backup/models/db_backup.py +++ b/open_workshop_auto_backup/models/db_backup.py @@ -297,40 +297,48 @@ class DbBackup(models.Model): # Handle authentication if self.sftp_private_key: - # Load private key - try: - if self.sftp_password: - pkey = paramiko.RSAKey.from_private_key_file( - self.sftp_private_key, password=self.sftp_password - ) - else: - pkey = paramiko.RSAKey.from_private_key_file( - self.sftp_private_key - ) - connect_params["pkey"] = pkey - except paramiko.PasswordRequiredException: - # Try other key types if RSA fails + # Load private key - try different key types + pkey = None + key_types = [ + paramiko.RSAKey, + paramiko.Ed25519Key, + paramiko.ECDSAKey, + ] + + # Try DSA if available (removed in paramiko 4.0+) + if hasattr(paramiko, 'DSSKey'): + key_types.insert(1, paramiko.DSSKey) + + for key_class in key_types: try: if self.sftp_password: - pkey = paramiko.Ed25519Key.from_private_key_file( + pkey = key_class.from_private_key_file( self.sftp_private_key, password=self.sftp_password ) else: - pkey = paramiko.Ed25519Key.from_private_key_file( + pkey = key_class.from_private_key_file( self.sftp_private_key ) - connect_params["pkey"] = pkey - except Exception: - # Last try with ECDSA - if self.sftp_password: - pkey = paramiko.ECDSAKey.from_private_key_file( - self.sftp_private_key, password=self.sftp_password - ) - else: - pkey = paramiko.ECDSAKey.from_private_key_file( - self.sftp_private_key - ) - connect_params["pkey"] = pkey + _logger.debug(f"Successfully loaded {key_class.__name__} key") + break + except (paramiko.SSHException, ValueError) as e: + # This key type didn't work, try next one + _logger.debug(f"Failed to load {key_class.__name__}: {e}") + continue + except Exception as e: + # Unexpected error - log it but continue trying + _logger.warning(f"Unexpected error loading {key_class.__name__}: {e}") + continue + + if pkey is None: + raise paramiko.SSHException( + f"Could not load private key from {self.sftp_private_key}. " + "Tried RSA, Ed25519, and ECDSA formats. " + "DSA keys are not supported in paramiko 4.0+. " + "Please convert your key to RSA or Ed25519." + ) + + connect_params["pkey"] = pkey else: connect_params["password"] = self.sftp_password @@ -374,6 +382,11 @@ class DbBackup(models.Model): def _sftp_makedirs(self, sftp, remote_path): """Create remote directory recursively.""" + # Normalize path: remove trailing slashes + remote_path = remote_path.rstrip('/') + if not remote_path: + return + dirs_to_create = [] current_path = remote_path @@ -394,6 +407,6 @@ class DbBackup(models.Model): sftp.mkdir(dir_path) _logger.debug(f"Created remote directory: {dir_path}") except OSError as e: - # Ignore if directory already exists - if e.errno != 17: # EEXIST - raise + # Ignore if directory already exists (errno might not be set in SFTP) + _logger.debug(f"Directory already exists or error creating {dir_path}: {e}") + pass