File Coverage

blib/lib/File/Save/Home.pm
Criterion Covered Total %
statement 97 107 90.6
branch 39 58 67.2
condition 2 3 66.6
subroutine 17 17 100.0
pod 7 7 100.0
total 162 192 84.3


line stmt bran cond sub pod time code
1             package File::Save::Home;
2             require 5.006_001;
3 5     5   4977 use strict;
  5         10  
  5         197  
4 5     5   30 use warnings;
  5         6  
  5         230  
5 5     5   34 use Exporter ();
  5         9  
  5         554  
6             our $VERSION = '0.09';
7             our @ISA = qw(Exporter);
8             our @EXPORT_OK = qw(
9             get_home_directory
10             get_subhome_directory_status
11             make_subhome_directory
12             restore_subhome_directory_status
13             conceal_target_file
14             reveal_target_file
15             make_subhome_temp_directory
16             );
17             our %EXPORT_TAGS = (
18             subhome_status => [ qw|
19             get_subhome_directory_status
20             restore_subhome_directory_status
21             | ],
22             target => [ qw|
23             conceal_target_file
24             reveal_target_file
25             | ],
26             );
27 5     5   27 use Carp;
  5         13  
  5         641  
28 5     5   31 use File::Path;
  5         7  
  5         325  
29 5         503 use File::Spec::Functions qw|
30             catdir
31             catfile
32             catpath
33             splitdir
34             splitpath
35 5     5   4671 |;
  5         4835  
36 5     5   6614 use File::Temp qw| tempdir |;
  5         129502  
  5         607  
37             *ok = *Test::More::ok;
38 5     5   46 use Cwd;
  5         10  
  5         311  
39 5     5   34 use File::Find;
  5         10  
  5         7211  
40              
41             #################### DOCUMENTATION ###################
42              
43             =head1 NAME
44              
45             File::Save::Home - Place file safely under user home directory
46              
47             =head1 VERSION
48              
49             This document refers to version 0.09, released December 14, 2012.
50              
51             =head1 SYNOPSIS
52              
53             use File::Save::Home qw(
54             get_home_directory
55             get_subhome_directory_status
56             make_subhome_directory
57             restore_subhome_directory_status
58             conceal_target_file
59             reveal_target_file
60             make_subhome_temp_directory
61             );
62              
63             $home_dir = get_home_directory();
64              
65             $desired_dir_ref = get_subhome_directory_status("desired/directory");
66              
67             $desired_dir_ref = get_subhome_directory_status(
68             "desired/directory",
69             "pseudohome/directory", # two-argument version
70             );
71              
72             $desired_dir = make_subhome_directory($desired_dir_ref);
73              
74             restore_subhome_directory_status($desired_dir_ref);
75              
76             $target_ref = conceal_target_file( {
77             dir => $desired_dir,
78             file => 'file_to_be_checked',
79             test => 0,
80             } );
81              
82             reveal_target_file($target_ref);
83              
84             $tmpdir = make_subhome_temp_directory();
85              
86             $tmpdir = make_subhome_temp_directory(
87             "pseudohome/directory", # optional argument version
88             );
89              
90             =head1 DESCRIPTION
91              
92             In the course of deploying an application on another user's system, you
93             sometimes need to place a file in or underneath that user's home
94             directory. Can you do so safely?
95              
96             This Perl extension provides several functions which try to determine whether
97             you can, indeed, safely create directories and files underneath a user's home
98             directory. Among other things, if you are placing a file in such a location
99             only temporarily -- say, for testing purposes -- you can temporarily hide
100             any already existing file with the same name and restore it to its original
101             name and timestamps when you are done.
102              
103             =head1 USAGE
104              
105             =head2 C
106              
107             Analyzes environmental information to determine whether there exists on the
108             system a 'HOME' or 'home-equivalent' directory. Takes no arguments. Returns
109             that directory if it exists; Cs otherwise.
110              
111             On Win32, this directory is the one returned by the following function from the Fmodule:
112              
113             Win32->import( qw(CSIDL_LOCAL_APPDATA) );
114             $realhome = Win32::GetFolderPath( CSIDL_LOCAL_APPDATA() );
115              
116             ... which translates to something like F.
117             (For a further discussion of Win32, see below L.)
118              
119             On Unix-like systems, things are much simpler. We simply check the value of
120             C<$ENV{HOME}>. We cannot do that on Win32 because C<$ENV{HOME}> is not
121             defined there.
122              
123             =cut
124              
125             sub get_home_directory {
126 9     9 1 16710 my $realhome;
127 9 50       67 if ($^O eq 'MSWin32') {
128 0         0 require Win32;
129 0         0 Win32->import( qw(CSIDL_LOCAL_APPDATA) ); # 0x001c
130 0         0 $realhome = Win32::GetFolderPath( CSIDL_LOCAL_APPDATA() );
131 0         0 $realhome =~ s{ }{\ }g;
132 0 0       0 return $realhome if (-d $realhome);
133 0         0 $realhome =~ s|(.*?)\\Local Settings(.*)|$1$2|;
134 0 0       0 return $realhome if (-d $realhome);
135 0         0 croak "Unable to identify directory equivalent to 'HOME' on Win32: $!";
136             } else { # Unix-like systems
137 9         44 $realhome = $ENV{HOME};
138 9         27 $realhome =~ s{ }{\ }g;
139 9 50       265 return $realhome if (-d $realhome);
140 0         0 croak "Unable to identify 'HOME' directory: $!";
141             }
142             }
143              
144             =head2 C
145              
146             =head3 Single argument version
147              
148             Takes as argument a string holding the name of a directory, either
149             single-level (C) or multi-level (C). Determines
150             whether that directory already exists underneath the user's
151             home or home-equivalent directory. Calls C internally,
152             then tacks on the path passed as argument.
153              
154             =head3 Two-argument version
155              
156             Suppose you want to determine the name of a user's home directory by some
157             other route than C. Suppose, for example, that you're
158             on Win32 and want to use the C method supplied by CPAN distribution
159             File::HomeDir -- a method which returns a different result from that of our
160             C -- but you still want to use those File::Save::Home
161             functions which normally call C internally. Or, suppose
162             you want to supply an arbitrary path.
163              
164             You can now do so by supplying an I to
165             C. This argument should be a valid path name
166             for a directory to which you have write privileges.
167             C will determine if the directory exists and, if
168             so, determine whether the I argument is a subdirectory of the I
169             argument.
170              
171             =head3 Both versions
172              
173             Whether you use the single argument version or the two-argument version,
174             C returns a reference to a four-element hash
175             whose keys are:
176              
177             =over 4
178              
179             =item home
180              
181             The absolute path of the home directory.
182              
183             =item abs
184              
185             The absolute path of the specified directory.
186              
187             =item flag
188              
189             A Boolean value indicating whether that directory already exists (a true value)
190             or not (C).
191              
192             =item top
193              
194             The uppermost subdirectory passed as the argument to this function.
195              
196             =back
197              
198             =cut
199              
200             sub get_subhome_directory_status {
201 6     6 1 23639 my $subdir = shift;
202 6         18 my ($pseudohome, $home);
203 6 100       32 $pseudohome = $_[0] if $_[0];
204 6 100       43 if (defined $pseudohome) {
205 2 100       378 -d $pseudohome or croak "$pseudohome is not a valid directory: $!";
206             }
207 5 100       43 $home = defined $pseudohome
208             ? $pseudohome
209             : get_home_directory();
210 5         23 my $dirname = "$home/$subdir";
211 5         42 my $subdir_top = (splitdir($subdir))[0];
212            
213 5 100       760 if (-d $dirname) {
214             return {
215 1         21 home => $home,
216             top => $subdir_top,
217             abs => $dirname,
218             flag => 1,
219             };
220             } else {
221             return {
222 4         46 home => $home,
223             top => $subdir_top,
224             abs => $dirname,
225             flag => undef,
226             };
227             }
228             }
229              
230             =head2 C
231              
232             Takes as argument the hash reference returned by
233             C. Examines the first element in that array --
234             the directory name -- and creates the directory if it doesn't already exist.
235             The function Cs if the directory cannot be created.
236              
237             =cut
238              
239             sub make_subhome_directory {
240 3     3 1 1232 my $desired_dir_ref = shift;
241 3         7 my $dirname = $desired_dir_ref->{abs};
242 3 50       59 if (! -d $dirname) {
243 3 50       44555 mkpath $dirname
244             or croak "Unable to create desired directory $dirname: $!";
245             }
246 3         19 return $dirname;
247             }
248              
249             =head2 C
250              
251             Undoes C, I if there was no specified
252             directory under the user's home directory on the user's system before
253             testing, any such directory created during testing is removed. On the
254             other hand, if there I such a directory present before testing,
255             it is left unchanged.
256              
257             =cut
258              
259             sub restore_subhome_directory_status {
260 3     3 1 8274 my $desired_dir_ref = shift;
261 3         13 my $home = $desired_dir_ref->{home};
262 3         7 my $desired_dir = $desired_dir_ref->{abs};
263 3         8 my $subdir_top = $desired_dir_ref->{top};
264 3 50       16 if (! defined $desired_dir_ref->{flag}) {
265 3         22760 my $cwd = cwd();
266             find {
267             bydepth => 1,
268             no_chdir => 1,
269             wanted => sub {
270 6 100 66 6   350 if (! -l && -d _) {
271 5 50       1240 rmdir or warn "Couldn't rmdir $_: $!";
272             } else {
273 1 50       173 unlink or warn "Couldn't unlink $_: $!";
274             }
275             }
276 3         1669 } => ("$home/$subdir_top");
277 3 50       296 (! -d $desired_dir)
278             ? return 1
279             : croak "Unable to restore directory created during test: $!";
280             } else {
281 0         0 return 1;
282             }
283             }
284              
285             =head2 C
286              
287             =head3 Regular version: no arguments
288              
289             Creates a randomly named temporary directory underneath the home or
290             home-equivalent directory returned by C.
291              
292             =head3 Optional argument version
293              
294             Creates a randomly named temporary directory underneath the directory supplied
295             as the single argument. This version is analogous to the two-argument verion
296             of L above. You could use it if, for
297             example, you wanted to use Cmy_home()> to supply a value for
298             the user's home directory instead of our C.
299              
300             =head3 Both versions
301              
302             In both versions, the temporary subdirectory is created by calling
303             C $home, CLEANUP => 1)>. The function
304             returns the directory path if successful; Cs otherwise.
305              
306             B Any temporary directory so created remains in existence for
307             the duration of the program, but is deleted (along with all its contents)
308             when the program exits.
309              
310             =cut
311              
312             sub make_subhome_temp_directory {
313 3     3 1 3302 my ($pseudohome, $home);
314 3 100       15 $pseudohome = $_[0] if $_[0];
315 3 100       13 if (defined $pseudohome) {
316 2 100       219 -d $pseudohome or croak "$pseudohome is not a valid directory: $!";
317             }
318 2 100       10 $home = defined $pseudohome
319             ? $pseudohome
320             : get_home_directory();
321             # my $tdir = tempdir(DIR => get_home_directory(), CLEANUP => 1);
322 2         26 my $tdir = tempdir(DIR => $home, CLEANUP => 1);
323 2 50       1317 return $tdir ? $tdir : croak "Unable to create temp dir under home: $!";
324             }
325              
326             =head2 C
327              
328             Determines whether file with specified name already exists in specified
329             directory and, if so, temporarily hides it by renaming it with a F<.hidden>
330             suffix and storing away its last access and modification times. Takes as
331             argument a reference to a hash with these keys:
332              
333             =over 4
334              
335             =item dir
336              
337             The directory in which the file is presumed to exist.
338              
339             =item file
340              
341             The targeted file, I the file to be temporarily hidden if it already
342             exists.
343              
344             =item test
345              
346             Boolean value which, if turned on (C<1>), will cause the function, when
347             called, to run two C tests. Defaults to off (C<0>).
348              
349             =back
350              
351             Returns a reference to a hash with these keys:
352              
353             =over 4
354              
355             =item full
356              
357             The absolute path to the target file.
358              
359             =item hidden
360              
361             The absolute path to the now-hidden file.
362              
363             =item atime
364              
365             The last access time to the target file (C<(stat($file{full}))[8]>).
366              
367             =item modtime
368              
369             The last modification time to the target file (C<(stat($file{full}))[9]>).
370              
371             =item test
372              
373             The value of the key C in the hash passed by reference as an argument to
374             this function.
375              
376             =back
377              
378             =cut
379              
380             sub conceal_target_file {
381 2     2 1 1131 my $arg_ref = shift;
382 2         6 my $desired_dir = $arg_ref->{dir};
383 2         4 my $target_file = $arg_ref->{file};
384 2         4 my $test_flag = $arg_ref->{test};
385 2         44 my $target_file_hidden = $target_file . '.hidden';
386 2         5 my %targ;
387 2         14 $targ{full} = catfile( $desired_dir, $target_file );
388 2         11 $targ{hidden} = catfile( $desired_dir, $target_file_hidden );
389 2 100       45 if (-f $targ{full}) {
390 1         23 $targ{atime} = (stat($targ{full}))[8];
391 1         17 $targ{modtime} = (stat($targ{full}))[9];
392 1 50       82 rename $targ{full}, $targ{hidden}
393             or croak "Unable to rename $targ{full}: $!";
394 1 50       4 if ($test_flag) {
395 1         20 ok(! -f $targ{full}, "target file temporarily suppressed");
396 1         663 ok(-f $targ{hidden}, "target file now hidden");
397             }
398             } else {
399 1 50       4 if ($test_flag) {
400 1         11 ok(! -f $targ{full}, "target file not found");
401 1         356 ok(1, "target file not found");
402             }
403             }
404 2         592 $targ{test} = $test_flag;
405 2         16 return { %targ };
406             }
407              
408             =head2 C
409              
410             Used in conjunction with C to restore the original
411             status of the file targeted by C, I renames the
412             hidden file to its original name by removing the F<.hidden> suffix, thereby
413             deleting any other file with the original name created between the calls tothe
414             two functions. Cs if the hidden file cannot be renamed. Takes as
415             argument the hash reference returned by C. If the
416             value for the C key in the hash passed as an argument to
417             C was true, then a call to C
418             will run three C tests.
419              
420             =cut
421              
422             sub reveal_target_file {
423 2     2 1 10 my $target_ref = shift;;
424 2 100       60 if(-f $target_ref->{hidden} ) {
425 1 50       50 rename $target_ref->{hidden}, $target_ref->{full},
426             or croak "Unable to rename $target_ref->{hidden}: $!";
427 1 50       5 if ($target_ref->{test}) {
428 1         17 ok(-f $target_ref->{full},
429             "target file re-established");
430 1         265 ok(! -f $target_ref->{hidden},
431             "hidden target now gone");
432 1         269 ok( (utime $target_ref->{atime},
433             $target_ref->{modtime},
434             ($target_ref->{full})
435             ), "atime and modtime of target file restored");
436             }
437             } else {
438 1 50       4 if ($target_ref->{test}) {
439 1         4 ok(1, "test not relevant");
440 1         363 ok(1, "test not relevant");
441 1         355 ok(1, "test not relevant");
442             }
443             }
444             }
445              
446             =head1 BUGS AND TODO
447              
448             So far tested only on Unix-like systems and Win32.
449              
450             =head1 SEE ALSO
451              
452             perl(1). ExtUtils::ModuleMaker::Auxiliary. ExtUtils::ModuleMaker::Utility.
453             The latter two packages are part of the ExtUtils::ModuleMaker distribution
454             available from the same author on CPAN. They and the ExtUtils::ModuleMaker
455             test suite provide examples of the use of File::Save::Home.
456              
457             Two other distributions located on CPAN, File::HomeDir and
458             File::HomeDir::Win32, may also be used to locate a suitable value for a user's
459             home directory. It should be noted, however, that those modules and
460             File::Save::Home each take a different approach to defining a home directory
461             on Win32 systems. Hence, each may deliver a different result on a given
462             system. I cannot say that one distribution's approach is any more or less
463             correct than the other two's approaches. The following comments should be
464             viewed as my subjective impressions; YMMV.
465              
466             File::HomeDir was originally written by Sean M Burke and is now maintained by
467             Adam Kennedy. As of version 0.52 its interface provides three methods for the
468             ''current user'':
469              
470             $home = File::HomeDir->my_home;
471             $docs = File::HomeDir->my_documents;
472             $data = File::HomeDir->my_data;
473            
474             When I ran these three methods on a Win2K Pro system running ActivePerl 8, I
475             got these results:
476              
477             C:\WINNT\system32>perl -MFile::HomeDir -e "print File::HomeDir->my_home"
478             C:\Documents and Settings\localuser
479              
480             C:\WINNT\system32>perl -MFile::HomeDir -e "print File::HomeDir->my_documents"
481             C:\Documents and Settings\localuser\My Documents
482              
483             C:\WINNT\system32>perl -MFile::HomeDir -e "print File::HomeDir->my_data"
484             C:\Documents and Settings\localuser\Local Settings\Application Data
485              
486             In contrast, when I ran the closest equivalent method in File::Save::Home,
487             C, I got this result:
488              
489             C:\WINNT\system32>perl -MFile::Save::Home -e "print File::Save::Home->get_home_directory"
490             C:\Documents and Settings\localuser\Local Settings\Application Data
491              
492             In other words, Cget_home_directory> gave the same result
493             as Cmy_data>, I, as I might have expected, the same
494             result as Cmy_home>.
495              
496             These results can be explained by peeking behind the curtains and looking at
497             the source code for each module.
498              
499             =head2 File::HomeDir
500              
501             File::HomeDir's objective is to provide a value for a user's home directory on
502             a wide variety of operating systems. When invoked, it detects the operating
503             system you're on and calls a subclassed module. When used on a Win32 system,
504             that subclass is called File::HomeDir::Windows (not to be confused with the
505             separate CPAN distribution File::HomeDir::Win32).
506             Cmy_home()> looks like this:
507              
508             sub my_home {
509             my $class = shift;
510             if ( $ENV{USERPROFILE} ) { return $ENV{USERPROFILE}; }
511             if ( $ENV{HOMEDRIVE} and $ENV{HOMEPATH} ) {
512             return File::Spec->catpath( $ENV{HOMEDRIVE}, $ENV{HOMEPATH}, '',);
513             }
514             Carp::croak("Could not locate current user's home directory");
515             }
516              
517             In other words, determine the current user's home directory simply by checking
518             environmental variables analogous to the C<$ENV{HOME}> on Unix-like systems.
519             A very straightforward approach!
520              
521             As mentioned above, File::Save::Home takes a different approach. It uses the
522             Win32 module to, in effect, check a particular key in the registry.
523              
524             Win32->import( qw(CSIDL_LOCAL_APPDATA) );
525             $realhome = Win32::GetFolderPath( CSIDL_LOCAL_APPDATA() );
526              
527             This approach was suggested to me in August 2005 by several members of
528             Perlmonks. (See threads I
529             (L) and I
530             (L).) I adopted this approach in part
531             because the people recommending it knew more about Windows than I did, and in
532             part because File::HomeDir was not quite as mature as it has since become.
533              
534             But don't trust me; trust Microsoft! Here's their explanation for the use of
535             CSIDL values in general and CSIDL_LOCAL_APPDATA() in particular:
536              
537             =over 4
538              
539             =item *
540              
541             I
542             to identify special folders used frequently by
543             applications, but which may not have the same name or
544             location on any given system. For example, the system
545             folder may be ''C:\Windows'' on one system and
546             ''C:\Winnt'' on another. These constants are defined in
547             Shlobj.h and Shfolder.h.>
548              
549             =item *
550              
551             I
552             Version 5.0. The file system directory that serves as
553             a data repository for local (nonroaming) applications.
554             A typical path is C:\Documents and
555             Settings\username\Local Settings\Application Data.>
556              
557             =back
558              
559             (Source:
560             L.
561             Link valid as of Feb 18 2006. Thanks to Soren Andersen for reminding me of
562             this citation.)
563              
564             It is interesting that the I File::HomeDir methods listed above,
565             C and C both rely on using a Win32 module to peer
566             into the registry, albeit in a slightly different manner from
567             Cget_home_directory>. TIMTOWTDI.
568              
569             In an event, File::Save::Home has a number of useful methods I
570             C which merit your consideration. And, as noted above,
571             you can supply any valid directory as an optional additional argument to the
572             two File::Save::Home functions which normally default to calling
573             C internally.
574              
575             =head2 File::HomeDir::Win32
576              
577             File::HomeDir::Win32 was originally written by Rob Rothenberg and is now
578             maintained by Randy Kobes. According to Adam Kennedy
579             (L),
580             ''The functionality in File::HomeDir::Win32 is gradually being merged into
581             File::HomeDir over time and will eventually be deprecated (although left in
582             place for compatibility purposes).'' Because I have not yet fully installed
583             File::HomeDir::Win32, I will defer further comparison between it and
584             File::Save::Home to a later date.
585              
586             =head1 AUTHOR
587              
588             James E Keenan
589             CPAN ID: JKEENAN
590             jkeenan@cpan.org
591             http://search.cpan.org/~jkeenan
592              
593             =head1 ACKNOWLEDGMENTS
594              
595             File::Save::Home has its origins in the maintenance revisions I was doing on
596             CPAN distribution ExtUtils::ModuleMaker in the summer of 2005.
597             After I made a presentation about that distribution to the Toronto Perlmongers
598             on October 27, 2005, Michael Graham suggested that certain utility functions
599             could be extracted to a separate Perl extension for more general applicability.
600             This module is the implementation of Michael's suggestion.
601              
602             While I was developing those utility functions for ExtUtils::ModuleMaker, I
603             turned to the Perlmonks for assistance with the problem of determining a
604             suitable value for the user's home directory on Win32 systems. In the
605             Perlmonks discussion threads referred to above I received helpful suggestions
606             from monks CountZero, Tanktalus, xdg and holli, among others.
607              
608             Thanks to Rob Rothenberg for prodding me to expand the SEE ALSO section and to
609             Adam Kennedy for responding to questions about File::HomeDir.
610              
611             =head1 COPYRIGHT
612              
613             Copyright (c) 2005-06 James E. Keenan. United States. All rights reserved.
614              
615             This program is free software; you can redistribute
616             it and/or modify it under the same terms as Perl itself.
617              
618             The full text of the license can be found in the
619             LICENSE file included with this module.
620              
621             =head1 DISCLAIMER OF WARRANTY
622              
623             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
624             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
625             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
626             PROVIDE THE SOFTWARE ''AS IS'' WITHOUT WARRANTY OF ANY KIND, EITHER
627             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
628             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
629             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
630             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
631             NECESSARY SERVICING, REPAIR, OR CORRECTION.
632              
633             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
634             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
635             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
636             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
637             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
638             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
639             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
640             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
641             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
642             SUCH DAMAGES.
643              
644             =cut
645              
646             1;
647