From b7ec3f4ad34d0c4d72fe6b05587d1cb3be75f573 Mon Sep 17 00:00:00 2001 From: Amir Khodak Date: Sun, 7 Jun 2026 02:34:01 +0000 Subject: [PATCH] =?UTF-8?q?v1.0.71:=20SFTP=20connection=20pooling=20?= =?UTF-8?q?=E2=80=94=20reuse=20SSH=20session=20across=20all=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously every listFiles/uploadFile/downloadFile/deleteFile call created a fresh SSH connection (connect → auth → use → disconnect). For zahra's folder with 69 subdirectories, the recursive listing alone made 70 full SSH handshakes, then one more per downloaded file — causing connection timeouts and 65 upload/download failures reported as PARTIAL. Now the provider holds a persistent SSH session and reuses it for all calls, reconnecting automatically if the connection drops. --- .../data/providers/sftp/SftpProvider.kt | 37 ++++++++++++++----- version.properties | 4 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/syncflow/data/providers/sftp/SftpProvider.kt b/app/src/main/kotlin/com/syncflow/data/providers/sftp/SftpProvider.kt index 853569e..ef0c570 100644 --- a/app/src/main/kotlin/com/syncflow/data/providers/sftp/SftpProvider.kt +++ b/app/src/main/kotlin/com/syncflow/data/providers/sftp/SftpProvider.kt @@ -23,19 +23,36 @@ class SftpProvider(private val account: CloudAccount, private val credentialStor private val password = creds["password"]?.jsonPrimitive?.content private val privateKey = creds["private_key"]?.jsonPrimitive?.content - private fun withSftp(block: (SFTPClient) -> T): T { + // Persistent SSH connection reused across all operations in the provider's lifetime. + // Each call to withSftp checks liveness and reconnects if the connection dropped. + // This eliminates the per-operation connect/auth/disconnect cycle that caused + // 100+ SSH handshakes during a recursive directory listing + file-transfer sync, + // leading to connection timeouts on large folder trees (e.g. 69 subdirectories). + private var sshClient: SSHClient? = null + + private fun getOrCreateSsh(): SSHClient { + val existing = sshClient + if (existing != null && existing.isConnected && existing.isAuthenticated) return existing val ssh = SSHClient() ssh.addHostKeyVerifier(TofuHostKeyVerifier(credentialStore)) ssh.connect(host, port) - try { - if (!privateKey.isNullOrBlank()) { - ssh.authPublickey(username, ssh.loadKeys(privateKey, null, null)) - } else { - ssh.authPassword(username, password ?: "") - } - return ssh.newSFTPClient().use(block) - } finally { - ssh.disconnect() + if (!privateKey.isNullOrBlank()) { + ssh.authPublickey(username, ssh.loadKeys(privateKey, null, null)) + } else { + ssh.authPassword(username, password ?: "") + } + sshClient = ssh + return ssh + } + + private fun withSftp(block: (SFTPClient) -> T): T { + return try { + getOrCreateSsh().newSFTPClient().use(block) + } catch (e: Exception) { + // Connection may have gone stale — reset and retry once with a fresh connection. + runCatching { sshClient?.disconnect() } + sshClient = null + getOrCreateSsh().newSFTPClient().use(block) } } diff --git a/version.properties b/version.properties index 282f4b2..b1719d3 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=1.0.70 -VERSION_CODE=70 +VERSION_NAME=1.0.71 +VERSION_CODE=71