File Coverage

blib/lib/File/Copy/Undoable.pm
Criterion Covered Total %
statement 50 52 96.1
branch 19 30 63.3
condition 8 14 57.1
subroutine 9 9 100.0
pod 1 1 100.0
total 87 106 82.0


line stmt bran cond sub pod time code
1             package File::Copy::Undoable;
2              
3             our $DATE = '2017-07-10'; # DATE
4             our $VERSION = '0.12'; # VERSION
5              
6 3     3   25881 use 5.010001;
  3         15  
7 3     3   22 use strict;
  3         9  
  3         77  
8 3     3   33 use warnings;
  3         9  
  3         121  
9 3     3   12372 use Log::ger;
  3         330  
  3         20  
10              
11 3     3   6398 use IPC::System::Options 'system', -log=>1;
  3         16520  
  3         32  
12 3     3   1969 use File::MoreUtil qw(file_exists);
  3         1673  
  3         202  
13 3     3   2005 use File::Trash::Undoable;
  3         37932  
  3         135  
14             #use PerlX::Maybe;
15 3     3   33 use Proc::ChildError qw(explain_child_error);
  3         9  
  3         1962  
16              
17             our %SPEC;
18              
19             $SPEC{cp} = {
20             v => 1.1,
21             summary => 'Copy file/directory using rsync, with undo support',
22             description => <<'_',
23              
24             On do, will copy `source` to `target` (which must not exist beforehand). On
25             undo, will trash `target`.
26              
27             Fixed state: `source` exists and `target` exists. Content or sizes are not
28             checked; only existence.
29              
30             Fixable state: `source` exists and `target` doesn't exist.
31              
32             Unfixable state: `source` does not exist.
33              
34             _
35             args => {
36             source => {
37             schema => 'str*',
38             req => 1,
39             pos => 0,
40             },
41             target => {
42             schema => 'str*',
43             summary => 'Target location',
44             description => <<'_',
45              
46             Note that to avoid ambiguity, you must specify full location instead of just
47             directory name. For example: cp(source=>'/dir', target=>'/a') will copy /dir to
48             /a and cp(source=>'/dir', target=>'/a/dir') will copy /dir to /a/dir.
49              
50             _
51             req => 1,
52             pos => 1,
53             },
54             target_owner => {
55             schema => 'str*',
56             summary => 'Set ownership of target',
57             description => <<'_',
58              
59             If set, will do a `chmod -Rh` on the target after rsync to set ownership. This
60             usually requires super-user privileges. An example of this is copying files on
61             behalf of user from a source that is inaccessible by the user (e.g. a system
62             backup location). Or, setting up user's home directory when creating a user.
63              
64             Will do nothing if not running as super-user.
65              
66             _
67             },
68             target_group => {
69             schema => 'str*',
70             summary => 'Set group of target',
71             description => <<'_',
72              
73             See `target_owner`.
74              
75             _
76             },
77             rsync_opts => {
78             schema => [array => {of=>'str*', default=>['-a']}],
79             summary => 'Rsync options',
80             description => <<'_',
81              
82             By default, `-a` is used. You can add, for example, `--delete` or other rsync
83             options.
84              
85             _
86             },
87             },
88             features => {
89             tx => {v=>2},
90             idempotent => 1,
91             },
92             deps => {
93             prog => 'rsync',
94             },
95             };
96             sub cp {
97 55     55 1 6363505 my %args = @_;
98              
99             # TMP, schema
100 55   50     550 my $tx_action = $args{-tx_action} // '';
101 55         249 my $dry_run = $args{-dry_run};
102 55         367 my $source = $args{source};
103 55 50       269 defined($source) or return [400, "Please specify source"];
104 55         250 my $target = $args{target};
105 55 50       238 defined($target) or return [400, "Please specify target"];
106 55   100     331 my $rsync_opts = $args{rsync_opts} // ['-a'];
107 55 50       396 $rsync_opts = [$rsync_opts] unless ref($rsync_opts) eq 'ARRAY';
108              
109 55 100       377 if ($tx_action eq 'check_state') {
    50          
110 32 100       423 return [412, "Source $source does not exist"]
111             unless file_exists($source);
112 30         1079 my $te = file_exists($target);
113 30 50 33     847 unless ($args{-tx_recovery} || $args{-tx_rollback}) {
114             # in rollback/recovery, we might need to continue interrupted
115             # transfer, so we allow target to exist
116 30 100       271 return [304, "Target $target already exists"] if $te;
117             }
118 26 0       140 log_info("(DRY) ".
    50          
119             ($te ? "Syncing" : "Copying")." $source -> $target ...")
120             if $dry_run;
121 26 50       1176 return [200, "$source needs to be ".($te ? "synced":"copied").
122             " to $target", undef, {undo_actions=>[
123             ["File::Trash::Undoable::trash" => {path=>$target}],
124             ]}];
125              
126             } elsif ($tx_action eq 'fix_state') {
127 23         231 my @cmd = ("rsync", @$rsync_opts, "$source/", "$target/");
128 23         504 log_info("Rsync-ing $source -> $target ...");
129 23         249 system @cmd;
130 23 100       1264222 return [500, "Can't rsync: ".explain_child_error($?)] if $?;
131 22 100 66     2719 if (defined($args{target_owner}) || defined($args{target_group})) {
132 11 50       119 if ($> == 0) {
133 11         338 log_info("Chown-ing $target ...");
134             @cmd = (
135             "chown", "-Rh",
136             join("", $args{target_owner}//"", ":",
137 11   50     567 $args{target_group}//""),
      50        
138             $target);
139 11         108 system @cmd;
140 11 50       91399 return [500, "Can't chown: ".explain_child_error($?)] if $?;
141             } else {
142 0         0 log_debug("Not running as root, not doing chown");
143             }
144             }
145 22         3628 return [200, "OK"];
146             }
147 0           [400, "Invalid -tx_action"];
148             }
149              
150             1;
151             # ABSTRACT: Copy file/directory using rsync, with undo support
152              
153             __END__
154              
155             =pod
156              
157             =encoding UTF-8
158              
159             =head1 NAME
160              
161             File::Copy::Undoable - Copy file/directory using rsync, with undo support
162              
163             =head1 VERSION
164              
165             This document describes version 0.12 of File::Copy::Undoable (from Perl distribution File-Copy-Undoable), released on 2017-07-10.
166              
167             =head1 FUNCTIONS
168              
169              
170             =head2 cp
171              
172             Usage:
173              
174             cp(%args) -> [status, msg, result, meta]
175              
176             Copy file/directory using rsync, with undo support.
177              
178             On do, will copy C<source> to C<target> (which must not exist beforehand). On
179             undo, will trash C<target>.
180              
181             Fixed state: C<source> exists and C<target> exists. Content or sizes are not
182             checked; only existence.
183              
184             Fixable state: C<source> exists and C<target> doesn't exist.
185              
186             Unfixable state: C<source> does not exist.
187              
188             This function is not exported.
189              
190             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
191              
192              
193             Arguments ('*' denotes required arguments):
194              
195             =over 4
196              
197             =item * B<rsync_opts> => I<array[str]> (default: ["-a"])
198              
199             Rsync options.
200              
201             By default, C<-a> is used. You can add, for example, C<--delete> or other rsync
202             options.
203              
204             =item * B<source>* => I<str>
205              
206             =item * B<target>* => I<str>
207              
208             Target location.
209              
210             Note that to avoid ambiguity, you must specify full location instead of just
211             directory name. For example: cp(source=>'/dir', target=>'/a') will copy /dir to
212             /a and cp(source=>'/dir', target=>'/a/dir') will copy /dir to /a/dir.
213              
214             =item * B<target_group> => I<str>
215              
216             Set group of target.
217              
218             See C<target_owner>.
219              
220             =item * B<target_owner> => I<str>
221              
222             Set ownership of target.
223              
224             If set, will do a C<chmod -Rh> on the target after rsync to set ownership. This
225             usually requires super-user privileges. An example of this is copying files on
226             behalf of user from a source that is inaccessible by the user (e.g. a system
227             backup location). Or, setting up user's home directory when creating a user.
228              
229             Will do nothing if not running as super-user.
230              
231             =back
232              
233             Special arguments:
234              
235             =over 4
236              
237             =item * B<-tx_action> => I<str>
238              
239             For more information on transaction, see L<Rinci::Transaction>.
240              
241             =item * B<-tx_action_id> => I<str>
242              
243             For more information on transaction, see L<Rinci::Transaction>.
244              
245             =item * B<-tx_recovery> => I<str>
246              
247             For more information on transaction, see L<Rinci::Transaction>.
248              
249             =item * B<-tx_rollback> => I<str>
250              
251             For more information on transaction, see L<Rinci::Transaction>.
252              
253             =item * B<-tx_v> => I<str>
254              
255             For more information on transaction, see L<Rinci::Transaction>.
256              
257             =back
258              
259             Returns an enveloped result (an array).
260              
261             First element (status) is an integer containing HTTP status code
262             (200 means OK, 4xx caller error, 5xx function error). Second element
263             (msg) is a string containing error message, or 'OK' if status is
264             200. Third element (result) is optional, the actual result. Fourth
265             element (meta) is called result metadata and is optional, a hash
266             that contains extra information.
267              
268             Return value: (any)
269              
270             =head1 FAQ
271              
272             =head2 Why do you use rsync? Why not, say, File::Copy::Recursive?
273              
274             With C<rsync>, we can continue interrupted transfer. We need this ability for
275             recovery. Also, C<rsync> can handle hardlinks and preservation of ownership,
276             something which L<File::Copy::Recursive> currently does not do. And, being
277             implemented in C, it might be faster when processing large files/trees.
278              
279             =head1 HOMEPAGE
280              
281             Please visit the project's homepage at L<https://metacpan.org/release/File-Copy-Undoable>.
282              
283             =head1 SOURCE
284              
285             Source repository is at L<https://github.com/perlancar/perl-File-Copy-Undoable>.
286              
287             =head1 BUGS
288              
289             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=File-Copy-Undoable>
290              
291             When submitting a bug or request, please include a test-file or a
292             patch to an existing test-file that illustrates the bug or desired
293             feature.
294              
295             =head1 SEE ALSO
296              
297             L<Setup>
298              
299             L<Rinci::Transaction>
300              
301             =head1 AUTHOR
302              
303             perlancar <perlancar@cpan.org>
304              
305             =head1 COPYRIGHT AND LICENSE
306              
307             This software is copyright (c) 2017, 2016, 2015, 2014, 2013, 2012 by perlancar@cpan.org.
308              
309             This is free software; you can redistribute it and/or modify it under
310             the same terms as the Perl 5 programming language system itself.
311              
312             =cut