diff --git a/web-nodejs/public/js/rdclient/client.js b/web-nodejs/public/js/rdclient/client.js index e2f0439a..c2c46874 100644 --- a/web-nodejs/public/js/rdclient/client.js +++ b/web-nodejs/public/js/rdclient/client.js @@ -46,7 +46,8 @@ class RDClient { proto: this.proto, sendMessage: (msg) => this._sendFileTransferMessage(msg), emit: (event, ...args) => this._emit(event, ...args), - ensureConnected: () => this.ensureFileConnection() + ensureConnected: () => this.ensureFileConnection(), + isConnected: () => this.isFileConnectionReady() }); // State @@ -254,6 +255,10 @@ class RDClient { * Open dedicated FILE_TRANSFER relay (lazy). Reuses desktop session password. * @returns {Promise} */ + isFileConnectionReady() { + return !!(this._fileConnection && this._fileConnection.state === 'ready'); + } + async ensureFileConnection() { if (typeof RDFileConnection !== 'function') { throw new Error('File transfer module not loaded'); @@ -272,9 +277,19 @@ class RDClient { this._fileConnection.on('2fa_required', () => this._emit('2fa_required')); this._fileConnection.on('2fa_error', (err) => this._emit('2fa_error', err)); this._fileConnection.on('login_error', (err) => this._emit('login_error', err)); + this._fileConnection.on('disconnected', () => { + if (this._fileConnection && this._fileConnection.state !== 'ready') { + this._fileConnection = null; + } + }); } if (this._fileConnection.state === 'ready') return; - await this._fileConnection.connect(this._sessionPassword || ''); + try { + await this._fileConnection.connect(this._sessionPassword || ''); + } catch (err) { + this.disconnectFileConnection(); + throw err; + } } _sendFileTransferMessage(msgObj) { @@ -1103,6 +1118,9 @@ class RDClient { // Enable file transfer this.fileTransfer.enable(); + this.ensureFileConnection().catch(function (err) { + console.warn('[RDClient] File transfer preconnect:', err.message || err); + }); // Initialize video decoder callbacks this.video.onFrame = (frame) => this.renderer.pushFrame(frame); diff --git a/web-nodejs/public/js/rdclient/file-connection.js b/web-nodejs/public/js/rdclient/file-connection.js index 2039ec05..23b20157 100644 --- a/web-nodejs/public/js/rdclient/file-connection.js +++ b/web-nodejs/public/js/rdclient/file-connection.js @@ -99,6 +99,11 @@ class RDFileConnection { async _doConnect(password) { try { + if (this.conn) { + try { this.conn.close(); } catch (_e) { /* ignore */ } + } + this.conn = new RDConnection(); + this.crypto = new RDCrypto(); this._setState('connecting'); this._emit('log', 'Opening file transfer session…'); @@ -110,6 +115,9 @@ class RDFileConnection { this._keyExchangePending = false; this._keyExchangeDone = false; this._peerEncryptionConfirmed = false; + this._loginChallenge = ''; + this._loginSalt = ''; + this._pendingPassword = password; const ct = this._connType(); await this.conn.connectRendezvous(); @@ -230,7 +238,8 @@ class RDFileConnection { if (this._loginReject) { this._loginReject(new Error('Disconnected')); } - this.conn.close(); + this._connectPromise = null; + try { this.conn.close(); } catch (_e) { /* ignore */ } this._setState('disconnected'); } diff --git a/web-nodejs/public/js/rdclient/file-modal.js b/web-nodejs/public/js/rdclient/file-modal.js index f688dfb9..dcd1ec1b 100644 --- a/web-nodejs/public/js/rdclient/file-modal.js +++ b/web-nodejs/public/js/rdclient/file-modal.js @@ -186,6 +186,11 @@ t('remote.file_transfer', 'File Transfer') + ' — ' + (session.deviceName || session.deviceId); this._updateLocalHint(); this._updateLocalToolbarState(); + this._renderLocalList(); + this._remoteEntries = []; + this._selectedRemote = null; + this._el.querySelector('.ft-remote-path').textContent = ''; + this._renderRemoteLoading(t('remote.file_connecting', 'Connecting file transfer session…')); this._el.style.display = 'flex'; document.getElementById('btn-file-transfer')?.classList.add('active'); if (this._wiredSessionId !== session.deviceId) { @@ -204,9 +209,6 @@ if (this._session && this._session.client && this._session.client.fileTransfer) { this._session.client.fileTransfer._saveDownload = null; } - if (this._session && this._session.client && this._session.client.disconnectFileConnection) { - this._session.client.disconnectFileConnection(); - } }; FileTransferModal.prototype.isOpen = function () { @@ -216,9 +218,15 @@ FileTransferModal.prototype._wireClient = function (session) { var self = this; var client = session.client; - client.on('file_browsing', function () { self._renderRemoteLoading(); }); + client.on('file_browsing', function () { + if (!self._remoteEntries.length) { + self._renderRemoteLoading(t('remote.file_loading', 'Loading…')); + } + }); client.on('file_connecting', function () { - self._renderRemoteLoading(t('remote.file_connecting', 'Connecting file transfer session…')); + if (!self._remoteEntries.length) { + self._renderRemoteLoading(t('remote.file_connecting', 'Connecting file transfer session…')); + } }); client.on('file_connect_error', function (data) { self._renderRemoteError(data.error || t('remote.file_connect_failed', 'Could not open file transfer session')); diff --git a/web-nodejs/public/js/rdclient/filetransfer.js b/web-nodejs/public/js/rdclient/filetransfer.js index 884b677c..67e46ef0 100644 --- a/web-nodejs/public/js/rdclient/filetransfer.js +++ b/web-nodejs/public/js/rdclient/filetransfer.js @@ -19,12 +19,14 @@ class RDFileTransfer { * @param {Function} opts.sendMessage - Function to send peer message: (msgObj) => void * @param {Function} opts.emit - Event emitter: (event, ...args) => void * @param {Function} [opts.ensureConnected] - Async hook before browse/upload + * @param {Function} [opts.isConnected] - Returns true when file relay is ready */ constructor(opts) { this._proto = opts.proto; this._sendMessage = opts.sendMessage; this._emit = opts.emit; this._ensureConnected = opts.ensureConnected || null; + this._isConnected = opts.isConnected || null; /** @type {string} Current remote directory path */ this._currentPath = ''; @@ -95,10 +97,16 @@ class RDFileTransfer { }); } + _needsFileConnection() { + return this._ensureConnected && (!this._isConnected || !this._isConnected()); + } + _runWithConnection(run) { const self = this; if (this._ensureConnected) { - this._emit('file_connecting'); + if (this._needsFileConnection()) { + this._emit('file_connecting'); + } return this._ensureConnected().then(function () { run(); }).catch(function (err) { @@ -188,7 +196,9 @@ class RDFileTransfer { }, 5000); }; if (this._ensureConnected) { - this._emit('file_connecting'); + if (this._needsFileConnection()) { + this._emit('file_connecting'); + } this._ensureConnected().then(run).catch(function (err) { self._emit('file_connect_error', { error: err.message || String(err) }); });