@@ -9,6 +9,7 @@ import GObject from 'gi://GObject';
99import Config from '../../config.js' ;
1010import * as Core from '../core.js' ;
1111import Plugin from '../plugin.js' ;
12+ import { safe_dirname } from '../utils/file.js' ;
1213
1314
1415export const Metadata = {
@@ -38,9 +39,6 @@ export const Metadata = {
3839} ;
3940
4041
41- const MAX_MOUNT_DIRS = 12 ;
42-
43-
4442/**
4543 * SFTP Plugin
4644 * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sftp
@@ -54,6 +52,8 @@ const SFTPPlugin = GObject.registerClass({
5452 super . _init ( device , 'sftp' ) ;
5553
5654 this . _gmount = null ;
55+ this . _directories = { } ;
56+ this . _device_dir = null ;
5757 this . _mounting = false ;
5858
5959 // A reusable launcher for ssh processes
@@ -90,7 +90,7 @@ const SFTPPlugin = GObject.registerClass({
9090 if ( regex . test ( uri ) ) {
9191 this . _gmount = mount ;
9292 this . _addSubmenu ( mount ) ;
93- this . _addSymlink ( mount ) ;
93+ this . _addSymlinks ( mount , this . _directories ) ;
9494
9595 break ;
9696 }
@@ -105,8 +105,11 @@ const SFTPPlugin = GObject.registerClass({
105105
106106 // Only enable for Lan connections
107107 if ( this . device . channel . constructor . name === 'LanChannel' ) { // FIXME: Circular import workaround
108- if ( this . settings . get_boolean ( 'automount' ) )
108+ if ( this . settings . get_boolean ( 'automount' ) ) {
109+ debug (
110+ `Initial SFTP automount for ${ this . device . name } ` ) ;
109111 this . mount ( ) ;
112+ }
110113 } else {
111114 this . device . lookup_action ( 'mount' ) . enabled = false ;
112115 this . device . lookup_action ( 'unmount' ) . enabled = false ;
@@ -136,40 +139,20 @@ const SFTPPlugin = GObject.registerClass({
136139 if ( ! regex . test ( uri ) )
137140 return ;
138141
142+ debug ( `Found new SFTP mount for ${ this . device . name } ` ) ;
139143 this . _gmount = mount ;
140144 this . _addSubmenu ( mount ) ;
141- this . _addSymlink ( mount ) ;
145+ this . _addSymlinks ( mount , this . _directories ) ;
142146 }
143147
144148 _onMountRemoved ( monitor , mount ) {
145149 if ( this . gmount !== mount )
146150 return ;
147151
152+ debug ( `Mount for ${ this . device . name } removed, cleaning up` ) ;
148153 this . _gmount = null ;
149154 this . _removeSubmenu ( ) ;
150- }
151-
152- async _listDirectories ( mount ) {
153- const file = mount . get_root ( ) ;
154-
155- const iter = await file . enumerate_children_async (
156- Gio . FILE_ATTRIBUTE_STANDARD_NAME ,
157- Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS ,
158- GLib . PRIORITY_DEFAULT ,
159- this . cancellable ) ;
160-
161- const infos = await iter . next_files_async ( MAX_MOUNT_DIRS ,
162- GLib . PRIORITY_DEFAULT , this . cancellable ) ;
163- iter . close_async ( GLib . PRIORITY_DEFAULT , null , null ) ;
164-
165- const directories = { } ;
166-
167- for ( const info of infos ) {
168- const name = info . get_name ( ) ;
169- directories [ name ] = `${ file . get_uri ( ) } ${ name } /` ;
170- }
171-
172- return directories ;
155+ this . _cleanupDirectories ( ) ;
173156 }
174157
175158 _onAskQuestion ( op , message , choices ) {
@@ -221,11 +204,23 @@ const SFTPPlugin = GObject.registerClass({
221204 op . connect ( 'ask-question' , this . _onAskQuestion ) ;
222205 op . connect ( 'ask-password' , this . _onAskPassword ) ;
223206
224- // This is the actual call to mount the device
225207 const host = this . device . channel . host ;
226208 const uri = `sftp://${ host } :${ packet . body . port } /` ;
227209 const file = Gio . File . new_for_uri ( uri ) ;
228210
211+ const _directories = { } ;
212+ for ( let i = 0 ; i < packet . body . multiPaths . length ; ++ i ) {
213+ try {
214+ const _name = packet . body . pathNames [ i ] ;
215+ const _dir = packet . body . multiPaths [ i ] ;
216+ _directories [ _name ] = _dir ;
217+ } catch { }
218+ }
219+ this . _directories = _directories ;
220+ debug ( `Directories: ${ Object . entries ( this . _directories ) } ` ) ;
221+
222+ debug ( `Mounting ${ this . device . name } SFTP server as ${ uri } ` ) ;
223+ // This is the actual call to mount the device
229224 await file . mount_enclosing_volume ( GLib . PRIORITY_DEFAULT , op ,
230225 this . cancellable ) ;
231226 } catch ( e ) {
@@ -259,7 +254,7 @@ const SFTPPlugin = GObject.registerClass({
259254 this . cancellable ) ;
260255
261256 if ( ssh_add . get_exit_status ( ) !== 0 )
262- debug ( stdout . trim ( ) , this . device . name ) ;
257+ logError ( stdout . trim ( ) , this . device . name ) ;
263258 }
264259
265260 /**
@@ -335,16 +330,17 @@ const SFTPPlugin = GObject.registerClass({
335330 return this . _filesMenuItem ;
336331 }
337332
338- async _addSubmenu ( mount ) {
333+ _addSubmenu ( mount ) {
339334 try {
340- const directories = await this . _listDirectories ( mount ) ;
341335
342336 // Submenu sections
343337 const dirSection = new Gio . Menu ( ) ;
344338 const unmountSection = this . _getUnmountSection ( ) ;
345339
346- for ( const [ name , uri ] of Object . entries ( directories ) )
340+ for ( const [ name , path ] of Object . entries ( this . _directories ) ) {
341+ const uri = `${ mount . get_root ( ) . get_uri ( ) } ${ path } ` ;
347342 dirSection . append ( name , `device.openPath::${ uri } ` ) ;
343+ }
348344
349345 // Files submenu
350346 const filesSubmenu = new Gio . Menu ( ) ;
@@ -368,6 +364,7 @@ const SFTPPlugin = GObject.registerClass({
368364 }
369365
370366 _removeSubmenu ( ) {
367+ debug ( 'Removing device.mount submenu and restoring mount action' ) ;
371368 try {
372369 const index = this . device . removeMenuAction ( 'device.mount' ) ;
373370 const action = this . device . lookup_action ( 'mount' ) ;
@@ -389,54 +386,95 @@ const SFTPPlugin = GObject.registerClass({
389386 * Create a symbolic link referring to the device by name
390387 *
391388 * @param {Gio.Mount } mount - A GMount to link to
389+ * @param {object } directories - The name:path mappings for
390+ * the directory symlinks.
392391 */
393- async _addSymlink ( mount ) {
392+ async _addSymlinks ( mount , directories ) {
393+ if ( ! directories )
394+ return ;
395+ debug ( `Building symbolic links for ${ this . device . name } ` ) ;
394396 try {
395- const by_name_dir = Gio . File . new_for_path (
396- `${ Config . RUNTIMEDIR } /by-name/`
397+ // Replace path separator with a Unicode lookalike:
398+ const safe_device_name = safe_dirname ( this . device . name ) ;
399+
400+ const device_dir = Gio . File . new_for_path (
401+ `${ Config . RUNTIMEDIR } /by-name/${ safe_device_name } `
397402 ) ;
403+ // Check for and remove any existing links or other cruft
404+ if ( device_dir . query_exists ( null ) &&
405+ device_dir . query_file_type (
406+ Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS , null ) !==
407+ Gio . FileType . DIRECTORY ) {
408+ await device_dir . delete_async (
409+ GLib . PRIORITY_DEFAULT , this . cancellable ) ;
410+ }
398411
399412 try {
400- by_name_dir . make_directory_with_parents ( null ) ;
413+ device_dir . make_directory_with_parents ( null ) ;
401414 } catch ( e ) {
402415 if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . EXISTS ) )
403416 throw e ;
404417 }
418+ this . _device_dir = device_dir ;
419+
420+ const base_path = mount . get_root ( ) . get_path ( ) ;
421+ for ( const [ _name , _path ] of Object . entries ( directories ) ) {
422+ const safe_name = safe_dirname ( _name ) ;
423+ const link_target = `${ base_path } ${ _path } ` ;
424+ const link = Gio . File . new_for_path (
425+ `${ device_dir . get_path ( ) } /${ safe_name } ` ) ;
426+
427+ // Check for and remove any existing stale link
428+ try {
429+ const link_stat = await link . query_info_async (
430+ 'standard::symlink-target' ,
431+ Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS ,
432+ GLib . PRIORITY_DEFAULT ,
433+ this . cancellable ) ;
434+
435+ if ( link_stat . get_symlink_target ( ) === link_target )
436+ continue ;
437+
438+ await link . delete_async ( GLib . PRIORITY_DEFAULT ,
439+ this . cancellable ) ;
440+ } catch ( e ) {
441+ if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . NOT_FOUND ) )
442+ throw e ;
443+ }
405444
406- // Replace path separator with a Unicode lookalike:
407- let safe_device_name = this . device . name . replace ( '/' , '∕' ) ;
408-
409- if ( safe_device_name === '.' )
410- safe_device_name = '·' ;
411- else if ( safe_device_name === '..' )
412- safe_device_name = '··' ;
445+ debug ( `Linking ' ${ _name } ' to device path ${ _path } ` ) ;
446+ link . make_symbolic_link ( link_target , this . cancellable ) ;
447+ }
448+ } catch ( e ) {
449+ debug ( e , this . device . name ) ;
450+ }
451+ }
413452
414- const link_target = mount . get_root ( ) . get_path ( ) ;
415- const link = Gio . File . new_for_path (
416- `${ by_name_dir . get_path ( ) } /${ safe_device_name } ` ) ;
453+ /**
454+ * Remove the directory symlinks placed in the by-name path for the
455+ * device.
456+ */
457+ async _cleanupDirectories ( ) {
458+ if ( this . _device_dir === null || ! this . _directories )
459+ return ;
417460
418- // Check for and remove any existing stale link
461+ for ( const _name of Object . keys ( this . _directories ) ) {
419462 try {
420- const link_stat = await link . query_info_async (
421- 'standard::symlink-target' ,
422- Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS ,
423- GLib . PRIORITY_DEFAULT ,
424- this . cancellable ) ;
463+ const safe_name = safe_dirname ( _name ) ;
425464
426- if ( link_stat . get_symlink_target ( ) === link_target )
427- return ;
428-
429- await link . delete_async ( GLib . PRIORITY_DEFAULT ,
430- this . cancellable ) ;
465+ debug ( `Destroying symlink '${ safe_name } '` ) ;
466+ const link = Gio . File . new_for_path (
467+ `${ this . _device_dir . get_path ( ) } /${ safe_name } ` ) ;
468+ await link . delete_async ( GLib . PRIORITY_DEFAULT , null ) ;
431469 } catch ( e ) {
432470 if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . NOT_FOUND ) )
433- throw e ;
471+ debug ( e , this . device . name ) ;
434472 }
435-
436- link . make_symbolic_link ( link_target , this . cancellable ) ;
437- } catch ( e ) {
438- debug ( e , this . device . name ) ;
439473 }
474+ this . _device_dir = null ;
475+ // We don't clean up this._directories here, because a new mount may
476+ // be created in the future without another packet being received,
477+ // and we'll need to know the pathnames to re-create.
440478 }
441479
442480 /**
@@ -463,6 +501,7 @@ const SFTPPlugin = GObject.registerClass({
463501 return ;
464502
465503 this . _removeSubmenu ( ) ;
504+ this . _cleanupDirectories ( ) ;
466505 this . _mounting = false ;
467506
468507 await this . gmount . unmount_with_operation (
0 commit comments