File Coverage

blib/lib/App/Smbxfer.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             #! /usr/bin/perl
2              
3             package App::Smbxfer;
4             our $VERSION = 0.01;
5              
6 1     1   38776 use strict;
  1         2  
  1         43  
7 1     1   5 use warnings;
  1         2  
  1         31  
8 1     1   5 use Carp;
  1         5  
  1         99  
9              
10 1     1   5 use Exporter;
  1         2  
  1         38  
11 1     1   1372 use Getopt::Long;
  1         14206  
  1         5  
12 1     1   710 use IO::Prompt;
  0            
  0            
13             use Filesys::SmbClient;
14              
15             # Exports...
16             use base qw( Exporter );
17             our @EXPORT_OK = qw(
18             credentials do_smb_transfer parse_smb_spec
19             create_smb_dir_path create_local_dir_path smb_element_type
20             smb_upload smb_download
21             );
22              
23             __PACKAGE__->run unless caller;
24              
25             #######
26              
27             sub usage {
28             qq{
29             USAGE
30             Smbxfer ///[/[/]]
31             Smbxfer ///[//]
32              
33             }
34             }
35              
36             #######
37              
38             sub options {
39             qq{
40             OPTIONS
41             Usage information:
42             --usage|help
43              
44             Command-line options:
45             --options
46              
47             Name of file containing credentials (standard smb credentials file):
48             --cred
49            
50             Transfer directory :
51             --recursive
52              
53             Create parent directories:
54             --parents
55              
56             }
57             }
58              
59             #######
60              
61             sub run {
62             # Process command-line options...
63             my ($cred, $recursive, $create_parents, $usage, $options);
64             my $options_ok = GetOptions(
65             'cred=s' => \$cred,
66             'recursive' => \$recursive,
67             'parents' => \$create_parents,
68             'usage|help' => \$usage,
69             'options' => \$options,
70             );
71             die usage unless $options_ok;
72              
73             ( defined $usage ) && die usage;
74             ( defined $options ) && die options;
75            
76             my ( $source, $dest ) = @ARGV;
77             die usage unless defined $source && defined $dest;
78            
79             # Ensure that exactly one of source/dest is in "SMB path spec" format...
80             ($dest =~ m|^//|) xor ($source =~ m|^//|) or die usage;
81            
82             # Get access credentials for SMB connection...
83             my ($username, $password, $domain) = credentials($cred);
84            
85             # Prepare SMB connection object...
86             my $smb = Filesys::SmbClient->new(
87             username => $username, password => $password, workgroup => $domain
88             );
89              
90             # Determine if source is local (not in "SMB path spec" format)...
91             my $source_is_local = ($source !~ m|^//|);
92              
93             my ($local_path, $remote_smb_path_spec) = validated_paths(
94             SMB => $smb,
95             SOURCE => $source,
96             DEST => $dest,
97             SOURCE_IS_LOCAL => $source_is_local
98             );
99            
100             # Initiate transfer...
101             do_smb_transfer(
102             SMB_OBJECT => $smb,
103             LOCAL_PATH => $local_path,
104             SMB_PATH_SPEC => $remote_smb_path_spec,
105             SOURCE_IS_LOCAL => $source_is_local,
106             RECURSIVE => $recursive,
107             CREATE_PARENTS => $create_parents
108             );
109             }
110              
111             #########################
112              
113             sub credentials {
114             my ($credentials_filename) = @_;
115              
116             my ($username, $password, $domain);
117              
118             if ($credentials_filename) {
119             # Read access credentials from file formatted using standard smbmount
120             # syntax...
121             open( my $credentials, '<', "$credentials_filename" )
122             or croak "cannot open credentials file: $!";
123            
124             my @lines;
125             while( <$credentials> ){
126             my ($value) = (m/.*=\s+?(.*)$/);
127             push @lines, $value;
128             }
129             close $credentials;
130             ($username, $password, $domain) = @lines;
131             }
132             else {
133             # Getting credentials interactively...
134             $username = prompt( "username? " );
135             $password = prompt( "password? ", -e => '*' );
136             $domain = prompt( "domain? " );
137             }
138              
139             return $username, $password, $domain;
140             }
141              
142             #########################
143              
144             sub validated_paths {
145             my %param = @_;
146              
147             my $smb = $param{SMB} or croak "SMB object required";
148             my $source = $param{SOURCE};
149             my $dest = $param{DEST};
150             my $source_is_local = $param{SOURCE_IS_LOCAL};
151              
152             defined $source or croak "Source path required";
153             defined $dest or croak "Destination path required";
154             defined $source_is_local or croak "SOURCE_IS_LOCAL param required";
155              
156             # Ensure that exactly one of source/dest is in "SMB path spec" format...
157             ($dest =~ m|^//|) xor ($source =~ m|^//|)
158             or croak 'source OR destination must be in "SMB path spec" format';
159            
160             my ($local_path, $remote_smb_path_spec) = ($source, $dest);
161             ($local_path, $remote_smb_path_spec) = ($dest, $source) unless $source_is_local;
162              
163             # Normalize form of local and remote paths...
164             $local_path =~ s|//|/|g;
165             $local_path =~ s|/$||;
166             $remote_smb_path_spec =~ s|^/+||; # temporarily remove valid leading '//'
167             $remote_smb_path_spec =~ s|//|/|g;
168             $remote_smb_path_spec =~ s|/$||; # no trailing slash
169             $remote_smb_path_spec = 'smb://' . $remote_smb_path_spec;
170              
171             # Find type of remote element...
172             my $remote_element_type = smb_element_type( $smb, $remote_smb_path_spec )
173             or croak "Error: SMB specification $remote_smb_path_spec not found";
174              
175             # Check types of source and destination...
176             my ($source_is_dir, $dest_is_dir_or_nonexistent);
177             if( $source_is_local ) {
178             croak "Error: local source $source is not a file or a directory"
179             unless( -f $source or -d $source );
180             $source_is_dir = -d $source;
181             $dest_is_dir_or_nonexistent = 1 unless defined $remote_element_type;
182             # Consider file shares to be directories for purposes of file transfer...
183             $dest_is_dir_or_nonexistent = 1 if $remote_element_type == SMBC_DIR or $remote_element_type == SMBC_FILE_SHARE;
184             }
185             else {
186             croak "Error: SMB source $source is not a file or a directory"
187             unless( $remote_element_type == SMBC_FILE or $remote_element_type == SMBC_DIR );
188             $source_is_dir = ( $remote_element_type == SMBC_DIR );
189             $dest_is_dir_or_nonexistent = (not -e $dest or -d $dest);
190             }
191              
192             # If source is a dir, any existing dest must also be a dir...
193             croak "Error: when transferring a directory source, any existing destination must also be a directory"
194             if( $source_is_dir and not $dest_is_dir_or_nonexistent );
195              
196             return $local_path, $remote_smb_path_spec;
197             }
198              
199             #########################
200              
201             sub do_smb_transfer {
202             my %param = @_;
203              
204             my $smb = $param{SMB_OBJECT} or croak "SMB object required";
205             my $local_path = $param{LOCAL_PATH} or croak "local path required";
206             my $smb_path_spec = $param{SMB_PATH_SPEC} or croak "remote SMB path specification required";
207             my $source_is_local = $param{SOURCE_IS_LOCAL};
208             my $recursive = $param{RECURSIVE};
209             my $create_parents = $param{CREATE_PARENTS};
210              
211             # Create leading directories of destination path if requested...
212             if( $create_parents ) {
213             my ($smb_parent_path) = ( parse_smb_spec( $smb_path_spec ) )[2];
214              
215             if( $source_is_local ) {
216             # Create remote SMB path to hold local source...
217             my ($local_path_parent_dirs) = ( $local_path =~ m|/?(.*)/[^/]+/?$| );
218              
219             my $element_type = smb_element_type( $smb, $smb_path_spec );
220             unless( $element_type == SMBC_DIR or $element_type == SMBC_FILE_SHARE ) {
221             die "Error: destination must be a directory with --parents option.";
222             }
223             create_smb_dir_path( $smb, $smb_path_spec, $local_path_parent_dirs );
224              
225             # postfix destination path with parent dirs we just created from source...
226             $smb_path_spec .= '/' . $local_path_parent_dirs;
227             }
228             else {
229             # Create local path to hold remote SMB source...
230             unless( -d $local_path ) {
231             die "Error: destination must be a directory with --parents option.";
232             }
233             create_local_dir_path( $local_path, $smb_parent_path );
234              
235             # postfix destination path with parent dirs we just created from source...
236             $local_path .= '/' . $smb_parent_path;
237             }
238             }
239              
240             my $rc = 0;
241             if( $source_is_local ) {
242             # Transfer: local -> remote...
243             $rc = smb_upload(
244             SMB_OBJ => $smb,
245             SOURCE => $local_path,
246             SMB_PATH_SPEC => $smb_path_spec,
247             RECURSIVE => $recursive
248             );
249             }
250             else {
251             # Transfer: remote -> local...
252             $rc = smb_download(
253             SMB_OBJ => $smb,
254             SMB_PATH_SPEC => $smb_path_spec,
255             LOCAL_DEST_NAME => $local_path,
256             RECURSIVE => $recursive
257             );
258             }
259              
260             return $rc;
261             }
262              
263             #########################
264              
265             sub parse_smb_spec {
266             my ($path) = @_;
267              
268             my ($server, $share);
269             ($server, $share, $path) =
270             ($path =~ m|
271             //([\w\.]+) # //server
272             /(\w+) # /share
273             /?(.+)? # /path/to/something
274             |x);
275              
276             # Path spec is invalid...
277             return unless $server && $share;
278              
279             my ($share_spec, $parent_path, $path_spec, $parent_path_spec, $basename);
280              
281             $share_spec = "smb://$server/$share/";
282             $path_spec = $share_spec;
283             $parent_path_spec = $share_spec;
284              
285             if( defined $path ) {
286             ($parent_path) = ( $path =~ m|(.*/)[^/]+/?$| );
287             $path_spec .= $path;
288             $parent_path_spec .= $parent_path if $parent_path;
289             ($basename) = ($path =~ m|([^/]*)$| );
290             }
291              
292             return $server, $share, $parent_path, $path,
293             $share_spec, $path_spec, $parent_path_spec,
294             $basename;
295             }
296              
297              
298             #########################
299              
300             sub create_smb_dir_path {
301             my ( $smb, $smb_path_spec_prefix, $path_to_create ) = @_;
302              
303             my ($root, $remaining_path) =
304             ($path_to_create =~ m|
305             ^/? # optional leading '/'
306             ([^/]+?) # 1st capture: anything but dir separators
307             (/.*)? # 2nd capture (optional): separator followed by dir names with separators
308             /?$ # optional trailing '/'
309             |x);
310              
311             if( $root ) {
312             $smb->mkdir( $smb_path_spec_prefix . '/' . $root, '0666' )
313             or croak "SMB error: cannot mkdir $smb_path_spec_prefix/$root: $!";
314             }
315             else {
316             # We were called without a valid path to create...
317             return;
318             }
319              
320             if( $remaining_path ) {
321             create_smb_dir_path( $smb, $smb_path_spec_prefix . '/' . $root, $remaining_path );
322             }
323              
324             return 1;
325             }
326              
327             #########################
328              
329             sub create_local_dir_path {
330             my ( $local_prefix, $path_to_create ) = @_;
331              
332             my ($root, $remaining_path) =
333             ($path_to_create =~ m|
334             ^/? # optional leading '/'
335             ([^/]+?) # 1st capture: anything but dir separators
336             (/.*)? # 2nd capture (optional): separator followed by dir names with separators
337             /?$ # optional trailing '/'
338             |x);
339              
340             if( $root ) {
341             mkdir( $local_prefix . '/' . $root ) or croak "cannot mkdir $root: $!";
342             }
343             else {
344             # We were called without a valid path to create...
345             return;
346             }
347              
348             if( $remaining_path ) {
349             create_local_dir_path( $local_prefix . '/' . $root, $remaining_path );
350             }
351              
352             return 1;
353             }
354              
355             #########################
356              
357             sub smb_element_type {
358             my ($smb, $smb_path_spec) = @_;
359              
360             my ($share_name, $smb_parent_path, $smb_path, $smb_share_spec,
361             $smb_basename) = ( parse_smb_spec( $smb_path_spec ) )[1,2,3,4,7];
362              
363             my $base_type;
364             if( $smb_path ) {
365             if( $smb_basename ) {
366             # Look in parent directory for base of path and find type of base
367             my $parent = $smb_share_spec;
368             $parent .= $smb_parent_path if $smb_parent_path;
369             my $smb_fd = $smb->opendir( $parent )
370             or croak "SMB error: cannot opendir: $!";
371              
372             while( my $share_root_item = $smb->readdir_struct( $smb_fd ) ) {
373             if( lc $share_root_item->[1] eq lc $smb_basename ) {
374             $base_type = $share_root_item->[0];
375             last;
376             }
377             }
378             $smb->closedir( $smb_fd );
379             }
380             else {
381             # Path does not have multiple levels...
382             # Open root dir of share and look for path...
383             my $smb_fd = $smb->opendir( $smb_share_spec )
384             or croak "SMB error: cannot opendir: $!";
385              
386             while( my $share_root_item = $smb->readdir_struct( $smb_fd ) ) {
387             if( lc $share_root_item->[1] eq lc $smb_path ) {
388             $base_type = $share_root_item->[0];
389             last;
390             }
391             }
392             $smb->closedir( $smb_fd );
393             }
394             }
395             elsif( $smb_share_spec ) {
396             # No path given; does the given SMB spec identify a file share?
397             my ($server) = ( $smb_share_spec =~ m|smb://([^/]+)/| );
398             my $smb_fd = $smb->opendir( "smb://$server/" )
399             or croak "SMB error: cannot opendir: $!";
400             while( my $share = $smb->readdir_struct( $smb_fd ) ) {
401             if( lc $share->[1] eq lc $share_name ) {
402             $base_type = $share->[0];
403             last;
404             }
405             }
406             }
407             else {
408             croak "SMB specification $smb_path_spec not found";
409             }
410              
411             # Element not found...
412             return if not defined $base_type;
413              
414             return $base_type;
415             }
416              
417             #########################
418              
419             sub smb_download {
420             my %param = @_;
421              
422             my $smb = $param{SMB_OBJ} or croak "Filesys::SmbClient object required for download";
423             my $src_smb_path_spec = $param{SMB_PATH_SPEC} or croak "SMB path specification of source required to download";
424             my $local_dest_name = $param{LOCAL_DEST_NAME};
425             my $recursive = $param{RECURSIVE};
426              
427             my ($src_smb_path, $src_basename) =
428             (parse_smb_spec( $src_smb_path_spec ))[3,7];
429              
430             my $elem_type = smb_element_type( $smb, $src_smb_path_spec );
431              
432             if( $elem_type == SMBC_DIR ) {
433             # Download directory...
434             unless( $recursive ) {
435             print "Omitting directory $src_smb_path in non-recursive mode.\n";
436             return;
437             }
438             else {
439             # Create dir at destination...
440             mkdir( $local_dest_name . '/' . $src_basename )
441             or croak "cannot mkdir: $!";
442             }
443              
444             my $smb_fd = $smb->opendir( $src_smb_path_spec )
445             or croak "SMB error: cannot opendir: $!";
446            
447             while( my $smb_elem = $smb->readdir( $smb_fd ) ) {
448             next if $smb_elem =~ /^\.{1,2}$/; # skip . and ..
449             smb_download(
450             SMB_OBJ => $smb,
451             SMB_PATH_SPEC => $src_smb_path_spec . '/' . $smb_elem,
452             LOCAL_DEST_NAME => $local_dest_name . '/' . $src_basename,
453             RECURSIVE => 1,
454             );
455             }
456             $smb->closedir( $smb_fd );
457             }
458             elsif( $elem_type == SMBC_FILE ) {
459             # Download file...
460              
461             # If destination is a dir then file goes inside it...
462             $local_dest_name .= '/' . $src_basename if( -d $local_dest_name );
463              
464             open( my $localfile, '>', $local_dest_name )
465             or croak "cannot open file: $!";
466            
467             my $smb_fd = $smb->open( $src_smb_path_spec )
468             or croak "SMB error: cannot open: $!";
469              
470             while( my $buf = $smb->read( $smb_fd ) ) {
471             print $localfile $buf;
472             }
473              
474             $smb->close( $smb_fd );
475             close( $localfile );
476             }
477             else {
478             warn "$src_basename is not a directory or a file...ignoring.\n";
479             }
480              
481             return 1;
482             }
483              
484             #########################
485              
486             sub smb_upload {
487             my %param = @_;
488              
489             my $smb = $param{SMB_OBJ} or croak "Filesys::SmbClient object required for upload";
490             my $local_src = $param{SOURCE} or croak "Name of local file or directory required for upload";
491             my $smb_path_spec = $param{SMB_PATH_SPEC} or croak "SMB path specification of destination required for upload";
492             my $recursive = $param{RECURSIVE};
493              
494             my $elem_type = smb_element_type( $smb, $smb_path_spec );
495             my ($src_basename) = ($local_src =~ m|([^/]*)$| );
496              
497             if( -d $local_src ) {
498             # Upload directory...
499             unless( $recursive ) {
500             print "Omitting directory $local_src in non-recursive mode.\n";
501             return;
502             }
503             else {
504             # Create dir at destination...
505             $smb->mkdir( $smb_path_spec . '/' . $src_basename, '0666' )
506             or croak "SMB error: cannot mkdir: $!";
507             }
508              
509             opendir( my $local_dir, $local_src )
510             or croak "cannot opendir: $!";
511              
512             while ( my $local_dir_elem = readdir( $local_dir ) ) {
513             next if $local_dir_elem =~ /^\.{1,2}$/; # skip . and ..
514              
515             smb_upload(
516             SMB_OBJ => $smb,
517             SOURCE => $local_src . '/' . $local_dir_elem,
518             SMB_PATH_SPEC => $smb_path_spec . '/' . $src_basename,
519             RECURSIVE => 1
520             );
521             }
522             closedir( $local_dir );
523             }
524             elsif( -f $local_src ) {
525             # Upload file...
526            
527             if( $elem_type == SMBC_FILE ) {
528             # Destination is an existing file; remove file remotely...
529             $smb->unlink( $smb_path_spec )
530             or croak "SMB error: cannot unlink: $!";
531             }
532             elsif( $elem_type == SMBC_DIR ) {
533             # Destination is an existing dir => file goes inside it...
534             $smb_path_spec .= '/' . $src_basename;
535             }
536              
537             open( my $sourcefile, '<', $local_src )
538             or croak "cannot open file: $!";
539            
540             my $smb_fd = $smb->open('>' . $smb_path_spec, '0777')
541             or croak "SMB error: cannot create file: $!";
542            
543             $smb->write( $smb_fd, $_ ) while( <$sourcefile> );
544            
545             $smb->close( $smb_fd );
546             close( $sourcefile );
547             }
548             else {
549             warn "$local_src is not a directory or a file...ignoring.\n";
550             }
551              
552             return 1;
553             }
554              
555             #########################
556             1;
557              
558             __END__