@@ -8,6 +8,7 @@ import GObject from 'gi://GObject';
88
99import Config from '../../config.js' ;
1010import Plugin from '../plugin.js' ;
11+ import { safe_dirname } from '../utils/file.js' ;
1112
1213
1314export const Metadata = {
@@ -37,9 +38,6 @@ export const Metadata = {
3738} ;
3839
3940
40- const MAX_MOUNT_DIRS = 12 ;
41-
42-
4341/**
4442 * SFTP Plugin
4543 * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sftp
@@ -53,6 +51,8 @@ const SFTPPlugin = GObject.registerClass({
5351 super . _init ( device , 'sftp' ) ;
5452
5553 this . _gmount = null ;
54+ this . _directories = { } ;
55+ this . _device_dir = null ;
5656 this . _mounting = false ;
5757
5858 // A reusable launcher for ssh processes
@@ -89,7 +89,7 @@ const SFTPPlugin = GObject.registerClass({
8989 if ( regex . test ( uri ) ) {
9090 this . _gmount = mount ;
9191 this . _addSubmenu ( mount ) ;
92- this . _addSymlink ( mount ) ;
92+ this . _addSymlinks ( mount , this . _directories ) ;
9393
9494 break ;
9595 }
@@ -104,8 +104,11 @@ const SFTPPlugin = GObject.registerClass({
104104
105105 // Only enable for Lan connections
106106 if ( this . device . channel . constructor . name === 'LanChannel' ) { // FIXME: Circular import workaround
107- if ( this . settings . get_boolean ( 'automount' ) )
107+ if ( this . settings . get_boolean ( 'automount' ) ) {
108+ debug (
109+ `Initial SFTP automount for ${ this . device . name } ` ) ;
108110 this . mount ( ) ;
111+ }
109112 } else {
110113 this . device . lookup_action ( 'mount' ) . enabled = false ;
111114 this . device . lookup_action ( 'unmount' ) . enabled = false ;
@@ -135,40 +138,20 @@ const SFTPPlugin = GObject.registerClass({
135138 if ( ! regex . test ( uri ) )
136139 return ;
137140
141+ debug ( `Found new SFTP mount for ${ this . device . name } ` ) ;
138142 this . _gmount = mount ;
139143 this . _addSubmenu ( mount ) ;
140- this . _addSymlink ( mount ) ;
144+ this . _addSymlinks ( mount , this . _directories ) ;
141145 }
142146
143147 _onMountRemoved ( monitor , mount ) {
144148 if ( this . gmount !== mount )
145149 return ;
146150
151+ debug ( `Mount for ${ this . device . name } removed, cleaning up` ) ;
147152 this . _gmount = null ;
148153 this . _removeSubmenu ( ) ;
149- }
150-
151- async _listDirectories ( mount ) {
152- const file = mount . get_root ( ) ;
153-
154- const iter = await file . enumerate_children_async (
155- Gio . FILE_ATTRIBUTE_STANDARD_NAME ,
156- Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS ,
157- GLib . PRIORITY_DEFAULT ,
158- this . cancellable ) ;
159-
160- const infos = await iter . next_files_async ( MAX_MOUNT_DIRS ,
161- GLib . PRIORITY_DEFAULT , this . cancellable ) ;
162- iter . close_async ( GLib . PRIORITY_DEFAULT , null , null ) ;
163-
164- const directories = { } ;
165-
166- for ( const info of infos ) {
167- const name = info . get_name ( ) ;
168- directories [ name ] = `${ file . get_uri ( ) } ${ name } /` ;
169- }
170-
171- return directories ;
154+ this . _cleanupDirectories ( ) ;
172155 }
173156
174157 _onAskQuestion ( op , message , choices ) {
@@ -220,11 +203,23 @@ const SFTPPlugin = GObject.registerClass({
220203 op . connect ( 'ask-question' , this . _onAskQuestion ) ;
221204 op . connect ( 'ask-password' , this . _onAskPassword ) ;
222205
223- // This is the actual call to mount the device
224206 const host = this . device . channel . host ;
225207 const uri = `sftp://${ host } :${ packet . body . port } /` ;
226208 const file = Gio . File . new_for_uri ( uri ) ;
227209
210+ const _directories = { } ;
211+ for ( let i = 0 ; i < packet . body . multiPaths . length ; ++ i ) {
212+ try {
213+ const _name = packet . body . pathNames [ i ] ;
214+ const _dir = packet . body . multiPaths [ i ] ;
215+ _directories [ _name ] = _dir ;
216+ } catch { }
217+ }
218+ this . _directories = _directories ;
219+ debug ( `Directories: ${ Object . entries ( this . _directories ) } ` ) ;
220+
221+ debug ( `Mounting ${ this . device . name } SFTP server as ${ uri } ` ) ;
222+ // This is the actual call to mount the device
228223 await file . mount_enclosing_volume ( GLib . PRIORITY_DEFAULT , op ,
229224 this . cancellable ) ;
230225 } catch ( e ) {
@@ -258,7 +253,7 @@ const SFTPPlugin = GObject.registerClass({
258253 this . cancellable ) ;
259254
260255 if ( ssh_add . get_exit_status ( ) !== 0 )
261- debug ( stdout . trim ( ) , this . device . name ) ;
256+ logError ( stdout . trim ( ) , this . device . name ) ;
262257 }
263258
264259 /**
@@ -334,16 +329,17 @@ const SFTPPlugin = GObject.registerClass({
334329 return this . _filesMenuItem ;
335330 }
336331
337- async _addSubmenu ( mount ) {
332+ _addSubmenu ( mount ) {
338333 try {
339- const directories = await this . _listDirectories ( mount ) ;
340334
341335 // Submenu sections
342336 const dirSection = new Gio . Menu ( ) ;
343337 const unmountSection = this . _getUnmountSection ( ) ;
344338
345- for ( const [ name , uri ] of Object . entries ( directories ) )
339+ for ( const [ name , path ] of Object . entries ( this . _directories ) ) {
340+ const uri = `${ mount . get_root ( ) . get_uri ( ) } ${ path } ` ;
346341 dirSection . append ( name , `device.openPath::${ uri } ` ) ;
342+ }
347343
348344 // Files submenu
349345 const filesSubmenu = new Gio . Menu ( ) ;
@@ -367,6 +363,7 @@ const SFTPPlugin = GObject.registerClass({
367363 }
368364
369365 _removeSubmenu ( ) {
366+ debug ( 'Removing device.mount submenu and restoring mount action' ) ;
370367 try {
371368 const index = this . device . removeMenuAction ( 'device.mount' ) ;
372369 const action = this . device . lookup_action ( 'mount' ) ;
@@ -388,54 +385,95 @@ const SFTPPlugin = GObject.registerClass({
388385 * Create a symbolic link referring to the device by name
389386 *
390387 * @param {Gio.Mount } mount - A GMount to link to
388+ * @param {object } directories - The name:path mappings for
389+ * the directory symlinks.
391390 */
392- async _addSymlink ( mount ) {
391+ async _addSymlinks ( mount , directories ) {
392+ if ( ! directories )
393+ return ;
394+ debug ( `Building symbolic links for ${ this . device . name } ` ) ;
393395 try {
394- const by_name_dir = Gio . File . new_for_path (
395- `${ Config . RUNTIMEDIR } /by-name/`
396+ // Replace path separator with a Unicode lookalike:
397+ const safe_device_name = safe_dirname ( this . device . name ) ;
398+
399+ const device_dir = Gio . File . new_for_path (
400+ `${ Config . RUNTIMEDIR } /by-name/${ safe_device_name } `
396401 ) ;
402+ // Check for and remove any existing links or other cruft
403+ if ( device_dir . query_exists ( null ) &&
404+ device_dir . query_file_type (
405+ Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS , null ) !==
406+ Gio . FileType . DIRECTORY ) {
407+ await device_dir . delete_async (
408+ GLib . PRIORITY_DEFAULT , this . cancellable ) ;
409+ }
397410
398411 try {
399- by_name_dir . make_directory_with_parents ( null ) ;
412+ device_dir . make_directory_with_parents ( null ) ;
400413 } catch ( e ) {
401414 if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . EXISTS ) )
402415 throw e ;
403416 }
417+ this . _device_dir = device_dir ;
418+
419+ const base_path = mount . get_root ( ) . get_path ( ) ;
420+ for ( const [ _name , _path ] of Object . entries ( directories ) ) {
421+ const safe_name = safe_dirname ( _name ) ;
422+ const link_target = `${ base_path } ${ _path } ` ;
423+ const link = Gio . File . new_for_path (
424+ `${ device_dir . get_path ( ) } /${ safe_name } ` ) ;
425+
426+ // Check for and remove any existing stale link
427+ try {
428+ const link_stat = await link . query_info_async (
429+ 'standard::symlink-target' ,
430+ Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS ,
431+ GLib . PRIORITY_DEFAULT ,
432+ this . cancellable ) ;
433+
434+ if ( link_stat . get_symlink_target ( ) === link_target )
435+ continue ;
436+
437+ await link . delete_async ( GLib . PRIORITY_DEFAULT ,
438+ this . cancellable ) ;
439+ } catch ( e ) {
440+ if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . NOT_FOUND ) )
441+ throw e ;
442+ }
404443
405- // Replace path separator with a Unicode lookalike:
406- let safe_device_name = this . device . name . replace ( '/' , '∕' ) ;
407-
408- if ( safe_device_name === '.' )
409- safe_device_name = '·' ;
410- else if ( safe_device_name === '..' )
411- safe_device_name = '··' ;
444+ debug ( `Linking ' ${ _name } ' to device path ${ _path } ` ) ;
445+ link . make_symbolic_link ( link_target , this . cancellable ) ;
446+ }
447+ } catch ( e ) {
448+ debug ( e , this . device . name ) ;
449+ }
450+ }
412451
413- const link_target = mount . get_root ( ) . get_path ( ) ;
414- const link = Gio . File . new_for_path (
415- `${ by_name_dir . get_path ( ) } /${ safe_device_name } ` ) ;
452+ /**
453+ * Remove the directory symlinks placed in the by-name path for the
454+ * device.
455+ */
456+ async _cleanupDirectories ( ) {
457+ if ( this . _device_dir === null || ! this . _directories )
458+ return ;
416459
417- // Check for and remove any existing stale link
460+ for ( const _name of Object . keys ( this . _directories ) ) {
418461 try {
419- const link_stat = await link . query_info_async (
420- 'standard::symlink-target' ,
421- Gio . FileQueryInfoFlags . NOFOLLOW_SYMLINKS ,
422- GLib . PRIORITY_DEFAULT ,
423- this . cancellable ) ;
462+ const safe_name = safe_dirname ( _name ) ;
424463
425- if ( link_stat . get_symlink_target ( ) === link_target )
426- return ;
427-
428- await link . delete_async ( GLib . PRIORITY_DEFAULT ,
429- this . cancellable ) ;
464+ debug ( `Destroying symlink '${ safe_name } '` ) ;
465+ const link = Gio . File . new_for_path (
466+ `${ this . _device_dir . get_path ( ) } /${ safe_name } ` ) ;
467+ await link . delete_async ( GLib . PRIORITY_DEFAULT , null ) ;
430468 } catch ( e ) {
431469 if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . NOT_FOUND ) )
432- throw e ;
470+ debug ( e , this . device . name ) ;
433471 }
434-
435- link . make_symbolic_link ( link_target , this . cancellable ) ;
436- } catch ( e ) {
437- debug ( e , this . device . name ) ;
438472 }
473+ this . _device_dir = null ;
474+ // We don't clean up this._directories here, because a new mount may
475+ // be created in the future without another packet being received,
476+ // and we'll need to know the pathnames to re-create.
439477 }
440478
441479 /**
@@ -462,6 +500,7 @@ const SFTPPlugin = GObject.registerClass({
462500 return ;
463501
464502 this . _removeSubmenu ( ) ;
503+ this . _cleanupDirectories ( ) ;
465504 this . _mounting = false ;
466505
467506 await this . gmount . unmount_with_operation (
0 commit comments