File Coverage

blib/lib/File/Move/Undoable.pm
Criterion Covered Total %
statement 58 67 86.5
branch 22 40 55.0
condition 7 13 53.8
subroutine 9 9 100.0
pod 1 1 100.0
total 97 130 74.6


line stmt bran cond sub pod time code
1             package File::Move::Undoable;
2              
3             our $DATE = '2017-07-10'; # DATE
4             our $VERSION = '0.09'; # VERSION
5              
6 1     1   9100 use 5.010001;
  1         5  
7 1     1   8 use strict;
  1         3  
  1         28  
8 1     1   7 use warnings;
  1         9  
  1         39  
9 1     1   4637 use Log::ger;
  1         121  
  1         7  
10              
11 1     1   2166 use File::MoreUtil qw(file_exists l_abs_path);
  1         607  
  1         87  
12 1     1   626 use File::Trash::Undoable;
  1         14927  
  1         60  
13 1     1   731 use IPC::System::Options 'system', -log=>1;
  1         6101  
  1         11  
14 1     1   96 use Proc::ChildError qw(explain_child_error);
  1         3  
  1         964  
15              
16             our %SPEC;
17              
18             $SPEC{mv} = {
19             v => 1.1,
20             summary => 'Move file/directory using rename/rsync, with undo support',
21             description => <<'_',
22              
23             If moving to the same filesystem, will move using `rename()`. On undo will
24             restore the old name.
25              
26             If moving to a different filesystem, will copy to `target` using `rsync` and
27             then trash `source`. On undo, will trash `target` and restore `source` from
28             trash.
29              
30             Fixed state: `source` does not exist and `target` exists. Content or sizes are
31             not checked; only existence.
32              
33             Fixable state: `source` exists and `target` doesn't exist.
34              
35             Unfixable state: `source` does not exist, or both `source` and `target` exist
36             (unless we are moving to a different filesystem, in which it means an
37             interrupted transfer and thus fixable).
38              
39             _
40             args => {
41             source => {
42             schema => 'str*',
43             req => 1,
44             pos => 0,
45             },
46             target => {
47             schema => 'str*',
48             summary => 'Target location',
49             description => <<'_',
50              
51             Note that to avoid ambiguity, you must specify full location instead of just
52             directory name. For example: mv(source=>'/dir', target=>'/a') will move /dir to
53             /a and mv(source=>'/dir', target=>'/a/dir') will move /dir to /a/dir.
54              
55             _
56             req => 1,
57             pos => 1,
58             },
59             rsync_opts => {
60             schema => [array => {of=>'str*', default=>['-a']}],
61             summary => 'Rsync options',
62             description => <<'_',
63              
64             By default, `-a` is used. You should not use rsync options that modify or
65             destroy source, like `--remove-source-files` as it will make recovery of
66             interrupted move impossible.
67              
68             _
69             },
70             },
71             features => {
72             tx => {v=>2},
73             idempotent => 1,
74             },
75             deps => {
76             prog => 'rsync',
77             },
78             };
79             sub mv {
80 45     45 1 2358313 require Sys::Filesystem::MountPoint; # a bit heavy
81              
82 45         535 my %args = @_;
83              
84             # TMP, schema
85 45   50     293 my $tx_action = $args{-tx_action} // '';
86             my $taid = $args{-tx_action_id}
87 45 100       247 or return [412, "Please specify -tx_action_id"];
88 42         154 my $dry_run = $args{-dry_run};
89 42         165 my $source = $args{source};
90 42 50       161 defined($source) or return [400, "Please specify source"];
91 42         145 my $target = $args{target};
92 42 50       174 defined($target) or return [400, "Please specify target"];
93 42   50     278 my $rsync_opts = $args{rsync_opts} // ['-a'];
94 42 50       197 $rsync_opts = [$rsync_opts] unless ref($rsync_opts) eq 'ARRAY';
95              
96 42         355 my $se = file_exists($source);
97 42         1193 my $te = file_exists($target);
98 42 50       1112 my $asource = l_abs_path($source) or return [400, "Invalid path $source"];
99 42 50       1884 my $atarget = l_abs_path($target) or return [400, "Invalid path $target"];
100             # since path_to_mount_point resolves symlink (sigh), we need to remove the
101             # leaf. otherwise: /mnt/sym -> / will cause mount point to become / instead
102             # of /mnt
103 42         1285 for ($asource, $atarget) {
104 84 50       968 s!/[^/]+\z!! if (-l $_);
105             }
106 42         414 my $mpsource = Sys::Filesystem::MountPoint::path_to_mount_point($asource);
107 42         33001 my $mptarget = Sys::Filesystem::MountPoint::path_to_mount_point($atarget);
108 42         26759 my $same_fs = $mpsource eq $mptarget;
109 42 50       165 if ($same_fs) {
110 42         243 log_trace("Source %s & target %s are on the same filesystem (%s)",
111             $source, $target, $mpsource);
112             } else {
113 0         0 log_trace("Source %s and target %s are on different filesystems ".
114             "(%s and %s)", $source, $target, $mpsource, $mptarget);
115             }
116              
117 42 100       322 if ($tx_action eq 'check_state') {
    50          
118 23 100 100     292 return [304, "Source $source already does not exist and ".
119             "target $target exists"] if !$se && $te;
120 20 100       135 return [412, "Source $source does not exist"] unless $se;
121 19 50 33     96 return [412, "Target $target already exists"] if $te && $same_fs;
122              
123 19         53 my @undo;
124 19 50 33     150 if ($te || !$same_fs) {
125 0         0 unshift @undo, (
126             ["File::Trash::Undoable::trash" =>
127             {path=>$target, suffix=>substr($taid,0,8)}],
128             ["File::Trash::Undoable::untrash" =>
129             {path=>$source, suffix=>substr($taid,0,8)}],
130             );
131             } else {
132 19         167 unshift @undo, (
133             [mv => {source=>$target, target=>$source}],
134             );
135             }
136              
137 19 0       103 log_info("(DRY) ".($te ? "Continue moving" : "Moving").
    50          
138             " $source -> $target ...") if $dry_run;
139 19 50       1366 return [200, "$source needs to be ".
140             ($te ? "continued to be moved":"moved")." to $target",
141             undef, {undo_actions=>\@undo}];
142              
143             } elsif ($tx_action eq 'fix_state') {
144 19 50       139 if ($same_fs) {
145 19         112 log_info("Renaming %s -> %s ...", $source, $target);
146 19 50       5384 if (rename $source, $target) {
147 19         802 return [200, "OK"];
148             } else {
149 0           return [500, "Can't rename: $!"];
150             }
151             } else {
152 0           my @cmd = ("rsync", @$rsync_opts, "$source/", "$target/");
153 0           log_info("Rsync-ing %s -> %s ...", $source, $target);
154 0           system @cmd;
155 0 0         return [500, "rsync: ".explain_child_error($?)] if $?;
156 0           return File::Trash::Undoable::trash(
157             -tx_action=>'fix_state',
158             path=>$source, suffix=>substr($taid,0,8));
159             }
160             }
161 0           [400, "Invalid -tx_action"];
162             }
163              
164             1;
165             # ABSTRACT: Move file/directory using rename/rsync, with undo support
166              
167             __END__
168              
169             =pod
170              
171             =encoding UTF-8
172              
173             =head1 NAME
174              
175             File::Move::Undoable - Move file/directory using rename/rsync, with undo support
176              
177             =head1 VERSION
178              
179             This document describes version 0.09 of File::Move::Undoable (from Perl distribution File-Move-Undoable), released on 2017-07-10.
180              
181             =head1 FUNCTIONS
182              
183              
184             =head2 mv
185              
186             Usage:
187              
188             mv(%args) -> [status, msg, result, meta]
189              
190             Move file/directory using rename/rsync, with undo support.
191              
192             If moving to the same filesystem, will move using C<rename()>. On undo will
193             restore the old name.
194              
195             If moving to a different filesystem, will copy to C<target> using C<rsync> and
196             then trash C<source>. On undo, will trash C<target> and restore C<source> from
197             trash.
198              
199             Fixed state: C<source> does not exist and C<target> exists. Content or sizes are
200             not checked; only existence.
201              
202             Fixable state: C<source> exists and C<target> doesn't exist.
203              
204             Unfixable state: C<source> does not exist, or both C<source> and C<target> exist
205             (unless we are moving to a different filesystem, in which it means an
206             interrupted transfer and thus fixable).
207              
208             This function is not exported.
209              
210             This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions.
211              
212              
213             Arguments ('*' denotes required arguments):
214              
215             =over 4
216              
217             =item * B<rsync_opts> => I<array[str]> (default: ["-a"])
218              
219             Rsync options.
220              
221             By default, C<-a> is used. You should not use rsync options that modify or
222             destroy source, like C<--remove-source-files> as it will make recovery of
223             interrupted move impossible.
224              
225             =item * B<source>* => I<str>
226              
227             =item * B<target>* => I<str>
228              
229             Target location.
230              
231             Note that to avoid ambiguity, you must specify full location instead of just
232             directory name. For example: mv(source=>'/dir', target=>'/a') will move /dir to
233             /a and mv(source=>'/dir', target=>'/a/dir') will move /dir to /a/dir.
234              
235             =back
236              
237             Special arguments:
238              
239             =over 4
240              
241             =item * B<-tx_action> => I<str>
242              
243             For more information on transaction, see L<Rinci::Transaction>.
244              
245             =item * B<-tx_action_id> => I<str>
246              
247             For more information on transaction, see L<Rinci::Transaction>.
248              
249             =item * B<-tx_recovery> => I<str>
250              
251             For more information on transaction, see L<Rinci::Transaction>.
252              
253             =item * B<-tx_rollback> => I<str>
254              
255             For more information on transaction, see L<Rinci::Transaction>.
256              
257             =item * B<-tx_v> => I<str>
258              
259             For more information on transaction, see L<Rinci::Transaction>.
260              
261             =back
262              
263             Returns an enveloped result (an array).
264              
265             First element (status) is an integer containing HTTP status code
266             (200 means OK, 4xx caller error, 5xx function error). Second element
267             (msg) is a string containing error message, or 'OK' if status is
268             200. Third element (result) is optional, the actual result. Fourth
269             element (meta) is called result metadata and is optional, a hash
270             that contains extra information.
271              
272             Return value: (any)
273              
274             =head1 FAQ
275              
276             =head2 Why do you use rsync? Why not, say, File::Copy::Recursive?
277              
278             With C<rsync>, we can continue interrupted transfer. We need this ability for
279             recovery. Also, C<rsync> can handle hardlinks and preservation of ownership,
280             something which L<File::Copy::Recursive> currently does not do. And, being
281             implemented in C, it might be faster when processing large files/trees.
282              
283             =head1 HOMEPAGE
284              
285             Please visit the project's homepage at L<https://metacpan.org/release/File-Move-Undoable>.
286              
287             =head1 SOURCE
288              
289             Source repository is at L<https://github.com/perlancar/perl-File-Move-Undoable>.
290              
291             =head1 BUGS
292              
293             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=File-Move-Undoable>
294              
295             When submitting a bug or request, please include a test-file or a
296             patch to an existing test-file that illustrates the bug or desired
297             feature.
298              
299             =head1 SEE ALSO
300              
301             L<Setup>
302              
303             L<Rinci::Transaction>
304              
305             =head1 AUTHOR
306              
307             perlancar <perlancar@cpan.org>
308              
309             =head1 COPYRIGHT AND LICENSE
310              
311             This software is copyright (c) 2017, 2016, 2015, 2014, 2012 by perlancar@cpan.org.
312              
313             This is free software; you can redistribute it and/or modify it under
314             the same terms as the Perl 5 programming language system itself.
315              
316             =cut