File Coverage

blib/lib/Setup/File/Symlink.pm
Criterion Covered Total %
statement 108 112 96.4
branch 66 84 78.5
condition 57 67 85.0
subroutine 8 8 100.0
pod 3 3 100.0
total 242 274 88.3


line stmt bran cond sub pod time code
1             package Setup::File::Symlink;
2              
3             our $DATE = '2017-07-10'; # DATE
4             our $VERSION = '0.29'; # VERSION
5              
6 3     3   324504 use 5.010001;
  3         17  
7 3     3   25 use strict;
  3         9  
  3         71  
8 3     3   18 use warnings;
  3         7  
  3         103  
9 3     3   11541 use Log::ger;
  3         315  
  3         21  
10              
11 3     3   6041 use File::Trash::Undoable;
  3         43727  
  3         5276  
12              
13             require Exporter;
14             our @ISA = qw(Exporter);
15             our @EXPORT_OK = qw(setup_symlink);
16              
17             our %SPEC;
18              
19             $SPEC{':package'} = {
20             v => 1.1,
21             summary => 'Setup symlink (existence, target)',
22             };
23              
24             $SPEC{rmsym} = {
25             v => 1.1,
26             summary => 'Delete symlink',
27             description => <<'_',
28              
29             Will not delete non-symlinks.
30              
31             Fixed state: `path` doesn't exist.
32              
33             Fixable state: `path` exists, is a symlink, (and if `target` is defined, points
34             to `target`).
35              
36             _
37             args => {
38             path => {
39             schema => 'str*',
40             },
41             target => {
42             summary => 'Only delete if existing symlink has this target',
43             schema => 'str*',
44             },
45             },
46             features => {
47             tx => {v=>2},
48             idempotent => 1,
49             },
50             };
51             sub rmsym {
52 281     281 1 7773751 my %args = @_;
53              
54             # TMP, schema
55 281   50     2325 my $tx_action = $args{-tx_action} // '';
56 281         1325 my $dry_run = $args{-dry_run};
57 281         1056 my $path = $args{path};
58 281 50       1633 defined($path) or return [400, "Please specify path"];
59 281         1039 my $target = $args{target};
60              
61 281         7126 my $is_sym = (-l $path);
62 281   100     1704 my $exists = $is_sym || (-e _);
63 281 100       1264 my $curtarget; $curtarget = readlink($path) if $is_sym;
  281         3718  
64              
65 281         1415 my @undo;
66              
67 281 100       2259 if ($tx_action eq 'check_state') {
    50          
68 148 100 100     2034 return [412, "$path is not a symlink"] if $exists && !$is_sym;
69 146 100 100     1432 return [412, "Target of symlink $path does not match ($curtarget)"]
      66        
70             if $is_sym && defined($target) && $curtarget ne $target;
71 144 100       726 if ($exists) {
72 134   33     1692 unshift @undo, ['ln_s', {
73             symlink => $path,
74             target => $target // $curtarget,
75             }];
76             }
77 144 100       713 if (@undo) {
78 134 50       654 log_info("(DRY) Deleting symlink $path ...") if $dry_run;
79 134         6246 return [200, "Symlink $path should be removed", undef,
80             {undo_actions=>\@undo}];
81             } else {
82 10         351 return [304, "Symlink $path already does not exist"];
83             }
84             } elsif ($tx_action eq 'fix_state') {
85 133         1941 log_info("Deleting symlink $path ...");
86 133 50       25515 if (unlink $path) {
87 133         8250 return [200, "OK"];
88             } else {
89 0         0 return [500, "Can't remove symlink $path: $!"];
90             }
91             }
92 0         0 [400, "Invalid -tx_action"];
93             }
94              
95             $SPEC{ln_s} = {
96             v => 1.1,
97             summary => 'Create symlink',
98             description => <<'_',
99              
100             Fixed state: `symlink` exists and points to `target`.
101              
102             Fixable state: `symlink` doesn't exist.
103              
104             _
105             args => {
106             symlink => {
107             summary => 'Path to symlink',
108             schema => 'str*',
109             },
110             target => {
111             summary => 'Path to target',
112             schema => 'str*',
113             },
114             },
115             features => {
116             tx => {v=>2},
117             idempotent => 1,
118             },
119             };
120             sub ln_s {
121 301     301 1 11217373 my %args = @_;
122              
123             # TMP, schema
124 301   50     3127 my $tx_action = $args{-tx_action} // '';
125 301         1689 my $dry_run = $args{-dry_run};
126 301         1448 my $symlink = $args{symlink};
127 301 50       2307 defined($symlink) or return [400, "Please specify symlink"];
128 301         1216 my $target = $args{target};
129 301 50       1477 defined($target) or return [400, "Please specify target"];
130              
131 301         8553 my $is_sym = (-l $symlink);
132 301   100     2928 my $exists = $is_sym || (-e _);
133 301 100       980 my $curtarget; $curtarget = readlink($symlink) if $is_sym;
  301         2348  
134 301         955 my @undo;
135              
136 301 100       2115 if ($tx_action eq 'check_state') {
    50          
137 157 100 100     949 return [412, "Path $symlink already exists"] if $exists && !$is_sym;
138 155 50 66     992 return [412, "Symlink $symlink points to another target"] if $is_sym &&
139             $curtarget ne $target;
140 155 100       720 if (!$exists) {
141 145         1399 unshift @undo, ['rmsym', {path => $symlink}];
142             }
143 155 100       798 if (@undo) {
144 145 50       666 log_info("(DRY) Creating symlink $symlink -> $target ...")
145             if $dry_run;
146 145         5631 return [200, "Symlink $symlink needs to be created", undef,
147             {undo_actions=>\@undo}];
148             } else {
149 10         306 return [304, "Symlink $symlink already exists"];
150             }
151             } elsif ($tx_action eq 'fix_state') {
152 144         1994 log_info("Creating symlink $symlink -> $target ...");
153 144 50       23640 if (symlink $target, $symlink) {
154 144         6639 return [200, "Fixed"];
155             } else {
156 0         0 return [500, "Can't symlink $symlink -> $target: $!"];
157             }
158             }
159 0         0 [400, "Invalid -tx_action"];
160             }
161              
162             $SPEC{setup_symlink} = {
163             v => 1.1,
164             summary => "Setup symlink (existence, target)",
165             description => <<'_',
166              
167             When `should_exist=>1` (the default): On do, will create symlink which points to
168             specified target. If symlink already exists but points to another target, it
169             will be replaced with the correct symlink if `replace_symlink` option is true.
170             If a file/dir already exists and `replace_file`/`replace_dir` option is true, it
171             will be moved (trashed) first before the symlink is created. On undo, will
172             delete symlink if it was created by this function, and restore the original
173             symlink/file/dir if it was replaced during do.
174              
175             When `should_exist=>0`: On do, will remove symlink if it exists (and
176             `replace_symlink` is true). If `replace_file`/`replace_dir` is true, will also
177             remove file/dir. On undo, will restore deleted symlink/file/dir.
178              
179             _
180             args => {
181             should_exist => {
182             summary => "Whether symlink should exist",
183             schema => ['bool' => {default => 1}],
184             },
185             symlink => {
186             summary => 'Path to symlink',
187             schema => ['str*' => {match => qr!^/!}],
188             req => 1,
189             pos => 0,
190             },
191             target => {
192             summary => 'Target path of symlink',
193             schema => 'str*',
194             req => 0, # XXX only when should_exist=1
195             description => <<'_',
196              
197             Required, unless `should_exist => 0`.
198              
199             _
200             },
201             create => {
202             summary => "Create if symlink doesn't exist",
203             schema => [bool => {default=>1}],
204             description => <<'_',
205              
206             If set to false, then setup will fail (412) if this condition is encountered.
207              
208             _
209             },
210             replace_symlink => {
211             summary => "Replace previous symlink if it already exists ".
212             "but doesn't point to the wanted target",
213             schema => ['bool' => {default => 1}],
214             description => <<'_',
215              
216             If set to false, then setup will fail (412) if this condition is encountered.
217              
218             _
219             },
220             replace_file => {
221             summary => "Replace if there is existing non-symlink file",
222             schema => ['bool' => {default => 0}],
223             description => <<'_',
224              
225             If set to false, then setup will fail (412) if this condition is encountered.
226              
227             _
228             },
229             replace_dir => {
230             summary => "Replace if there is existing dir",
231             schema => ['bool' => {default => 0}],
232             description => <<'_',
233              
234             If set to false, then setup will fail (412) if this condition is encountered.
235              
236             _
237             },
238             },
239             features => {
240             tx => {v=>2},
241             idempotent => 1,
242             },
243             };
244             sub setup_symlink {
245 123     123 1 16144405 require UUID::Random;
246              
247 123         1259 my %args = @_;
248              
249             # TMP, schema
250 123   50     1377 my $tx_action = $args{-tx_action} // '';
251 123         491 my $dry_run = $args{-dry_run};
252 123   100     794 my $should_exist = $args{should_exist} // 1;
253 123 50       663 my $symlink = $args{symlink} or return [400, "Please specify symlink"];
254 123         504 my $target = $args{target};
255 123 100       600 if ($should_exist) {
256 80 50       354 defined($target) or return [400, "Please specify target"];
257             }
258 123   100     696 my $create = $args{create} // 1;
259 123   100     641 my $replace_file = $args{replace_file} // 0;
260 123   100     686 my $replace_dir = $args{replace_dir} // 0;
261 123   100     604 my $replace_symlink = $args{replace_symlink} // 1;
262              
263 123         3061 my $is_sym = (-l $symlink); # -l performs lstat()
264 123         455 my $exists = (-e _); # now we can use -e
265 123         407 my $is_dir = (-d _);
266 123 100       1066 my $cur_target = $is_sym ? readlink($symlink) : "";
267              
268 123   66     1030 my $taid = $args{-tx_action_id} // UUID::Random::generate();
269 123         1457 my $suffix = substr($taid,0,8);
270              
271 123         427 my (@do, @undo);
272              
273 123 100       652 if ($should_exist) {
    100          
274 80 100 100     903 if ($exists && !$is_sym) {
    100 100        
    100          
275 40 100 100     717 if ($is_dir && !$replace_dir) {
    100 100        
276 2         34 return [412, "Must replace dir $symlink with symlink ".
277             "but instructed not to"];
278             } elsif (!$is_dir && !$replace_file) {
279 2         56 return [412, "Must replace file $symlink with symlink ".
280             "but instructed not to"];
281             }
282 36 50       187 log_info("(DRY) Replacing file/dir $symlink with symlink ...")
283             if $dry_run;
284 36         450 push @do, (
285             ["File::Trash::Undoable::trash",
286             {path=>$symlink, suffix=>$suffix}],
287             ["ln_s", {symlink=>$symlink, target=>$target}],
288             );
289 36         1124 unshift @undo, (
290             ["rmsym", {path=>$symlink, target=>$target}],
291             ["File::Trash::Undoable::untrash",
292             {path=>$symlink, suffix=>$suffix}],
293             );
294             } elsif ($is_sym && $cur_target ne $target) {
295 20 100       95 if (!$replace_symlink) {
296 2         34 return [412, "Must replace symlink $symlink ".
297             "but instructed not to"];
298             }
299 18 50       94 log_info("(DRY) Replacing symlink $symlink ...") if $dry_run;
300 18         274 push @do, (
301             [rmsym => {path=>$symlink}],
302             [ln_s => {symlink=>$symlink, target=>$target}],
303             );
304 18         199 unshift @undo, (
305             ["rmsym", {path=>$symlink, target=>$target}],
306             ["ln_s", {symlink=>$symlink, target=>$cur_target}],
307             );
308             } elsif (!$exists) {
309 14 100       65 if (!$create) {
310 2         40 return [412, "Must create symlink $symlink ".
311             "but instructed not to"];
312             }
313 12 50       60 log_info("(DRY) Creating symlink $symlink ...") if $dry_run;
314 12         93 push @do, (
315             ["ln_s", {symlink=>$symlink, target=>$target}],
316             );
317 12         90 unshift @undo, (
318             ["rmsym", {path=>$symlink}],
319             );
320             }
321             } elsif ($exists) {
322 38 50 66     331 return [412, "Must delete symlink $symlink but instructed not to"]
323             if $is_sym && !$replace_symlink;
324 38 50 66     303 return [412, "Must delete dir $symlink but instructed not to"]
325             if $is_dir && !$replace_dir;
326 38 100 100     438 return [412, "Must delete file $symlink but instructed not to"]
      100        
327             if !$is_sym && !$is_dir && !$replace_file;
328 36 50       639 log_info("(DRY) Removing symlink $symlink ...") if $dry_run;
329 36         305 push @do , ["File::Trash::Undoable::trash",
330             {path=>$symlink, suffix=>$suffix}];
331 36         323 unshift @undo, ["File::Trash::Undoable::untrash",
332             {path=>$symlink, suffix=>$suffix}];
333             }
334              
335 113 100       683 if (@do) {
336 102         4364 return [200, "", undef, {do_actions=>\@do, undo_actions=>\@undo}];
337             } else {
338 11         370 return [304, "Already fixed"];
339             }
340             }
341              
342             1;
343             # ABSTRACT: Setup symlink (existence, target)
344              
345             __END__
346              
347             =pod
348              
349             =encoding UTF-8
350              
351             =head1 NAME
352              
353             Setup::File::Symlink - Setup symlink (existence, target)
354              
355             =head1 VERSION
356              
357             This document describes version 0.29 of Setup::File::Symlink (from Perl distribution Setup-File-Symlink), released on 2017-07-10.
358              
359             =head1 FUNCTIONS
360              
361              
362             =head2 ln_s
363              
364             Usage:
365              
366             ln_s(%args) -> [status, msg, result, meta]
367              
368             Create symlink.
369              
370             Fixed state: C<symlink> exists and points to C<target>.
371              
372             Fixable state: C<symlink> doesn't exist.
373              
374             This function is not exported.
375              
376             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
377              
378              
379             Arguments ('*' denotes required arguments):
380              
381             =over 4
382              
383             =item * B<symlink> => I<str>
384              
385             Path to symlink.
386              
387             =item * B<target> => I<str>
388              
389             Path to target.
390              
391             =back
392              
393             Special arguments:
394              
395             =over 4
396              
397             =item * B<-tx_action> => I<str>
398              
399             For more information on transaction, see L<Rinci::Transaction>.
400              
401             =item * B<-tx_action_id> => I<str>
402              
403             For more information on transaction, see L<Rinci::Transaction>.
404              
405             =item * B<-tx_recovery> => I<str>
406              
407             For more information on transaction, see L<Rinci::Transaction>.
408              
409             =item * B<-tx_rollback> => I<str>
410              
411             For more information on transaction, see L<Rinci::Transaction>.
412              
413             =item * B<-tx_v> => I<str>
414              
415             For more information on transaction, see L<Rinci::Transaction>.
416              
417             =back
418              
419             Returns an enveloped result (an array).
420              
421             First element (status) is an integer containing HTTP status code
422             (200 means OK, 4xx caller error, 5xx function error). Second element
423             (msg) is a string containing error message, or 'OK' if status is
424             200. Third element (result) is optional, the actual result. Fourth
425             element (meta) is called result metadata and is optional, a hash
426             that contains extra information.
427              
428             Return value: (any)
429              
430              
431             =head2 rmsym
432              
433             Usage:
434              
435             rmsym(%args) -> [status, msg, result, meta]
436              
437             Delete symlink.
438              
439             Will not delete non-symlinks.
440              
441             Fixed state: C<path> doesn't exist.
442              
443             Fixable state: C<path> exists, is a symlink, (and if C<target> is defined, points
444             to C<target>).
445              
446             This function is not exported.
447              
448             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
449              
450              
451             Arguments ('*' denotes required arguments):
452              
453             =over 4
454              
455             =item * B<path> => I<str>
456              
457             =item * B<target> => I<str>
458              
459             Only delete if existing symlink has this target.
460              
461             =back
462              
463             Special arguments:
464              
465             =over 4
466              
467             =item * B<-tx_action> => I<str>
468              
469             For more information on transaction, see L<Rinci::Transaction>.
470              
471             =item * B<-tx_action_id> => I<str>
472              
473             For more information on transaction, see L<Rinci::Transaction>.
474              
475             =item * B<-tx_recovery> => I<str>
476              
477             For more information on transaction, see L<Rinci::Transaction>.
478              
479             =item * B<-tx_rollback> => I<str>
480              
481             For more information on transaction, see L<Rinci::Transaction>.
482              
483             =item * B<-tx_v> => I<str>
484              
485             For more information on transaction, see L<Rinci::Transaction>.
486              
487             =back
488              
489             Returns an enveloped result (an array).
490              
491             First element (status) is an integer containing HTTP status code
492             (200 means OK, 4xx caller error, 5xx function error). Second element
493             (msg) is a string containing error message, or 'OK' if status is
494             200. Third element (result) is optional, the actual result. Fourth
495             element (meta) is called result metadata and is optional, a hash
496             that contains extra information.
497              
498             Return value: (any)
499              
500              
501             =head2 setup_symlink
502              
503             Usage:
504              
505             setup_symlink(%args) -> [status, msg, result, meta]
506              
507             Setup symlink (existence, target).
508              
509             When C<< should_exist=E<gt>1 >> (the default): On do, will create symlink which points to
510             specified target. If symlink already exists but points to another target, it
511             will be replaced with the correct symlink if C<replace_symlink> option is true.
512             If a file/dir already exists and C<replace_file>/C<replace_dir> option is true, it
513             will be moved (trashed) first before the symlink is created. On undo, will
514             delete symlink if it was created by this function, and restore the original
515             symlink/file/dir if it was replaced during do.
516              
517             When C<< should_exist=E<gt>0 >>: On do, will remove symlink if it exists (and
518             C<replace_symlink> is true). If C<replace_file>/C<replace_dir> is true, will also
519             remove file/dir. On undo, will restore deleted symlink/file/dir.
520              
521             This function is not exported by default, but exportable.
522              
523             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
524              
525              
526             Arguments ('*' denotes required arguments):
527              
528             =over 4
529              
530             =item * B<create> => I<bool> (default: 1)
531              
532             Create if symlink doesn't exist.
533              
534             If set to false, then setup will fail (412) if this condition is encountered.
535              
536             =item * B<replace_dir> => I<bool> (default: 0)
537              
538             Replace if there is existing dir.
539              
540             If set to false, then setup will fail (412) if this condition is encountered.
541              
542             =item * B<replace_file> => I<bool> (default: 0)
543              
544             Replace if there is existing non-symlink file.
545              
546             If set to false, then setup will fail (412) if this condition is encountered.
547              
548             =item * B<replace_symlink> => I<bool> (default: 1)
549              
550             Replace previous symlink if it already exists but doesn't point to the wanted target.
551              
552             If set to false, then setup will fail (412) if this condition is encountered.
553              
554             =item * B<should_exist> => I<bool> (default: 1)
555              
556             Whether symlink should exist.
557              
558             =item * B<symlink>* => I<str>
559              
560             Path to symlink.
561              
562             =item * B<target> => I<str>
563              
564             Target path of symlink.
565              
566             Required, unless C<< should_exist =E<gt> 0 >>.
567              
568             =back
569              
570             Special arguments:
571              
572             =over 4
573              
574             =item * B<-tx_action> => I<str>
575              
576             For more information on transaction, see L<Rinci::Transaction>.
577              
578             =item * B<-tx_action_id> => I<str>
579              
580             For more information on transaction, see L<Rinci::Transaction>.
581              
582             =item * B<-tx_recovery> => I<str>
583              
584             For more information on transaction, see L<Rinci::Transaction>.
585              
586             =item * B<-tx_rollback> => I<str>
587              
588             For more information on transaction, see L<Rinci::Transaction>.
589              
590             =item * B<-tx_v> => I<str>
591              
592             For more information on transaction, see L<Rinci::Transaction>.
593              
594             =back
595              
596             Returns an enveloped result (an array).
597              
598             First element (status) is an integer containing HTTP status code
599             (200 means OK, 4xx caller error, 5xx function error). Second element
600             (msg) is a string containing error message, or 'OK' if status is
601             200. Third element (result) is optional, the actual result. Fourth
602             element (meta) is called result metadata and is optional, a hash
603             that contains extra information.
604              
605             Return value: (any)
606              
607             =head1 HOMEPAGE
608              
609             Please visit the project's homepage at L<https://metacpan.org/release/Setup-File-Symlink>.
610              
611             =head1 SOURCE
612              
613             Source repository is at L<https://github.com/perlancar/perl-Setup-File-Symlink>.
614              
615             =head1 BUGS
616              
617             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Setup-File-Symlink>
618              
619             When submitting a bug or request, please include a test-file or a
620             patch to an existing test-file that illustrates the bug or desired
621             feature.
622              
623             =head1 SEE ALSO
624              
625             L<Setup>
626              
627             L<Setup::File>
628              
629             =head1 AUTHOR
630              
631             perlancar <perlancar@cpan.org>
632              
633             =head1 COPYRIGHT AND LICENSE
634              
635             This software is copyright (c) 2017, 2015, 2014, 2013, 2012, 2011 by perlancar@cpan.org.
636              
637             This is free software; you can redistribute it and/or modify it under
638             the same terms as the Perl 5 programming language system itself.
639              
640             =cut