File Coverage

blib/lib/Fuse/PerlSSH/FS.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Fuse::PerlSSH::FS;
2              
3 2     2   51085 use strict;
  2         4  
  2         132  
4 2     2   10 use warnings;
  2         3  
  2         55  
5              
6 2     2   2442 use Data::Dumper;
  2         30299  
  2         275  
7 2     2   2218 use IPC::PerlSSH;
  2         23907  
  2         67  
8 2     2   2141 use Fuse ':xattr';
  0            
  0            
9             use POSIX qw(ENOENT ENOSYS EEXIST EPERM O_RDONLY O_RDWR O_APPEND O_CREAT EOPNOTSUPP);
10             use Fcntl qw(S_ISBLK S_ISCHR S_ISFIFO);
11              
12             our $VERSION = '0.13';
13             our $self;
14              
15             sub new {
16             my $class = shift;
17              
18             $self = bless({
19             host => undef,
20             port => 22,
21             user => undef,
22             root => '/',
23             umask=> umask(),
24             @_
25             }, $class);
26              
27             die "Options to Fuse::PerlSSH::FS should be key/value pairs passed in as a hash (got an odd number of elements)" if @_ % 2 != 0;
28             die "Fuse::PerlSSH::FS needs a host to work" if !$self->{host};
29             die "Fuse::PerlSSH::FS only accepts password interactively!" if $self->{password};
30              
31             $self->{root} = '/' if !$self->{root};
32             chop($self->{root}) if length($self->{root}) > 1 && $self->{root} =~ /\/$/; # chop trailing slashes
33              
34             print STDERR '## Fuse::PerlSSH::FS::self'.Dumper($self) if $self->{debug};
35              
36             ## setup ssh connection to remote host
37             $self->_remote();
38              
39             ## test remote capabilities
40             eval { %{$self->{capabilities}} = $self->_remote->call("test_capabilities",$self->{root}); };
41             die "Fuse::PerlSSH::FS capabilities test failed! $@" if $@;
42             unless($self->{capabilities}->{can_xattr}){
43             my $testfile = '/perlsshfs-xattr-test-'.time();
44             my $mknod = local_mknod($testfile, 33204,0);
45             my $setxattr = local_setxattr($testfile, 'user.abc','123',0);
46             my $val = local_getxattr($testfile, 'user.abc');
47             my $unlink = local_unlink($testfile, 'user.abc');
48             $self->{capabilities}->{can_xattr} = 2 if $val eq '123';
49             print STDERR "## test_capabilities xattr with testfile: mknod:$mknod, setxattr:$setxattr, val:$val, unlink:$unlink \n" if $self->{debug};
50             }
51             print STDERR "## new: test_capabilities: ".Dumper($self->{capabilities}) if $self->{debug};
52              
53             return $self;
54             }
55              
56             sub _remote {
57             my $self = shift || $self;
58              
59             if( !$self->{ssh} ){
60             $self->{ssh} = IPC::PerlSSH->new( Host => $self->{host}, Port => $self->{port}, User => $self->{user} );
61              
62             if( $self->{ssh} ){
63             $self->{ssh}->store(
64             test_connection => q{ return "HELO"; },
65             test_capabilities => q{
66             my $root = shift;
67              
68             eval { require File::ExtAttr; };
69             my $xattr_module = $@ ? $@ : 1;
70              
71             eval { require Filesys::DfPortable; };
72             my $dfportable_module = $@ ? $@ : 1;
73              
74             my @mount = `mount`;
75             my $can_xattr = 1 if $mount[0] =~ /,user_xattr/;
76              
77             return ('remote_perl_version', $], 'xattr_module', $xattr_module, 'dfportable_module', $dfportable_module, 'can_xattr', $can_xattr);
78             },
79             # remote_mknod => q{ require 'syscall.ph'; syscall(&SYS_mknod,$_[0],$_[1],$_[2]); }, # creates unusable socket files! (probably a problem with dec vs. oct mode, todo: lookup what the syscall requires..)
80             remote_mknod => q{ # todo: replace with Unix::Mknod
81             # todo: does not use mode/dev
82             my $result = open(my $fh,'>', $_[0]) or die "Cannot mknod/open('$_[0]') - $!";
83             close($fh);
84             return $result;
85             },
86             remote_mkdir => q{ # because PerlSSH's mkdir does not propagate the 2nd param $mode (mask)
87             my $result = mkdir($_[0],$_[1]) or die "Cannot mkdir('$_[0]','$_[1]') - $!";
88             return $result;
89             },
90             remote_link => q{ return link($_[0],$_[1]) or die "Cannot link('$_[0]','$_[1]') - $!"; },
91             remote_truncate => q{ # because PerlSSH's truncate only works on filehandles
92             return truncate($_[0],$_[1]) or die "Cannot truncate('$_[0]','$_[1]') - $!";
93             },
94             remote_readdir => q{ # because PerlSSH's readdir removes dotfiles
95             opendir( my $dh, $_[0] ) or die "Cannot opendir('$_[0]') - $!";
96             my @ents = readdir($dh);
97             return @ents;
98             },
99             statfs => q{
100             # @_ = (root,method)
101             my ($blocks,$bavail) = (10000000,5000000);
102             if($_[1] eq 'dfportable'){
103             my $df = dfportable($_[0]);
104             $blocks = $df->{blocks} if defined($df);
105             $bavail = $df->{bavail} if defined($df);
106             }elsif($_[1] eq 'df'){
107             my @df = `df $_[0]`;
108             (my $fsystem,$blocks,my $bused,$bavail,my $capacity,my $mounted) = split(/\s+/,$df[1]);
109             $blocks = $blocks if $df[1];
110             $bavail = $bavail if $df[1];
111             }
112              
113             return (255,1000000,500000,$blocks,$bavail,1024);
114             }, # a pseudo statfs
115             remote_listxattr => q{
116             use File::ExtAttr;
117             my @list = File::ExtAttr::listfattr($_[0]) or die "Cannot listfattr('$_[0]') - $!";
118             for(@list){ $_ = 'user.'.$_ if $_ !~ /\./; } # fix missing ns
119             return @list;
120             },
121             remote_getxattr => q{
122             use File::ExtAttr;
123             die "Cannot getfattr('$_[0]','$_[1]') - no namespace" if $_[1] !~ /\./;
124             my ($ns,$key) = split(/\./,$_[1],2);
125             # return " (".$ns.":$key) ".File::ExtAttr::getfattr($_[0], $key, { namespace => $ns }) or die "Cannot getfattr('$_[0]','$_[1]') - $!";
126             return File::ExtAttr::getfattr($_[0], $key, { namespace => $ns }) or die "Cannot getfattr('$_[0]','$_[1]') - $!";
127             },
128             remote_setxattr => q{
129             use File::ExtAttr;
130             die "Cannot setfattr('$_[0]','$_[1]','$_[2]','$_[3]') - no namespace" if $_[1] !~ /\./;
131             my ($ns,$key) = split(/\./,$_[1],2);
132              
133             # File::ExtAttr: %flags allows control of whether the attribute should be created or should replace an existing attribute's value.
134             # If the key create is true, setfattr will fail if the attribute already exists. If the key replace is true, setfattr will fail if the attribute does not already exist. If neither is specified, then the attribute will be created (if necessary) or silently replaced.
135             my %flags = (create => 1);
136             %flags = (replace => 1) if $_[3] > 1; # OR-ed constants are XATTR_CREATE 1, XATTR_REPLACE 2
137             return File::ExtAttr::setfattr($_[0], $key, $_[2], { namespace => $ns, %flags }) or die "Cannot setfattr('$_[0]','$_[1]','$_[2]','$_[3]') - $!";
138             },
139             remote_removexattr => q{
140             use File::ExtAttr;
141             die "Cannot delfattr('$_[0]','$_[1]') - no namespace" if $_[1] !~ /\./;
142             my ($ns,$key) = split(/\./,$_[1],2);
143             File::ExtAttr::delfattr($_[0], $key, { namespace => $ns }) or die "Cannot delfattr('$_[0]','$_[1]') - $!";
144             },
145             );
146             $self->{ssh}->use_library('FS', qw( chown chmod lstat readlink rename rmdir symlink unlink utime ) );
147             $self->{ssh}->use_library('Fuse::PerlSSH::RemoteFunctions');
148              
149             my $rval;
150             eval { $rval = $self->_remote->call("test_connection"); };
151             die "Fuse::PerlSSH::FS ssh connection not working!" if $@;
152             print STDERR "## _remote: test_connection: ".Dumper($rval) if $self->{debug};
153             }else{
154             die "Fuse::PerlSSH::FS was not able to log in to host $self->{host} on port $self->{port} with user $self->{user}";
155             return undef;
156             }
157             }
158            
159             return $self->{ssh};
160             }
161              
162             sub mount {
163             my $self = shift;
164              
165             ## check local mount point
166             if(!-d $self->{mountpoint}){
167             die 'Fuse::PerlSSH::FS: Mountpoint '.$self->{mountpoint}.' does not exists!';
168             }
169              
170             my %add_xattr;
171             if($self->{no_xattr}){
172             print STDERR "## mount: xattr bindings omitted. --no-xattr option in effect.\n" if $self->{debug};
173             }elsif(!$self->{capabilities}->{can_xattr}){
174             print STDERR "## mount: xattr bindings omitted. Remote host seems to be incapable.\n" if $self->{debug};
175             }else{
176             %add_xattr = (
177             listxattr=> \&local_listxattr,
178             getxattr => \&local_getxattr,
179             setxattr => \&local_setxattr,
180             removexattr=>\&local_removexattr,
181             );
182             }
183              
184             my %fuse = (
185             mountpoint => $self->{mountpoint},
186             threaded => $self->{threaded} ? 1 : 0,
187             debug => $self->{debug} > 1 ? 1 : 0,
188              
189             readdir => \&local_readdir,
190             getattr => \&local_getattr,
191             mknod => \&local_mknod,
192             mkdir => \&local_mkdir,
193             rmdir => \&local_rmdir,
194             rename => \&local_rename,
195             unlink => \&local_unlink,
196             open => \&local_open,
197             read => \&local_read,
198             write => \&local_write,
199             release => \&local_release,
200             symlink => \&local_symlink,
201             link => \&local_link,
202             readlink => \&local_readlink,
203             utime => \&local_utime,
204             truncate => \&local_truncate,
205             ftruncate=> \&local_ftruncate,
206             statfs => \&local_statfs,
207             %add_xattr
208             );
209              
210             Fuse::main( %fuse );
211             return;
212             }
213              
214             sub umount {
215             my $self = shift;
216             print STDERR "## umount: sending 'exit'\n" if $self->{debug};
217             eval { $self->_remote->eval('exit 1'); };
218             }
219              
220             sub path {
221             return $self->{root} if $_[0] eq '/';
222             return $self->{root} . $_[0];
223             }
224              
225             sub local_readdir {
226             my $path = path(shift);
227             print STDERR "## local_readdir: $path \n" if $self->{debug};
228              
229             my @dir;
230             eval { @dir = _remote->call("remote_readdir", $path ); };
231             return -ENOENT() if $@;
232              
233             return @dir ? (@dir, 0) : 0;
234             }
235              
236             sub local_getattr {
237             my $path = path(shift);
238             print STDERR "## local_getattr: $path \n" if $self->{debug};
239              
240             ## Fuse-perl docs say "FIXME: the "ino" field is currently ignored. I tried setting it to 0 in an example script, which consistently caused segfaults."
241             ## $stat[1] = 0; # in case we get segfaults
242              
243             my @stat;
244             eval { @stat = _remote->call("lstat", $path ); };
245              
246             return -ENOENT() if $@; # ENOENT = "file not found"
247             return @stat;
248             }
249              
250             ## Arguments: New directory pathname, numeric modes. Returns an errno.
251             ## Called to create a directory.
252             sub local_mkdir {
253             my $path = path(shift);
254             my $mode = shift;
255              
256             ## pass the "mode as modified by umask"
257             # $mode &= ~$self->{umask} if defined $mode;
258              
259             print STDERR "## local_mkdir: $path perm:decimal($mode),octal(".sprintf("%o", $mode).")\n" if $self->{debug};
260              
261             my $result;
262             eval { $result = _remote->call("remote_mkdir", $path, $mode ); };
263              
264             return -ENOENT() if $@;
265             return $result ? 0 : -$!;
266             }
267              
268             ## Arguments: Filename, numeric modes, numeric device Returns an errno (0 upon success, as usual).
269             ## This function is called for all non-directory, non-symlink nodes, not just devices.
270             sub local_mknod {
271             my $path = path(shift);
272             my $mode = shift;
273             my $dev = shift;
274             print STDERR "## local_mknod: $path perm:decimal($mode),octal(".sprintf("%o", $mode)."), dev:$dev\n" if $self->{debug};
275              
276             # from Fuse::PDF: don't support special files
277             # my $is_special = !S_ISREG($mode) && !S_ISDIR($mode) && !S_ISLNK($mode);
278             # return -EIO() if $is_special;
279              
280             # since this is called for ALL files, not just devices, I'll do some checks
281             # and possibly run the real mknod command.
282             $! = 0;
283              
284             ## pass the "mode as modified by umask"
285             # $mode &= ~$self->{umask} if defined $mode;
286              
287             my $result;
288             eval { $result = _remote->call("remote_mknod", $path,$mode,$dev ); };
289              
290             return -ENOENT() if $@;
291             return $result ? 0 : -$!;
292             }
293              
294              
295             ## Arguments: Pathname. Returns an errno.
296             ## Called to remove a directory.
297             sub local_rmdir {
298             my $path = path(shift);
299             print STDERR "## local_rmdir: $path\n" if $self->{debug};
300              
301             my $result;
302             eval { $result = _remote->call("rmdir", $path ); };
303              
304             return -ENOENT() if $@;
305             return $result ? 0 : -$!;
306             }
307              
308             ## Arguments: old filename, new filename. Returns an errno.
309             ## Called to rename a file, and/or move a file from one directory to another.
310             sub local_rename {
311             my $path = path(shift);
312             my $newpath = path(shift);
313             print STDERR "## local_rename: $path -> $newpath\n" if $self->{debug};
314              
315             my $result;
316             eval { $result = _remote->call("rename", $path, $newpath ); };
317              
318             return -ENOENT() if $@;
319             return $result ? 0 : -$!;
320             }
321              
322              
323             ## Arguments: Filename. Returns an errno.
324             ## Called to remove a file, device, or symlink.
325             sub local_unlink {
326             my $path = path(shift);
327             print STDERR "## local_unlink: $path\n" if $self->{debug};
328              
329             my $result;
330             eval { $result = _remote->call("unlink", $path ); };
331              
332             return -ENOENT() if $@;
333             return $result ? 0 : -$!;
334             }
335              
336             sub local_open {
337             my $path = path(shift);
338             my $mode = shift;
339             # my $fileinfo = shift;
340             print STDERR "## local_open: $path mode:$mode," if $self->{debug};
341              
342             my $fd;
343             eval { $fd = _remote->call("sysopen", $mode, $path ); };
344             print STDERR " fd:$fd\n" if $self->{debug};
345              
346             if($@){
347             print STDERR "## local_open: remote_sysopen failed: $@\n" if $self->{debug};
348             return -ENOENT();
349             }
350              
351             return -$! unless $fd;
352              
353             return (0,$fd);
354             }
355              
356             ## Arguments: Pathname, numeric requested size, numeric offset, file handle Returns a numeric errno, or a string scalar with up to $requestedsize bytes of data.
357             ## Called in an attempt to fetch a portion of the file.
358             sub local_read {
359             my ($path,$length,$offset,$fd) = @_;
360             $path = path(shift);
361             print STDERR "## local_read: $path length:$length, offset:$offset, fd:$fd\n" if $self->{debug};
362              
363             return -ENOENT() unless $fd; # as good as checking if the file exists, no handle, no file
364              
365             my $buf = -ENOSYS(); # init return_value with an error, in case we can't fill it with data
366             eval { $buf = _remote->call("read", $fd, $length, $offset ); };
367              
368             return -ENOSYS() if $@;
369             return $buf;
370             }
371              
372             ## Arguments: Pathname, scalar buffer, numeric offset, file handle. You can use length($buffer) to find the buffersize. Returns length($buffer) if successful (number of bytes written).
373             ## Called in an attempt to write (or overwrite) a portion of the file. Be prepared because $buffer could contain random binary data with NULs and all sorts of other wonderful stuff.
374             sub local_write {
375             my ($path,$buf,$offset,$fd) = @_;
376             $path = path(shift);
377             print STDERR "## local_write: $path buf-length:".length($buf).", offset:$offset, fd:$fd\n" if $self->{debug};
378              
379             return -ENOSYS() unless $fd; # as good as checking if the file exists, no handle, no file
380              
381             # write sadly does not return how many bytes were written
382             eval { _remote->call( "write", $fd, $buf, $offset ) };
383             if($@){
384             print STDERR "## local_write: write failed: $@\n" if $self->{debug};
385             return -ENOSYS();
386             }
387              
388             return length($buf);
389             }
390              
391             ## Arguments: Pathname, numeric flags passed to open, file handle Returns an errno or 0 on success.
392             ## Called to indicate that there are no more references to the file. Called once for every file with the same pathname and flags as were passed to open.
393             sub local_release {
394             my $path = path(shift);
395             my $mode = shift;
396             my $fd = shift;
397              
398             print STDERR "## local_release: $path mode:$mode, fd:$fd\n" if $self->{debug};
399              
400             return -ENOSYS() unless $fd;
401              
402             my $result;
403             eval { $result = _remote->call( "close", $fd ); };
404              
405             return -ENOENT() if $@;
406              
407             return $result ? 0 : -$!;
408             return 0;
409             }
410              
411             ## Arguments: Existing filename, symlink name. Returns an errno.
412             ## Called to create a symbolic link.
413             sub local_symlink {
414             my $path = shift;
415             my $sympath = path(shift);
416             print STDERR "## local_symlink: $path <- $sympath\n" if $self->{debug};
417              
418             my $result;
419             eval { $result = _remote->call("symlink", $path, $sympath ); };
420              
421             return -ENOENT() if $@;
422             return $result ? 0 : -EEXIST(); # if symlink fails, most probably, because it exists
423             }
424              
425             ## Arguments: Existing filename, hardlink name. Returns an errno.
426             ## Called to create hard links.
427             sub local_link {
428             my $path = path(shift);
429             my $linkpath = path(shift);
430             print STDERR "## local_link: $path -> $linkpath\n" if $self->{debug};
431              
432             my $result;
433             eval { $result = _remote->call("remote_link", $path, $linkpath ); };
434              
435             return -ENOENT() if $@;
436             return $result ? 0 : -EEXIST(); # if link fails, most probably, because it exists
437             }
438              
439             ## Arguments: link pathname. Returns a scalar: either a numeric constant, or a text string.
440             ## This is called when dereferencing symbolic links, to learn the target.
441             sub local_readlink {
442             my $path = path(shift);
443             print STDERR "## local_readlink: $path\n" if $self->{debug};
444              
445             my $result;
446             eval { $result = _remote->call("readlink", $path ); };
447              
448             return -ENOENT() if $@;
449             return $result ? $result : -$!;
450             }
451              
452             ## Arguments: Pathname, numeric actime, numeric modtime. Returns an errno.
453             ## Called to change access/modification times for a file/directory/device/symlink.
454             sub local_utime {
455             ## arg order is reversed between perl (path(s) last, and what is passed-in here (path first), probably because the perl way is not atomic as it may die after a few files, and the fuse way makes it atomic: one file at a time
456             my $path = path(shift);
457             my ($atime,$mtime) = (shift,shift);
458             print STDERR "## local_utime: $path atime:$atime,mtime:$mtime\n" if $self->{debug};
459              
460             my $result;
461             eval { $result = _remote->call("utime", $atime, $mtime, $path ); };
462              
463             return -ENOENT() if $@;
464             return $result ? 0 : -$!;
465             }
466              
467             sub local_truncate {
468             my $path = path(shift);
469             my $offset = shift;
470             print STDERR "## local_truncate: $path offset:$offset\n" if $self->{debug};
471              
472             my $result;
473             eval { $result = _remote->call("remote_truncate", $path, $offset ); };
474              
475             return -ENOENT() if $@;
476             return $result ? 0 : -$!;
477             }
478              
479             ## Arguments: Pathname, numeric offset. Returns an errno.
480             ## Called to truncate a file, at the given offset.
481             ## sub x_truncate { return truncate(fixup(shift),shift) ? 0 : -$! ; }
482             sub local_ftruncate {
483             my $path = path(shift);
484             my $offset = shift;
485             my $fd = shift;
486             print STDERR "## local_ftruncate: $path offset:$offset, fd:$fd\n" if $self->{debug};
487              
488             my $result;
489             eval { $result = _remote->call("truncate", $fd, $offset ); }; # as PerlSSH's truncate *only* operates on filehandles, resolved via fd, it's effectively a ftruncate() (see FUSE docs for that)
490              
491             return -ENOENT() if $@;
492             return $result ? 0 : -$!;
493             }
494              
495             ## Arguments: none, Returns any of the following:
496             ## -ENOANO()
497             ## or $namelen, $files, $files_free, $blocks, $blocks_avail, $blocksize
498             ## or -ENOANO(), $namelen, $files, $files_free, $blocks, $blocks_avail, $blocksize
499             sub local_statfs {
500             my @statfs;
501              
502             my $method = $self->{capabilities}->{dfportable_module} eq 1 ? 'dfportable' : 'df';
503              
504             eval { @statfs = _remote->call("statfs", $self->{root}, $method ); };
505             print STDERR "## local_statfs root:$self->{root}, method:$method, (@statfs)\n" if $self->{debug};
506              
507             return -ENOENT() if $@;
508             return @statfs;
509             }
510              
511             sub local_listxattr {
512             my $path = path(shift);
513             print STDERR "## listxattr: $path \n" if $self->{debug};
514              
515             my @list;
516             eval { @list = _remote->call("remote_listxattr", $path ); };
517             return -ENOENT() if $@;
518              
519             print STDERR "## local_listxattr: list:@list\n" if $self->{debug};
520              
521             return @list ? (@list, 0) : 0;
522             }
523              
524             sub local_getxattr {
525             my $path = path(shift);
526             my $key = shift;
527             print STDERR "## local_getxattr: $path key:$key\n" if $self->{debug};
528              
529             my $val;
530             eval { $val = _remote->call("remote_getxattr", $path, $key ); };
531             print STDERR "## local_getxattr: eval:$@ \n" if $self->{debug};
532             return -EOPNOTSUPP() if $@ =~ / no namespace /;
533             return -ENOSYS() if $@;
534              
535             print STDERR "## local_getxattr: ".$key."=".($val||'')." \n" if $self->{debug};
536              
537             return $val ? $val : 0;
538             }
539              
540             sub local_setxattr {
541             my $path = path(shift);
542             my $key = shift;
543             my $val = shift;
544             my $create_replace = shift;
545             print STDERR "## local_setxattr: $path key:$key, val:$val, create|replace:$create_replace\n" if $self->{debug};
546              
547             my $ret;
548             eval { $ret = _remote->call("remote_setxattr", $path, $key, $val, $create_replace ); };
549             print STDERR "## local_setxattr: ret:".($ret||'').", eval:$@ \n" if $self->{debug};
550             return -EOPNOTSUPP() if $@ =~ / no namespace /; # we force the user only to supply a namespace
551             # return -EEXIST() if !defined($ret); # If flags is set to XATTR_CREATE and the extended attribute already exists, this should fail with - EEXIST.
552             # return -ENOATTR() if $@ =~ / no data available/; # If flags is set to XATTR_REPLACE and the extended attribute doesn't exist, this should fail with - ENOATTR.
553              
554             return -ENOSYS() if $@;
555              
556             return $ret ? 0 : $create_replace ? -ENOSYS() : -EEXIST(); # ENOATTR() is missing, thus ENOSYS
557             }
558              
559             sub local_removexattr {
560             my $path = path(shift);
561             my $key = shift;
562             print STDERR "## local_removexattr: $path key:$key\n" if $self->{debug};
563              
564             my ($ret,$err);
565             eval { ($ret,$err) = _remote->call("remote_removexattr", $path, $key ); };
566             print STDERR "## local_removexattr: ret:".($ret||'').", eval:$@ \n" if $self->{debug};
567             return -EOPNOTSUPP() if $@ =~ / no namespace /;
568             return -ENOSYS() if $@;
569              
570             # return -ENOATTR() if !defined($ret); # if $@ =~ / no data available/;
571             return $ret ? 0 : -ENOSYS(); # ENOATTR() is missing, thus ENOSYS
572             }
573              
574              
575             1;
576              
577             __END__