File Coverage

blib/lib/File/Save/Home.pm
Criterion Covered Total %
statement 94 104 90.3
branch 40 60 66.6
condition 2 3 66.6
subroutine 16 16 100.0
pod 7 7 100.0
total 159 190 83.6


line stmt bran cond sub pod time code
1             package File::Save::Home;
2             require 5.006_001;
3 5     5   2999 use strict;
  5         7  
  5         132  
4 5     5   20 use warnings;
  5         6  
  5         148  
5 5     5   27 use Exporter ();
  5         6  
  5         435  
6             our $VERSION = '0.10';
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   16 use Carp;
  5         5  
  5         318  
28 5     5   22 use File::Path;
  5         5  
  5         229  
29 5         303 use File::Spec::Functions qw|
30             catdir
31             catfile
32             catpath
33             splitdir
34             splitpath
35 5     5   1773 |;
  5         2165  
36 5     5   3119 use File::Temp qw| tempdir |;
  5         63754  
  5         311  
37             *ok = *Test::More::ok;
38 5     5   29 use File::Find;
  5         7  
  5         4264  
39              
40             #################### DOCUMENTATION ###################
41              
42             =head1 NAME
43              
44             File::Save::Home - Place file safely under user home directory
45              
46             =head1 VERSION
47              
48             This document refers to version 0.10, released February 10 2017.
49              
50             =head1 SYNOPSIS
51              
52             use File::Save::Home qw(
53             get_home_directory
54             get_subhome_directory_status
55             make_subhome_directory
56             restore_subhome_directory_status
57             conceal_target_file
58             reveal_target_file
59             make_subhome_temp_directory
60             );
61              
62             $home_dir = get_home_directory();
63              
64             $desired_dir_ref = get_subhome_directory_status("desired/directory");
65              
66             $desired_dir_ref = get_subhome_directory_status(
67             "desired/directory",
68             "pseudohome/directory", # two-argument version
69             );
70              
71             $desired_dir = make_subhome_directory($desired_dir_ref);
72              
73             restore_subhome_directory_status($desired_dir_ref);
74              
75             $target_ref = conceal_target_file( {
76             dir => $desired_dir,
77             file => 'file_to_be_checked',
78             test => 0,
79             } );
80              
81             reveal_target_file($target_ref);
82              
83             $tmpdir = make_subhome_temp_directory();
84              
85             $tmpdir = make_subhome_temp_directory(
86             "pseudohome/directory", # optional argument version
87             );
88              
89             =head1 DESCRIPTION
90              
91             In the course of deploying an application on another user's system, you
92             sometimes need to place a file in or underneath that user's home
93             directory. Can you do so safely?
94              
95             This Perl extension provides several functions which try to determine whether
96             you can, indeed, safely create directories and files underneath a user's home
97             directory. Among other things, if you are placing a file in such a location
98             only temporarily -- say, for testing purposes -- you can temporarily hide
99             any already existing file with the same name and restore it to its original
100             name and timestamps when you are done.
101              
102             =head1 USAGE
103              
104             =head2 C
105              
106             Analyzes environmental information to determine whether there exists on the
107             system a 'HOME' or 'home-equivalent' directory. Takes no arguments. Returns
108             that directory if it exists; Cs otherwise.
109              
110             On Win32, this directory is the one returned by the following function from the Fmodule:
111              
112             Win32->import( qw(CSIDL_LOCAL_APPDATA) );
113             $realhome = Win32::GetFolderPath( CSIDL_LOCAL_APPDATA() );
114              
115             ... which translates to something like F.
116             (For a further discussion of Win32, see below L.)
117              
118             On Unix-like systems, things are much simpler. We simply check the value of
119             C<$ENV{HOME}>. We cannot do that on Win32 because C<$ENV{HOME}> is not
120             defined there.
121              
122             =cut
123              
124             sub get_home_directory {
125 9     9 1 9044 my $realhome;
126 9 50       40 if ($^O eq 'MSWin32') {
127 0         0 require Win32;
128 0         0 Win32->import( qw(CSIDL_LOCAL_APPDATA) ); # 0x001c
129 0         0 $realhome = Win32::GetFolderPath( CSIDL_LOCAL_APPDATA() );
130 0         0 $realhome =~ s{ }{\ }g;
131 0 0       0 return $realhome if (-d $realhome);
132 0         0 $realhome =~ s|(.*?)\\Local Settings(.*)|$1$2|;
133 0 0       0 return $realhome if (-d $realhome);
134 0         0 croak "Unable to identify directory equivalent to 'HOME' on Win32: $!";
135             } else { # Unix-like systems
136 9         21 $realhome = $ENV{HOME};
137 9         17 $realhome =~ s{ }{\ }g;
138 9 50       145 return $realhome if (-d $realhome);
139 0         0 croak "Unable to identify 'HOME' directory: $!";
140             }
141             }
142              
143             =head2 C
144              
145             =head3 Single argument version
146              
147             Takes as argument a string holding the name of a directory, either
148             single-level (C) or multi-level (C). Determines
149             whether that directory already exists underneath the user's
150             home or home-equivalent directory. Calls C internally,
151             then tacks on the path passed as argument.
152              
153             =head3 Two-argument version
154              
155             Suppose you want to determine the name of a user's home directory by some
156             other route than C. Suppose, for example, that you're
157             on Win32 and want to use the C method supplied by CPAN distribution
158             File::HomeDir -- a method which returns a different result from that of our
159             C -- but you still want to use those File::Save::Home
160             functions which normally call C internally. Or, suppose
161             you want to supply an arbitrary path.
162              
163             You can now do so by supplying an I to
164             C. This argument should be a valid path name
165             for a directory to which you have write privileges.
166             C will determine if the directory exists and, if
167             so, determine whether the I argument is a subdirectory of the I
168             argument.
169              
170             =head3 Both versions
171              
172             Whether you use the single argument version or the two-argument version,
173             C returns a reference to a four-element hash
174             whose keys are:
175              
176             =over 4
177              
178             =item home
179              
180             The absolute path of the home directory.
181              
182             =item abs
183              
184             The absolute path of the directory specified as first argument to the function.
185              
186             =item flag
187              
188             A Boolean value indicating whether the desired directory already exists (a
189             true value) or not (C).
190              
191             =item top
192              
193             The uppermost subdirectory passed as the argument to this function.
194              
195             =back
196              
197             =cut
198              
199             sub get_subhome_directory_status {
200 6     6 1 7678 my $subdir = shift;
201 6         10 my ($pseudohome, $home);
202 6 100       21 $pseudohome = $_[0] if $_[0];
203 6 100       23 if (defined $pseudohome) {
204 2 100       222 -d $pseudohome or croak "$pseudohome is not a valid directory: $!";
205             }
206 5 100       25 $home = defined $pseudohome
207             ? $pseudohome
208             : get_home_directory();
209 5         25 my $dirname = catdir($home, $subdir);
210 5         23 my $subdir_top = (splitdir($subdir))[0];
211              
212 5 100       177 if (-d $dirname) {
213             return {
214 1         18 home => $home,
215             top => $subdir_top,
216             abs => $dirname,
217             flag => 1,
218             };
219             } else {
220             return {
221 4         23 home => $home,
222             top => $subdir_top,
223             abs => $dirname,
224             flag => undef,
225             };
226             }
227             }
228              
229             =head2 C
230              
231             Takes as argument the hash reference returned by
232             C. Examines the first element in that array --
233             the directory name -- and creates the directory if it doesn't already exist.
234             The function Cs if the directory cannot be created.
235              
236             =cut
237              
238             sub make_subhome_directory {
239 3     3 1 627 my $desired_dir_ref = shift;
240 3         7 my $dirname = $desired_dir_ref->{abs};
241 3 50       38 if (! -d $dirname) {
242 3 50       676 mkpath $dirname
243             or croak "Unable to create desired directory $dirname: $!";
244             }
245 3         7 return $dirname;
246             }
247              
248             =head2 C
249              
250             Undoes C, I if there was no specified
251             directory under the user's home directory on the user's system before
252             testing, any such directory created during testing is removed. On the
253             other hand, if there I such a directory present before testing,
254             it is left unchanged.
255              
256             =cut
257              
258             sub restore_subhome_directory_status {
259 3     3 1 713 my $desired_dir_ref = shift;
260 3         7 my $home = $desired_dir_ref->{home};
261 3 50       39 croak "Home directory '$home' apparently lost"
262             unless (-d $home);
263 3         4 my $desired_dir = $desired_dir_ref->{abs};
264 3         6 my $subdir_top = $desired_dir_ref->{top};
265 3 50       8 if (! defined $desired_dir_ref->{flag}) {
266             find {
267             bydepth => 1,
268             no_chdir => 1,
269             wanted => sub {
270 6 100 66 6   90 if (! -l && -d _) {
271 5 50       369 rmdir or croak "Couldn't rmdir $_: $!";
272             } else {
273 1 50       80 unlink or croak "Couldn't unlink $_: $!";
274             }
275             }
276 3         683 } => (catdir($home, $subdir_top));
277 3 50       63 (! -d $desired_dir)
278             ? return 1
279             : croak "Unable to restore directory created during test: $!";
280             }
281             else {
282 0         0 return 1;
283             }
284             }
285              
286             =head2 C
287              
288             =head3 Regular version: no arguments
289              
290             Creates a randomly named temporary directory underneath the home or
291             home-equivalent directory returned by C.
292              
293             =head3 Optional argument version
294              
295             Creates a randomly named temporary directory underneath the directory supplied
296             as the single argument. This version is analogous to the two-argument verion
297             of L above. You could use it if, for
298             example, you wanted to use Cmy_home()> to supply a value for
299             the user's home directory instead of our C.
300              
301             =head3 Both versions
302              
303             In both versions, the temporary subdirectory is created by calling
304             C $home, CLEANUP => 1)>. The function
305             returns the directory path if successful; Cs otherwise.
306              
307             B Any temporary directory so created remains in existence for
308             the duration of the program, but is deleted (along with all its contents)
309             when the program exits.
310              
311             =cut
312              
313             sub make_subhome_temp_directory {
314 3     3 1 1907 my ($pseudohome, $home);
315 3 100       12 $pseudohome = $_[0] if $_[0];
316 3 100       7 if (defined $pseudohome) {
317 2 100       125 -d $pseudohome or croak "$pseudohome is not a valid directory: $!";
318             }
319 2 100       9 $home = defined $pseudohome
320             ? $pseudohome
321             : get_home_directory();
322 2         45 my $tdir = tempdir(DIR => $home, CLEANUP => 1);
323 2 50       614 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 682 my $arg_ref = shift;
382 2         3 my $desired_dir = $arg_ref->{dir};
383 2         3 my $target_file = $arg_ref->{file};
384 2         2 my $test_flag = $arg_ref->{test};
385 2         4 my $target_file_hidden = $target_file . '.hidden';
386 2         2 my %targ;
387 2         9 $targ{full} = catfile( $desired_dir, $target_file );
388 2         36 $targ{hidden} = catfile( $desired_dir, $target_file_hidden );
389 2 100       34 if (-f $targ{full}) {
390 1         8 $targ{atime} = (stat($targ{full}))[8];
391 1         7 $targ{modtime} = (stat($targ{full}))[9];
392             rename $targ{full}, $targ{hidden}
393 1 50       52 or croak "Unable to rename $targ{full}: $!";
394 1 50       2 if ($test_flag) {
395 1         11 ok(! -f $targ{full}, "target file temporarily suppressed");
396 1         201 ok(-f $targ{hidden}, "target file now hidden");
397             }
398             } else {
399 1 50       3 if ($test_flag) {
400 1         7 ok(! -f $targ{full}, "target file not found");
401 1         165 ok(1, "target file not found");
402             }
403             }
404 2         321 $targ{test} = $test_flag;
405 2         12 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 8 my $target_ref = shift;;
424 2 100       29 if(-f $target_ref->{hidden} ) {
425             rename $target_ref->{hidden}, $target_ref->{full},
426 1 50       26 or croak "Unable to rename $target_ref->{hidden}: $!";
427 1 50       2 if ($target_ref->{test}) {
428             ok(-f $target_ref->{full},
429 1         9 "target file re-established");
430             ok(! -f $target_ref->{hidden},
431 1         188 "hidden target now gone");
432             ok( (utime $target_ref->{atime},
433             $target_ref->{modtime},
434             ($target_ref->{full})
435 1         201 ), "atime and modtime of target file restored");
436             }
437             } else {
438 1 50       3 if ($target_ref->{test}) {
439 1         3 ok(1, "test not relevant");
440 1         162 ok(1, "test not relevant");
441 1         150 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