line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package File::Move::Undoable; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
our $DATE = '2016-06-10'; # DATE |
4
|
|
|
|
|
|
|
our $VERSION = '0.08'; # VERSION |
5
|
|
|
|
|
|
|
|
6
|
1
|
|
|
1
|
|
5348
|
use 5.010001; |
|
1
|
|
|
|
|
2
|
|
7
|
1
|
|
|
1
|
|
3
|
use strict; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
13
|
|
8
|
1
|
|
|
1
|
|
2
|
use warnings; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
21
|
|
9
|
1
|
|
|
1
|
|
689
|
use Log::Any::IfLOG '$log'; |
|
1
|
|
|
|
|
9
|
|
|
1
|
|
|
|
|
4
|
|
10
|
|
|
|
|
|
|
|
11
|
1
|
|
|
1
|
|
463
|
use File::MoreUtil qw(file_exists l_abs_path); |
|
1
|
|
|
|
|
311
|
|
|
1
|
|
|
|
|
45
|
|
12
|
1
|
|
|
1
|
|
417
|
use File::Trash::Undoable; |
|
1
|
|
|
|
|
6850
|
|
|
1
|
|
|
|
|
48
|
|
13
|
1
|
|
|
1
|
|
439
|
use IPC::System::Options 'system', -log=>1; |
|
1
|
|
|
|
|
2801
|
|
|
1
|
|
|
|
|
6
|
|
14
|
1
|
|
|
1
|
|
46
|
use Proc::ChildError qw(explain_child_error); |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
634
|
|
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
|
3636424
|
require Sys::Filesystem::MountPoint; # a bit heavy |
81
|
|
|
|
|
|
|
|
82
|
45
|
|
|
|
|
242
|
my %args = @_; |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# TMP, schema |
85
|
45
|
|
50
|
|
|
237
|
my $tx_action = $args{-tx_action} // ''; |
86
|
|
|
|
|
|
|
my $taid = $args{-tx_action_id} |
87
|
45
|
100
|
|
|
|
167
|
or return [412, "Please specify -tx_action_id"]; |
88
|
42
|
|
|
|
|
121
|
my $dry_run = $args{-dry_run}; |
89
|
42
|
|
|
|
|
85
|
my $source = $args{source}; |
90
|
42
|
50
|
|
|
|
126
|
defined($source) or return [400, "Please specify source"]; |
91
|
42
|
|
|
|
|
92
|
my $target = $args{target}; |
92
|
42
|
50
|
|
|
|
131
|
defined($target) or return [400, "Please specify target"]; |
93
|
42
|
|
50
|
|
|
136
|
my $rsync_opts = $args{rsync_opts} // ['-a']; |
94
|
42
|
50
|
|
|
|
134
|
$rsync_opts = [$rsync_opts] unless ref($rsync_opts) eq 'ARRAY'; |
95
|
|
|
|
|
|
|
|
96
|
42
|
|
|
|
|
172
|
my $se = file_exists($source); |
97
|
42
|
|
|
|
|
813
|
my $te = file_exists($target); |
98
|
42
|
50
|
|
|
|
652
|
my $asource = l_abs_path($source) or return [400, "Invalid path $source"]; |
99
|
42
|
50
|
|
|
|
1207
|
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
|
|
|
|
|
1058
|
for ($asource, $atarget) { |
104
|
84
|
50
|
|
|
|
715
|
s!/[^/]+\z!! if (-l $_); |
105
|
|
|
|
|
|
|
} |
106
|
42
|
|
|
|
|
309
|
my $mpsource = Sys::Filesystem::MountPoint::path_to_mount_point($asource); |
107
|
42
|
|
|
|
|
12295
|
my $mptarget = Sys::Filesystem::MountPoint::path_to_mount_point($atarget); |
108
|
42
|
|
|
|
|
11915
|
my $same_fs = $mpsource eq $mptarget; |
109
|
42
|
50
|
|
|
|
116
|
if ($same_fs) { |
110
|
42
|
|
|
|
|
333
|
$log->tracef("Source %s & target %s are on the same filesystem (%s)", |
111
|
|
|
|
|
|
|
$source, $target, $mpsource); |
112
|
|
|
|
|
|
|
} else { |
113
|
0
|
|
|
|
|
0
|
$log->tracef("Source %s and target %s are on different filesystems ". |
114
|
|
|
|
|
|
|
"(%s and %s)", $source, $target, $mpsource, $mptarget); |
115
|
|
|
|
|
|
|
} |
116
|
|
|
|
|
|
|
|
117
|
42
|
100
|
|
|
|
228
|
if ($tx_action eq 'check_state') { |
|
|
50
|
|
|
|
|
|
118
|
23
|
100
|
100
|
|
|
184
|
return [304, "Source $source already does not exist and ". |
119
|
|
|
|
|
|
|
"target $target exists"] if !$se && $te; |
120
|
20
|
100
|
|
|
|
80
|
return [412, "Source $source does not exist"] unless $se; |
121
|
19
|
50
|
33
|
|
|
70
|
return [412, "Target $target already exists"] if $te && $same_fs; |
122
|
|
|
|
|
|
|
|
123
|
19
|
|
|
|
|
29
|
my @undo; |
124
|
19
|
50
|
33
|
|
|
134
|
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
|
|
|
|
|
109
|
unshift @undo, ( |
133
|
|
|
|
|
|
|
[mv => {source=>$target, target=>$source}], |
134
|
|
|
|
|
|
|
); |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
19
|
0
|
|
|
|
71
|
$log->info("(DRY) ".($te ? "Continue moving" : "Moving"). |
|
|
50
|
|
|
|
|
|
138
|
|
|
|
|
|
|
" $source -> $target ...") if $dry_run; |
139
|
19
|
50
|
|
|
|
721
|
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
|
|
|
|
55
|
if ($same_fs) { |
145
|
19
|
|
|
|
|
105
|
$log->infof("Renaming %s -> %s ...", $source, $target); |
146
|
19
|
50
|
|
|
|
1098
|
if (rename $source, $target) { |
147
|
19
|
|
|
|
|
664
|
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->infof("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.08 of File::Move::Undoable (from Perl distribution File-Move-Undoable), released on 2016-06-10. |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
=head1 FUNCTIONS |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
=head2 mv(%args) -> [status, msg, result, meta] |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
Move file/directory using rename/rsync, with undo support. |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
If moving to the same filesystem, will move using C<rename()>. On undo will |
189
|
|
|
|
|
|
|
restore the old name. |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
If moving to a different filesystem, will copy to C<target> using C<rsync> and |
192
|
|
|
|
|
|
|
then trash C<source>. On undo, will trash C<target> and restore C<source> from |
193
|
|
|
|
|
|
|
trash. |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
Fixed state: C<source> does not exist and C<target> exists. Content or sizes are |
196
|
|
|
|
|
|
|
not checked; only existence. |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
Fixable state: C<source> exists and C<target> doesn't exist. |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
Unfixable state: C<source> does not exist, or both C<source> and C<target> exist |
201
|
|
|
|
|
|
|
(unless we are moving to a different filesystem, in which it means an |
202
|
|
|
|
|
|
|
interrupted transfer and thus fixable). |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
This function is not exported. |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
This function is idempotent (repeated invocations with same arguments has the same effect as single invocation). This function supports transactions. |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
Arguments ('*' denotes required arguments): |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
=over 4 |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
=item * B<rsync_opts> => I<array[str]> (default: ["-a"]) |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
Rsync options. |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
By default, C<-a> is used. You should not use rsync options that modify or |
218
|
|
|
|
|
|
|
destroy source, like C<--remove-source-files> as it will make recovery of |
219
|
|
|
|
|
|
|
interrupted move impossible. |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
=item * B<source>* => I<str> |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
=item * B<target>* => I<str> |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
Target location. |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
Note that to avoid ambiguity, you must specify full location instead of just |
228
|
|
|
|
|
|
|
directory name. For example: mv(source=>'/dir', target=>'/a') will move /dir to |
229
|
|
|
|
|
|
|
/a and mv(source=>'/dir', target=>'/a/dir') will move /dir to /a/dir. |
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-Move-Undoable>. |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
=head1 SOURCE |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
Source repository is at L<https://github.com/perlancar/perl-File-Move-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-Move-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) 2016 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 |